]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/ircbot.rb
Oops, wrong way to remove the path from the module names; fix it, and provide test...
[user/henk/code/ruby/rbot.git] / lib / rbot / ircbot.rb
index 6d76cc4962795f76baf9c3f24986c195736ff843..4df071f835c0a7476913ee9f6ed1d6a2c6a1e765 100644 (file)
@@ -5,15 +5,27 @@ require 'fileutils'
 $debug = false unless $debug
 $daemonize = false unless $daemonize
 
+# TODO we should use the actual Logger class
 def rawlog(code="", message=nil)
   if !code || code.empty?
     c = "  "
   else
     c = code.to_s[0,1].upcase + ":"
   end
+  call_stack = caller
+  case call_stack.length
+  when 0
+    $stderr.puts "ERROR IN THE LOGGING SYSTEM, THIS CAN'T HAPPEN"
+    who = "WTF1??  "
+  when 1
+    $stderr.puts "ERROR IN THE LOGGING SYSTEM, THIS CAN'T HAPPEN"
+    who = "WTF2??  "
+  else
+    who = call_stack[1].sub(%r{(?:.+)/([^/]+):(\d+)(:in .*)?}) { "#{$1}:#{$2}#{$3}" }
+  end
   stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
   message.to_s.each_line { |l|
-    $stdout.puts "#{c} [#{stamp}] #{l}"
+    $stdout.puts "#{c} [#{stamp}] #{who} -- #{l}"
   }
   $stdout.flush
 end
@@ -23,7 +35,7 @@ def log(message=nil)
 end
 
 def log_session_end
-   log("\n=== #{botclass} session ended ===") if $daemonize
+   rawlog("", "\n=== #{botclass} session ended ===") if $daemonize
 end
 
 def debug(message=nil)
@@ -38,6 +50,11 @@ def error(message=nil)
   rawlog("E", message)
 end
 
+debug "debug test"
+log "log test"
+warning "warning test"
+error "error test"
+
 # The following global is used for the improved signal handling.
 $interrupted = 0
 
@@ -83,6 +100,9 @@ class IrcBot
   # bot's Language data
   attr_reader :lang
 
+  # capabilities info for the server
+  attr_reader :capabilities
+
   # channel info for channels the bot is in
   attr_reader :channels
 
@@ -94,6 +114,9 @@ class IrcBot
   # and restore objects in their own namespaces.)
   attr_reader :registry
 
+  # bot's plugins. This is an instance of class Plugins
+  attr_reader :plugins
+
   # bot's httputil help object, for fetching resources via http. Sets up
   # proxies etc as defined by the bot configuration/environment
   attr_reader :httputil
@@ -134,7 +157,7 @@ class IrcBot
       :desc => "(flood prevention) max bytes/seconds rate to send the server. Most ircd's have limits of 512 bytes/2 seconds",
       :on_change => Proc.new {|bot, v| bot.socket.byterate = v })
     BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
-      :default => 10, :validate => Proc.new{|v| v >= 0},
+      :default => 30, :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)")
 
@@ -177,8 +200,23 @@ class IrcBot
       exit 2
     end
 
-    botclass = "#{Etc.getpwuid(Process::Sys.geteuid)[:dir]}/.rbot" unless botclass
-    #botclass = "#{ENV['HOME']}/.rbot" unless botclass
+    unless botclass and not botclass.empty?
+      # We want to find a sensible default.
+      #  * On POSIX systems we prefer ~/.rbot for the effective uid of the process
+      #  * On Windows (at least the NT versions) we want to put our stuff in the
+      #    Application Data folder.
+      # We don't use any particular O/S detection magic, exploiting the fact that
+      # Etc.getpwuid is nil on Windows
+      if Etc.getpwuid(Process::Sys.geteuid)
+        botclass = Etc.getpwuid(Process::Sys.geteuid)[:dir].dup
+      else
+        if ENV.has_key?('APPDATA')
+          botclass = ENV['APPDATA'].dup
+          botclass.gsub!("\\","/")
+        end
+      end
+      botclass += "/.rbot"
+    end
     botclass = File.expand_path(botclass)
     @botclass = botclass.gsub(/\/$/, "")
 
