]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/rfc2812.rb
Fix casemap/server mismatch problems when moving the bots between servers with differ...
[user/henk/code/ruby/rbot.git] / lib / rbot / rfc2812.rb
index 965da0a1f884fcb7b362c1540fe6c7ab77756166..9ca6571a85b374e1f9723b0f3cd590344ab7ff71 100644 (file)
@@ -815,10 +815,19 @@ module Irc
   # clients register handler proc{}s for different server events and IrcClient
   # handles dispatch
   class IrcClient
+
+    attr_reader :server, :client
+
     # create a new IrcClient instance
     def initialize
+      @server = Server.new         # The Server
+      @client = @server.user("")   # The User representing the client on this Server
+
       @handlers = Hash.new
-      @users = Array.new
+
+      # This is used by some messages to build lists of users that
+      # will be delegated when the ENDOF... message is received
+      @tmpusers = []
     end
 
     # key::   server event to handle
@@ -827,8 +836,10 @@ module Irc
     #
     # ==server events currently supported:
     #
-    # created::     when the server was started
+    # welcome::     server welcome message on connect
     # yourhost::    your host details (on connection)
+    # created::     when the server was started
+    # isupport::    information about what this server supports
     # ping::        server pings you (default handler returns a pong)
     # nicktaken::   you tried to change nick to one that's in use
     # badnick::     you tried to change nick to one that's invalid
@@ -836,7 +847,6 @@ module Irc
     # topicinfo::   on joining a channel or asking for the topic, tells you
     #               who set it and when
     # names::       server sends list of channel members when you join
-    # welcome::     server welcome message on connect
     # motd::        server message of the day
     # privmsg::     privmsg, the core of IRC, a message to you from someone
     # public::      optionally instead of getting privmsg you can hook to only
@@ -876,10 +886,27 @@ module Irc
       prefix, command, params = $2, $3, $5
 
       if prefix != nil
-        data[:source] = prefix
-        if prefix =~ /^(\S+)!(\S+)$/
-          data[:sourcenick] = $1
-          data[:sourceaddress] = $2
+        # Most servers will send a full nick!user@host prefix for
+        # messages from users. Therefore, when the prefix doesn't match this
+        # syntax it's usually the server hostname.
+        #
+        # This is not always true, though, since some servers do not send a
+        # full hostmask for user messages.
+        #
+        if prefix =~ /^(?:\S+)(?:!\S+)?@(?:\S+)$/
+          data[:source] = @server.user(prefix)
+        else
+          if @server.hostname
+            if @server.hostname != prefix
+              debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname, assuming it's a nick"
+              data[:source] = @server.user(prefix)
+            else
+              data[:source] = @server
+            end
+          else
+            @server.instance_variable_set(:@hostname, data[:source])
+            data[:source] = @server
+          end
         end
       end
 
@@ -894,13 +921,30 @@ module Irc
       when 'PONG'
         data[:pingid] = argv[0]
         handle(:pong, data)
-      when /^(\d+)$/           # numeric server message
+      when /^(\d+)$/            # numerical server message
         num=command.to_i
         case num
+        when RPL_WELCOME
+          # "Welcome to the Internet Relay Network
+          # <nick>!<user>@<host>"
+          case argv[1]
+          when /((\S+)!(\S+))/
+            data[:netmask] = $1
+            data[:nick] = $2
+            data[:address] = $3
+            @client = @server.user(data[:netmask])
+            set = true
+          when /Welcome to the Internet Relay Network\s(\S+)/
+            data[:nick] = $1
+          when /Welcome.*\s+(\S+)$/
+            data[:nick] = $1
+          when /^(\S+)$/
+            data[:nick] = $1
+          end
+          @client = @server.user(data[:nick]) unless set
+          handle(:welcome, data)
         when RPL_YOURHOST
           # "Your host is <servername>, running version <ver>"
-          # TODO how standard is this "version <ver>? should i parse it?
-          data[:message] = argv[1]
           handle(:yourhost, data)
         when RPL_CREATED
           # "This server was created <date>"
@@ -909,10 +953,21 @@ module Irc
         when RPL_MYINFO
           # "<servername> <version> <available user modes>
           # <available channel modes>"
-          data[:servername] = argv[1]
-          data[:version] = argv[2]
-          data[:usermodes] = argv[3]
-          data[:chanmodes] = argv[4]
+          @server.parse_my_info(params.split(' ', 2).last)
+          data[:servername] = @server.hostname
+          data[:version] = @server.version
+          data[:usermodes] = @server.usermodes
+          data[:chanmodes] = @server.chanmodes
+          handle(:myinfo, data)
+        when RPL_ISUPPORT
+          # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server"
+          # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63
+          # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=#
+          # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available
+          # on this server"
+          #
+          @server.parse_isupport(argv[1..-2].join(' '))
+          handle(:isupport, data)
         when ERR_NICKNAMEINUSE
           # "* <nick> :Nickname is already in use"
           data[:nick] = argv[1]
