]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/rfc2812.rb
fix: TCPSocked.gethostbyname is deprecated
[user/henk/code/ruby/rbot.git] / lib / rbot / rfc2812.rb
index 9de781a8276845763e8b3a311d041ff7a686961b..0839d1d52593db514d4458a92c19b57c4c6c7b1a 100644 (file)
@@ -4,7 +4,10 @@
 # :title: RFC 2821 Client Protocol module
 #
 # This module defines the Irc::Client class, a class that can handle and
-# dispatch messages based on RFC 2821i (Internet Relay Chat: Client Protocol)
+# dispatch messages based on RFC 2821 (Internet Relay Chat: Client Protocol)
+
+class ServerMessageParseError < ServerError
+end
 
 module Irc
   # - The server sends Replies 001 to 004 to a user upon
@@ -144,6 +147,12 @@ module Irc
   # "<channel> <mode> <mode params>"
   RPL_CHANNELMODEIS=324
 
+  # "<channel> <unixtime>"
+  RPL_CREATIONTIME=329
+
+  # "<channel> <url>"
+  RPL_CHANNEL_URL=328
+
   # "<channel> :No topic is set"
   RPL_NOTOPIC=331
 
@@ -940,6 +949,9 @@ module Irc
   ERR_NOSERVICEHOST=492
   RPL_DATASTR=290
 
+  # A structure to hold LIST data, in the Irc namespace
+  ListData = Struct.new :channel, :users, :topic
+
   # Implements RFC 2812 and prior IRC RFCs.
   #
   # Clients should register Proc{}s to handle the various server events, and
@@ -954,19 +966,22 @@ module Irc
     # Create a new Client instance
     def initialize
       @server = Server.new         # The Server
-      @user = @server.user("")     # The User representing the client on this Server
+      @user = @server.user("*!*@*")     # The User representing the client on this Server
 
       @handlers = Hash.new
 
       # This is used by some messages to build lists of users that
       # will be delegated when the ENDOF... message is received
       @tmpusers = []
+
+      # Same as above, just for bans
+      @tmpbans = []
     end
 
     # Clear the server and reset the user
     def reset
       @server.clear
-      @user = @server.user("")
+      @user = @server.user("*!*@*")
     end
 
     # key::   server event to handle
@@ -975,7 +990,6 @@ module Irc
     #
     # ==server events currently supported:
     #
-    # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL
     # TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
     #
     # welcome::     server welcome message on connect
@@ -1022,7 +1036,7 @@ module Irc
       data[:serverstring] = serverstring
 
       unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/
-        raise "Unparseable Server Message!!!: #{serverstring.inspect}"
+        raise ServerMessageParseError, (serverstring.chomp rescue serverstring)
       end
 
       prefix, command, params = $2, $3, $5
@@ -1069,13 +1083,14 @@ module Irc
         num=command.to_i
         case num
         when RPL_WELCOME
+          data[:message] = argv[1]
           # "Welcome to the Internet Relay Network
           # <nick>!<user>@<host>"
           if not_us
             warning "Server thinks client (#{@user.inspect}) has a different nick"
             @user.nick = data[:target]
           end
-          if argv[1] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
+          if data[:message] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
             nick = $1
             user = $2
             host = $3
@@ -1121,19 +1136,14 @@ module Irc
           data[:message] = argv[2]
           handle(:badnick, data)
         when RPL_TOPIC
-          data[:channel] = @server.get_channel(argv[1])
+          data[:channel] = @server.channel(argv[1])
           data[:topic] = argv[2]
-
-          if data[:channel]
-            data[:channel].topic.text = data[:topic]
-          else
-            warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
-          end
+          data[:channel].topic.text = data[:topic]
 
           handle(:topic, data)
         when RPL_TOPIC_INFO
           data[:nick] = @server.user(argv[0])
-          data[:channel] = @server.get_channel(argv[1])
+          data[:channel] = @server.channel(argv[1])
 
           # This must not be an IRC::User because it might not be an actual User,
           # and we risk overwriting valid User data
@@ -1141,12 +1151,8 @@ module Irc
 
           data[:time] = Time.at(argv[3].to_i)
 
-          if data[:channel]
-            data[:channel].topic.set_by = data[:source]
-            data[:channel].topic.set_on = data[:time]
-          else
-            warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
-          end
+          data[:channel].topic.set_by = data[:source]
+          data[:channel].topic.set_on = data[:time]
 
           handle(:topicinfo, data)
         when RPL_NAMREPLY
