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