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