@@ -924,49 +979,71 @@ module Irc
           data[:message] = argv[2]
           handle(:badnick, data)
         when RPL_TOPIC
-          data[:channel] = argv[1]
+          data[:channel] = @server.get_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
+
           handle(:topic, data)
         when RPL_TOPIC_INFO
-          data[:nick] = argv[0]
-          data[:channel] = argv[1]
-          data[:source] = argv[2]
-          data[:unixtime] = argv[3]
+          data[:nick] = @server.user(argv[0])
+          data[:channel] = @server.get_channel(argv[1])
+          data[:source] = @server.user(argv[2])
+          data[:time] = Time.at(argv[3].to_i)
+
+          if data[:channel]
+            data[:channel].topic.set_by = data[:nick]
+            data[:channel].topic.set_on = data[:time]
+          else
+            warning "Received topic #{data[:topic].inspect} for channel #{data[:channel].inspect} I was not on"
+          end
+
           handle(:topicinfo, data)
         when RPL_NAMREPLY
           # "( "=" / "*" / "@" ) <channel>
           # :[ "@" / "+" ] <nick> *( " " [ "@" / "+" ] <nick> )
           # - "@" 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
+
+          users = []
           argv[3].scan(/\S+/).each { |u|
-            if(u =~ /^([@+])?(.*)$/)
-              umode = $1 || ""
+            # FIXME beware of servers that allow multiple prefixes
+            if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/)
+              umode = $1
               user = $2
-              @users << [user, umode]
+              users << [user, umode]
+            end
+          }
+
+          users.each { |ar|
+            u = @server.user(ar[0])
+            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]
+              debug "\twith mode #{ar[1]} (#{ms})"
+              chan.mode[ms].set(u)
             end
           }
+          @tmpusers += users
         when RPL_ENDOFNAMES
           data[:channel] = argv[1]
-          data[:users] = @users
+          data[:users] = @tmpusers
           handle(:names, data)
-          @users = Array.new
-        when RPL_ISUPPORT
-          # "PREFIX=(ov)@+ CHANTYPES=#& :are supported by this server"
-          # "MODES=4 CHANLIMIT=#:20 NICKLEN=16 USERLEN=10 HOSTLEN=63
-          # TOPICLEN=450 KICKLEN=450 CHANNELLEN=30 KEYLEN=23 CHANTYPES=#
-          # PREFIX=(ov)@+ CASEMAPPING=ascii CAPAB IRCD=dancer :are available
-          # on this server"
-          #
-          argv[0,argv.length-1].each {|a|
-            if a =~ /^(.*)=(.*)$/
-              data[$1.downcase.to_sym] = $2
-              debug "server's #{$1.downcase.to_sym} is #{$2}"
-            else
-              data[a.downcase.to_sym] = true
-              debug "server supports #{a.downcase.to_sym}"
-            end
-          }
-          handle(:isupport, data)
+          @tmpusers = Array.new
         when RPL_LUSERCLIENT
           # ":There are <integer> users and <integer>
           # services on <integer> servers"
@@ -1005,22 +1082,6 @@ module Irc
           # (re)started)"
           data[:message] = argv[1]
           handle(:statsconn, data)
-        when RPL_WELCOME
-          # "Welcome to the Internet Relay Network
-          # <nick>!<user>@<host>"
-          case argv[1]
-          when /((\S+)!(\S+))/
-            data[:netmask] = $1
-            data[:nick] = $2
-            data[:address] = $3
-          when /Welcome to the Internet Relay Network\s(\S+)/
-            data[:nick] = $1
-          when /Welcome.*\s+(\S+)$/
-            data[:nick] = $1
-          when /^(\S+)$/
-            data[:nick] = $1
-          end
-          handle(:welcome, data)
         when RPL_MOTDSTART
           # "<nick> :- <server> Message of the Day -"
           if argv[1] =~ /^-\s+(\S+)\s/
@@ -1046,57 +1107,182 @@ module Irc
         # you can either bind to 'PRIVMSG', to get every one and
         # parse it yourself, or you can bind to 'MSG', 'PUBLIC',
         # etc and get it all nicely split up for you.
