ViewVC Help
View File | Revision Log | Show Annotations | Revision Graph | Root Listing
root/i-scream/projects/cms/source/server/uk/org/iscream/cms/server/client/alerters/IRC__Alerter.java
(Generate patch)

Comparing projects/cms/source/server/uk/org/iscream/cms/server/client/alerters/IRC__Alerter.java (file contents):
Revision 1.9 by tdb, Sun Mar 4 03:34:50 2001 UTC vs.
Revision 1.23 by tdb, Fri Mar 16 17:13:49 2001 UTC

# Line 1 | Line 1
1   //---PACKAGE DECLARATION---
2 < package uk.ac.ukc.iscream.client.alerters;
2 > package uk.org.iscream.client.alerters;
3  
4   //---IMPORTS---
5 < import uk.ac.ukc.iscream.client.*;
6 < import uk.ac.ukc.iscream.core.*;
7 < import uk.ac.ukc.iscream.util.*;
8 < import uk.ac.ukc.iscream.componentmanager.*;
5 > import uk.org.iscream.client.*;
6 > import uk.org.iscream.core.*;
7 > import uk.org.iscream.util.*;
8 > import uk.org.iscream.componentmanager.*;
9  
10   import java.io.*;
11   import java.net.*;
12   import java.util.*;
13 + import java.text.*;
14  
15   /**
16   * This Alert sends an IRC message.
# Line 39 | Line 40 | public class IRC__Alerter implements PluginAlerter {
40       */
41      public final int DEFAULT_RECONNECT_DELAY = 30;
42      
43 +    public final String DEFAULT_LEVEL = Alert.alertLevels[0];
44 +    
45 +    public final String NOT_CONFIGURED = "<NOT CONFIGURED>";
46 +    
47   //---STATIC METHODS---
48  
49   //---CONSTRUCTORS---
# Line 47 | Line 52 | public class IRC__Alerter implements PluginAlerter {
52                          
53          // connect to the IRC server
54          _ircbot = new IRCBot();
55 +        // set it's name and start it
56 +        _ircbot.setName("client.IRC__Alerter$IRCBot");
57          _ircbot.start();
58 +        _startTime = System.currentTimeMillis();
59          
60          _logger.write(toString(), Logger.SYSINIT, "IRC Alerter started");
61      }
# Line 58 | Line 66 | public class IRC__Alerter implements PluginAlerter {
66          // only send alerts if we're active
67          if(_active) {
68              ConfigurationProxy cp = ConfigurationProxy.getInstance();
69 <            String levelName = cp.getProperty(_name, "Alerter.IRC.level");
69 >            
70 >            String levelName;
71 >            try {
72 >                levelName = cp.getProperty(_name, "Alerter.IRC.level");
73 >            } catch (PropertyNotFoundException e) {
74 >                levelName = DEFAULT_LEVEL;
75 >                _logger.write(toString(), Logger.WARNING, "Alerter.IRC.level value unavailable using default of " + levelName);
76 >            }
77              int level = StringUtils.getStringPos(levelName, Alert.alertLevels);
78              // only send if it's equal (or above) our level
79 <            if(alert.getLevel() >= level) {
79 >            if(((alert.getLevel() == 0) && (alert.getLastLevel() >= level)) || (alert.getLevel() >= level)) {
80                  String alertType = Alert.alertLevels[alert.getLevel()];
81                  String thresholdType = Alert.thresholdLevels[alert.getThreshold()];
82 <                // sort out the message
83 <                String message = cp.getProperty(_name, "Alerter.IRC.message");
82 >                String timeFirstSince = DateUtils.formatTime((System.currentTimeMillis() - alert.getInitialAlertTime())/1000, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
83 >                String timeFirstOccured = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date(alert.getInitialAlertTime()));
84 >                // sort out the message              
85 >                String message;
86 >                try {
87 >                    message = cp.getProperty(_name, "Alerter.IRC.message");
88 >                } catch (PropertyNotFoundException e) {
89 >                    message = NOT_CONFIGURED;
90 >                    _logger.write(toString(), Logger.WARNING, "Alerter.IRC.message value unavailable using default of " + message);
91 >                }
92 >                
93                  message = StringUtils.replaceText(message, "%level%", alertType);
94                  message = StringUtils.replaceText(message, "%threshold%", thresholdType);
95                  message = StringUtils.replaceText(message, "%source%", alert.getSource());
96                  message = StringUtils.replaceText(message, "%value%", alert.getValue());
97                  message = StringUtils.replaceText(message, "%thresholdValue%", alert.getThresholdValue());
98                  message = StringUtils.replaceText(message, "%attributeName%", alert.getAttributeName());
99 <                message = StringUtils.replaceText(message, "%timeTillNextAlert%",  getTimeString(Long.parseLong(alert.getTimeTillNextAlert())));
99 >                message = StringUtils.replaceText(message, "%timeTillNextAlert%",  DateUtils.getTimeString(Long.parseLong(alert.getTimeTillNextAlert())));
100 >                message = StringUtils.replaceText(message, "%timeSinceFirstAlert%", timeFirstSince);
101 >                message = StringUtils.replaceText(message, "%timeOfFirstAlert%", timeFirstOccured);
102                  
103                  // send the message
104 <                _logger.write(toString(), Logger.DEBUG, "Sending " + _name + " at "+ levelName + " level");
104 >                _logger.write(toString(), Logger.DEBUG, "Sending " + _name + " at "+ alertType + " level");
105                  _ircbot.sendMsg(message);
106                  _lastAlert = message;
107 +                _lastAlertTime = System.currentTimeMillis();
108 +                _alertCount ++;
109              }
110          }
111 +        else {
112 +            _ignoredCount ++;
113 +        }
114      }
115  
116      /**
117       * Overrides the {@link java.lang.Object#toString() Object.toString()}
118       * method to provide clean logging (every class should have this).
119       *
120 <     * This uses the uk.ac.ukc.iscream.util.NameFormat class
120 >     * This uses the uk.org.iscream.util.NameFormat class
121       * to format the toString()
122       *
123       * @return the name of this class and its CVS revision
# Line 107 | Line 138 | public class IRC__Alerter implements PluginAlerter {
138  
139   //---PRIVATE METHODS---
140  
110    private String getTimeString(long time) {
111        String timeString = null;
112        if (time >= 60) {
113            timeString = (time / 60) + " minute(s)";
114        } else if (time >= 3600) {
115            timeString = ((time/60) / 60) + " hour(s)";
116        } else {
117            timeString = time + " second(s)";
118        }
119        return timeString;
120    }
121
141   //---ACCESSOR/MUTATOR METHODS---
142  
143   //---ATTRIBUTES---
# Line 139 | Line 158 | public class IRC__Alerter implements PluginAlerter {
158      private String _lastAlert = "no alerts have been sent";
159      
160      /**
161 +     * The time of the last alert
162 +     */
163 +    private long _lastAlertTime = -1;
164 +    
165 +    /**
166 +     * Number of alerts sent
167 +     */
168 +    private int _alertCount = 0;
169 +    
170 +    /**
171 +     * Number of alerts ignored when in "stopped" mode
172 +     */
173 +    private int _ignoredCount = 0;
174 +    
175 +    /**
176 +     * Time of IRCBot startup
177 +     */
178 +    private long _startTime;
179 +    
180 +    /**
181       * This is the friendly identifier of the
182       * component this class is running in.
183       * eg, a Filter may be called "filter1",
# Line 167 | Line 206 | public class IRC__Alerter implements PluginAlerter {
206       */
207      class IRCBot extends Thread {
208          
209 +        public static final String DEFAULT_STARTUP_NOTICE = "i-scream ircbot starting...";
210 +        
211          /**
212           * Main thread loop, this part of the class listens for
213           * messages from the server, and acts accordingly. At the
# Line 178 | Line 219 | public class IRC__Alerter implements PluginAlerter {
219              while(run) {
220                  // flag so we can stop the loop
221                  boolean doRead = true;
222 +                // get the startup notice
223 +                String startupNotice;
224 +                try {
225 +                    startupNotice = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.startupNotice");
226 +                } catch (PropertyNotFoundException e) {
227 +                    startupNotice = DEFAULT_STARTUP_NOTICE;
228 +                    _logger.write(this.toString(), Logger.WARNING, "Configuration error: "+e);
229 +                }
230                  // connect to the IRC server
231                  try {
232                      connect();
233 <                    sendNotice(ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.startupNotice"));
233 >                    sendNotice(startupNotice);
234                  } catch(IOException e) {
235                      doRead=false;
236                      _logger.write(this.toString(), Logger.ERROR, "Error connecting: "+e);
# Line 217 | Line 266 | public class IRC__Alerter implements PluginAlerter {
266                  } catch (NumberFormatException e) {
267                      delayTime = DEFAULT_RECONNECT_DELAY;
268                      _logger.write(this.toString(), Logger.WARNING, "Erronous Alerter.IRC.reconnectDelay value in configuration using default of " + delayTime + " seconds");
269 <                } catch (org.omg.CORBA.MARSHAL e2) {
269 >                } catch (PropertyNotFoundException e) {
270                      delayTime = DEFAULT_RECONNECT_DELAY;
271                      _logger.write(this.toString(), Logger.WARNING, "Alerter.IRC.reconnectDelay value unavailable using default of " + delayTime + " seconds");
272                  }
# Line 236 | Line 285 | public class IRC__Alerter implements PluginAlerter {
285           */
286          public void sendMsg(String msg) {
287              _socketOut.println("PRIVMSG "+_channel+" :"+msg);
288 +            // wait a second before returning...
289 +            // this ensures messages can't be sent too fast
290 +            try {Thread.sleep(1000);} catch (InterruptedException e) {}
291          }
292          
293          /**
# Line 246 | Line 298 | public class IRC__Alerter implements PluginAlerter {
298           */
299          public void sendPrivMsg(String user, String msg) {
300              _socketOut.println("PRIVMSG "+user+" :"+msg);
301 +            // wait a second before returning...
302 +            // this ensures messages can't be sent too fast
303 +            try {Thread.sleep(1000);} catch (InterruptedException e) {}
304          }
305          
306          /**
# Line 256 | Line 311 | public class IRC__Alerter implements PluginAlerter {
311          public void sendAction(String msg) {
312              char esc = 001;
313              sendMsg(esc+"ACTION "+msg+esc);
314 +            // wait a second before returning...
315 +            // this ensures messages can't be sent too fast
316 +            try {Thread.sleep(1000);} catch (InterruptedException e) {}
317          }
318          
319          /**
# Line 265 | Line 323 | public class IRC__Alerter implements PluginAlerter {
323           */
324          public void sendNotice(String msg) {
325              _socketOut.println("NOTICE "+_channel+" :"+msg);
326 +            // wait a second before returning...
327 +            // this ensures messages can't be sent too fast
328 +            try {Thread.sleep(1000);} catch (InterruptedException e) {}
329          }
330          
331          /**
# Line 275 | Line 336 | public class IRC__Alerter implements PluginAlerter {
336          public void connect() throws IOException {
337              ConfigurationProxy cp = ConfigurationProxy.getInstance();
338              // setup the socket, reader and writer
339 <            String server = cp.getProperty(_name, "Alerter.IRC.IRCServer");
340 <            int port = Integer.parseInt(cp.getProperty(_name, "Alerter.IRC.IRCPort"));
339 >            String server;
340 >            int port;
341 >            try {
342 >                server = cp.getProperty(_name, "Alerter.IRC.IRCServer");
343 >                port = Integer.parseInt(cp.getProperty(_name, "Alerter.IRC.IRCPort"));
344 >            } catch (PropertyNotFoundException e) {
345 >                _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
346 >                throw new IOException("Can't get irc server details due to configuration error");
347 >            } catch (NumberFormatException e) {
348 >                _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
349 >                throw new IOException("Can't get irc server details due to malformed configuration");
350 >            }
351              _socket = new Socket(server, port);
352              _socketIn = new BufferedReader(new InputStreamReader(_socket.getInputStream()));
353              _socketOut = new PrintWriter(_socket.getOutputStream(), true);
354              //_socketOut.println("PASS");
355              // send USER details
356 <            String user = cp.getProperty(_name, "Alerter.IRC.user");
357 <            String comment = cp.getProperty(_name, "Alerter.IRC.comment");
356 >            String user, comment;
357 >            try {
358 >                user = cp.getProperty(_name, "Alerter.IRC.user");
359 >                comment = cp.getProperty(_name, "Alerter.IRC.comment");
360 >            } catch (PropertyNotFoundException e) {
361 >                _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
362 >                throw new IOException("Can't get user details due to configuration error");
363 >            }
364              _socketOut.println("USER "+user+" 8 * :"+comment);
365              // attempt to get a nick
366 <            String nickList = cp.getProperty(_name, "Alerter.IRC.nickList");
366 >            String nickList;
367 >            try {
368 >                nickList = cp.getProperty(_name, "Alerter.IRC.nickList");
369 >            } catch (PropertyNotFoundException e) {
370 >                _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
371 >                throw new IOException("Can't get nickname due to configuration error");
372 >            }
373              StringTokenizer st = new StringTokenizer(nickList, ";");
374              boolean ok = false;
375              // try until we exhaust our list
# Line 318 | Line 401 | public class IRC__Alerter implements PluginAlerter {
401                  throw new IOException("All nicknames in use");
402              }
403              // join the channel
404 <            _channel = cp.getProperty(_name, "Alerter.IRC.channel");
404 >            try {
405 >                _channel = cp.getProperty(_name, "Alerter.IRC.channel");
406 >            } catch (PropertyNotFoundException e) {
407 >                _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
408 >                throw new IOException("Can't get channel name due to configuration error");
409 >            }
410              _socketOut.println("JOIN "+_channel);
411              // allow alerts
412              _active = true;
# Line 344 | Line 432 | public class IRC__Alerter implements PluginAlerter {
432           * Overrides the {@link java.lang.Object#toString() Object.toString()}
433           * method to provide clean logging (every class should have this).
434           *
435 <         * This uses the uk.ac.ukc.iscream.util.NameFormat class
435 >         * This uses the uk.org.iscream.util.NameFormat class
436           * to format the toString()
437           *
438           * @return the name of this class and its CVS revision
# Line 369 | Line 457 | public class IRC__Alerter implements PluginAlerter {
457                  _socketOut.println("PONG" + line.substring(4));
458              }
459              // see if it's for us
460 <            else if(getMsg(line).startsWith(_nickname)) {
460 >            else if(getMsg(line).startsWith(_nickname+",") || getMsg(line).startsWith(_nickname+":") || getMsg(line).startsWith(_nickname+" ")) {
461 >                // setup some String's
462 >                String stopCommand, startCommand, timeSinceLastAlertCommand, lastAlertCommand, joinCommand;
463 >                String nickChangeCommand, versionCommand, helpCommand, statCommand, uptimeCommand;
464 >                // get the command set
465 >                try {
466 >                    stopCommand = cp.getProperty(_name, "Alerter.IRC.stopCommand");
467 >                    startCommand = cp.getProperty(_name, "Alerter.IRC.startCommand");
468 >                    timeSinceLastAlertCommand = cp.getProperty(_name, "Alerter.IRC.timeSinceLastAlertCommand");
469 >                    lastAlertCommand = cp.getProperty(_name, "Alerter.IRC.lastAlertCommand");
470 >                    joinCommand = cp.getProperty(_name, "Alerter.IRC.joinCommand");
471 >                    nickChangeCommand = cp.getProperty(_name, "Alerter.IRC.nickChangeCommand");
472 >                    versionCommand = cp.getProperty(_name, "Alerter.IRC.versionCommand");
473 >                    helpCommand = cp.getProperty(_name, "Alerter.IRC.helpCommand");
474 >                    statCommand = cp.getProperty(_name, "Alerter.IRC.statCommand");
475 >                    uptimeCommand = cp.getProperty(_name, "Alerter.IRC.uptimeCommand");
476 >                } catch (PropertyNotFoundException e) {
477 >                    _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
478 >                    // lets bail from handling this line...
479 >                    // ...it's gonna be hard without a command set!
480 >                    return;
481 >                }
482 >                
483                  // we have a message for us
484                  String message = getMsg(line).substring(_nickname.length());
485 <                if(message.indexOf(cp.getProperty(_name, "Alerter.IRC.stopCommand"))!=-1) {
485 >                if(message.indexOf(stopCommand)!=-1) {
486                      _active = false;
487                      sendMsg(getMsgSender(line)+", alerts have been stopped");
488                  }
489 <                else if(message.indexOf(cp.getProperty(_name, "Alerter.IRC.startCommand"))!=-1) {
489 >                else if(message.indexOf(startCommand)!=-1) {
490                      _active = true;
491                      sendMsg(getMsgSender(line)+", alerts have been activated");
492                  }
493 <                else if(message.indexOf(cp.getProperty(_name, "Alerter.IRC.lastAlertCommand"))!=-1) {
494 <                    sendMsg(getMsgSender(line)+", last alert was: "+_lastAlert);
493 >                // this needs to go here if it contains the same words as the lastAlertCommand
494 >                else if(message.indexOf(timeSinceLastAlertCommand)!=-1) {
495 >                    if(_lastAlertTime != -1) {
496 >                        long uptime = (System.currentTimeMillis() - _lastAlertTime) / 1000;
497 >                        String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
498 >                        sendMsg(getMsgSender(line)+", I last sent an alert "+uptimeText+ " ago");
499 >                    }
500 >                    else {
501 >                        sendMsg(getMsgSender(line)+", I've never sent an alert!");
502 >                    }
503                  }
504 <                else if(message.indexOf(cp.getProperty(_name, "Alerter.IRC.joinCommand"))!=-1) {
505 <                    String joinCmd = cp.getProperty(_name, "Alerter.IRC.joinCommand");
504 >                else if(message.indexOf(lastAlertCommand)!=-1) {
505 >                    if(_lastAlertTime != -1) {
506 >                        String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date(_lastAlertTime));
507 >                        sendMsg(getMsgSender(line)+", last alert was at "+date+"; "+_lastAlert);
508 >                    }
509 >                    else {
510 >                        sendMsg(getMsgSender(line)+", I've never sent an alert!");
511 >                    }
512 >                    
513 >                }
514 >                else if(message.indexOf(joinCommand)!=-1) {
515 >                    String joinCmd = joinCommand;
516                      String newChan = message.substring(message.indexOf(joinCmd) + joinCmd.length() + 1);
517                      int endOfChan = newChan.indexOf(" ");
518                      if(endOfChan == -1) {
519                          endOfChan = newChan.length();
520                      }
521                      newChan = newChan.substring(0, endOfChan);
522 <                    sendMsg(getMsgSender(line)+", okay, I'm off to "+newChan);
523 <                    _socketOut.println("PART "+_channel);
524 <                    _socketOut.println("JOIN "+newChan);
525 <                    _channel = newChan;
522 >                    if(newChan.equals(_channel)) {
523 >                        sendMsg(getMsgSender(line)+", I'm already on "+newChan+"!");
524 >                    } else {
525 >                        sendMsg(getMsgSender(line)+", okay, I'm off to "+newChan);
526 >                        _socketOut.println("PART "+_channel);
527 >                        _socketOut.println("JOIN "+newChan);
528 >                        _channel = newChan;
529 >                    }
530                  }
531 <                else if(message.indexOf(cp.getProperty(_name, "Alerter.IRC.helpCommand"))!=-1) {
532 <                    sendPrivMsg(getMsgSender(line), "I am the i-scream alerting bot revision "+REVISION.substring(11, REVISION.length() -2));
531 >                else if(message.indexOf(nickChangeCommand)!=-1) {
532 >                    String nickChangeCmd = nickChangeCommand;
533 >                    String newNick = message.substring(message.indexOf(nickChangeCmd) + nickChangeCmd.length() + 1);
534 >                    int endOfNick = newNick.indexOf(" ");
535 >                    if(endOfNick == -1) {
536 >                        endOfNick = newNick.length();
537 >                    }
538 >                    newNick = newNick.substring(0, endOfNick);
539 >                    sendMsg(getMsgSender(line)+", okay, changing my nickname to "+newNick);
540 >                    _socketOut.println("NICK "+newNick);
541 >                    _nickname = newNick;
542 >                }
543 >                else if(message.indexOf(versionCommand)!=-1) {
544 >                    sendMsg(getMsgSender(line)+", I am version "+REVISION.substring(11, REVISION.length() -2)+" of the i-scream alerting bot");
545 >                }
546 >                else if(message.indexOf(helpCommand)!=-1) {
547 >                    sendPrivMsg(getMsgSender(line), "Hello, I am the i-scream alerting bot version "+REVISION.substring(11, REVISION.length() -2));
548                      sendPrivMsg(getMsgSender(line), "I understand the following commands;");
549 <                    sendPrivMsg(getMsgSender(line), cp.getProperty(_name, "Alerter.IRC.stopCommand"));
550 <                    sendPrivMsg(getMsgSender(line), cp.getProperty(_name, "Alerter.IRC.startCommand"));
551 <                    sendPrivMsg(getMsgSender(line), cp.getProperty(_name, "Alerter.IRC.lastAlertCommand"));
549 >                    sendPrivMsg(getMsgSender(line), stopCommand);
550 >                    sendPrivMsg(getMsgSender(line), startCommand);
551 >                    sendPrivMsg(getMsgSender(line), lastAlertCommand);
552 >                    sendPrivMsg(getMsgSender(line), joinCommand);
553 >                    sendPrivMsg(getMsgSender(line), nickChangeCommand);
554 >                    sendPrivMsg(getMsgSender(line), statCommand);
555 >                    sendPrivMsg(getMsgSender(line), uptimeCommand);
556 >                    sendPrivMsg(getMsgSender(line), timeSinceLastAlertCommand);
557 >                    sendPrivMsg(getMsgSender(line), helpCommand);
558                  }
559 +                else if(message.indexOf(statCommand)!=-1) {
560 +                    sendMsg(getMsgSender(line)+", I have sent a total of "+_alertCount+" alerts, and ignored a total of "+_ignoredCount+"!");
561 +                }
562 +                else if(message.indexOf(uptimeCommand)!=-1) {
563 +                    long uptime = (System.currentTimeMillis() - _startTime) / 1000;
564 +                    String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
565 +                    sendMsg(getMsgSender(line)+", I have been running for "+uptimeText);
566 +                }
567 +                else if(message.indexOf("ping")!=-1) {
568 +                    sendMsg("pong");
569 +                }
570                  else if(message.indexOf("do a jibble dance")!=-1) {
571                      // little joke :)
572                      sendAction("jives to the funky beat shouting \"ii--screeeaaammm\"");
573                  }
574                  else {
575 <                    sendMsg(getMsgSender(line)+", "+cp.getProperty(_name, "Alerter.IRC.rejectMessage"));
575 >                    String rejectMessage = NOT_CONFIGURED;
576 >                    try {
577 >                        rejectMessage = cp.getProperty(_name, "Alerter.IRC.rejectMessage");
578 >                    } catch(PropertyNotFoundException e) {
579 >                        _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
580 >                    }
581 >                    sendMsg(getMsgSender(line)+", "+rejectMessage);
582                  }
583              }
584              else if(line.indexOf(_nickname)!=-1 && line.indexOf(_channel)!=-1 && line.indexOf("KICK")!=-1) {
585                  sendPrivMsg(getMsgSender(line), "That wasn't a nice thing to do...");
586 <                _channel = cp.getProperty(_name, "Alerter.IRC.channel");
586 >                try {
587 >                    _channel = cp.getProperty(_name, "Alerter.IRC.channel");
588 >                } catch(PropertyNotFoundException e) {
589 >                    _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
590 >                }
591                  _socketOut.println("JOIN "+_channel);
592              }
593          }

Diff Legend

Removed lines
+ Added lines
< Changed lines
> Changed lines