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