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.32
Committed: Sat May 18 18:16:00 2002 UTC (22 years ago) by tdb
Branch: MAIN
Changes since 1.31: +21 -2 lines
Log Message:
i-scream is now licensed under the GPL. I've added the GPL headers to every
source file, and put a full copy of the license in the appropriate places.
I think I've covered everything. This is going to be a mad commit ;)

File Contents

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