]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/ircbot.rb
windows hates logging to files with : in them :p
[user/henk/code/ruby/rbot.git] / lib / rbot / ircbot.rb
index 7cfda371b13c11db067f820ebcf377c451cb6229..d07c4977aa9542cd94867446950a3e9b65257d95 100644 (file)
@@ -5,7 +5,8 @@ require 'fileutils'
 $debug = false unless $debug
 # print +message+ if debugging is enabled
 def debug(message=nil)
-  print "DEBUG: #{message}\n" if($debug && message)
+  stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
+  print "D: [#{stamp}] #{message}\n" if($debug && message)
   #yield
 end
 
@@ -51,9 +52,6 @@ class IrcBot
   # bot's Language data
   attr_reader :lang
 
-  # bot's configured addressing prefixes
-  attr_reader :addressing_prefixes
-
   # channel info for channels the bot is in
   attr_reader :channels
 
@@ -112,15 +110,20 @@ class IrcBot
       :default => 4, :validate => Proc.new{|v| v >= 0},
       :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines, with non-burst limits of 512 bytes/2 seconds",
       :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
+    BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
+      :default => 10, :validate => Proc.new{|v| v >= 0},
+      :on_change => Proc.new {|bot, v| bot.start_server_pings},
+      :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
 
     @argv = params[:argv]
 
     unless FileTest.directory? Config::datadir
-      puts "data directory '#{Config::datadir}' not found, did you install.rb?"
+      puts "data directory '#{Config::datadir}' not found, did you setup.rb?"
       exit 2
     end
     
-    botclass = "/home/#{Etc.getlogin}/.rbot" unless botclass
+    #botclass = "#{Etc.getpwnam(Etc.getlogin).dir}/.rbot" unless botclass
+    botclass = "#{ENV['HOME']}/.rbot" unless botclass
     @botclass = botclass.gsub(/\/$/, "")
 
     unless FileTest.directory? botclass
@@ -134,6 +137,9 @@ class IrcBot
     
     Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
 
+    @ping_timer = nil
+    @pong_timer = nil
+    @last_ping = nil
     @startup_time = Time.new
     @config = BotConfig.new(self)
 # TODO background self after botconfig has a chance to run wizard
@@ -142,7 +148,6 @@ class IrcBot
     @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
     @channels = Hash.new
     @logs = Hash.new
-    
     @httputil = Utils::HttpUtil.new(self)
     @lang = Language::Language.new(@config['core.language'])
     @keywords = Keywords.new(self)
@@ -153,7 +158,7 @@ class IrcBot
 
     @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
     @nick = @config['irc.nick']
-    
+
     @client = IrcClient.new
     @client[:privmsg] = proc { |data|
       message = PrivMessage.new(self, data[:source], data[:target], data[:message])
@@ -179,6 +184,9 @@ class IrcBot
       # (jump the queue for pongs)
       @socket.puts "PONG #{data[:pingid]}"
     }
