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.30.2.3
Committed: Tue Feb 5 18:00:15 2002 UTC (22 years, 3 months ago) by tdb
Branch: SERVER_PIRCBOT
Changes since 1.30.2.2: +146 -37 lines
Log Message:
Mostly tidying of code and commenting. Now much more complete, although
there are still some issues with the underlying pircbot (sometimes hangs
when the help command is sent, for example).

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.2 2002/02/04 23:14:38 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.2 $";
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 public 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, comment, finger, nickList;
249 try {
250 user = cp.getProperty(_name, "Alerter.IRC.user");
251 comment = cp.getProperty(_name, "Alerter.IRC.comment");
252 finger = cp.getProperty(_name, "Alerter.IRC.finger");
253 nickList = cp.getProperty(_name, "Alerter.IRC.nickList");
254 } catch (PropertyNotFoundException e) {
255 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
256 throw new IOException("Can't get user/nickname details due to configuration error");
257 }
258
259 // put these details into the bot
260 this.setLogin(user);
261 this.setVersion(comment);
262 this.setFinger(finger);
263
264 // attempt to connect, trying each nickname
265 // in turn until sucessfully connected
266 StringTokenizer st = new StringTokenizer(nickList, ";");
267 boolean ok = false;
268 while(!ok && st.hasMoreTokens()) {
269 String nick = st.nextToken();
270 try {
271 // try to connect with a nickname
272 _logger.write(this.toString(), Logger.DEBUG, "Trying nick: "+nick);
273 this.setName(nick);
274 this.connect(server, port);
275 // at this point we know the nickname was accepted
276 _nickname = nick;
277 ok = true;
278 }
279 catch(IOException e) {
280 _logger.write(this.toString(), Logger.ERROR, "IO error when connecting to server: "+e);
281 throw new IOException("IO error when connecting to server");
282 }
283 catch(IrcException e) {
284 _logger.write(this.toString(), Logger.ERROR, "IRC error when connecting to server: "+e);
285 throw new IOException("IRC error when connecting to server");
286 }
287 catch(NickAlreadyInUseException e) {
288 _logger.write(this.toString(), Logger.ERROR, "Nickname "+nick+" is already in use: "+e);
289 // don't do anything, instead just loop round
290 // and try the next nickname in the list
291 }
292 }
293 if(!ok) {
294 // must have tried all the nicknames, best bail out
295 _logger.write(this.toString(), Logger.ERROR, "All nicknames already in use");
296 throw new IOException("All nicknames already in use");
297 }
298
299 // get hold of the channel details, and attempt
300 // to join the specified channel
301 try {
302 _channel = cp.getProperty(_name, "Alerter.IRC.channel");
303 this.joinChannel(_channel);
304 } catch (PropertyNotFoundException e) {
305 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
306 throw new IOException("Can't get channel name due to configuration error");
307 }
308
309 // get hold of the startup notice, and send
310 // it if it's defined
311 String startupNotice;
312 try {
313 startupNotice = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.startupNotice");
314 sendNotice(_channel, startupNotice);
315 } catch (PropertyNotFoundException e) {
316 _logger.write(this.toString(), Logger.DEBUG, "Startup notice not defined, so not sending: "+e);
317 }
318
319 // at this point initialisation is complete, so
320 // we can return and set this flag to allow alerts
321 _active = true;
322 }
323
324 /**
325 * Send a message to the current channel.
326 *
327 * @param message The message to send
328 */
329 public void sendMessage(String message) {
330 sendMessage(_channel, message);
331 }
332
333 /**
334 * If we get disconnected this method will be
335 * called, so we must take action. We'll simply
336 * keep trying to reinitialise, and thus
337 * reconnect to the server.
338 */
339 public void onDisconnect() {
340 // stop alerts being sent for now
341 _active = false;
342 while(true) {
343 // wait for a while, defined in the config
344 reconnectSleep();
345 try {
346 init();
347 break;
348 }
349 catch (IOException e) {
350 _logger.write(this.toString(), Logger.ERROR, "Error initialising IRCBot: "+e);
351 }
352 }
353 }
354
355 /**
356 * If we receive a message this method will
357 * be called. We'll check if the message is
358 * for us, and call handleInput if it is.
359 *
360 * @param channel The channel the message came from
361 * @param sender The sender of the message
362 * @param login The login of the sender
363 * @param hostname The hostname of the sender
364 * @param message The message sent
365 */
366 public void onMessage(String channel, String sender, String login, String hostname, String message) {
367 if(isForMe(message)) {
368 handleInput(message, channel);
369 }
370 }
371
372 /**
373 * If we receive a private message this method
374 * will be called. No need to check if it's for
375 * us -- it has to be if it's a private message.
376 *
377 * @param sender The sender of the message
378 * @param login The login of the sender
379 * @param hostname The hostname of the sender
380 * @param message The message sent
381 */
382 public void onPrivateMessage(String sender, String login, String hostname, String message) {
383 handleInput(message, sender);
384 }
385
386 /**
387 * If we receive a nick change message, this
388 * method gets called. We don't care about
389 * other users changing their nick, so we only
390 * take notice if our nick changes. If it does
391 * we need to change our internal nickname
392 * state.
393 *
394 * @param oldNick the old nickname
395 * @param login the login of the nick changer
396 * @param hostname the hostname of the nick changer
397 * @param newNick the new nickname
398 */
399 public void onNickChange(String oldNick, String login, String hostname, String newNick) {
400 if(oldNick.equals(_nickname)) {
401 _nickname = newNick;
402 }
403 }
404
405 /**
406 * If we receive a kick message this method
407 * gets called. We only take notice if it's
408 * us getting kicked, and then we'll rejoin
409 * the channel after a short wait.
410 *
411 * @param channel the channel the kick happened on
412 * @param kickerNick the person who performed the kick
413 * @param kickerLogin the login of the person who performed the kick
414 * @param kickerHostname the hostname of the person who performed the kick
415 * @param recipientNick the nickname of the person being kicked
416 * @param reason the reason for the kick
417 */
418 public void onKick(String channel, String kickerNick, String kickerLogin, String kickerHostname, String recipientNick, String reason) {
419 if(recipientNick.equals(_nickname) && channel.equals(_channel)) {
420 // remind the person it's not very nice :)
421 sendMessage(kickerNick, "That wasn't a nice thing to do...");
422 // get hold of the channel details, in case they've changed
423 try {
424 _channel = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.channel");
425 } catch(PropertyNotFoundException e) {
426 _logger.write(this.toString(), Logger.ERROR, "Can't get channel name due to configuration error: "+e);
427 }
428 // wait for a while, defined in the config
429 reconnectSleep();
430 // we'll try and rejoin the channel regardless
431 // otherwise we might end up doing nothing!
432 joinChannel(_channel);
433 }
434 }
435
436 /**
437 * This method handles input directed to us, and
438 * responds accordingly if required.
439 *
440 * nb. matching user input is quite bad right now,
441 * and should be improved one day.
442 *
443 * @param message the message from someone
444 * @param source where the message came from, so we know where to send the response
445 */
446 private void handleInput(String message, String source) {
447 // get hold of the configuration system
448 ConfigurationProxy cp = ConfigurationProxy.getInstance();
449 // setup some String's
450 String stopCommand, startCommand, timeSinceLastAlertCommand, lastAlertCommand, joinCommand;
451 String nickChangeCommand, versionCommand, helpCommand, statCommand, uptimeCommand;
452 // get the command set from the configuration
453 try {
454 stopCommand = cp.getProperty(_name, "Alerter.IRC.stopCommand");
455 startCommand = cp.getProperty(_name, "Alerter.IRC.startCommand");
456 timeSinceLastAlertCommand = cp.getProperty(_name, "Alerter.IRC.timeSinceLastAlertCommand");
457 lastAlertCommand = cp.getProperty(_name, "Alerter.IRC.lastAlertCommand");
458 joinCommand = cp.getProperty(_name, "Alerter.IRC.joinCommand");
459 nickChangeCommand = cp.getProperty(_name, "Alerter.IRC.nickChangeCommand");
460 versionCommand = cp.getProperty(_name, "Alerter.IRC.versionCommand");
461 helpCommand = cp.getProperty(_name, "Alerter.IRC.helpCommand");
462 statCommand = cp.getProperty(_name, "Alerter.IRC.statCommand");
463 uptimeCommand = cp.getProperty(_name, "Alerter.IRC.uptimeCommand");
464 } catch (PropertyNotFoundException e) {
465 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
466 // lets bail from handling this line...
467 // ...it's gonna be hard without a command set!
468 return;
469 }
470
471 // see if the message matches (loosely!) any
472 // of our known commands
473 if(message.indexOf(stopCommand) != -1) {
474 _active = false;
475 sendMessage(source, "alerts have been stopped");
476 }
477 else if(message.indexOf(startCommand) != -1) {
478 _active = true;
479 sendMessage(source, "alerts have been activated");
480 }
481 // this needs to go before lastAlertCommand if it contains
482 // the same words as the lastAlertCommand.
483 else if(message.indexOf(timeSinceLastAlertCommand) != -1) {
484 if(_lastAlertTime != -1) {
485 long uptime = (System.currentTimeMillis() - _lastAlertTime) / 1000;
486 String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
487 sendMessage(source, "I last sent an alert "+uptimeText+ " ago");
488 }
489 else {
490 sendMessage(source, "I've never sent an alert!");
491 }
492 }
493 else if(message.indexOf(lastAlertCommand) != -1) {
494 if(_lastAlertTime != -1) {
495 String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date(_lastAlertTime));
496 sendMessage(source, "last alert was at "+date+"; "+_lastAlert);
497 }
498 else {
499 sendMessage(source, "I've never sent an alert!");
500 }
501
502 }
503 else if(message.indexOf(joinCommand) != -1) {
504 String joinCmd = joinCommand;
505 String newChan = message.substring(message.indexOf(joinCmd) + joinCmd.length() + 1);
506 int endOfChan = newChan.indexOf(" ");
507 if(endOfChan == -1) {
508 endOfChan = newChan.length();
509 }
510 newChan = newChan.substring(0, endOfChan);
511 if(newChan.equals(_channel)) {
512 sendMessage(source, "I'm already on "+newChan+"!");
513 } else {
514 partChannel(_channel);
515 joinChannel(newChan);
516 _channel = newChan;
517 }
518 }
519 else if(message.indexOf(nickChangeCommand) != -1) {
520 String nickChangeCmd = nickChangeCommand;
521 String newNick = message.substring(message.indexOf(nickChangeCmd) + nickChangeCmd.length() + 1);
522 int endOfNick = newNick.indexOf(" ");
523 if(endOfNick == -1) {
524 endOfNick = newNick.length();
525 }
526 newNick = newNick.substring(0, endOfNick);
527 changeNick(newNick);
528 }
529 else if(message.indexOf(versionCommand) != -1) {
530 sendMessage(source, "I am version "+REVISION.substring(11, REVISION.length() -2)+" of the i-scream alerting bot");
531 }
532 else if(message.indexOf(helpCommand) != -1) {
533 sendMessage(source, "Hello, I am the i-scream alerting bot version "+REVISION.substring(11, REVISION.length() -2));
534 sendMessage(source, "I understand the following commands;");
535 sendMessage(source, stopCommand);
536 sendMessage(source, startCommand);
537 sendMessage(source, lastAlertCommand);
538 sendMessage(source, joinCommand);
539 sendMessage(source, nickChangeCommand);
540 sendMessage(source, statCommand);
541 sendMessage(source, uptimeCommand);
542 sendMessage(source, timeSinceLastAlertCommand);
543 sendMessage(source, helpCommand);
544 }
545 else if(message.indexOf(statCommand) != -1) {
546 sendMessage(source, "I have sent a total of "+_alertCount+" alerts, and ignored a total of "+_ignoredCount+"!");
547 }
548 else if(message.indexOf(uptimeCommand) != -1) {
549 long uptime = (System.currentTimeMillis() - _startTime) / 1000;
550 String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
551 sendMessage(source, "I have been running for "+uptimeText);
552 }
553 else if(message.indexOf("ping") != -1) {
554 sendMessage(source, "pong");
555 }
556 else if(message.indexOf("do a jibble dance") != -1) {
557 // little joke :)
558 sendAction(source, "jives to the funky beat shouting \"ii--screeeaaammm\"");
559 }
560 else {
561 String rejectMessage = NOT_CONFIGURED;
562 try {
563 rejectMessage = cp.getProperty(_name, "Alerter.IRC.rejectMessage");
564 } catch(PropertyNotFoundException e) {
565 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
566 }
567 sendMessage(source, rejectMessage);
568 }
569 }
570
571 /**
572 * Quick method to check if the message appears
573 * to be directed at us. We simply check to see
574 * if it starts with our nick in some fashion.
575 *
576 * @param message the message to check
577 * @return if the message is for us
578 */
579 private boolean isForMe(String message) {
580 String nick = _nickname.toLowerCase();
581 String msg = message.toLowerCase();
582 if(msg.startsWith(nick + ", ") ||
583 msg.startsWith(nick + ": ") ||
584 msg.startsWith(nick + " ")) {
585 return true;
586 }
587 else {
588 return false;
589 }
590 }
591
592 /**
593 * Sleep for a configurable amount of time and
594 * then return. This is used when reconnecting
595 * to the server or a channel.
596 */
597 private void reconnectSleep() {
598 int delayTime = 0;
599 try {
600 delayTime = Integer.parseInt(ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.reconnectDelay"));
601 } catch (NumberFormatException e) {
602 delayTime = DEFAULT_RECONNECT_DELAY;
603 _logger.write(this.toString(), Logger.WARNING, "Erronous Alerter.IRC.reconnectDelay value in configuration using default of " + delayTime + " seconds");
604 } catch (PropertyNotFoundException e) {
605 delayTime = DEFAULT_RECONNECT_DELAY;
606 _logger.write(this.toString(), Logger.WARNING, "Alerter.IRC.reconnectDelay value unavailable using default of " + delayTime + " seconds");
607 }
608 _logger.write(this.toString(), Logger.ERROR, "Waiting "+delayTime+" seconds for reconnect...");
609 try {
610 Thread.sleep(delayTime * 1000);
611 } catch (InterruptedException e) {}
612 }
613
614 /**
615 * Overrides the {@link java.lang.Object#toString() Object.toString()}
616 * method to provide clean logging (every class should have this).
617 *
618 * This uses the uk.org.iscream.cms.server.util.NameFormat class
619 * to format the toString()
620 *
621 * @return the name of this class and its CVS revision
622 */
623 public String toString() {
624 return FormatName.getName(
625 _name,
626 getClass().getName(),
627 REVISION);
628 }
629
630 /**
631 * Just a reminder to what channel we're on...
632 * this can't be dynamic :)
633 */
634 private String _channel;
635
636 /**
637 * A reminder of our current nickname...
638 */
639 private String _nickname;
640
641 }
642
643 }