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