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