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.36
Committed: Sun Aug 1 10:40:43 2004 UTC (19 years, 9 months ago) by tdb
Branch: MAIN
CVS Tags: HEAD
Changes since 1.35: +3 -3 lines
Log Message:
Catch a lot of old URL's and update them. Also remove a couple of old files
that aren't used.

File Contents

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