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.4
Committed: Tue Feb 5 21:40:03 2002 UTC (22 years, 3 months ago) by tdb
Branch: SERVER_PIRCBOT
Changes since 1.30.2.3: +44 -26 lines
Log Message:
Improved the parsing of user input. Now will only match the exact String
for the command - ignoring case sensitivity. Also tweaked the use of the
config to stop the bot failing when some non-essential things can't be got
from the config.

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.3 2002/02/05 18:00:15 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.3 $";
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(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 }
414 }
415
416 /**
417 * If we receive a kick message this method
418 * gets called. We only take notice if it's
419 * us getting kicked, and then we'll rejoin
420 * the channel after a short wait.
421 *
422 * @param channel the channel the kick happened on
423 * @param kickerNick the person who performed the kick
424 * @param kickerLogin the login of the person who performed the kick
425 * @param kickerHostname the hostname of the person who performed the kick
426 * @param recipientNick the nickname of the person being kicked
427 * @param reason the reason for the kick
428 */
429 public void onKick(String channel, String kickerNick, String kickerLogin, String kickerHostname, String recipientNick, String reason) {
430 if(recipientNick.equals(_nickname) && channel.equals(_channel)) {
431 // remind the person it's not very nice :)
432 sendMessage(kickerNick, "That wasn't a nice thing to do...");
433 // get hold of the channel details, in case they've changed
434 try {
435 _channel = ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.channel");
436 } catch(PropertyNotFoundException e) {
437 _logger.write(this.toString(), Logger.ERROR, "Can't get channel name due to configuration error: "+e);
438 }
439 // wait for a while, defined in the config
440 reconnectSleep();
441 // we'll try and rejoin the channel regardless
442 // otherwise we might end up doing nothing!
443 joinChannel(_channel);
444 }
445 }
446
447 /**
448 * This method handles input directed to us, and
449 * responds accordingly if required.
450 *
451 * nb. matching user input is quite bad right now,
452 * and should be improved one day.
453 *
454 * @param message the message from someone
455 * @param source where the message came from, so we know where to send the response
456 */
457 private void handleInput(String message, String source) {
458 // get hold of the configuration system
459 ConfigurationProxy cp = ConfigurationProxy.getInstance();
460 // setup some String's
461 String stopCommand, startCommand, timeSinceLastAlertCommand, lastAlertCommand, joinCommand;
462 String nickChangeCommand, versionCommand, helpCommand, statCommand, uptimeCommand;
463 // get the command set from the configuration
464 try {
465 stopCommand = cp.getProperty(_name, "Alerter.IRC.stopCommand");
466 startCommand = cp.getProperty(_name, "Alerter.IRC.startCommand");
467 timeSinceLastAlertCommand = cp.getProperty(_name, "Alerter.IRC.timeSinceLastAlertCommand");
468 lastAlertCommand = cp.getProperty(_name, "Alerter.IRC.lastAlertCommand");
469 joinCommand = cp.getProperty(_name, "Alerter.IRC.joinCommand");
470 nickChangeCommand = cp.getProperty(_name, "Alerter.IRC.nickChangeCommand");
471 versionCommand = cp.getProperty(_name, "Alerter.IRC.versionCommand");
472 helpCommand = cp.getProperty(_name, "Alerter.IRC.helpCommand");
473 statCommand = cp.getProperty(_name, "Alerter.IRC.statCommand");
474 uptimeCommand = cp.getProperty(_name, "Alerter.IRC.uptimeCommand");
475 } catch (PropertyNotFoundException e) {
476 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
477 // lets bail from handling this line...
478 // ...it's gonna be hard without a command set!
479 return;
480 }
481
482 // see if the message matches (loosely!) any
483 // of our known commands
484 if(message.equalsIgnoreCase(stopCommand)) {
485 _active = false;
486 sendMessage(source, "alerts have been stopped");
487 }
488 else if(message.equalsIgnoreCase(startCommand)) {
489 _active = true;
490 sendMessage(source, "alerts have been activated");
491 }
492 // this needs to go before lastAlertCommand if it contains
493 // the same words as the lastAlertCommand.
494 else if(message.equalsIgnoreCase(timeSinceLastAlertCommand)) {
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 sendMessage(source, "I last sent an alert "+uptimeText+ " ago");
499 }
500 else {
501 sendMessage(source, "I've never sent an alert!");
502 }
503 }
504 else if(message.equalsIgnoreCase(lastAlertCommand)) {
505 if(_lastAlertTime != -1) {
506 String date = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT, Locale.UK).format(new Date(_lastAlertTime));
507 sendMessage(source, "last alert was at "+date+"; "+_lastAlert);
508 }
509 else {
510 sendMessage(source, "I've never sent an alert!");
511 }
512
513 }
514 else if(message.equalsIgnoreCase(joinCommand)) {
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 if(newChan.equals(_channel)) {
523 sendMessage(source, "I'm already on "+newChan+"!");
524 } else {
525 partChannel(_channel);
526 joinChannel(newChan);
527 _channel = newChan;
528 }
529 }
530 else if(message.equalsIgnoreCase(nickChangeCommand)) {
531 String nickChangeCmd = nickChangeCommand;
532 String newNick = message.substring(message.indexOf(nickChangeCmd) + nickChangeCmd.length() + 1);
533 int endOfNick = newNick.indexOf(" ");
534 if(endOfNick == -1) {
535 endOfNick = newNick.length();
536 }
537 newNick = newNick.substring(0, endOfNick);
538 changeNick(newNick);
539 }
540 else if(message.equalsIgnoreCase(versionCommand)) {
541 sendMessage(source, "I am version "+REVISION.substring(11, REVISION.length()-2)+" of the i-scream alerting bot");
542 }
543 else if(message.equalsIgnoreCase(helpCommand)) {
544 sendMessage(source, "Hello, I am the i-scream alerting bot version "+REVISION.substring(11, REVISION.length() -2));
545 sendMessage(source, "I understand the following commands;");
546 sendMessage(source, stopCommand);
547 sendMessage(source, startCommand);
548 sendMessage(source, lastAlertCommand);
549 sendMessage(source, joinCommand);
550 sendMessage(source, nickChangeCommand);
551 sendMessage(source, statCommand);
552 sendMessage(source, uptimeCommand);
553 sendMessage(source, timeSinceLastAlertCommand);
554 sendMessage(source, helpCommand);
555 }
556 else if(message.equalsIgnoreCase(statCommand)) {
557 sendMessage(source, "I have sent a total of "+_alertCount+" alerts, and ignored a total of "+_ignoredCount+"!");
558 }
559 else if(message.equalsIgnoreCase(uptimeCommand)) {
560 long uptime = (System.currentTimeMillis() - _startTime) / 1000;
561 String uptimeText = DateUtils.formatTime(uptime, "%DAYS% days, %HOURS% hours, %MINS% mins, and %SECS% secs");
562 sendMessage(source, "I have been running for "+uptimeText);
563 }
564 else if(message.equalsIgnoreCase("ping")) {
565 sendMessage(source, "pong");
566 }
567 else if(message.equalsIgnoreCase("do a jibble dance")) {
568 // little joke :)
569 sendAction(source, "jives to the funky beat shouting \"ii--screeeaaammm\"");
570 }
571 else {
572 String rejectMessage = NOT_CONFIGURED;
573 try {
574 rejectMessage = cp.getProperty(_name, "Alerter.IRC.rejectMessage");
575 } catch(PropertyNotFoundException e) {
576 _logger.write(this.toString(), Logger.ERROR, "Configuration error: "+e);
577 }
578 sendMessage(source, rejectMessage);
579 }
580 }
581
582 /**
583 * Quick method to check if the message appears
584 * to be directed at us. We simply check to see
585 * if it starts with our nick in some fashion.
586 * This will return null if the message is not
587 * for us - thus allowing this method to perform
588 * two tasks at once.
589 *
590 * @param message the message to check
591 * @return the message (with our nick removed), or null if it's not for us.
592 */
593 private String isForMe(String message) {
594 // change to lower case to remove
595 // case ambiguities
596 String nick = _nickname.toLowerCase();
597 String msg = message.toLowerCase();
598 if(msg.startsWith(nick + ", ") ||
599 msg.startsWith(nick + ": ") ||
600 msg.startsWith(nick + " ")) {
601 // NOTE that the 1 here is hardcoded to be the length
602 // of the above 'extra' character (eg , or :)
603 return message.substring(nick.length()+1).trim();
604 }
605 else {
606 return null;
607 }
608 }
609
610 /**
611 * Sleep for a configurable amount of time and
612 * then return. This is used when reconnecting
613 * to the server or a channel.
614 */
615 private void reconnectSleep() {
616 int delayTime = 0;
617 try {
618 delayTime = Integer.parseInt(ConfigurationProxy.getInstance().getProperty(_name, "Alerter.IRC.reconnectDelay"));
619 } catch (NumberFormatException e) {
620 delayTime = DEFAULT_RECONNECT_DELAY;
621 _logger.write(this.toString(), Logger.WARNING, "Erronous Alerter.IRC.reconnectDelay value in configuration using default of " + delayTime + " seconds");
622 } catch (PropertyNotFoundException e) {
623 delayTime = DEFAULT_RECONNECT_DELAY;
624 _logger.write(this.toString(), Logger.WARNING, "Alerter.IRC.reconnectDelay value unavailable using default of " + delayTime + " seconds");
625 }
626 _logger.write(this.toString(), Logger.ERROR, "Waiting "+delayTime+" seconds for reconnect...");
627 try {
628 Thread.sleep(delayTime * 1000);
629 } catch (InterruptedException e) {}
630 }
631
632 /**
633 * Overrides the {@link java.lang.Object#toString() Object.toString()}
634 * method to provide clean logging (every class should have this).
635 *
636 * This uses the uk.org.iscream.cms.server.util.NameFormat class
637 * to format the toString()
638 *
639 * @return the name of this class and its CVS revision
640 */
641 public String toString() {
642 return FormatName.getName(
643 _name,
644 getClass().getName(),
645 REVISION);
646 }
647
648 /**
649 * Just a reminder to what channel we're on...
650 * this can't be dynamic :)
651 */
652 private String _channel;
653
654 /**
655 * A reminder of our current nickname...
656 */
657 private String _nickname;
658
659 }
660
661 }