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