@@ -250,6 +288,19 @@ class IrcBot
     @nick = @config['irc.nick']
 
     @client = IrcClient.new
+    @client[:isupport] = proc { |data|
+      if data[:capab]
+        sendq "CAPAB IDENTIFY-MSG"
+      end
+    }
+    @client[:datastr] = proc { |data|
+      debug data.inspect
+      if data[:text] == "IDENTIFY-MSG"
+        @capabilities["identify-msg".to_sym] = true
+      else
+        debug "Not handling RPL_DATASTR #{data[:servermessage]}"
+      end
+    }
     @client[:privmsg] = proc { |data|
       message = PrivMessage.new(self, data[:source], data[:target], data[:message])
       onprivmsg(message)
@@ -272,8 +323,7 @@ class IrcBot
       warning "bad nick (#{data[:nick]})"
     }
     @client[:ping] = proc {|data|
-      # (jump the queue for pongs)
-      @socket.puts "PONG #{data[:pingid]}"
+      @socket.queue "PONG #{data[:pingid]}"
     }
     @client[:pong] = proc {|data|
       @last_ping = nil
@@ -424,12 +474,14 @@ class IrcBot
       debug "failed to trap signals: #{e.inspect}"
     end
     begin
-      @socket.connect unless $interrupted > 0
+      quit if $interrupted > 0
+      @socket.connect
     rescue => e
       raise e.class, "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"
+    @socket.emergency_puts "PASS " + @config['server.password'] if @config['server.password']
+    @socket.emergency_puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
+    @capabilities = Hash.new
     start_server_pings
   end
 
@@ -437,6 +489,7 @@ class IrcBot
   def mainloop
     while true
       begin
+       quit if $interrupted > 0
         connect
         @timer.start
 
@@ -445,7 +498,7 @@ class IrcBot
             break unless reply = @socket.gets
             @client.process reply
           end
-          quit if $interrupted > 0
+         quit if $interrupted > 0
         end
 
       # I despair of this. Some of my users get "connection reset by peer"
@@ -454,34 +507,34 @@ class IrcBot
       rescue SystemExit
         log_session_end
         exit 0
-      rescue TimeoutError, SocketError => e
+      rescue Errno::ETIMEDOUT, TimeoutError, SocketError => e
         error "network exception: #{e.class}: #{e}"
-        debug e.inspect
         debug e.backtrace.join("\n")
       rescue BDB::Fatal => e
         error "fatal bdb error: #{e.class}: #{e}"
-        error e.inspect
         error e.backtrace.join("\n")
         DBTree.stats
         restart("Oops, we seem to have registry problems ...")
       rescue Exception => e
         error "non-net exception: #{e.class}: #{e}"
-        error e.inspect
         error e.backtrace.join("\n")
-        @socket.shutdown # now we reconnect
       rescue => e
         error "unexpected exception: #{e.class}: #{e}"
-        error e.inspect
         error e.backtrace.join("\n")
         log_session_end
         exit 2
       end
 
-      log "disconnected"
-
       stop_server_pings
       @channels.clear
-      @socket.clearq
+      if @socket.connected?
+        @socket.clearq
+        @socket.shutdown
+      end
+
+      log "disconnected"
+
+      quit if $interrupted > 0
 
       log "waiting to reconnect"
       sleep @config['server.reconnect_wait']
@@ -495,7 +548,7 @@ class IrcBot
   # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
   # relevant say() or notice() methods. This one should be used for IRCd
   # extensions you want to use in modules.
-  def sendmsg(type, where, message)
+  def sendmsg(type, where, message, chan=nil, ring=0)
     # limit it according to the byterate, splitting the message
     # taking into consideration the actual message length
     # and all the extra stuff
@@ -504,7 +557,7 @@ class IrcBot
     left = @socket.bytes_per - type.length - where.length - 4
     begin
       if(left >= message.length)
-        sendq("#{type} #{where} :#{message}")
+        sendq "#{type} #{where} :#{message}", chan, ring
         log_sent(type, where, message)
         return
       end
@@ -514,46 +567,88 @@ class IrcBot
         message = line.slice!(lastspace, line.length) + message
         message.gsub!(/^\s+/, "")
       end
-      sendq("#{type} #{where} :#{line}")
+      sendq "#{type} #{where} :#{line}", chan, ring
       log_sent(type, where, line)
     end while(message.length > 0)
   end
 
   # queue an arbitraty message for the server
-  def sendq(message="")
+  def sendq(message="", chan=nil, ring=0)
     # temporary
-    @socket.queue(message)
+    @socket.queue(message, chan, ring)
   end
 
   # send a notice message to channel/nick +where+
-  def notice(where, message)
+  def notice(where, message, mchan=nil, mring=-1)
+    if mchan == ""
+      chan = where
+    else
+      chan = mchan
+    end
+    if mring < 0
+      if where =~ /^#/
+        ring = 2
+      else
+        ring = 1
+      end
+    else
+      ring = mring
+    end
     message.each_line { |line|
       line.chomp!
       next unless(line.length > 0)
-      sendmsg("NOTICE", where, line)
+      sendmsg "NOTICE", where, line, chan, ring
     }
   end
 
   # say something (PRIVMSG) to channel/nick +where+
-  def say(where, message)
+  def say(where, message, mchan="", mring=-1)
+    if mchan == ""
+      chan = where
+    else
+      chan = mchan
+    end
+    if mring < 0
+      if where =~ /^#/
+        ring = 2
+      else
+        ring = 1
+      end
+    else
+      ring = mring
+    end
     message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
       line.chomp!
       next unless(line.length > 0)
       unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
-        sendmsg("PRIVMSG", where, line)
+        sendmsg "PRIVMSG", where, line, chan, ring 
       end
     }
   end
 
   # perform a CTCP action with message +message+ to channel/nick +where+
