]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - rbot/ircbot.rb
initial import of rbot
[user/henk/code/ruby/rbot.git] / rbot / ircbot.rb
1 # Copyright (C) 2002 Tom Gilbert.
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining a copy
4 # of this software and associated documentation files (the "Software"), to
5 # deal in the Software without restriction, including without limitation the
6 # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7 # sell copies of the Software, and to permit persons to whom the Software is
8 # furnished to do so, subject to the following conditions:
9 #
10 # The above copyright notice and this permission notice shall be included in
11 # all copies of the Software and its documentation and acknowledgment shall be
12 # given in the documentation and software packages that this Software was
13 # used.
14 #
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 # THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22 require 'thread'
23
24 require 'rbot/rfc2812'
25 require 'rbot/keywords'
26 require 'rbot/config'
27 require 'rbot/ircsocket'
28 require 'rbot/auth'
29 require 'rbot/timer'
30 require 'rbot/plugins'
31 require 'rbot/channel'
32 require 'rbot/utils'
33 require 'rbot/message'
34 require 'rbot/language'
35 require 'rbot/dbhash'
36 require 'rbot/registry'
37
38 module Irc
39
40 # Main bot class, which receives messages, handles them or passes them to
41 # plugins, and stores runtime data
42 class IrcBot
43   # the bot's current nickname
44   attr_reader :nick
45   
46   # the bot's IrcAuth data
47   attr_reader :auth
48   
49   # the bot's BotConfig data
50   attr_reader :config
51   
52   # the botclass for this bot (determines configdir among other things)
53   attr_reader :botclass
54   
55   # used to perform actions periodically (saves configuration once per minute
56   # by default)
57   attr_reader :timer
58   
59   # bot's Language data
60   attr_reader :lang
61
62   # bot's configured addressing prefixes
63   attr_reader :addressing_prefixes
64
65   # channel info for channels the bot is in
66   attr_reader :channels
67
68   # bot's object registry, plugins get an interface to this for persistant
69   # storage (hash interface tied to a bdb file, plugins use Accessors to store
70   # and restore objects in their own namespaces.)
71   attr_reader :registry
72   
73   # create a new IrcBot with botclass +botclass+
74   def initialize(botclass)
75     @botclass = botclass.gsub(/\/$/, "")
76     @startup_time = Time.new
77     
78     Dir.mkdir("#{botclass}") if(!File.exist?("#{botclass}"))
79     Dir.mkdir("#{botclass}/logs") if(!File.exist?("#{botclass}/logs"))
80
81     @config = Irc::BotConfig.new(self)
82     @timer = Timer::Timer.new
83     @registry = BotRegistry.new self
84     @timer.add(@config["SAVE_EVERY"].to_i) { save }
85     @channels = Hash.new
86     @logs = Hash.new
87     
88     @lang = Irc::Language.new(@config["LANGUAGE"])
89     @keywords = Irc::Keywords.new(self)
90     @auth = Irc::IrcAuth.new(self)
91     @plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"])
92     @socket = Irc::IrcSocket.new(@config["SERVER"], @config["PORT"], @config["HOST"], @config["SENDQ_DELAY"], @config["SENDQ_BURST"])
93     @nick = @config["NICK"]
94     @server_password = @config["SERVER_PASSWORD"]
95     if @config["ADDRESS_PREFIX"]
96       @addressing_prefixes = @config["ADDRESS_PREFIX"].split(" ")
97     else
98       @addressing_prefixes = Array.new
99     end
100     
101     @client = Irc::IrcClient.new
102     @client["PRIVMSG"] = proc { |data|
103       message = PrivMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"])
104       onprivmsg(message)
105     }
106     @client["NOTICE"] = proc { |data|
107       message = NoticeMessage.new(self, data["SOURCE"], data["TARGET"], data["MESSAGE"])
108       # pass it off to plugins that want to hear everything
109       @plugins.delegate "listen", message
110     }
111     @client["MOTD"] = proc { |data|
112       data['MOTD'].each_line { |line|
113         log "MOTD: #{line}", "server"
114       }
115     }
116     @client["NICKTAKEN"] = proc { |data| 
117       nickchg "#{@nick}_"
118     }
119     @client["BADNICK"] = proc {|data| 
120       puts "WARNING, bad nick (#{data['NICK']})"
121     }
122     @client["PING"] = proc {|data|
123       # (jump the queue for pongs)
124       @socket.puts "PONG #{data['PINGID']}"
125     }
126     @client["NICK"] = proc {|data|
127       sourcenick = data["SOURCENICK"]
128       nick = data["NICK"]
129       m = NickMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["NICK"])
130       if(sourcenick == @nick)
131         @nick = nick
132       end
133       @channels.each {|k,v|
134         if(v.users.has_key?(sourcenick))
135           log "@ #{sourcenick} is now known as #{nick}", k
136           v.users[nick] = v.users[sourcenick]
137           v.users.delete(sourcenick)
138         end
139       }
140       @plugins.delegate("listen", m)
141       @plugins.delegate("nick", m)
142     }
143     @client["QUIT"] = proc {|data|
144       source = data["SOURCE"]
145       sourcenick = data["SOURCENICK"]
146       sourceurl = data["SOURCEADDRESS"]
147       message = data["MESSAGE"]
148       m = QuitMessage.new(self, data["SOURCE"], data["SOURCENICK"], data["MESSAGE"])
149       if(data["SOURCENICK"] =~ /#{@nick}/i)
150       else
151         @channels.each {|k,v|
152           if(v.users.has_key?(sourcenick))
153             log "@ Quit: #{sourcenick}: #{message}", k
154             v.users.delete(sourcenick)
155           end
156         }
157       end
158       @plugins.delegate("listen", m)
159       @plugins.delegate("quit", m)
160     }
161     @client["MODE"] = proc {|data|
162       source = data["SOURCE"]
163       sourcenick = data["SOURCENICK"]
164       sourceurl = data["SOURCEADDRESS"]
165       channel = data["CHANNEL"]
166       targets = data["TARGETS"]
167       modestring = data["MODESTRING"]
168       log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
169     }
170     @client["WELCOME"] = proc {|data|
171       log "joined server #{data['SOURCE']} as #{data['NICK']}", "server"
172       debug "I think my nick is #{@nick}, server thinks #{data['NICK']}"
173       if data['NICK'] && data['NICK'].length > 0
174         @nick = data['NICK']
175       end
176       if(@config["QUSER"])
177         puts "authing with Q using  #{@config["QUSER"]} #{@config["QAUTH"]}"
178         @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config["QUSER"]} #{@config["QAUTH"]}"
179       end
180
181       if(@config["JOIN_CHANNELS"])
182         @config["JOIN_CHANNELS"].split(", ").each {|c|
183           puts "autojoining channel #{c}"
184           if(c =~ /^(\S+)\s+(\S+)$/i)
185             join $1, $2
186           else
187             join c if(c)
188           end
189         }
190       end
191     }
192     @client["JOIN"] = proc {|data|
193       m = JoinMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"])
194       onjoin(m)
195     }
196     @client["PART"] = proc {|data|
197       m = PartMessage.new(self, data["SOURCE"], data["CHANNEL"], data["MESSAGE"])
198       onpart(m)
199     }
200     @client["KICK"] = proc {|data|
201       m = KickMessage.new(self, data["SOURCE"], data["TARGET"],data["CHANNEL"],data["MESSAGE"]) 
202       onkick(m)
203     }
204     @client["INVITE"] = proc {|data|
205       if(data["TARGET"] =~ /^#{@nick}$/i)
206         join data["CHANNEL"] if (@auth.allow?("join", data["SOURCE"], data["SOURCENICK"]))
207       end
208     }
209     @client["CHANGETOPIC"] = proc {|data|
210       channel = data["CHANNEL"]
211       sourcenick = data["SOURCENICK"]
212       topic = data["TOPIC"]
213       timestamp = data["UNIXTIME"] || Time.now.to_i
214       if(sourcenick == @nick)
215         log "@ I set topic \"#{topic}\"", channel
216       else
217         log "@ #{sourcenick} set topic \"#{topic}\"", channel
218       end
219       m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"])
220
221       ontopic(m)
222       @plugins.delegate("topic", m)
223       @plugins.delegate("listen", m)
224     }
225     @client["TOPIC"] = @client["TOPICINFO"] = proc {|data|
226       channel = data["CHANNEL"]
227       m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], data["UNIXTIME"], data["TOPIC"])
228         ontopic(m)
229     }
230     @client["NAMES"] = proc {|data|
231       channel = data["CHANNEL"]
232       users = data["USERS"]
233       unless(@channels[channel])
234         puts "bug: got names for channel '#{channel}' I didn't think I was in\n"
235         exit 2
236       end
237       @channels[channel].users.clear
238       users.each {|u|
239         @channels[channel].users[u[0]] = ["mode", u[1]]
240       }
241     }
242     @client["UNKNOWN"] = proc {|data|
243       debug "UNKNOWN: #{data['SERVERSTRING']}"
244     }
245   end
246
247   # connect the bot to IRC
248   def connect
249     trap("SIGTERM") { quit }
250     trap("SIGHUP") { quit }
251     trap("SIGINT") { quit }
252     begin
253       @socket.connect
254       rescue => e
255       raise "failed to connect to IRC server at #{@config['SERVER']} #{@config['PORT']}: " + e
256     end
257     @socket.puts "PASS " + @server_password if @server_password
258     @socket.puts "NICK #{@nick}\nUSER #{@config['USER']} 4 #{@config['SERVER']} :Ruby bot. (c) Tom Gilbert"
259   end
260
261   # begin event handling loop
262   def mainloop
263     socket_timeout = 0.2
264     reconnect_wait = 5
265     
266     while true
267       connect
268       
269       begin
270         while true
271           if @socket.select socket_timeout
272             break unless reply = @socket.gets
273             @client.process reply
274           end
275           @timer.tick
276         end
277       rescue => e
278         puts "connection closed: #{e}"
279         puts e.backtrace.join("\n")
280       end
281       
282       puts "disconnected"
283       @channels.clear
284       @socket.clearq
285       
286       puts "waiting to reconnect"
287       sleep reconnect_wait
288     end
289   end
290   
291   # type:: message type
292   # where:: message target
293   # message:: message text
294   # send message +message+ of type +type+ to target +where+
295   # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
296   # relevant say() or notice() methods. This one should be used for IRCd
297   # extensions you want to use in modules.
298   def sendmsg(type, where, message)
299     # limit it 440 chars + CRLF.. so we have to split long lines
300     left = 440 - type.length - where.length - 3
301     begin
302       if(left >= message.length)
303         sendq("#{type} #{where} :#{message}")
304         log_sent(type, where, message)
305         return
306       end
307       line = message.slice!(0, left)
308       lastspace = line.rindex(/\s+/)
309       if(lastspace)
310         message = line.slice!(lastspace, line.length) + message
311         message.gsub!(/^\s+/, "")
312       end
313       sendq("#{type} #{where} :#{line}")
314       log_sent(type, where, line)
315     end while(message.length > 0)
316   end
317
318   def sendq(message="")
319     # temporary
320     @socket.queue(message)
321   end
322
323   # send a notice message to channel/nick +where+
324   def notice(where, message)
325     message.each_line { |line|
326       line.chomp!
327       next unless(line.length > 0)
328       sendmsg("NOTICE", where, line)
329     }
330   end
331
332   # say something (PRIVMSG) to channel/nick +where+
333   def say(where, message)
334     message.to_s.each_line { |line|
335       line.chomp!
336       next unless(line.length > 0)
337       unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
338         sendmsg("PRIVMSG", where, line)
339       end
340     }
341   end
342
343   # perform a CTCP action with message +message+ to channel/nick +where+
344   def action(where, message)
345     sendq("PRIVMSG #{where} :\001ACTION #{message}\001")
346     if(where =~ /^#/)
347       log "* #{@nick} #{message}", where
348     elsif (where =~ /^(\S*)!.*$/)
349          log "* #{@nick}[#{where}] #{message}", $1
350     else
351          log "* #{@nick}[#{where}] #{message}", where
352     end
353   end
354
355   # quick way to say "okay" (or equivalent) to +where+
356   def okay(where)
357     say where, @lang.get("okay")
358   end
359
360   # log message +message+ to a file determined by +where+. +where+ can be a
361   # channel name, or a nick for private message logging
362   def log(message, where="server")
363     message.chomp!
364     stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
365     unless(@logs.has_key?(where))
366       @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
367       @logs[where].sync = true
368     end
369     @logs[where].puts "[#{stamp}] #{message}"
370     #debug "[#{stamp}] <#{where}> #{message}"
371   end
372   
373   # set topic of channel +where+ to +topic+
374   def topic(where, topic)
375     sendq "TOPIC #{where} :#{topic}"
376   end
377   
378   # message:: optional IRC quit message
379   # quit IRC, shutdown the bot
380   def quit(message=nil)
381     trap("SIGTERM", "DEFAULT")
382     trap("SIGHUP", "DEFAULT")
383     trap("SIGINT", "DEFAULT")
384     message = @lang.get("quit") if (!message || message.length < 1)
385     @socket.clearq
386     save
387     @plugins.cleanup
388     @channels.each_value {|v|
389       log "@ quit (#{message})", v.name
390     }
391     @socket.puts "QUIT :#{message}"
392     @socket.flush
393     @socket.shutdown
394     @registry.close
395     puts "rbot quit (#{message})"
396     exit 0
397   end
398
399   # call the save method for bot's config, keywords, auth and all plugins
400   def save
401     @registry.flush
402     @config.save
403     @keywords.save
404     @auth.save
405     @plugins.save
406   end
407
408   # call the rescan method for the bot's lang, keywords and all plugins
409   def rescan
410     @lang.rescan
411     @plugins.rescan
412     @keywords.rescan
413   end
414   
415   # channel:: channel to join
416   # key::     optional channel key if channel is +s
417   # join a channel
418   def join(channel, key=nil)
419     if(key)
420       sendq "JOIN #{channel} :#{key}"
421     else
422       sendq "JOIN #{channel}"
423     end
424   end
425
426   # part a channel
427   def part(channel, message="")
428     sendq "PART #{channel} :#{message}"
429   end
430
431   # attempt to change bot's nick to +name+
432   def nickchg(name)
433       sendq "NICK #{name}"
434   end
435   
436   # m::     message asking for help
437   # topic:: optional topic help is requested for
438   # respond to online help requests
439   def help(topic=nil)
440     topic = nil if topic == ""
441     case topic
442     when nil
443       helpstr = "help topics: core, auth, keywords"
444       helpstr += @plugins.helptopics
445       helpstr += " (help <topic> for more info)"
446     when /^core$/i
447       helpstr = corehelp
448     when /^core\s+(.+)$/i
449       helpstr = corehelp $1
450     when /^auth$/i
451       helpstr = @auth.help
452     when /^auth\s+(.+)$/i
453       helpstr = @auth.help $1
454     when /^keywords$/i
455       helpstr = @keywords.help
456     when /^keywords\s+(.+)$/i
457       helpstr = @keywords.help $1
458     else
459       unless(helpstr = @plugins.help(topic))
460         helpstr = "no help for topic #{topic}"
461       end
462     end
463     return helpstr
464   end
465
466   private
467
468   # handle help requests for "core" topics
469   def corehelp(topic="")
470     case topic
471       when "quit"
472         return "quit [<message>] => quit IRC with message <message>"
473       when "join"
474         return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level"
475       when "part"
476         return "part <channel> => part channel <channel>"
477       when "hide"
478         return "hide => part all channels"
479       when "save"
480         return "save => save current dynamic data and configuration"
481       when "rescan"
482         return "rescan => reload modules and static facts"
483       when "nick"
484         return "nick <nick> => attempt to change nick to <nick>"
485       when "say"
486         return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
487       when "action"
488         return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>"
489       when "topic"
490         return "topic <channel> <message> => set topic of <channel> to <message>"
491       when "quiet"
492         return "quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>"
493       when "talk"
494         return "talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>"
495       when "version"
496         return "version => describes software version"
497       when "botsnack"
498         return "botsnack => reward #{@nick} for being good"
499       when "hello"
500         return "hello|hi|hey|yo [#{@nick}] => greet the bot"
501       else
502         return "Core help topics: quit, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
503     end
504   end
505
506   # handle incoming IRC PRIVMSG +m+
507   def onprivmsg(m)
508     # log it first
509     if(m.action?)
510       if(m.private?)
511         log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
512       else
513         log "* #{m.sourcenick} #{m.message}", m.target
514       end
515     else
516       if(m.public?)
517         log "<#{m.sourcenick}> #{m.message}", m.target
518       else
519         log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
520       end
521     end
522
523     # pass it off to plugins that want to hear everything
524     @plugins.delegate "listen", m
525
526     if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
527       notice m.sourcenick, "\001PING #$1\001"
528       log "@ #{m.sourcenick} pinged me"
529       return
530     end
531
532     if(m.address?)
533       case m.message
534         when (/^join\s+(\S+)\s+(\S+)$/i)
535           join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
536         when (/^join\s+(\S+)$/i)
537           join $1 if(@auth.allow?("join", m.source, m.replyto))
538         when (/^part$/i)
539           part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto))
540         when (/^part\s+(\S+)$/i)
541           part $1 if(@auth.allow?("join", m.source, m.replyto))
542         when (/^quit(?:\s+(.*))?$/i)
543           quit $1 if(@auth.allow?("quit", m.source, m.replyto))
544         when (/^hide$/i)
545           join 0 if(@auth.allow?("join", m.source, m.replyto))
546         when (/^save$/i)
547           if(@auth.allow?("config", m.source, m.replyto))
548             okay m.replyto
549             save
550           end
551         when (/^nick\s+(\S+)$/i)
552           nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
553         when (/^say\s+(\S+)\s+(.*)$/i)
554           say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
555         when (/^action\s+(\S+)\s+(.*)$/i)
556           action $1, $2 if(@auth.allow?("say", m.source, m.replyto))
557         when (/^topic\s+(\S+)\s+(.*)$/i)
558           topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
559         when (/^ping$/i)
560           say m.replyto, "pong"
561         when (/^rescan$/i)
562           if(@auth.allow?("config", m.source, m.replyto))
563             okay m.replyto
564             rescan
565           end
566         when (/^quiet$/i)
567           if(auth.allow?("talk", m.source, m.replyto))
568             say m.replyto, @lang.get("okay")
569             @channels.each_value {|c| c.quiet = true }
570           end
571         when (/^quiet in (\S+)$/i)
572           where = $1
573           if(auth.allow?("talk", m.source, m.replyto))
574             say m.replyto, @lang.get("okay")
575             where.gsub!(/^here$/, m.target) if m.public?
576             @channels[where].quiet = true if(@channels.has_key?(where))
577           end
578         when (/^talk$/i)
579           if(auth.allow?("talk", m.source, m.replyto))
580             @channels.each_value {|c| c.quiet = false }
581             okay m.replyto
582           end
583         when (/^talk in (\S+)$/i)
584           where = $1
585           if(auth.allow?("talk", m.source, m.replyto))
586             where.gsub!(/^here$/, m.target) if m.public?
587             @channels[where].quiet = false if(@channels.has_key?(where))
588             okay m.replyto
589           end
590         # TODO break this out into an options module
591         when (/^options get sendq_delay$/i)
592           if auth.allow?("config", m.source, m.replyto)
593             m.reply "options->sendq_delay = #{@socket.get_sendq}"
594           end
595         when (/^options get sendq_burst$/i)
596           if auth.allow?("config", m.source, m.replyto)
597             m.reply "options->sendq_burst = #{@socket.get_maxburst}"
598           end
599         when (/^options set sendq_burst (.*)$/i)
600           num = $1.to_i
601           if auth.allow?("config", m.source, m.replyto)
602             @socket.set_maxburst(num)
603             @config["SENDQ_BURST"] = num
604             okay m.replyto
605           end
606         when (/^options set sendq_delay (.*)$/i)
607           freq = $1.to_f
608           if auth.allow?("config", m.source, m.replyto)
609             @socket.set_sendq(freq)
610             @config["SENDQ_DELAY"] = freq
611             okay m.replyto
612           end
613         when (/^status$/i)
614           m.reply status if auth.allow?("status", m.source, m.replyto)
615         when (/^registry stats$/i)
616           if auth.allow?("config", m.source, m.replyto)
617             m.reply @registry.stat.inspect
618           end
619         when (/^(version)|(introduce yourself)$/i)
620           say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/"
621         when (/^help(?:\s+(.*))?$/i)
622           say m.replyto, help($1)
623         when (/^(botsnack|ciggie)$/i)
624           say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
625           say m.replyto, @lang.get("thanks") if(m.private?)
626         when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
627           say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
628           say m.replyto, @lang.get("hello") if(m.private?)
629         else
630           delegate_privmsg(m)
631       end
632     else
633       # stuff to handle when not addressed
634       case m.message
635         when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))\s+#{@nick}$/i)
636           say m.replyto, @lang.get("hello_X") % m.sourcenick
637         when (/^#{@nick}!*$/)
638           say m.replyto, @lang.get("hello_X") % m.sourcenick
639         else
640           @keywords.privmsg(m)
641       end
642     end
643   end
644
645   # log a message. Internal use only.
646   def log_sent(type, where, message)
647     case type
648       when "NOTICE"
649         if(where =~ /^#/)
650           log "-=#{@nick}=- #{message}", where
651         elsif (where =~ /(\S*)!.*/)
652              log "[-=#{where}=-] #{message}", $1
653         else
654              log "[-=#{where}=-] #{message}"
655         end
656       when "PRIVMSG"
657         if(where =~ /^#/)
658           log "<#{@nick}> #{message}", where
659         elsif (where =~ /^(\S*)!.*$/)
660           log "[msg(#{where})] #{message}", $1
661         else
662           log "[msg(#{where})] #{message}", where
663         end
664     end
665   end
666
667   def onjoin(m)
668     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
669     if(m.address?)
670       log "@ Joined channel #{m.channel}", m.channel
671       puts "joined channel #{m.channel}"
672     else
673       log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
674       @channels[m.channel].users[m.sourcenick] = Hash.new
675       @channels[m.channel].users[m.sourcenick]["mode"] = ""
676     end
677
678     @plugins.delegate("listen", m)
679     @plugins.delegate("join", m)
680   end
681
682   def onpart(m)
683     if(m.address?)
684       log "@ Left channel #{m.channel} (#{m.message})", m.channel
685       @channels.delete(m.channel)
686       puts "left channel #{m.channel}"
687     else
688       log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
689       @channels[m.channel].users.delete(m.sourcenick)
690     end
691     
692     # delegate to plugins
693     @plugins.delegate("listen", m)
694     @plugins.delegate("part", m)
695   end
696
697   # respond to being kicked from a channel
698   def onkick(m)
699     if(m.address?)
700       @channels.delete(m.channel)
701       log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
702       puts "kicked from channel #{m.channel}"
703     else
704       @channels[m.channel].users.delete(m.sourcenick)
705       log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
706     end
707
708     @plugins.delegate("listen", m)
709     @plugins.delegate("kick", m)
710   end
711
712   def ontopic(m)
713     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
714     @channels[m.channel].topic = m.topic if !m.topic.nil?
715     @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
716     @channels[m.channel].topic.by = m.source if !m.source.nil?
717
718         puts @channels[m.channel].topic
719   end
720
721   def status
722     secs_up = Time.new - @startup_time
723     uptime = Utils.secs_to_string secs_up
724     return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
725   end
726
727   # delegate a privmsg to auth, keyword or plugin handlers
728   def delegate_privmsg(message)
729     [@auth, @plugins, @keywords].each {|m|
730       break if m.privmsg(message)
731     }
732   end
733
734 end
735
736 end