-        data[:target] = argv[0]
+
+        begin
+          data[:target] = @server.user_or_channel(argv[0])
+        rescue
+          # The previous may fail e.g. when the target is a server or something
+          # like that (e.g. $<mask>). In any of these cases, we just use the
+          # String as a target
+          # FIXME we probably want to explicitly check for the #<mask> $<mask>
+          data[:target] = argv[0]
+        end
         data[:message] = argv[1]
         handle(:privmsg, data)
 
         # Now we split it
-        if(data[:target] =~ /^[#&!+].*/)
+        if data[:target].kind_of?(Channel)
           handle(:public, data)
         else
           handle(:msg, data)
         end
+      when 'NOTICE'
+        begin
+          data[:target] = @server.user_or_channel(argv[0])
+        rescue
+          # The previous may fail e.g. when the target is a server or something
+          # like that (e.g. $<mask>). In any of these cases, we just use the
+          # String as a target
+          # FIXME we probably want to explicitly check for the #<mask> $<mask>
+          data[:target] = argv[0]
+        end
+        data[:message] = argv[1]
+        case data[:source]
+        when User
+          handle(:notice, data)
+        else
+          # "server notice" (not from user, noone to reply to)
+          handle(:snotice, data)
+        end
       when 'KICK'
-        data[:channel] = argv[0]
-        data[:target] = argv[1]
+        data[:channel] = @server.channel(argv[0])
+        data[:target] = @server.user(argv[1])
         data[:message] = argv[2]
+
+        @server.delete_user_from_channel(data[:target], data[:channel])
+        if data[:target] == @client
+          @server.delete_channel(data[:channel])
+        end
+
         handle(:kick, data)
       when 'PART'
-        data[:channel] = argv[0]
+        data[:channel] = @server.channel(argv[0])
         data[:message] = argv[1]
+
+        @server.delete_user_from_channel(data[:source], data[:channel])
+        if data[:source] == @client
+          @server.delete_channel(data[:channel])
+        end
+
         handle(:part, data)
       when 'QUIT'
         data[:message] = argv[0]
+        data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch|
+          list << ch if ch.has_user?(data[:source])
+          list
+        }
+
+        @server.delete_user(data[:source])
+
         handle(:quit, data)
       when 'JOIN'
-        data[:channel] = argv[0]
+        data[:channel] = @server.channel(argv[0])
+        data[:channel].add_user(data[:source])
+
         handle(:join, data)
       when 'TOPIC'
-        data[:channel] = argv[0]
-        data[:topic] = argv[1]
+        data[:channel] = @server.channel(argv[0])
+        data[:topic] = Channel::Topic.new(argv[1], data[:source], Time.new)
+        data[:channel].topic.replace(data[:topic])
+
         handle(:changetopic, data)
       when 'INVITE'
-        data[:target] = argv[0]
-        data[:channel] = argv[1]
+        data[:target] = @server.user(argv[0])
+        data[:channel] = @server.channel(argv[1])
+
         handle(:invite, data)
       when 'NICK'
-        data[:nick] = argv[0]
+        data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch|
+          list << ch if ch.has_user?(data[:source])
+          list
+        }
+
+        data[:newnick] = argv[0]
+        data[:oldnick] = data[:source].nick.dup
+        data[:source].nick = data[:newnick]
+
+        debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}"
+
         handle(:nick, data)
       when 'MODE'
-        data[:channel] = argv[0]
-        data[:modestring] = argv[1]
-        data[:targets] = argv[2]
-        handle(:mode, data)
-      when 'NOTICE'
-        data[:target] = argv[0]
-        data[:message] = argv[1]
-        if data[:sourcenick]
-          handle(:notice, data)
+        # 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
+          warn "Unhandled user mode message '#{serverstring}'"
         else
-          # "server notice" (not from user, noone to reply to
-          handle(:snotice, data)
+          # 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
+                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]
+                    who_wants_params << data[:modes].length - 1
+                  else
+                    data[:modes] << setting + m
+                  end
+                when *@server.supports[:chanmodes][:typed]
+                  data[:modes] << setting + m
+                when *@server.supports[:prefix][:modes]
+                  data[:modes] << [setting + m]
+                  who_wants_params << data[:modes].length - 1
+                else
+                  warn "Unknown mode #{m} in #{serverstring.inspect}"
+                end
+              }
+            else
+              idx = who_wants_params.shift
+              if idx.nil?
+                warn "Oops, problems parsing #{serverstring.inspect}"
+                break
+              end
+              data[:modes][idx] << arg
+            end
+          }
         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)
+          else
+            set = mode[0].chr == "+" ? :set : :reset
+            key = mode[1].chr.to_sym
+            data[:channel].mode[key].send(set)
+          end
+        } if data[:modes]
+
+        handle(:mode, data)
       else
+        warn "Unknown message #{serverstring.inspect}"
         handle(:unknown, data)
       end
     end