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