+    @client[:pong] = proc {|data|
+      @last_ping = nil
+    }
     @client[:nick] = proc {|data|
       sourcenick = data[:sourcenick]
       nick = data[:nick]
@@ -203,7 +211,7 @@ class IrcBot
       sourceurl = data[:sourceaddress]
       message = data[:message]
       m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
-      if(data[:sourcenick] =~ /#{@nick}/i)
+      if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
       else
         @channels.each {|k,v|
           if(v.users.has_key?(sourcenick))
@@ -255,7 +263,7 @@ class IrcBot
       onkick(m)
     }
     @client[:invite] = proc {|data|
-      if(data[:target] =~ /^#{@nick}$/i)
+      if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
         join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
       end
     }
@@ -294,48 +302,60 @@ class IrcBot
     }
     @client[:unknown] = proc {|data|
       #debug "UNKNOWN: #{data[:serverstring]}"
-      log data[:serverstring], ":unknown"
+      log data[:serverstring], ".unknown"
     }
   end
 
   # connect the bot to IRC
   def connect
-    trap("SIGTERM") { quit }
-    trap("SIGHUP") { quit }
-    trap("SIGINT") { quit }
+    begin
+      trap("SIGINT") { quit }
+      trap("SIGTERM") { quit }
+      trap("SIGHUP") { quit }
+    rescue
+      debug "failed to trap signals, probably running on windows?"
+    end
     begin
       @socket.connect
-      rescue => e
+    rescue => e
       raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
     end
     @socket.puts "PASS " + @config['server.password'] if @config['server.password']
     @socket.puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
+    start_server_pings
   end
 
   # begin event handling loop
   def mainloop
     while true
+      begin
       connect
       @timer.start
       
-      begin
         while true
           if @socket.select
             break unless reply = @socket.gets
             @client.process reply
           end
         end
-      rescue TimeoutError, SocketError => e
-        puts "network exception: connection closed: #{e}"
+      # I despair of this. Some of my users get "connection reset by peer"
+      # exceptions that ARENT SocketError's. How am I supposed to handle
+      # that?
+      #rescue TimeoutError, SocketError => e
+      rescue SystemExit
+        exit 0
+      rescue Exception => e
+        puts "network exception: connection closed: #{e.inspect}"
         puts e.backtrace.join("\n")
-        @socket.close # now we reconnect
-      rescue => e # TODO be selective, only grab Network errors
-        puts "unexpected exception: connection closed: #{e}"
+        @socket.shutdown # now we reconnect
+      rescue => e
+        puts "unexpected exception: connection closed: #{e.inspect}"
         puts e.backtrace.join("\n")
         exit 2
       end
       
       puts "disconnected"
+      @last_ping = nil
       @channels.clear
       @socket.clearq
       
@@ -434,9 +454,13 @@ class IrcBot
 
   # disconnect from the server and cleanup all plugins and modules
   def shutdown(message = nil)
-    trap("SIGTERM", "DEFAULT")
-    trap("SIGHUP", "DEFAULT")
-    trap("SIGINT", "DEFAULT")
+    begin
+      trap("SIGINT", "DEFAULT")
+      trap("SIGTERM", "DEFAULT")
+      trap("SIGHUP", "DEFAULT")
+    rescue
+      debug "failed to trap signals, probably running on windows?"
+    end
     message = @lang.get("quit") if (message.nil? || message.empty?)
     @socket.clearq
     save
@@ -444,23 +468,27 @@ class IrcBot
     @channels.each_value {|v|
       log "@ quit (#{message})", v.name
     }
+    @registry.close
     @socket.puts "QUIT :#{message}"
     @socket.flush
     @socket.shutdown
-    @registry.close
     puts "rbot quit (#{message})"
   end
   
   # message:: optional IRC quit message
   # quit IRC, shutdown the bot
   def quit(message=nil)
-    shutdown(message)
-    exit 0
+    begin
+      shutdown(message)
+    ensure
+      exit 0
+    end
   end
 
   # totally shutdown and respawn the bot
-  def restart
-    shutdown("restarting, back in #{@config['server.reconnect_wait']}...")
+  def restart(message = false)
+    msg = message ? message : "restarting, back in #{@config['server.reconnect_wait']}..."
+    shutdown(msg)
     sleep @config['server.reconnect_wait']
     # now we re-exec
     exec($0, *@argv)
@@ -499,11 +527,6 @@ class IrcBot
   end
 
   # attempt to change bot's nick to +name+
-  # FIXME
-  # if rbot is already taken, this happens:
-  #   <giblet> rbot_, nick rbot
-  #   --- rbot_ is now known as rbot__
-  # he should of course just keep his existing nick and report the error :P
   def nickchg(name)
       sendq "NICK #{name}"
   end
@@ -550,6 +573,42 @@ class IrcBot
     return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
   end
 
+  # we'll ping the server every 30 seconds or so, and expect a response
+  # before the next one come around..
+  def start_server_pings
+    @last_ping = nil
+    # stop existing timers if running
+    unless @ping_timer.nil?
+      @timer.remove @ping_timer
+      @ping_timer = nil
+    end
+    unless @pong_timer.nil?
+      @timer.remove @pong_timer
+      @pong_timer = nil
+    end
+    return unless @config['server.ping_timeout'] > 0
+    # we want to respond to a hung server within 30 secs or so
+    @ping_timer = @timer.add(30) {
+      @last_ping = Time.now
+      @socket.puts "PING :rbot"
+    }
+    @pong_timer = @timer.add(10) {
+      unless @last_ping.nil?
+        diff = Time.now - @last_ping
+        unless diff < @config['server.ping_timeout']
+          debug "no PONG from server for #{diff} seconds, reconnecting"
+          begin
+            @socket.shutdown
+            # TODO
+            # raise an exception to get back to the mainloop
+          rescue
+            debug "couldn't shutdown connection (already shutdown?)"
+          end
+          @last_ping = nil
+        end
+      end
+    }
+  end
 
   private
 
@@ -631,8 +690,8 @@ class IrcBot
           part $1 if(@auth.allow?("join", m.source, m.replyto))
         when (/^quit(?:\s+(.*))?$/i)
           quit $1 if(@auth.allow?("quit", m.source, m.replyto))
-        when (/^restart$/i)
-          restart if(@auth.allow?("quit", m.source, m.replyto))
+        when (/^restart(?:\s+(.*))?$/i)
+          restart $1 if(@auth.allow?("quit", m.source, m.replyto))
         when (/^hide$/i)
           join 0 if(@auth.allow?("join", m.source, m.replyto))
         when (/^save$/i)
@@ -706,9 +765,9 @@ class IrcBot
     else
       # stuff to handle when not addressed
       case m.message
-        when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{@nick}$/i)
+        when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(@nick)}$/i)
           say m.replyto, @lang.get("hello_X") % m.sourcenick
-        when (/^#{@nick}!*$/)
+        when (/^#{Regexp.escape(@nick)}!*$/)
           say m.replyto, @lang.get("hello_X") % m.sourcenick
         else
           @keywords.privmsg(m)
@@ -798,7 +857,6 @@ class IrcBot
       break if m.privmsg(message)
     }
   end
-
 end
 
 end