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
Revision: 1.31
Committed: Thu Feb 7 17:15:19 2002 UTC (22 years, 3 months ago) by tdb
Branch: MAIN
Changes since 1.30: +380 -365 lines
Log Message:
Merged in changes from the SERVER_PIRCBOT branch. The IRC__Alerter now uses
pircbot (www.jibble.org/pircbot.php) for it's IRC connectivity. All the
features from the old version have been moved across to the new one, with a
few minor changes.

File Contents

# Content
1 //---PACKAGE DECLARATION---
2 package uk.org.iscream.cms.server.client.alerters;
3
4 //---IMPORTS---
5 import uk.org.iscream.cms.server.client.*;
6 import uk.org.iscream.cms.server.core.*;
7 import uk.org.iscream.cms.server.util.*;
8 import uk.org.iscream.cms.server.componentmanager.*;
9 import java.io.*;
10 import java.net.*;
11 import java.util.*;
12 import java.text.DateFormat;
13 import org.jibble.pircbot.*;
14
15 /**
16 * This Alert sends an IRC message.
17 *
18 * Clean shutdown could be achieved by stopping the run() method in the
19 * IRCBot inner class.
20 *
21 * @author $Author: tdb $
22 * @version $Id: IRC__Alerter.java,v 1.30.2.6 2002/02/06 22:59:06 tdb Exp $
23 */
24 public class IRC__Alerter extends AlerterSkeleton {
25
26 //---FINAL ATTRIBUTES---
27
28 /**
29 * The current CVS revision of this class
30 */
31 public final String REVISION = "$Revision: 1.30.2.6 $";
32
33 /**
34 * A description of this alerter
35 */
36 public final String DESC = "Sends alerts on an IRC channel";
37
38 //---STATIC METHODS---
39
40 //---CONSTRUCTORS---
41
42 public IRC__Alerter() {
43 super();
44 // construct and initialise the bot
45 _ircbot = new IRCBot();
46 _ircbot.setVerbose(false);
47 Thread ircThread = new Thread(_ircbot);
48 // set it's name and start it
49 ircThread.setName("client.IRC__Alerter$IRCBot");
50 ircThread.start();
51 // log our start time
52 _startTime = System.currentTimeMillis();
53 _logger.write(toString(), Logger.SYSINIT, "IRC Alerter started");
54 }
55
56 //---PUBLIC METHODS---
57
58 /**
59 * Implements the abstract method from the skeleton class.
60 * This method will attempt to send an alert
61 * message over the IRC channel.
62 *
63 * @param alert the alert to send
64 */
65 public void sendAlert(Alert alert) {
66 // sort out the message
67 String alertType = Alert.alertLevels[alert.getLevel()];
68 String message;
69 try {
70 message = _cp.getProperty(_name, "Alerter.IRC.message");
71 } catch (PropertyNotFoundException e) {
72 message = NOT_CONFIGURED;
73 _logger.write(toString(), Logger.WARNING, "Alerter.IRC.message value unavailable using default of " + message);
74 }
75 message = processAlertMessage(message, alert);
76 // only send alerts if we're active
77 if(_active) {
78 // send the message
79 _logger.write(toString(), Logger.DEBUG, "Sending " + _name + " at "+ alertType + " level");
80 _ircbot.sendMessage(message);
81 // count sent alerts
82 _alertCount++;
83 } else {
84 // don't send, but keep a count that we ignored it
85 _ignoredCount++;
86 }
87 // we'll always store the "last alert", regardless
88 // of whether we actually display it or not
89 _lastAlert = message;
90 _lastAlertTime = System.currentTimeMillis();
91 }
92
93 /**
94 * Overrides the {@link java.lang.Object#toString() Object.toString()}
95 * method to provide clean logging (every class should have this).
96 *
97 * This uses the uk.org.iscream.cms.server.util.NameFormat class
98 * to format the toString()
99 *
100 * @return the name of this class and its CVS revision
101 */
102 public String toString() {
103 return FormatName.getName(
104 _name,
105 getClass().getName(),
106 REVISION);
107 }
108
109 /**
110 * Return the String representation of what the alerter does
111 *
112 * @return the description
113 */
114 public String getDescription(){
115 return DESC;
116 }
117
118 //---PRIVATE METHODS---
119
120 //---ACCESSOR/MUTATOR METHODS---
121
122 /**
123 * Returns the "friendly" name of this class. This
124 * is simply an accessor for _name, required due to
125 * inheritance issues with extending AlerterSkeleton.
126 *
127 * @return the friendly name
128 */
129 protected String getFName() {
130 return _name;
131 }
132
133 //---ATTRIBUTES---
134
135 /**
136 * A reference to the IRCBot
137 */
138 private IRCBot _ircbot;
139
140 /**
141 * Are we "active"
142 */
143 private boolean _active = false;
144
145 /**
146 * The last alert that was sent
147 */
148 private String _lastAlert = "no alerts have been sent";
149
150 /**
151 * The time of the last alert
152 */
153 private long _lastAlertTime = -1;
154
155 /**
156 * Number of alerts sent
157 */
158 private int _alertCount = 0;
159
160 /**
161 * Number of alerts ignored when in "stopped" mode
162 */
163 private int _ignoredCount = 0;
164
165 /**
166 * Time of IRCBot startup
167 */
168 private long _startTime;
169
170 /**
171 * This holds a reference to the
172 * system logger that is being used.
173 */
174 protected Logger _logger = ReferenceManager.getInstance().getLogger();
175
176 /**
177 * This is the friendly identifier of the
178 * component this class is running in.
179 * eg, a Filter may be called "filter1",
180 * If this class does not have an owning
181 * component, a name from the configuration
182 * can be placed here. This name could also
183 * be changed to null for utility classes.
184 */
185 private String _name = "IRC";
186
187 //---STATIC ATTRIBUTES---
188
189 //---INNER CLASSES---
190
191 class IRCBot extends PircBot implements Runnable {
192
193 /**
194 * The default reconnect delay in seconds
195 */
196 public final int DEFAULT_RECONNECT_DELAY = 30;
197
198 /**
199 * Attempt to kick-start the IRCBot into action. Will
200 * keep calling init() until it doesn't throw an
201 * an IOException (which will only be thrown if there
202 * is an error initialising). After a successful call
203 * to init() the method finishes.
204 */
205 public void run() {
206 while(true) {
207 try {
208 init();
209 break;
210 }
211 catch (IOException e) {
212 _logger.write(this.toString(), Logger.ERROR, "Error initialising IRCBot: "+e);
213 // wait for a while, defined in the config
214 reconnectSleep();
215 }
216 }
217 }
218
219 /**
220 * Connects the bot to an irc server and joins a channel,
221 * using details supplied from the i-scream configuration
222 * system. If this method completes without an exception
223 * one can presume the bot is ready for use.
224 *
225 * @throws IOException if there is any problem initialising
226 */
227 private void init() throws IOException {
228 _logger.write(this.toString(), Logger.DEBUG, "Initialising IRCBot...");
229
230 // get a hook on the configuration system
231 ConfigurationProxy cp = ConfigurationProxy.getInstance();
232
233 // get hold of the server details
234 String server;
235 int port;
236 try {
237 server = cp.getProperty(_name, "Alerter.IRC.IRCServer");
238 port = Integer.parseInt(cp.getProperty(_name, "Alerter.IRC.IRCPort"));
239 } catch (PropertyNotFoundException e) {
240 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
241 throw new IOException("Can't get irc server details due to configuration error");
242 } catch (NumberFormatException e) {
243 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
244 throw new IOException("Can't get irc server details due to malformed configuration");
245 }
246
247 // get hold of the user details and nickname list
248 String user, nickList;
249 try {
250 user = cp.getProperty(_name, "Alerter.IRC.user");
251 nickList = cp.getProperty(_name, "Alerter.IRC.nickList");
252 } catch (PropertyNotFoundException e) {
253 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
254 throw new IOException("Can't get user/nickname details due to configuration error");
255 }
256
257 // get hold of comment and finger information
258 // -- we're not too fussed if these aren't set
259 String comment = "Alerter.IRC.comment is undefined";
260 String finger = "Alerter.IRC.finger is undefined";
261 try {
262 comment = cp.getProperty(_name, "Alerter.IRC.comment");
263 finger = cp.getProperty(_name, "Alerter.IRC.finger");
264 } catch (PropertyNotFoundException e) {
265 _logger.write(this.toString(), Logger.WARNING, "Configuration warning, using default: "+e);
266 }
267
268 // put these details into the bot
269 this.setLogin(user);
270 this.setVersion("[" + getRevision() + "] " + comment);
271 this.setFinger(finger);
272
273 // attempt to connect, trying each nickname
274 // in turn until sucessfully connected
275 StringTokenizer st = new StringTokenizer(nickList, ";");
276 boolean ok = false;
277 while(!ok && st.hasMoreTokens()) {
278 String nick = st.nextToken();
279 try {
280 // try to connect with a nickname
281 _logger.write(this.toString(), Logger.DEBUG, "Trying nick: "+nick);
282 this.setName(nick);
283 this.connect(server, port);
284 // at this point we know the nickname was accepted
285 _nickname = nick;
286 ok = true;
287 }
288 catch(IOException e) {
289 _logger.write(this.toString(), Logger.ERROR, "IO error when connecting to server: "+e);
290 throw new IOException("IO error when connecting to server");
291 }
292 catch(IrcException e) {
293 _logger.write(this.toString(), Logger.ERROR, "IRC error when connecting to server: "+e);
294 throw new IOException("IRC error when connecting to server");
295 }
296 catch(NickAlreadyInUseException e) {
297 _logger.write(this.toString(), Logger.ERROR, "Nickname "+nick+" is already in use: "+e);
298 // don't do anything, instead just loop round
299 // and try the next nickname in the list
300 }
301 }
302 if(!ok) {
303 // must have tried all the nicknames, best bail out
304 _logger.write(this.toString(), Logger.ERROR, "All nicknames already in use");
305 throw new IOException("All nicknames already in use");
306 }
307
308 // get hold of the channel details, and attempt
309 // to join the specified channel
310 try {
311 _channel = cp.getProperty(_name, "Alerter.IRC.channel");
312 this.joinChannel(_channel);
313 } catch (PropertyNotFoundException e) {
314 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
315 throw new IOException("Can't get channel name due to configuration error");
316 }
317
318 // get hold of the startup notice, and send
319 // it if it's defined
320 String startupNotice;
321 try {
322 startupNotice = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.startupNotice");
323 sendNotice(_channel, startupNotice);
324 } catch (PropertyNotFoundException e) {
325 _logger.write(this.toString(), Logger.DEBUG, "Startup notice not defined, so not sending: "+e);
326 }
327
328 // at this point initialisation is complete, so
329 // we can return and set this flag to allow alerts
330 _active = true;
331 }
332
333 /**
334 * Send a message to the current channel.
335 *
336 * @param message The message to send
337 */
338 private void sendMessage(String message) {
339 sendMessage(_channel, message);
340 }
341
342 /**
343 * If we get disconnected this method will be
344 * called, so we must take action. We'll simply
345 * keep trying to reinitialise, and thus
346 * reconnect to the server.
347 */
348 public void onDisconnect() {
349 // stop alerts being sent for now
350 _active = false;
351 while(true) {
352 // wait for a while, defined in the config
353 reconnectSleep();
354 try {
355 init();
356 break;
357 }
358 catch (IOException e) {
359 _logger.write(this.toString(), Logger.ERROR, "Error initialising IRCBot: "+e);
360 }
361 }
362 }
363
364 /**
365 * If we receive a message this method will
366 * be called. We'll check if the message is
367 * for us, and call handleInput if it is.
368 *
369 * @param channel The channel the message came from
370 * @param sender The sender of the message
371 * @param login The login of the sender
372 * @param hostname The hostname of the sender
373 * @param message The message sent
374 */
375 public void onMessage(String channel, String sender, String login, String hostname, String message) {
376 String trimmedMessage = isForMe(message);
377 // if trimmedMessage is null, it's not for us
378 if(trimmedMessage != null) {
379 handleInput(trimmedMessage, channel);
380 }
381 }
382
383 /**
384 * If we receive a private message this method
385 * will be called. No need to check if it's for
386 * us -- it has to be if it's a private message.
387 *
388 * @param sender The sender of the message
389 * @param login The login of the sender
390 * @param hostname The hostname of the sender
391 * @param message The message sent
392 */
393 public void onPrivateMessage(String sender, String login, String hostname, String message) {
394 handleInput(message, sender);
395 }
396
397 /**
398 * If we receive a nick change message, this
399 * method gets called. We don't care about
400 * other users changing their nick, so we only
401 * take notice if our nick changes. If it does
402 * we need to change our internal nickname
403 * state.
404 *
405 * @param oldNick the old nickname
406 * @param login the login of the nick changer
407 * @param hostname the hostname of the nick changer
408 * @param newNick the new nickname
409 */
410 public void onNickChange(String oldNick, String login, String hostname, String newNick) {
411 if(oldNick.equals(_nickname)) {
412 _nickname = newNick;
413 // tell the underlying pircbot that our nick has changed too :)
414 setName(newNick);
415 }
416 }
417
418 /**
419 * If we receive a kick message this method
420 * gets called. We only take notice if it's
421 * us getting kicked, and then we'll rejoin
422 * the channel after a short wait.
423 *
424 * @param channel the channel the kick happened on
425 * @param kickerNick the person who performed the kick
426 * @param kickerLogin the login of the person who performed the kick
427 * @param kickerHostname the hostname of the person who performed the kick
428 * @param recipientNick the nickname of the person being kicked
429 * @param reason the reason for the kick
430 */
431 public void onKick(String channel, String kickerNick, String kickerLogin, String kickerHostname, String recipientNick, String reason) {
432 if(recipientNick.equals(_nickname) && channel.equals(_channel)) {
433 // remind the person it's not very nice :)
434 sendMessage(kickerNick, "That wasn't a nice thing to do...");
435 // get hold of the channel details, in case they've changed
436 try {
437 _channel = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.channel");
438 } catch(PropertyNotFoundException e) {
439 _logger.write(this.toString(), Logger.ERROR, "Can't get channel name due to configuration error: "+e);
440 }
441 // wait for a while, defined in the config
442 reconnectSleep();
443 // we'll try and rejoin the channel regardless
444 // otherwise we might end up doing nothing!
445 joinChannel(_channel);
446 }
447 }
448
449 /**
450 * This method handles input directed to us, and
451 * responds accordingly if required.
452 *
453 * @param message the message from someone
454 * @param source where the message came from, so we know where to send the response
455 */
456 private void handleInput(String message, String source) {
457 // get hold of the configuration system
458 ConfigurationProxy cp = ConfigurationProxy.getInstance();
459 // setup some String's
460 String stopCommand, startCommand, timeSinceLastAlertCommand, lastAlertCommand, joinCommand;
461 String nickChangeCommand, versionCommand, helpCommand, statCommand, uptimeCommand;
462 // get the command set from the configuration
463 try {
464 stopCommand = cp.getProperty(_name, "Alerter.IRC.stopCommand");
465 startCommand = cp.getProperty(_name, "Alerter.IRC.startCommand");
466 timeSinceLastAlertCommand = cp.getProperty(_name, "Alerter.IRC.timeSinceLastAlertCommand");
467 lastAlertCommand = cp.getProperty(_name, "Alerter.IRC.lastAlertCommand");
468 joinCommand = cp.getProperty(_name, "Alerter.IRC.joinCommand");
469 nickChangeCommand = cp.getProperty(_name, "Alerter.IRC.nickChangeCommand");
470 versionCommand = cp.getProperty(_name, "Alerter.IRC.versionCommand");
471 helpCommand = cp.getProperty(_name, "Alerter.IRC.helpCommand");
472 statCommand = cp.getProperty(_name, "Alerter.IRC.statCommand");
473 uptimeCommand = cp.getProperty(_name, "Alerter.IRC.uptimeCommand");
474 } catch (PropertyNotFoundException e) {
475 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
476 // lets bail from handling this line...
477 // ...it's gonna be hard without a command set!
478 return;
479 }
480
481 if(message.equalsIgnoreCase(stopCommand)) {
482 _active = false;
483 sendMessage(source, "alerts have been stopped");
484 }
485 else if(message.equalsIgnoreCase(startCommand)) {
486 _active = true;
487 sendMessage(source, "alerts have been activated");
488 }
489 // this needs to go before lastAlertCommand if it contains
490 // the same words as the lastAlertCommand.
491 else if(message.equalsIgnoreCase(timeSinceLastAlertCommand)) {
492 if(_lastAlertTime != -1) {
493 long uptime = (System.currentTimeMillis() - _lastAlertTime) / 1000;
494 String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
495 sendMessage(source, "I last sent an alert "+uptimeText+ " ago");
496 }
497 else {
498 sendMessage(source, "I've never sent an alert!");
499 }
500 }
501 else if(message.equalsIgnoreCase(lastAlertCommand)) {
502 if(_lastAlertTime != -1) {
503 String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date(_lastAlertTime));
504 sendMessage(source, "last alert was at "+date+"; "+_lastAlert);
505 }
506 else {
507 sendMessage(source, "I've never sent an alert!");
508 }
509
510 }
511 else if(message.toLowerCase().startsWith(joinCommand.toLowerCase())) {
512 String joinCmd = joinCommand;
513 String newChan = message.substring(message.indexOf(joinCmd) + joinCmd.length() + 1);
514 int endOfChan = newChan.indexOf(" ");
515 if(endOfChan == -1) {
516 endOfChan = newChan.length();
517 }
518 newChan = newChan.substring(0, endOfChan);
519 if(newChan.equals(_channel)) {
520 sendMessage(source, "I'm already on "+newChan+"!");
521 } else {
522 partChannel(_channel);
523 joinChannel(newChan);
524 _channel = newChan;
525 }
526 }
527 else if(message.toLowerCase().startsWith(nickChangeCommand.toLowerCase())) {
528 String nickChangeCmd = nickChangeCommand;
529 String newNick = message.substring(message.indexOf(nickChangeCmd) + nickChangeCmd.length() + 1);
530 int endOfNick = newNick.indexOf(" ");
531 if(endOfNick == -1) {
532 endOfNick = newNick.length();
533 }
534 newNick = newNick.substring(0, endOfNick);
535 changeNick(newNick);
536 }
537 else if(message.equalsIgnoreCase(versionCommand)) {
538 sendMessage(source, "I am version " + getRevision() + " of the i-scream alerting bot");
539 }
540 else if(message.equalsIgnoreCase(helpCommand)) {
541 sendMessage(source, "Hello, I am the i-scream alerting bot version "+REVISION.substring(11, REVISION.length() -2));
542 sendMessage(source, "I understand the following commands;");
543 sendMessage(source, stopCommand);
544 sendMessage(source, startCommand);
545 sendMessage(source, lastAlertCommand);
546 sendMessage(source, joinCommand);
547 sendMessage(source, nickChangeCommand);
548 sendMessage(source, statCommand);
549 sendMessage(source, uptimeCommand);
550 sendMessage(source, timeSinceLastAlertCommand);
551 sendMessage(source, helpCommand);
552 }
553 else if(message.equalsIgnoreCase(statCommand)) {
554 sendMessage(source, "I have sent a total of "+_alertCount+" alerts, and ignored a total of "+_ignoredCount+"!");
555 }
556 else if(message.equalsIgnoreCase(uptimeCommand)) {
557 long uptime = (System.currentTimeMillis() - _startTime) / 1000;
558 String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
559 sendMessage(source, "I have been running for "+uptimeText);
560 }
561 else if(message.equalsIgnoreCase("ping")) {
562 sendMessage(source, "pong");
563 }
564 else if(message.equalsIgnoreCase("do a jibble dance")) {
565 // little joke :)
566 sendAction(source, "jives to the funky beat shouting \"ii--screeeaaammm\"");
567 }
568 else {
569 String rejectMessage = NOT_CONFIGURED;
570 try {
571 rejectMessage = cp.getProperty(_name, "Alerter.IRC.rejectMessage");
572 } catch(PropertyNotFoundException e) {
573 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
574 }
575 sendMessage(source, rejectMessage);
576 }
577 }
578
579 /**
580 * Quick method to check if the message appears
581 * to be directed at us. We simply check to see
582 * if it starts with our nick in some fashion.
583 * This will return null if the message is not
584 * for us - thus allowing this method to perform
585 * two tasks at once.
586 *
587 * @param message the message to check
588 * @return the message (with our nick removed), or null if it's not for us.
589 */
590 private String isForMe(String message) {
591 // change to lower case to remove
592 // case ambiguities
593 String nick = _nickname.toLowerCase();
594 String msg = message.toLowerCase();
595 if(msg.startsWith(nick + ", ") ||
596 msg.startsWith(nick + ": ") ||
597 msg.startsWith(nick + " ")) {
598 // NOTE that the 1 here is hardcoded to be the length
599 // of the above 'extra' character (eg , or :)
600 return message.substring(nick.length()+1).trim();
601 }
602 else {
603 return null;
604 }
605 }
606
607 /**
608 * Sleep for a configurable amount of time and
609 * then return. This is used when reconnecting
610 * to the server or a channel.
611 */
612 private void reconnectSleep() {
613 int delayTime = 0;
614 try {
615 delayTime = Integer.parseInt(ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.reconnectDelay"));
616 } catch (NumberFormatException e) {
617 delayTime = DEFAULT_RECONNECT_DELAY;
618 _logger.write(this.toString(), Logger.WARNING, "Erronous Alerter.IRC.reconnectDelay value in configuration using default of " + delayTime + " seconds");
619 } catch (PropertyNotFoundException e) {
620 delayTime = DEFAULT_RECONNECT_DELAY;
621 _logger.write(this.toString(), Logger.WARNING, "Alerter.IRC.reconnectDelay value unavailable using default of " + delayTime + " seconds");
622 }
623 _logger.write(this.toString(), Logger.ERROR, "Waiting "+delayTime+" seconds for reconnect...");
624 try {
625 Thread.sleep(delayTime * 1000);
626 } catch (InterruptedException e) {}
627 }
628
629 /**
630 * Returns the revision of the bot.
631 *
632 * @return the revision of this bot
633 */
634 private String getRevision() {
635 return REVISION.substring(11, REVISION.length()-2);
636 }
637
638 /**
639 * Overrides the {@link java.lang.Object#toString() Object.toString()}
640 * method to provide clean logging (every class should have this).
641 *
642 * This uses the uk.org.iscream.cms.server.util.NameFormat class
643 * to format the toString()
644 *
645 * @return the name of this class and its CVS revision
646 */
647 public String toString() {
648 return FormatName.getName(
649 _name,
650 getClass().getName(),
651 REVISION);
652 }
653
654 /**
655 * Just a reminder to what channel we're on...
656 * this can't be dynamic :)
657 */
658 private String _channel;
659
660 /**
661 * A reminder of our current nickname...
662 */
663 private String _nickname;
664
665 }
666
667 }