]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - rbot/ircbot.rb
that wasn't ideal
[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].sub(/^[@&~+]/, '')] = ["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.gsub(/[\r\n]+/, "\n").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   # changing mode
437   def mode(channel, mode, target)
438       sendq "MODE #{channel} #{mode} #{target}"
439   end
440   
441   # m::     message asking for help
442   # topic:: optional topic help is requested for
443   # respond to online help requests
444   def help(topic=nil)
445     topic = nil if topic == ""
446     case topic
447     when nil
448       helpstr = "help topics: core, auth, keywords"
449       helpstr += @plugins.helptopics
450       helpstr += " (help <topic> for more info)"
451     when /^core$/i
452       helpstr = corehelp
453     when /^core\s+(.+)$/i
454       helpstr = corehelp $1
455     when /^auth$/i
456       helpstr = @auth.help
457     when /^auth\s+(.+)$/i
458       helpstr = @auth.help $1
459     when /^keywords$/i
460       helpstr = @keywords.help
461     when /^keywords\s+(.+)$/i
462       helpstr = @keywords.help $1
463     else
464       unless(helpstr = @plugins.help(topic))
465         helpstr = "no help for topic #{topic}"
466       end
467     end
468     return helpstr
469   end
470
471   private
472
473   # handle help requests for "core" topics
474   def corehelp(topic="")
475     case topic
476       when "quit"
477         return "quit [<message>] => quit IRC with message <message>"
478       when "join"
479         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"
480       when "part"
481         return "part <channel> => part channel <channel>"
482       when "hide"
483         return "hide => part all channels"
484       when "save"
485         return "save => save current dynamic data and configuration"
486       when "rescan"
487         return "rescan => reload modules and static facts"
488       when "nick"
489         return "nick <nick> => attempt to change nick to <nick>"
490       when "say"
491         return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
492       when "action"
493         return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>"
494       when "topic"
495         return "topic <channel> <message> => set topic of <channel> to <message>"
496       when "quiet"
497         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>"
498       when "talk"
499         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>"
500       when "version"
501         return "version => describes software version"
502       when "botsnack"
503         return "botsnack => reward #{@nick} for being good"
504       when "hello"
505         return "hello|hi|hey|yo [#{@nick}] => greet the bot"
506       else
507         return "Core help topics: quit, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
508     end
509   end
510
511   # handle incoming IRC PRIVMSG +m+
512   def onprivmsg(m)
513     # log it first
514     if(m.action?)
515       if(m.private?)
516         log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
517       else
518         log "* #{m.sourcenick} #{m.message}", m.target
519       end
520     else
521       if(m.public?)
522         log "<#{m.sourcenick}> #{m.message}", m.target
523       else
524         log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
525       end
526     end
527
528     # pass it off to plugins that want to hear everything
529     @plugins.delegate "listen", m
530
531     if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
532       notice m.sourcenick, "\001PING #$1\001"
533       log "@ #{m.sourcenick} pinged me"
534       return
535     end
536
537     if(m.address?)
538       case m.message
539         when (/^join\s+(\S+)\s+(\S+)$/i)
540           join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
541         when (/^join\s+(\S+)$/i)
542           join $1 if(@auth.allow?("join", m.source, m.replyto))
543         when (/^part$/i)
544           part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto))
545         when (/^part\s+(\S+)$/i)
546           part $1 if(@auth.allow?("join", m.source, m.replyto))
547         when (/^quit(?:\s+(.*))?$/i)
548           quit $1 if(@auth.allow?("quit", m.source, m.replyto))
549         when (/^hide$/i)
550           join 0 if(@auth.allow?("join", m.source, m.replyto))
551         when (/^save$/i)
552           if(@auth.allow?("config", m.source, m.replyto))
553             okay m.replyto
554             save
555           end
556         when (/^nick\s+(\S+)$/i)
557           nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
558         when (/^say\s+(\S+)\s+(.*)$/i)
559           say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
560         when (/^action\s+(\S+)\s+(.*)$/i)
561           action $1, $2 if(@auth.allow?("say", m.source, m.replyto))
562         when (/^topic\s+(\S+)\s+(.*)$/i)
563           topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
564         when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i)
565           mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto))
566         when (/^ping$/i)
567           say m.replyto, "pong"
568         when (/^rescan$/i)
569           if(@auth.allow?("config", m.source, m.replyto))
570             okay m.replyto
571             rescan
572           end
573         when (/^quiet$/i)
574           if(auth.allow?("talk", m.source, m.replyto))
575             say m.replyto, @lang.get("okay")
576             @channels.each_value {|c| c.quiet = true }
577           end
578         when (/^quiet in (\S+)$/i)
579           where = $1
580           if(auth.allow?("talk", m.source, m.replyto))
581             say m.replyto, @lang.get("okay")
582             where.gsub!(/^here$/, m.target) if m.public?
583             @channels[where].quiet = true if(@channels.has_key?(where))
584           end
585         when (/^talk$/i)
586           if(auth.allow?("talk", m.source, m.replyto))
587             @channels.each_value {|c| c.quiet = false }
588             okay m.replyto
589           end
590         when (/^talk in (\S+)$/i)
591           where = $1
592           if(auth.allow?("talk", m.source, m.replyto))
593             where.gsub!(/^here$/, m.target) if m.public?
594             @channels[where].quiet = false if(@channels.has_key?(where))
595             okay m.replyto
596           end
597         # TODO break this out into an options module
598         when (/^options get sendq_delay$/i)
599           if auth.allow?("config", m.source, m.replyto)
600             m.reply "options->sendq_delay = #{@socket.get_sendq}"
601           end
602         when (/^options get sendq_burst$/i)
603           if auth.allow?("config", m.source, m.replyto)
604             m.reply "options->sendq_burst = #{@socket.get_maxburst}"
605           end
606         when (/^options set sendq_burst (.*)$/i)
607           num = $1.to_i
608           if auth.allow?("config", m.source, m.replyto)
609             @socket.set_maxburst(num)
610             @config["SENDQ_BURST"] = num
611             okay m.replyto
612           end
613         when (/^options set sendq_delay (.*)$/i)
614           freq = $1.to_f
615           if auth.allow?("config", m.source, m.replyto)
616             @socket.set_sendq(freq)
617             @config["SENDQ_DELAY"] = freq
618             okay m.replyto
619           end
620         when (/^status$/i)
621           m.reply status if auth.allow?("status", m.source, m.replyto)
622         when (/^registry stats$/i)
623           if auth.allow?("config", m.source, m.replyto)
624             m.reply @registry.stat.inspect
625           end
626         when (/^(version)|(introduce yourself)$/i)
627           say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/"
628         when (/^help(?:\s+(.*))?$/i)
629           say m.replyto, help($1)
630         when (/^(botsnack|ciggie)$/i)
631           say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
632           say m.replyto, @lang.get("thanks") if(m.private?)
633         when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
634           say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
635           say m.replyto, @lang.get("hello") if(m.private?)
636         else
637           delegate_privmsg(m)
638       end
639     else
640       # stuff to handle when not addressed
641       case m.message
642         when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))\s+#{@nick}$/i)
643           say m.replyto, @lang.get("hello_X") % m.sourcenick
644         when (/^#{@nick}!*$/)
645           say m.replyto, @lang.get("hello_X") % m.sourcenick
646         else
647           @keywords.privmsg(m)
648       end
649     end
650   end
651
652   # log a message. Internal use only.
653   def log_sent(type, where, message)
654     case type
655       when "NOTICE"
656         if(where =~ /^#/)
657           log "-=#{@nick}=- #{message}", where
658         elsif (where =~ /(\S*)!.*/)
659              log "[-=#{where}=-] #{message}", $1
660         else
661              log "[-=#{where}=-] #{message}"
662         end
663       when "PRIVMSG"
664         if(where =~ /^#/)
665           log "<#{@nick}> #{message}", where
666         elsif (where =~ /^(\S*)!.*$/)
667           log "[msg(#{where})] #{message}", $1
668         else
669           log "[msg(#{where})] #{message}", where
670         end
671     end
672   end
673
674   def onjoin(m)
675     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
676     if(m.address?)
677       log "@ Joined channel #{m.channel}", m.channel
678       puts "joined channel #{m.channel}"
679     else
680       log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
681       @channels[m.channel].users[m.sourcenick] = Hash.new
682       @channels[m.channel].users[m.sourcenick]["mode"] = ""
683     end
684
685     @plugins.delegate("listen", m)
686     @plugins.delegate("join", m)
687   end
688
689   def onpart(m)
690     if(m.address?)
691       log "@ Left channel #{m.channel} (#{m.message})", m.channel
692       @channels.delete(m.channel)
693       puts "left channel #{m.channel}"
694     else
695       log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
696       @channels[m.channel].users.delete(m.sourcenick)
697     end
698     
699     # delegate to plugins
700     @plugins.delegate("listen", m)
701     @plugins.delegate("part", m)
702   end
703
704   # respond to being kicked from a channel
705   def onkick(m)
706     if(m.address?)
707       @channels.delete(m.channel)
708       log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
709       puts "kicked from channel #{m.channel}"
710     else
711       @channels[m.channel].users.delete(m.sourcenick)
712       log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
713     end
714
715     @plugins.delegate("listen", m)
716     @plugins.delegate("kick", m)
717   end
718
719   def ontopic(m)
720     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
721     @channels[m.channel].topic = m.topic if !m.topic.nil?
722     @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
723     @channels[m.channel].topic.by = m.source if !m.source.nil?
724
725         puts @channels[m.channel].topic
726   end
727
728   def status
729     secs_up = Time.new - @startup_time
730     uptime = Utils.secs_to_string secs_up
731     return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
732   end
733
734   # delegate a privmsg to auth, keyword or plugin handlers
735   def delegate_privmsg(message)
736     [@auth, @plugins, @keywords].each {|m|
737       break if m.privmsg(message)
738     }
739   end
740
741 end
742
743 end