@@ -1155,13 +1161,7 @@ module Irc
           # - "@" is used for secret channels, "*" for private
           # channels, and "=" for others (public channels).
           data[:channeltype] = argv[1]
-          data[:channel] = argv[2]
-
-          chan = @server.get_channel(data[:channel])
-          unless chan
-            warning "Received names #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
-            return
-          end
+          data[:channel] = chan = @server.channel(argv[2])
 
           users = []
           argv[3].scan(/\S+/).each { |u|
@@ -1178,18 +1178,28 @@ module Irc
             chan.add_user(u, :silent => true)
             debug "Adding user #{u}"
             if ar[1]
-              m = @server.supports[:prefix][:prefixes].index(ar[1].to_sym)
-              ms = @server.supports[:prefix][:modes][m]
+              ms = @server.mode_for_prefix(ar[1].to_sym)
               debug "\twith mode #{ar[1]} (#{ms})"
               chan.mode[ms].set(u)
             end
           }
           @tmpusers += users
         when RPL_ENDOFNAMES
-          data[:channel] = argv[1]
+          data[:channel] = @server.channel(argv[1])
           data[:users] = @tmpusers
           handle(:names, data)
           @tmpusers = Array.new
+        when RPL_BANLIST
+          data[:channel] = @server.channel(argv[1])
+          data[:mask] = argv[2]
+          data[:by] = argv[3]
+          data[:at] = argv[4]
+          @tmpbans << data
+        when RPL_ENDOFBANLIST
+          data[:channel] = @server.channel(argv[1])
+          data[:bans] = @tmpbans
+          handle(:banlist, data)
+          @tmpbans = Array.new
         when RPL_LUSERCLIENT
           # ":There are <integer> users and <integer>
           # services on <integer> servers"
@@ -1247,12 +1257,17 @@ module Irc
         when RPL_DATASTR
           data[:text] = argv[1]
           handle(:datastr, data)
+        when RPL_AWAY
+          data[:nick] = user = @server.user(argv[1])
+          data[:message] = argv[-1]
+          user.away = data[:message]
+          handle(:away, data)
        when RPL_WHOREPLY
-          data[:channel] = argv[1]
+          data[:channel] = channel = @server.channel(argv[1])
           data[:user] = argv[2]
           data[:host] = argv[3]
           data[:userserver] = argv[4]
-          data[:nick] = argv[5]
+          data[:nick] = user = @server.user(argv[5])
           if argv[6] =~ /^(H|G)(\*)?(.*)?$/
             data[:away] = ($1 == 'G')
             data[:ircop] = $2
@@ -1265,8 +1280,6 @@ module Irc
           end
           data[:hopcount], data[:real_name] = argv[7].split(" ", 2)
 
-          user = @server.get_user(data[:nick])
-
           user.user = data[:user]
           user.host = data[:host]
           user.away = data[:away] # FIXME doesn't provide the actual message
@@ -1275,8 +1288,6 @@ module Irc
           # TODO hopcount
           user.real_name = data[:real_name]
 
