]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/ircbot.rb
4df071f835c0a7476913ee9f6ed1d6a2c6a1e765
[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 $daemonize = false unless $daemonize
7
8 # TODO we should use the actual Logger class
9 def rawlog(code="", message=nil)
10   if !code || code.empty?
11     c = "  "
12   else
13     c = code.to_s[0,1].upcase + ":"
14   end
15   call_stack = caller
16   case call_stack.length
17   when 0
18     $stderr.puts "ERROR IN THE LOGGING SYSTEM, THIS CAN'T HAPPEN"
19     who = "WTF1??  "
20   when 1
21     $stderr.puts "ERROR IN THE LOGGING SYSTEM, THIS CAN'T HAPPEN"
22     who = "WTF2??  "
23   else
24     who = call_stack[1].sub(%r{(?:.+)/([^/]+):(\d+)(:in .*)?}) { "#{$1}:#{$2}#{$3}" }
25   end
26   stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
27   message.to_s.each_line { |l|
28     $stdout.puts "#{c} [#{stamp}] #{who} -- #{l}"
29   }
30   $stdout.flush
31 end
32
33 def log(message=nil)
34   rawlog("", message)
35 end
36
37 def log_session_end
38    rawlog("", "\n=== #{botclass} session ended ===") if $daemonize
39 end
40
41 def debug(message=nil)
42   rawlog("D", message) if $debug
43 end
44
45 def warning(message=nil)
46   rawlog("W", message)
47 end
48
49 def error(message=nil)
50   rawlog("E", message)
51 end
52
53 debug "debug test"
54 log "log test"
55 warning "warning test"
56 error "error test"
57
58 # The following global is used for the improved signal handling.
59 $interrupted = 0
60
61 # these first
62 require 'rbot/rbotconfig'
63 require 'rbot/config'
64 require 'rbot/utils'
65
66 require 'rbot/rfc2812'
67 require 'rbot/keywords'
68 require 'rbot/ircsocket'
69 require 'rbot/auth'
70 require 'rbot/timer'
71 require 'rbot/plugins'
72 require 'rbot/channel'
73 require 'rbot/message'
74 require 'rbot/language'
75 require 'rbot/dbhash'
76 require 'rbot/registry'
77 require 'rbot/httputil'
78
79 module Irc
80
81 # Main bot class, which manages the various components, receives messages,
82 # handles them or passes them to plugins, and contains core functionality.
83 class IrcBot
84   # the bot's current nickname
85   attr_reader :nick
86
87   # the bot's IrcAuth data
88   attr_reader :auth
89
90   # the bot's BotConfig data
91   attr_reader :config
92
93   # the botclass for this bot (determines configdir among other things)
94   attr_reader :botclass
95
96   # used to perform actions periodically (saves configuration once per minute
97   # by default)
98   attr_reader :timer
99
100   # bot's Language data
101   attr_reader :lang
102
103   # capabilities info for the server
104   attr_reader :capabilities
105
106   # channel info for channels the bot is in
107   attr_reader :channels
108
109   # bot's irc socket
110   attr_reader :socket
111
112   # bot's object registry, plugins get an interface to this for persistant
113   # storage (hash interface tied to a bdb file, plugins use Accessors to store
114   # and restore objects in their own namespaces.)
115   attr_reader :registry
116
117   # bot's plugins. This is an instance of class Plugins
118   attr_reader :plugins
119
120   # bot's httputil help object, for fetching resources via http. Sets up
121   # proxies etc as defined by the bot configuration/environment
122   attr_reader :httputil
123
124   # create a new IrcBot with botclass +botclass+
125   def initialize(botclass, params = {})
126     # BotConfig for the core bot
127     # TODO should we split socket stuff into ircsocket, etc?
128     BotConfig.register BotConfigStringValue.new('server.name',
129       :default => "localhost", :requires_restart => true,
130       :desc => "What server should the bot connect to?",
131       :wizard => true)
132     BotConfig.register BotConfigIntegerValue.new('server.port',
133       :default => 6667, :type => :integer, :requires_restart => true,
134       :desc => "What port should the bot connect to?",
135       :validate => Proc.new {|v| v > 0}, :wizard => true)
136     BotConfig.register BotConfigStringValue.new('server.password',
137       :default => false, :requires_restart => true,
138       :desc => "Password for connecting to this server (if required)",
139       :wizard => true)
140     BotConfig.register BotConfigStringValue.new('server.bindhost',
141       :default => false, :requires_restart => true,
142       :desc => "Specific local host or IP for the bot to bind to (if required)",
143       :wizard => true)
144     BotConfig.register BotConfigIntegerValue.new('server.reconnect_wait',
145       :default => 5, :validate => Proc.new{|v| v >= 0},
146       :desc => "Seconds to wait before attempting to reconnect, on disconnect")
147     BotConfig.register BotConfigFloatValue.new('server.sendq_delay',
148       :default => 2.0, :validate => Proc.new{|v| v >= 0},
149       :desc => "(flood prevention) the delay between sending messages to the server (in seconds)",
150       :on_change => Proc.new {|bot, v| bot.socket.sendq_delay = v })
151     BotConfig.register BotConfigIntegerValue.new('server.sendq_burst',
152       :default => 4, :validate => Proc.new{|v| v >= 0},
153       :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines",
154       :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
155     BotConfig.register BotConfigStringValue.new('server.byterate',
156       :default => "400/2", :validate => Proc.new{|v| v.match(/\d+\/\d/)},
157       :desc => "(flood prevention) max bytes/seconds rate to send the server. Most ircd's have limits of 512 bytes/2 seconds",
158       :on_change => Proc.new {|bot, v| bot.socket.byterate = v })
159     BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
160       :default => 30, :validate => Proc.new{|v| v >= 0},
161       :on_change => Proc.new {|bot, v| bot.start_server_pings},
162       :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
163
164     BotConfig.register BotConfigStringValue.new('irc.nick', :default => "rbot",
165       :desc => "IRC nickname the bot should attempt to use", :wizard => true,
166       :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" })
167     BotConfig.register BotConfigStringValue.new('irc.user', :default => "rbot",
168       :requires_restart => true,
169       :desc => "local user the bot should appear to be", :wizard => true)
170     BotConfig.register BotConfigArrayValue.new('irc.join_channels',
171       :default => [], :wizard => true,
172       :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'")
173     BotConfig.register BotConfigArrayValue.new('irc.ignore_users',
174       :default => [], 
175       :desc => "Which users to ignore input from. This is mainly to avoid bot-wars triggered by creative people")
176
177     BotConfig.register BotConfigIntegerValue.new('core.save_every',
178       :default => 60, :validate => Proc.new{|v| v >= 0},
179       # TODO change timer via on_change proc
180       :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example)")
181       # BotConfig.register BotConfigBooleanValue.new('core.debug',
182       #   :default => false, :requires_restart => true,
183       #   :on_change => Proc.new { |v|
184       #     debug ((v ? "Enabling" : "Disabling") + " debug output.")
185       #     $debug = v
186       #     debug (($debug ? "Enabled" : "Disabled") + " debug output.")
187       #   },
188       #   :desc => "Should the bot produce debug output?")
189     BotConfig.register BotConfigBooleanValue.new('core.run_as_daemon',
190       :default => false, :requires_restart => true,
191       :desc => "Should the bot run as a daemon?")
192     BotConfig.register BotConfigStringValue.new('core.logfile',
193       :default => false, :requires_restart => true,
194       :desc => "Name of the logfile to which console messages will be redirected when the bot is run as a daemon")
195
196     @argv = params[:argv]
197
198     unless FileTest.directory? Config::datadir
199       error "data directory '#{Config::datadir}' not found, did you setup.rb?"
200       exit 2
201     end
202
203     unless botclass and not botclass.empty?
204       # We want to find a sensible default.
205       #  * On POSIX systems we prefer ~/.rbot for the effective uid of the process
206       #  * On Windows (at least the NT versions) we want to put our stuff in the
207       #    Application Data folder.
208       # We don't use any particular O/S detection magic, exploiting the fact that
209       # Etc.getpwuid is nil on Windows
210       if Etc.getpwuid(Process::Sys.geteuid)
211         botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup
212       else
213         if ENV.has_key?('APPDATA')
214           botclass = ENV['APPDATA'].dup
215           botclass.gsub!("\\","/")
216         end
217       end
218       botclass += "/.rbot"
219     end
220     botclass = File.expand_path(botclass)
221     @botclass = botclass.gsub(/\/$/, "")
222
223     unless FileTest.directory? botclass
224       log "no #{botclass} directory found, creating from templates.."
225       if FileTest.exist? botclass
226         error "file #{botclass} exists but isn't a directory"
227         exit 2
228       end
229       FileUtils.cp_r Config::datadir+'/templates', botclass
230     end
231
232     Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
233     Dir.mkdir("#{botclass}/registry") unless File.exist?("#{botclass}/registry")
234
235     @ping_timer = nil
236     @pong_timer = nil
237     @last_ping = nil
238     @startup_time = Time.new
239     @config = BotConfig.new(self)
240     # background self after botconfig has a chance to run wizard
241     @logfile = @config['core.logfile']
242     if @logfile.class!=String || @logfile.empty?
243       @logfile = File.basename(botclass)+".log"
244     end
245     if @config['core.run_as_daemon']
246       $daemonize = true
247     end
248     # See http://blog.humlab.umu.se/samuel/archives/000107.html
249     # for the backgrounding code 
250     if $daemonize
251       begin
252         exit if fork
253         Process.setsid
254         exit if fork
255       rescue NotImplementedError
256         warning "Could not background, fork not supported"
257       rescue => e
258         warning "Could not background. #{e.inspect}"
259       end
260       Dir.chdir botclass
261       # File.umask 0000                # Ensure sensible umask. Adjust as needed.
262       log "Redirecting standard input/output/error"
263       begin
264         STDIN.reopen "/dev/null"
265       rescue Errno::ENOENT
266         # On Windows, there's not such thing as /dev/null
267         STDIN.reopen "NUL"
268       end
269       STDOUT.reopen @logfile, "a"
270       STDERR.reopen STDOUT
271       log "\n=== #{botclass} session started ==="
272     end
273
274     @timer = Timer::Timer.new(1.0) # only need per-second granularity
275     @registry = BotRegistry.new self
276     @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
277     @channels = Hash.new
278     @logs = Hash.new
279     @httputil = Utils::HttpUtil.new(self)
280     @lang = Language::Language.new(@config['core.language'])
281     @keywords = Keywords.new(self)
282     @auth = IrcAuth.new(self)
283
284     Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
285     @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"])
286
287     @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
288     @nick = @config['irc.nick']
289
290     @client = IrcClient.new
291     @client[:isupport] = proc { |data|
292       if data[:capab]
293         sendq "CAPAB IDENTIFY-MSG"
294       end
295     }
296     @client[:datastr] = proc { |data|
297       debug data.inspect
298       if data[:text] == "IDENTIFY-MSG"
299         @capabilities["identify-msg".to_sym] = true
300       else
301         debug "Not handling RPL_DATASTR #{data[:servermessage]}"
302       end
303     }
304     @client[:privmsg] = proc { |data|
305       message = PrivMessage.new(self, data[:source], data[:target], data[:message])
306       onprivmsg(message)
307     }
308     @client[:notice] = proc { |data|
309       message = NoticeMessage.new(self, data[:source], data[:target], data[:message])
310       # pass it off to plugins that want to hear everything
311       @plugins.delegate "listen", message
312     }
313     @client[:motd] = proc { |data|
314       data[:motd].each_line { |line|
315         irclog "MOTD: #{line}", "server"
316       }
317     }
318     @client[:nicktaken] = proc { |data|
319       nickchg "#{data[:nick]}_"
320       @plugins.delegate "nicktaken", data[:nick]
321     }
322     @client[:badnick] = proc {|data|
323       warning "bad nick (#{data[:nick]})"
324     }
325     @client[:ping] = proc {|data|
326       @socket.queue "PONG #{data[:pingid]}"
327     }
328     @client[:pong] = proc {|data|
329       @last_ping = nil
330     }
331     @client[:nick] = proc {|data|
332       sourcenick = data[:sourcenick]
333       nick = data[:nick]
334       m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick])
335       if(sourcenick == @nick)
336         debug "my nick is now #{nick}"
337         @nick = nick
338       end
339       @channels.each {|k,v|
340         if(v.users.has_key?(sourcenick))
341           irclog "@ #{sourcenick} is now known as #{nick}", k
342           v.users[nick] = v.users[sourcenick]
343           v.users.delete(sourcenick)
344         end
345       }
346       @plugins.delegate("listen", m)
347       @plugins.delegate("nick", m)
348     }
349     @client[:quit] = proc {|data|
350       source = data[:source]
351       sourcenick = data[:sourcenick]
352       sourceurl = data[:sourceaddress]
353       message = data[:message]
354       m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
355       if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
356       else
357         @channels.each {|k,v|
358           if(v.users.has_key?(sourcenick))
359             irclog "@ Quit: #{sourcenick}: #{message}", k
360             v.users.delete(sourcenick)
361           end
362         }
363       end
364       @plugins.delegate("listen", m)
365       @plugins.delegate("quit", m)
366     }
367     @client[:mode] = proc {|data|
368       source = data[:source]
369       sourcenick = data[:sourcenick]
370       sourceurl = data[:sourceaddress]
371       channel = data[:channel]
372       targets = data[:targets]
373       modestring = data[:modestring]
374       irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
375     }
376     @client[:welcome] = proc {|data|
377       irclog "joined server #{data[:source]} as #{data[:nick]}", "server"
378       debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
379       if data[:nick] && data[:nick].length > 0
380         @nick = data[:nick]
381       end
382
383       @plugins.delegate("connect")
384
385       @config['irc.join_channels'].each {|c|
386         debug "autojoining channel #{c}"
387         if(c =~ /^(\S+)\s+(\S+)$/i)
388           join $1, $2
389         else
390           join c if(c)
391         end
392       }
393     }
394     @client[:join] = proc {|data|
395       m = JoinMessage.new(self, data[:source], data[:channel], data[:message])
396       onjoin(m)
397     }
398     @client[:part] = proc {|data|
399       m = PartMessage.new(self, data[:source], data[:channel], data[:message])
400       onpart(m)
401     }
402     @client[:kick] = proc {|data|
403       m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
404       onkick(m)
405     }
406     @client[:invite] = proc {|data|
407       if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
408         join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
409       end
410     }
411     @client[:changetopic] = proc {|data|
412       channel = data[:channel]
413       sourcenick = data[:sourcenick]
414       topic = data[:topic]
415       timestamp = data[:unixtime] || Time.now.to_i
416       if(sourcenick == @nick)
417         irclog "@ I set topic \"#{topic}\"", channel
418       else
419         irclog "@ #{sourcenick} set topic \"#{topic}\"", channel
420       end
421       m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
422
423       ontopic(m)
424       @plugins.delegate("listen", m)
425       @plugins.delegate("topic", m)
426     }
427     @client[:topic] = @client[:topicinfo] = proc {|data|
428       channel = data[:channel]
429       m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic])
430         ontopic(m)
431     }
432     @client[:names] = proc {|data|
433       channel = data[:channel]
434       users = data[:users]
435       unless(@channels[channel])
436         warning "got names for channel '#{channel}' I didn't think I was in\n"
437         # exit 2
438       end
439       @channels[channel].users.clear
440       users.each {|u|
441         @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
442       }
443       @plugins.delegate "names", data[:channel], data[:users]
444     }
445     @client[:unknown] = proc {|data|
446       #debug "UNKNOWN: #{data[:serverstring]}"
447       irclog data[:serverstring], ".unknown"
448     }
449   end
450
451   def got_sig(sig)
452     debug "received #{sig}, queueing quit"
453     $interrupted += 1
454     debug "interrupted #{$interrupted} times"
455     if $interrupted >= 5
456       debug "drastic!"
457       log_session_end
458       exit 2
459     elsif $interrupted >= 3
460       debug "quitting"
461       quit
462     end
463   end
464
465   # connect the bot to IRC
466   def connect
467     begin
468       trap("SIGINT") { got_sig("SIGINT") }
469       trap("SIGTERM") { got_sig("SIGTERM") }
470       trap("SIGHUP") { got_sig("SIGHUP") }
471     rescue ArgumentError => e
472       debug "failed to trap signals (#{e.inspect}): running on Windows?"
473     rescue => e
474       debug "failed to trap signals: #{e.inspect}"
475     end
476     begin
477       quit if $interrupted > 0
478       @socket.connect
479     rescue => e
480       raise e.class, "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
481     end
482     @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password']
483     @socket.emergency_puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
484     @capabilities = Hash.new
485     start_server_pings
486   end
487
488   # begin event handling loop
489   def mainloop
490     while true
491       begin
492         quit if $interrupted > 0
493         connect
494         @timer.start
495
496         while @socket.connected?
497           if @socket.select
498             break unless reply = @socket.gets
499             @client.process reply
500           end
501           quit if $interrupted > 0
502         end
503
504       # I despair of this. Some of my users get "connection reset by peer"
505       # exceptions that ARENT SocketError's. How am I supposed to handle
506       # that?
507       rescue SystemExit
508         log_session_end
509         exit 0
510       rescue Errno::ETIMEDOUT, TimeoutError, SocketError => e
511         error "network exception: #{e.class}: #{e}"
512         debug e.backtrace.join("\n")
513       rescue BDB::Fatal => e
514         error "fatal bdb error: #{e.class}: #{e}"
515         error e.backtrace.join("\n")
516         DBTree.stats
517         restart("Oops, we seem to have registry problems ...")
518       rescue Exception => e
519         error "non-net exception: #{e.class}: #{e}"
520         error e.backtrace.join("\n")
521       rescue => e
522         error "unexpected exception: #{e.class}: #{e}"
523         error e.backtrace.join("\n")
524         log_session_end
525         exit 2
526       end
527
528       stop_server_pings
529       @channels.clear
530       if @socket.connected?
531         @socket.clearq
532         @socket.shutdown
533       end
534
535       log "disconnected"
536
537       quit if $interrupted > 0
538
539       log "waiting to reconnect"
540       sleep @config['server.reconnect_wait']
541     end
542   end
543
544   # type:: message type
545   # where:: message target
546   # message:: message text
547   # send message +message+ of type +type+ to target +where+
548   # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
549   # relevant say() or notice() methods. This one should be used for IRCd
550   # extensions you want to use in modules.
551   def sendmsg(type, where, message, chan=nil, ring=0)
552     # limit it according to the byterate, splitting the message
553     # taking into consideration the actual message length
554     # and all the extra stuff
555     # TODO allow something to do for commands that produce too many messages
556     # TODO example: math 10**10000
557     left = @socket.bytes_per - type.length - where.length - 4
558     begin
559       if(left >= message.length)
560         sendq "#{type} #{where} :#{message}", chan, ring
561         log_sent(type, where, message)
562         return
563       end
564       line = message.slice!(0, left)
565       lastspace = line.rindex(/\s+/)
566       if(lastspace)
567         message = line.slice!(lastspace, line.length) + message
568         message.gsub!(/^\s+/, "")
569       end
570       sendq "#{type} #{where} :#{line}", chan, ring
571       log_sent(type, where, line)
572     end while(message.length > 0)
573   end
574
575   # queue an arbitraty message for the server
576   def sendq(message="", chan=nil, ring=0)
577     # temporary
578     @socket.queue(message, chan, ring)
579   end
580
581   # send a notice message to channel/nick +where+
582   def notice(where, message, mchan=nil, mring=-1)
583     if mchan == ""
584       chan = where
585     else
586       chan = mchan
587     end
588     if mring < 0
589       if where =~ /^#/
590         ring = 2
591       else
592         ring = 1
593       end
594     else
595       ring = mring
596     end
597     message.each_line { |line|
598       line.chomp!
599       next unless(line.length > 0)
600       sendmsg "NOTICE", where, line, chan, ring
601     }
602   end
603
604   # say something (PRIVMSG) to channel/nick +where+
605   def say(where, message, mchan="", mring=-1)
606     if mchan == ""
607       chan = where
608     else
609       chan = mchan
610     end
611     if mring < 0
612       if where =~ /^#/
613         ring = 2
614       else
615         ring = 1
616       end
617     else
618       ring = mring
619     end
620     message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
621       line.chomp!
622       next unless(line.length > 0)
623       unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
624         sendmsg "PRIVMSG", where, line, chan, ring 
625       end
626     }
627   end
628
629   # perform a CTCP action with message +message+ to channel/nick +where+
630   def action(where, message, mchan="", mring=-1)
631     if mchan == ""
632       chan = where
633     else
634       chan = mchan
635     end
636     if mring < 0
637       if where =~ /^#/
638         ring = 2
639       else
640         ring = 1
641       end
642     else
643       ring = mring
644     end
645     sendq "PRIVMSG #{where} :\001ACTION #{message}\001", chan, ring
646     if(where =~ /^#/)
647       irclog "* #{@nick} #{message}", where
648     elsif (where =~ /^(\S*)!.*$/)
649       irclog "* #{@nick}[#{where}] #{message}", $1
650     else
651       irclog "* #{@nick}[#{where}] #{message}", where
652     end
653   end
654
655   # quick way to say "okay" (or equivalent) to +where+
656   def okay(where)
657     say where, @lang.get("okay")
658   end
659
660   # log IRC-related message +message+ to a file determined by +where+.
661   # +where+ can be a channel name, or a nick for private message logging
662   def irclog(message, where="server")
663     message = message.chomp
664     stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
665     where = where.gsub(/[:!?$*()\/\\<>|"']/, "_")
666     unless(@logs.has_key?(where))
667       @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
668       @logs[where].sync = true
669     end
670     @logs[where].puts "[#{stamp}] #{message}"
671     #debug "[#{stamp}] <#{where}> #{message}"
672   end
673
674   # set topic of channel +where+ to +topic+
675   def topic(where, topic)
676     sendq "TOPIC #{where} :#{topic}", where, 2
677   end
678
679   # disconnect from the server and cleanup all plugins and modules
680   def shutdown(message = nil)
681     debug "Shutting down ..."
682     ## No we don't restore them ... let everything run through
683     # begin
684     #   trap("SIGINT", "DEFAULT")
685     #   trap("SIGTERM", "DEFAULT")
686     #   trap("SIGHUP", "DEFAULT")
687     # rescue => e
688     #   debug "failed to restore signals: #{e.inspect}\nProbably running on windows?"
689     # end
690     message = @lang.get("quit") if (message.nil? || message.empty?)
691     if @socket.connected?
692       debug "Clearing socket"
693       @socket.clearq
694       debug "Sending quit message"
695       @socket.emergency_puts "QUIT :#{message}"
696       debug "Flushing socket"
697       @socket.flush
698       debug "Shutting down socket"
699       @socket.shutdown
700     end
701     debug "Logging quits"
702     @channels.each_value {|v|
703       irclog "@ quit (#{message})", v.name
704     }
705     debug "Saving"
706     save
707     debug "Cleaning up"
708     @plugins.cleanup
709     # debug "Closing registries"
710     # @registry.close
711     debug "Cleaning up the db environment"
712     DBTree.cleanup_env
713     log "rbot quit (#{message})"
714   end
715
716   # message:: optional IRC quit message
717   # quit IRC, shutdown the bot
718   def quit(message=nil)
719     begin
720       shutdown(message)
721     ensure
722       log_session_end
723       exit 0
724     end
725   end
726
727   # totally shutdown and respawn the bot
728   def restart(message = false)
729     msg = message ? message : "restarting, back in #{@config['server.reconnect_wait']}..."
730     shutdown(msg)
731     sleep @config['server.reconnect_wait']
732     # now we re-exec
733     # Note, this fails on Windows
734     exec($0, *@argv)
735   end
736
737   # call the save method for bot's config, keywords, auth and all plugins
738   def save
739     @config.save
740     @keywords.save
741     @auth.save
742     @plugins.save
743     DBTree.cleanup_logs
744   end
745
746   # call the rescan method for the bot's lang, keywords and all plugins
747   def rescan
748     @lang.rescan
749     @plugins.rescan
750     @keywords.rescan
751   end
752
753   # channel:: channel to join
754   # key::     optional channel key if channel is +s
755   # join a channel
756   def join(channel, key=nil)
757     if(key)
758       sendq "JOIN #{channel} :#{key}", channel, 2
759     else
760       sendq "JOIN #{channel}", channel, 2
761     end
762   end
763
764   # part a channel
765   def part(channel, message="")
766     sendq "PART #{channel} :#{message}", channel, 2
767   end
768
769   # attempt to change bot's nick to +name+
770   def nickchg(name)
771       sendq "NICK #{name}"
772   end
773
774   # changing mode
775   def mode(channel, mode, target)
776       sendq "MODE #{channel} #{mode} #{target}", channel, 2
777   end
778
779   # m::     message asking for help
780   # topic:: optional topic help is requested for
781   # respond to online help requests
782   def help(topic=nil)
783     topic = nil if topic == ""
784     case topic
785     when nil
786       helpstr = "help topics: core, auth, keywords"
787       helpstr += @plugins.helptopics
788       helpstr += " (help <topic> for more info)"
789     when /^core$/i
790       helpstr = corehelp
791     when /^core\s+(.+)$/i
792       helpstr = corehelp $1
793     when /^auth$/i
794       helpstr = @auth.help
795     when /^auth\s+(.+)$/i
796       helpstr = @auth.help $1
797     when /^keywords$/i
798       helpstr = @keywords.help
799     when /^keywords\s+(.+)$/i
800       helpstr = @keywords.help $1
801     else
802       unless(helpstr = @plugins.help(topic))
803         helpstr = "no help for topic #{topic}"
804       end
805     end
806     return helpstr
807   end
808
809   # returns a string describing the current status of the bot (uptime etc)
810   def status
811     secs_up = Time.new - @startup_time
812     uptime = Utils.secs_to_string secs_up
813     # return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
814     return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
815   end
816
817   # we'll ping the server every 30 seconds or so, and expect a response
818   # before the next one come around..
819   def start_server_pings
820     stop_server_pings
821     return unless @config['server.ping_timeout'] > 0
822     # we want to respond to a hung server within 30 secs or so
823     @ping_timer = @timer.add(30) {
824       @last_ping = Time.now
825       @socket.queue "PING :rbot"
826     }
827     @pong_timer = @timer.add(10) {
828       unless @last_ping.nil?
829         diff = Time.now - @last_ping
830         unless diff < @config['server.ping_timeout']
831           debug "no PONG from server for #{diff} seconds, reconnecting"
832           begin
833             @socket.shutdown
834           rescue
835             debug "couldn't shutdown connection (already shutdown?)"
836           end
837           @last_ping = nil
838           raise TimeoutError, "no PONG from server in #{diff} seconds"
839         end
840       end
841     }
842   end
843
844   def stop_server_pings
845     @last_ping = nil
846     # stop existing timers if running
847     unless @ping_timer.nil?
848       @timer.remove @ping_timer
849       @ping_timer = nil
850     end
851     unless @pong_timer.nil?
852       @timer.remove @pong_timer
853       @pong_timer = nil
854     end
855   end
856
857   private
858
859   # handle help requests for "core" topics
860   def corehelp(topic="")
861     case topic
862       when "quit"
863         return "quit [<message>] => quit IRC with message <message>"
864       when "restart"
865         return "restart => completely stop and restart the bot (including reconnect)"
866       when "join"
867         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"
868       when "part"
869         return "part <channel> => part channel <channel>"
870       when "hide"
871         return "hide => part all channels"
872       when "save"
873         return "save => save current dynamic data and configuration"
874       when "rescan"
875         return "rescan => reload modules and static facts"
876       when "nick"
877         return "nick <nick> => attempt to change nick to <nick>"
878       when "say"
879         return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
880       when "action"
881         return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>"
882         #       when "topic"
883         #         return "topic <channel> <message> => set topic of <channel> to <message>"
884       when "quiet"
885         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>"
886       when "talk"
887         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>"
888       when "version"
889         return "version => describes software version"
890       when "botsnack"
891         return "botsnack => reward #{@nick} for being good"
892       when "hello"
893         return "hello|hi|hey|yo [#{@nick}] => greet the bot"
894       else
895         return "Core help topics: quit, restart, config, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
896     end
897   end
898
899   # handle incoming IRC PRIVMSG +m+
900   def onprivmsg(m)
901     # log it first
902     if(m.action?)
903       if(m.private?)
904         irclog "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
905       else
906         irclog "* #{m.sourcenick} #{m.message}", m.target
907       end
908     else
909       if(m.public?)
910         irclog "<#{m.sourcenick}> #{m.message}", m.target
911       else
912         irclog "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
913       end
914     end
915
916     @config['irc.ignore_users'].each { |mask| return if Irc.netmaskmatch(mask,m.source) }
917
918     # pass it off to plugins that want to hear everything
919     @plugins.delegate "listen", m
920
921     if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
922       notice m.sourcenick, "\001PING #$1\001"
923       irclog "@ #{m.sourcenick} pinged me"
924       return
925     end
926
927     if(m.address?)
928       delegate_privmsg(m)
929       case m.message
930         when (/^join\s+(\S+)\s+(\S+)$/i)
931           join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
932         when (/^join\s+(\S+)$/i)
933           join $1 if(@auth.allow?("join", m.source, m.replyto))
934         when (/^part$/i)
935           part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto))
936         when (/^part\s+(\S+)$/i)
937           part $1 if(@auth.allow?("join", m.source, m.replyto))
938         when (/^quit(?:\s+(.*))?$/i)
939           quit $1 if(@auth.allow?("quit", m.source, m.replyto))
940         when (/^restart(?:\s+(.*))?$/i)
941           restart $1 if(@auth.allow?("quit", m.source, m.replyto))
942         when (/^hide$/i)
943           join 0 if(@auth.allow?("join", m.source, m.replyto))
944         when (/^save$/i)
945           if(@auth.allow?("config", m.source, m.replyto))
946             save
947             m.okay
948           end
949         when (/^nick\s+(\S+)$/i)
950           nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
951         when (/^say\s+(\S+)\s+(.*)$/i)
952           say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
953         when (/^action\s+(\S+)\s+(.*)$/i)
954           action $1, $2 if(@auth.allow?("say", m.source, m.replyto))
955           # when (/^topic\s+(\S+)\s+(.*)$/i)
956           #   topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
957         when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i)
958           mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto))
959         when (/^ping$/i)
960           say m.replyto, "pong"
961         when (/^rescan$/i)
962           if(@auth.allow?("config", m.source, m.replyto))
963             m.reply "Saving ..."
964             save
965             m.reply "Rescanning ..."
966             rescan
967             m.okay
968           end
969         when (/^quiet$/i)
970           if(auth.allow?("talk", m.source, m.replyto))
971             m.okay
972             @channels.each_value {|c| c.quiet = true }
973           end
974         when (/^quiet in (\S+)$/i)
975           where = $1
976           if(auth.allow?("talk", m.source, m.replyto))
977             m.okay
978             where.gsub!(/^here$/, m.target) if m.public?
979             @channels[where].quiet = true if(@channels.has_key?(where))
980           end
981         when (/^talk$/i)
982           if(auth.allow?("talk", m.source, m.replyto))
983             @channels.each_value {|c| c.quiet = false }
984             m.okay
985           end
986         when (/^talk in (\S+)$/i)
987           where = $1
988           if(auth.allow?("talk", m.source, m.replyto))
989             where.gsub!(/^here$/, m.target) if m.public?
990             @channels[where].quiet = false if(@channels.has_key?(where))
991             m.okay
992           end
993         when (/^status\??$/i)
994           m.reply status if auth.allow?("status", m.source, m.replyto)
995         when (/^registry stats$/i)
996           if auth.allow?("config", m.source, m.replyto)
997             m.reply @registry.stat.inspect
998           end
999         when (/^(help\s+)?config(\s+|$)/)
1000           @config.privmsg(m)
1001         when (/^(version)|(introduce yourself)$/i)
1002           say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/"
1003         when (/^help(?:\s+(.*))?$/i)
1004           say m.replyto, help($1)
1005           #TODO move these to a "chatback" plugin
1006         when (/^(botsnack|ciggie)$/i)
1007           say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
1008           say m.replyto, @lang.get("thanks") if(m.private?)
1009         when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
1010           say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
1011           say m.replyto, @lang.get("hello") if(m.private?)
1012       end
1013     else
1014       # stuff to handle when not addressed
1015       case m.message
1016         when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(@nick)}$/i)
1017           say m.replyto, @lang.get("hello_X") % m.sourcenick
1018         when (/^#{Regexp.escape(@nick)}!*$/)
1019           say m.replyto, @lang.get("hello_X") % m.sourcenick
1020         else
1021           @keywords.privmsg(m)
1022       end
1023     end
1024   end
1025
1026   # log a message. Internal use only.
1027   def log_sent(type, where, message)
1028     case type
1029       when "NOTICE"
1030         if(where =~ /^#/)
1031           irclog "-=#{@nick}=- #{message}", where
1032         elsif (where =~ /(\S*)!.*/)
1033              irclog "[-=#{where}=-] #{message}", $1
1034         else
1035              irclog "[-=#{where}=-] #{message}"
1036         end
1037       when "PRIVMSG"
1038         if(where =~ /^#/)
1039           irclog "<#{@nick}> #{message}", where
1040         elsif (where =~ /^(\S*)!.*$/)
1041           irclog "[msg(#{where})] #{message}", $1
1042         else
1043           irclog "[msg(#{where})] #{message}", where
1044         end
1045     end
1046   end
1047
1048   def onjoin(m)
1049     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
1050     if(m.address?)
1051       debug "joined channel #{m.channel}"
1052       irclog "@ Joined channel #{m.channel}", m.channel
1053     else
1054       irclog "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
1055       @channels[m.channel].users[m.sourcenick] = Hash.new
1056       @channels[m.channel].users[m.sourcenick]["mode"] = ""
1057     end
1058
1059     @plugins.delegate("listen", m)
1060     @plugins.delegate("join", m)
1061   end
1062
1063   def onpart(m)
1064     if(m.address?)
1065       debug "left channel #{m.channel}"
1066       irclog "@ Left channel #{m.channel} (#{m.message})", m.channel
1067       @channels.delete(m.channel)
1068     else
1069       irclog "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
1070       if @channels.has_key?(m.channel)
1071         @channels[m.channel].users.delete(m.sourcenick)
1072       else
1073         warning "got part for channel '#{channel}' I didn't think I was in\n"
1074         # exit 2
1075       end
1076     end
1077
1078     # delegate to plugins
1079     @plugins.delegate("listen", m)
1080     @plugins.delegate("part", m)
1081   end
1082
1083   # respond to being kicked from a channel
1084   def onkick(m)
1085     if(m.address?)
1086       debug "kicked from channel #{m.channel}"
1087       @channels.delete(m.channel)
1088       irclog "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
1089     else
1090       @channels[m.channel].users.delete(m.sourcenick)
1091       irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
1092     end
1093
1094     @plugins.delegate("listen", m)
1095     @plugins.delegate("kick", m)
1096   end
1097
1098   def ontopic(m)
1099     @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
1100     @channels[m.channel].topic = m.topic if !m.topic.nil?
1101     @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
1102     @channels[m.channel].topic.by = m.source if !m.source.nil?
1103
1104     debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
1105   end
1106
1107   # delegate a privmsg to auth, keyword or plugin handlers
1108   def delegate_privmsg(message)
1109     [@auth, @plugins, @keywords].each {|m|
1110       break if m.privmsg(message)
1111     }
1112   end
1113 end
1114
1115 end