-  def action(where, message)
-    sendq("PRIVMSG #{where} :\001ACTION #{message}\001")
+  def action(where, message, mchan="", mring=-1)
+    if mchan == ""
+      chan = where
+    else
+      chan = mchan
+    end
+    if mring < 0
+      if where =~ /^#/
+        ring = 2
+      else
+        ring = 1
+      end
+    else
+      ring = mring
+    end
+    sendq "PRIVMSG #{where} :\001ACTION #{message}\001", chan, ring
     if(where =~ /^#/)
       irclog "* #{@nick} #{message}", where
     elsif (where =~ /^(\S*)!.*$/)
-         irclog "* #{@nick}[#{where}] #{message}", $1
+      irclog "* #{@nick}[#{where}] #{message}", $1
     else
-         irclog "* #{@nick}[#{where}] #{message}", where
+      irclog "* #{@nick}[#{where}] #{message}", where
     end
   end
 
@@ -578,7 +673,7 @@ class IrcBot
 
   # set topic of channel +where+ to +topic+
   def topic(where, topic)
-    sendq "TOPIC #{where} :#{topic}"
+    sendq "TOPIC #{where} :#{topic}", where, 2
   end
 
   # disconnect from the server and cleanup all plugins and modules
@@ -597,7 +692,7 @@ class IrcBot
       debug "Clearing socket"
       @socket.clearq
       debug "Sending quit message"
-      @socket.puts "QUIT :#{message}"
+      @socket.emergency_puts "QUIT :#{message}"
       debug "Flushing socket"
       @socket.flush
       debug "Shutting down socket"
@@ -660,15 +755,15 @@ class IrcBot
   # join a channel
   def join(channel, key=nil)
     if(key)
-      sendq "JOIN #{channel} :#{key}"
+      sendq "JOIN #{channel} :#{key}", channel, 2
     else
-      sendq "JOIN #{channel}"
+      sendq "JOIN #{channel}", channel, 2
     end
   end
 
   # part a channel
   def part(channel, message="")
-    sendq "PART #{channel} :#{message}"
+    sendq "PART #{channel} :#{message}", channel, 2
   end
 
   # attempt to change bot's nick to +name+
@@ -678,7 +773,7 @@ class IrcBot
 
   # changing mode
   def mode(channel, mode, target)
-      sendq "MODE #{channel} #{mode} #{target}"
+      sendq "MODE #{channel} #{mode} #{target}", channel, 2
   end
 
   # m::     message asking for help
@@ -727,7 +822,7 @@ class IrcBot
     # 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"
+      @socket.queue "PING :rbot"
     }
     @pong_timer = @timer.add(10) {
       unless @last_ping.nil?