-          channel = @server.get_channel(data[:channel])
-
           channel.add_user(user, :silent=>true)
           data[:modes].map { |mode|
             channel.mode[mode].set(user)
@@ -1285,7 +1296,101 @@ module Irc
           handle(:who, data)
         when RPL_ENDOFWHO
           handle(:eowho, data)
+        when RPL_WHOISUSER
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          @whois[:user] = argv[2]
+          @whois[:host] = argv[3]
+          @whois[:real_name] = argv[-1]
+
+          user = @server.user(@whois[:nick])
+          user.user = @whois[:user]
+          user.host = @whois[:host]
+          user.real_name = @whois[:real_name]
+        when RPL_WHOISSERVER
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          @whois[:server] = argv[2]
+          @whois[:server_info] = argv[-1]
+          # TODO update user info
+        when RPL_WHOISOPERATOR
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          @whois[:operator] = argv[-1]
+          # TODO update user info
+        when RPL_WHOISIDLE
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          user = @server.user(@whois[:nick])
+          @whois[:idle] = argv[2].to_i
+          user.idle_since = Time.now - @whois[:idle]
+          if argv[-1] == 'seconds idle, signon time'
+            @whois[:signon] = Time.at(argv[3].to_i)
+            user.signon = @whois[:signon]
+          end
+        when RPL_ENDOFWHOIS
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          data[:whois] = @whois.dup
+          @whois.clear
+          handle(:whois, data)
+        when RPL_WHOISCHANNELS
+          @whois ||= Hash.new
+          @whois[:nick] = argv[1]
+          @whois[:channels] ||= []
+          user = @server.user(@whois[:nick])
+          argv[-1].split.each do |prechan|
+            pfx = prechan.scan(/[#{@server.supports[:prefix][:prefixes].join}]/)
+            modes = pfx.map { |p| @server.mode_for_prefix p }
+            chan = prechan[pfx.length..prechan.length]
+
+            channel = @server.channel(chan)
+            channel.add_user(user, :silent => true)
+            modes.map { |mode| channel.mode[mode].set(user) }
+
+            @whois[:channels] << [chan, modes]
+          end
+        when RPL_LISTSTART
+          # ignore
+        when RPL_LIST
+          @list ||= Hash.new
+          chan = argv[1]
+          users = argv[2]
+          topic = argv[3]
+          @list[chan] = ListData.new(chan, users, topic)
+        when RPL_LISTEND
+          @list ||= Hash.new
+          data[:list] = @list
+          handle(:list, data)
+        when RPL_CHANNELMODEIS
+          parse_mode(serverstring, argv[1..-1], data)
+          handle(:mode, data)
+        when RPL_CREATIONTIME
+          data[:channel] = @server.channel(argv[1])
+          data[:time] = Time.at(argv[2].to_i)
+          data[:channel].creation_time=data[:time]
+          handle(:creationtime, data)
+        when RPL_CHANNEL_URL
+          data[:channel] = @server.channel(argv[1])
+          data[:url] = argv[2]
+          data[:channel].url=data[:url].dup
+          handle(:channel_url, data)
+        when ERR_NOSUCHNICK
+          data[:target] = argv[1]
+          data[:message] = argv[2]
+          handle(:nosuchtarget, data)
+          if user = @server.get_user(data[:target])
+            @server.delete_user(user)
+          end
+        when ERR_NOSUCHCHANNEL
+          data[:target] = argv[1]
+          data[:message] = argv[2]
+          handle(:nosuchtarget, data)
+          if channel = @server.get_channel(data[:target])
+            @server.delete_channel(channel)
+          end
         else
+          warning "Unknown message #{serverstring.inspect}"
           handle(:unknown, data)
         end
        return # We've processed the numeric reply
@@ -1401,96 +1506,136 @@ module Irc
 
         handle(:nick, data)
       when :MODE
-        # MODE ([+-]<modes> (<params>)*)*
-        # When a MODE message is received by a server,
-        # Type C will have parameters too, so we must
-        # be able to consume parameters for all
-        # but Type D modes
-
-        data[:channel] = @server.user_or_channel(argv[0])
-        data[:modestring] = argv[1..-1].join(" ")
-        case data[:channel]
-        when User
-          # TODO
-          warning "Unhandled user mode message '#{serverstring}'"
-        else
-          # data[:modes] is an array where each element
-          # is either a flag which doesn't need parameters
-          # or an array with a flag which needs parameters
-          # and the corresponding parameter
-          data[:modes] = []
-          # array of indices in data[:modes] where parameters
-          # are needed
-          who_wants_params = []
-
-          argv[1..-1].each { |arg|
-            setting = arg[0].chr
-            if "+-".include?(setting)
-              arg[1..-1].each_byte { |b|
-                m = b.chr
-                case m.to_sym
+        parse_mode(serverstring, argv, data)
+        handle(:mode, data)
+      when :ERROR
+        data[:message] = argv[1]
+        handle(:error, data)
+      else
+        warning "Unknown message #{serverstring.inspect}"
+        handle(:unknown, data)
+      end
+    end
+
+    private
+
+    # key::  server event name
+    # data:: hash containing data about the event, passed to the proc
+    # call client's proc for an event, if they set one as a handler
+    def handle(key, data)
+      if(@handlers.has_key?(key))
+        @handlers[key].call(data)
+      end
+    end
+
+    # RPL_CHANNELMODEIS
+    # MODE ([+-]<modes> (<params>)*)*
+    # When a MODE message is received by a server,
+    # Type C will have parameters too, so we must
+    # be able to consume parameters for all
+    # but Type D modes
+    def parse_mode(serverstring, argv, data)
+      data[:target] = @server.user_or_channel(argv[0])
+      data[:modestring] = argv[1..-1].join(" ")
+      # data[:modes] is an array where each element
+      # is an array with two elements, the first of which
+      # is either :set or :reset, and the second symbol
+      # is the mode letter. An optional third element
+      # is present e.g. for channel modes that need
+      # a parameter
+      data[:modes] = []
+      case data[:target]
+      when User
+        # User modes aren't currently handled internally,
+        # but we still parse them and delegate to the client
+        warning "Unhandled user mode message '#{serverstring}'"
+        argv[1..-1].each { |arg|
+          setting = arg[0].chr
+          if "+-".include?(setting)
+            setting = setting == "+" ? :set : :reset
+            arg[1..-1].each_byte { |b|
+              m = b.chr.intern
+              data[:modes] << [setting, m]
+            }
+          else
+            # Although typically User modes don't take an argument,
+            # this is not true for all modes on all servers. Since
+            # we have no knowledge of which modes take parameters
+            # and which don't we just assign it to the last
+            # mode. This is not going to do strange things often,
+            # as usually User modes are only set one at a time
+            warning "Unhandled user mode parameter #{arg} found"
+            data[:modes].last << arg
+          end
+        }
+      when Channel
+        # array of indices in data[:modes] where parameters
+        # are needed
+        who_wants_params = []
+
+        modes = argv[1..-1].dup
+        debug modes
+        getting_args = false
+        while arg = modes.shift
+          debug arg
+          if getting_args
+            # getting args for previously set modes
+            idx = who_wants_params.shift
+            if idx.nil?
+              warning "Oops, problems parsing #{serverstring.inspect}"
+              break
+            end
+            data[:modes][idx] << arg
+            getting_args = false if who_wants_params.empty?
+          else
+            debug @server.supports[:chanmodes]
+            setting = :set
+            arg.each_byte do |c|
+              m = c.chr.intern
+              case m
+              when :+
+                setting = :set
+              when :-
+                setting = :reset
+              else
+                data[:modes] << [setting, m]
+                case m
                 when *@server.supports[:chanmodes][:typea]
-                  data[:modes] << [setting + m]
                   who_wants_params << data[:modes].length - 1
                 when *@server.supports[:chanmodes][:typeb]
-                  data[:modes] << [setting + m]
                   who_wants_params << data[:modes].length - 1
                 when *@server.supports[:chanmodes][:typec]
-                  if setting == "+"
-                    data[:modes] << [setting + m]
+                  if setting == :set
                     who_wants_params << data[:modes].length - 1
-                  else
-                    data[:modes] << setting + m
                   end
                 when *@server.supports[:chanmodes][:typed]
-                  data[:modes] << setting + m
+                  # Nothing to do
                 when *@server.supports[:prefix][:modes]
-                  data[:modes] << [setting + m]
                   who_wants_params << data[:modes].length - 1
                 else
-                  warning "Unknown mode #{m} in #{serverstring.inspect}"
+                  warning "Ignoring unknown mode #{m} in #{serverstring.inspect}"
+                  data[:modes].pop
                 end
-              }
-            else
-              idx = who_wants_params.shift
-              if idx.nil?
-                warning "Oops, problems parsing #{serverstring.inspect}"
-                break
               end
-              data[:modes][idx] << arg
             end
-          }
+            getting_args = true unless who_wants_params.empty?
+          end
+        end
+        unless who_wants_params.empty?
+          warning "Unhandled malformed modeline #{data[:modestring]} (unexpected empty arguments)"
+          return
         end
 
         data[:modes].each { |mode|
-          case mode
-          when Array
-            set = mode[0][0].chr == "+" ? :set : :reset
-            key = mode[0][1].chr.to_sym
-            val = mode[1]
-            data[:channel].mode[key].send(set, val)
+          set, key, val = mode
+          if val
+            data[:target].mode[key].send(set, val)
           else
-            set = mode[0].chr == "+" ? :set : :reset
-            key = mode[1].chr.to_sym
-            data[:channel].mode[key].send(set)
+            data[:target].mode[key].send(set)
           end
-        } if data[:modes]
-
-        handle(:mode, data)
+        }
       else
-        warning "Unknown message #{serverstring.inspect}"
-        handle(:unknown, data)
-      end
-    end
-
-    private
-
-    # key::  server event name
-    # data:: hash containing data about the event, passed to the proc
-    # call client's proc for an event, if they set one as a handler
-    def handle(key, data)
-      if(@handlers.has_key?(key))
-        @handlers[key].call(data)
+        warning "Ignoring #{data[:modestring]} for unrecognized target #{argv[0]} (#{data[:target].inspect})"
       end
     end
   end