]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/rfc2812.rb
New Irc Framework: channel add_user was adding users indiscriminately when silent
[user/henk/code/ruby/rbot.git] / lib / rbot / rfc2812.rb
index 9fa81cb5f9eb5d5fe54caa8897c290a52cd9d7eb..2d4d323ba439db270d6fb188c3f62f6996ccfe7a 100644 (file)
@@ -1,67 +1,3 @@
-class ::String
-  # Calculate the penalty which will be assigned to this message
-  # by the IRCd
-  def irc_send_penalty
-    # According to eggrdop, the initial penalty is
-    penalty = 1 + self.length/100
-    # on everything but UnderNET where it's
-    # penalty = 2 + self.length/120
-
-    cmd, pars = self.split($;,2)
-    debug "cmd: #{cmd}, pars: #{pars.inspect}"
-    case cmd.to_sym
-    when :KICK
-      chan, nick, msg = pars.split
-      chan = chan.split(',')
-      nick = nick.split(',')
-      penalty += nick.length
-      penalty *= chan.length
-    when :MODE
-      chan, modes, argument = pars.split
-      extra = 0
-      if modes
-        extra = 1
-        if argument
-          extra += modes.split(/\+|-/).length
-        else
-          extra += 3 * modes.split(/\+|-/).length
-        end
-      end
-      if argument
-        extra += 2 * argument.split.length
-      end
-      penalty += extra * chan.split.length
-    when :TOPIC
-      penalty += 1
-      penalty += 2 unless pars.split.length < 2
-    when :PRIVMSG, :NOTICE
-      dests = pars.split($;,2).first
-      penalty += dests.split(',').length
-    when :WHO
-      # I'm too lazy to implement this one correctly
-      penalty += 5
-    when :AWAY, :JOIN, :VERSION, :TIME, :TRACE, :WHOIS, :DNS
-      penalty += 2
-    when :INVITE, :NICK
-      penalty += 3
-    when :ISON
-      penalty += 1
-    else # Unknown messages
-      penalty += 1
-    end
-    if penalty > 99
-      debug "Wow, more than 99 secs of penalty!"
-      penalty = 99
-    end
-    if penalty < 2
-      debug "Wow, less than 2 secs of penalty!"
-      penalty = 2
-    end
-    debug "penalty: #{penalty}"
-    return penalty
-  end
-end
-
 module Irc
   # RFC 2812   Internet Relay Chat: Client Protocol
   #
@@ -876,16 +812,16 @@ module Irc
   RPL_DATASTR=290
 
   # implements RFC 2812 and prior IRC RFCs.
-  # clients register handler proc{}s for different server events and IrcClient
+  # clients register handler proc{}s for different server events and Client
   # handles dispatch
-  class IrcClient
+  class Client
 
-    attr_reader :server, :client
+    attr_reader :server, :user
 
-    # create a new IrcClient instance
+    # create a new Client instance
     def initialize
       @server = Server.new         # The Server
-      @client = @server.user("")   # The User representing the client on this Server
+      @user = @server.user("")     # The User representing the client on this Server
 
       @handlers = Hash.new
 
@@ -894,12 +830,21 @@ module Irc
       @tmpusers = []
     end
 
+    # clear the server and reset the User
+    def reset
+      @server.clear
+      @user = @server.user("")
+    end
+
     # key::   server event to handle
     # value:: proc object called when event occurs
     # set a handler for a server event
     #
     # ==server events currently supported:
     #
+    # TODO handle errors ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL
+    # TODO handle errors ERR_CHANOPRIVSNEEDED, ERR_CANNOTSENDTOCHAN
+    #
     # welcome::     server welcome message on connect
     # yourhost::    your host details (on connection)
     # created::     when the server was started
@@ -943,23 +888,35 @@ module Irc
       data = Hash.new
       data[:serverstring] = serverstring
 
-      unless serverstring =~ /^(:(\S+)\s)?(\S+)(\s(.*))?/
-        raise "Unparseable Server Message!!!: #{serverstring}"
+      unless serverstring.chomp =~ /^(:(\S+)\s)?(\S+)(\s(.*))?$/
+        raise "Unparseable Server Message!!!: #{serverstring.inspect}"
       end
 
       prefix, command, params = $2, $3, $5
 
       if prefix != nil
-        data[:source] = prefix
-        if prefix =~ /^(\S+)!(\S+)$/
+        # 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 =~ /^#{Regexp::Irc::BANG_AT}$/
           data[:source] = @server.user(prefix)
         else
-          if @server.hostname && @server.hostname != data[:source]
-            warning "Unknown origin #{data[:source]} for message\n#{serverstring.inspect}"
+          if @server.hostname
+            if @server.hostname != prefix
+              # TODO do we want to be able to differentiate messages that are passed on to us from /other/ servers?
+              debug "Origin #{prefix} for message\n\t#{serverstring.inspect}\nis neither a user hostmask nor the server hostname\nI'll pretend that it's from the server anyway"
+              data[:source] = @server
+            else
+              data[:source] = @server
+            end
           else
-            @server.instance_variable_set(:@hostname, data[:source])
+            @server.instance_variable_set(:@hostname, prefix)
+            data[:source] = @server
           end
-          data[:source] = @server
         end
       end
 
@@ -967,37 +924,36 @@ module Irc
       argv = []
       params.scan(/(?!:)(\S+)|:(.*)/) { argv << ($1 || $2) } if params
 
-      case command
-      when 'PING'
-        data[:pingid] = argv[0]
-        handle(:ping, data)
-      when 'PONG'
-        data[:pingid] = argv[0]
-        handle(:pong, data)
-      when /^(\d+)$/            # numerical server message
+      if command =~ /^(\d+)$/ # Numeric replies
+       data[:target] = argv[0]
+        # A numeric reply /should/ be directed at the client, except when we're connecting with a used nick, in which case
+        # it's directed at '*'
+        not_us = !([@user.nick, '*'].include?(data[:target]))
+        if not_us
+          warning "Server reply #{serverstring.inspect} directed at #{data[:target]} instead of client (#{@user.nick})"
+        end
+
         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
+          if not_us
+            warning "Server thinks client (#{@user.inspect}) has a different nick"
+            @user.nick = data[:target]
+          end
+          if argv[1] =~ /([^@!\s]+)(?:!([^@!\s]+?))?@(\S+)/
+            nick = $1
+            user = $2
+            host = $3
+            warning "Welcome message nick mismatch (#{nick} vs #{data[:target]})" if nick != data[:target]
+            @user.user = user if user
+            @user.host = host if host
           end
-          @client = @server.user(data[:nick]) unless set
           handle(:welcome, data)
         when RPL_YOURHOST
           # "Your host is <servername>, running version <ver>"
+          data[:message] = argv[1]
           handle(:yourhost, data)
         when RPL_CREATED
           # "This server was created <date>"
@@ -1045,11 +1001,15 @@ module Irc
         when RPL_TOPIC_INFO
           data[:nick] = @server.user(argv[0])
           data[:channel] = @server.get_channel(argv[1])
-          data[:source] = @server.user(argv[2])
+
+          # This must not be an IRC::User because it might not be an actual User,
+          # and we risk overwriting valid User data
+          data[:source] = argv[2].to_irc_netmask(:server => @server)
+
           data[:time] = Time.at(argv[3].to_i)
 
           if data[:channel]
-            data[:channel].topic.set_by = data[:nick]
+            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"
@@ -1073,7 +1033,7 @@ module Irc
           users = []
           argv[3].scan(/\S+/).each { |u|
             # FIXME beware of servers that allow multiple prefixes
-            if(u =~ /^(#{@server.supports[:prefix][:prefixes].join})?(.*)$/)
+            if(u =~ /^([#{@server.supports[:prefix][:prefixes].join}])?(.*)$/)
               umode = $1
               user = $2
               users << [user, umode]
@@ -1082,11 +1042,13 @@ module Irc
 
           users.each { |ar|
             u = @server.user(ar[0])
-            chan.users << u unless chan.users.include?(u)
+            chan.add_user(u, :silent => true)
+            debug "Adding user #{u}"
             if ar[1]
               m = @server.supports[:prefix][:prefixes].index(ar[1].to_sym)
-              m = @server.supports[:prefix][:modes][m]
-              chan.mode[m.to_sym].set(u)
+              ms = @server.supports[:prefix][:modes][m]
+              debug "\twith mode #{ar[1]} (#{ms})"
+              chan.mode[ms].set(u)
             end
           }
           @tmpusers += users
@@ -1137,8 +1099,10 @@ module Irc
           # "<nick> :- <server> Message of the Day -"
           if argv[1] =~ /^-\s+(\S+)\s/
             server = $1
-            @motd = ""
+          else
+            warning "Server doesn't have an RFC compliant MOTD start."
           end
+          @motd = ""
         when RPL_MOTD
           if(argv[1] =~ /^-\s+(.*)$/)
             @motd << $1
@@ -1150,11 +1114,59 @@ module Irc
         when RPL_DATASTR
           data[:text] = argv[1]
           handle(:datastr, data)
+       when RPL_WHOREPLY
+          data[:channel] = argv[1]
+          data[:user] = argv[2]
+          data[:host] = argv[3]
+          data[:userserver] = argv[4]
+          data[:nick] = argv[5]
+          if argv[6] =~ /^(H|G)(\*)?(.*)?$/
+            data[:away] = ($1 == 'G')
+            data[:ircop] = $2
+            data[:modes] = $3.scan(/./).map { |mode|
+              m = @server.supports[:prefix][:prefixes].index(mode.to_sym)
+              @server.supports[:prefix][:modes][m]
+            } rescue []
+          else
+            warning "Strange WHO reply: #{serverstring.inspect}"
+          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
+          # TODO ircop status
+          # TODO userserver
+          # 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)
+          }
+
+          handle(:who, data)
+        when RPL_ENDOFWHO
+          handle(:eowho, data)
         else
           handle(:unknown, data)
         end
-      # end of numeric replies
-      when 'PRIVMSG'
+       return # We've processed the numeric reply
+      end
+
+      # Otherwise, the command should be a single word
+      case command.to_sym
+      when :PING
+        data[:pingid] = argv[0]
+        handle(:ping, data)
+      when :PONG
+        data[:pingid] = argv[0]
+        handle(:pong, data)
+      when :PRIVMSG
         # 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.
@@ -1177,7 +1189,7 @@ module Irc
         else
           handle(:msg, data)
         end
-      when 'NOTICE'
+      when :NOTICE
         begin
           data[:target] = @server.user_or_channel(argv[0])
         rescue
@@ -1195,56 +1207,56 @@ module Irc
           # "server notice" (not from user, noone to reply to)
           handle(:snotice, data)
         end
-      when 'KICK'
+      when :KICK
         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
+        if data[:target] == @user
           @server.delete_channel(data[:channel])
         end
 
         handle(:kick, data)
-      when 'PART'
+      when :PART
         data[:channel] = @server.channel(argv[0])
         data[:message] = argv[1]
 
         @server.delete_user_from_channel(data[:source], data[:channel])
-        if data[:source] == @client
+        if data[:source] == @user
           @server.delete_channel(data[:channel])
         end
 
         handle(:part, data)
-      when 'QUIT'
+      when :QUIT
         data[:message] = argv[0]
         data[:was_on] = @server.channels.inject(ChannelList.new) { |list, ch|
-          list << ch if ch.users.include?(data[:source])
-         list
+          list << ch if ch.has_user?(data[:source])
+          list
         }
 
         @server.delete_user(data[:source])
 
         handle(:quit, data)
-      when 'JOIN'
+      when :JOIN
         data[:channel] = @server.channel(argv[0])
-        data[:channel].users << data[:source]
+        data[:channel].add_user(data[:source])
 
         handle(:join, data)
-      when 'TOPIC'
+      when :TOPIC
         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'
+      when :INVITE
         data[:target] = @server.user(argv[0])
         data[:channel] = @server.channel(argv[1])
 
         handle(:invite, data)
-      when 'NICK'
+      when :NICK
         data[:is_on] = @server.channels.inject(ChannelList.new) { |list, ch|
-          list << ch if ch.users.include?(data[:source])
+          list << ch if ch.has_user?(data[:source])
           list
         }
 
@@ -1255,7 +1267,7 @@ module Irc
         debug "#{data[:oldnick]} (now #{data[:newnick]}) was on #{data[:is_on].join(', ')}"
 
         handle(:nick, data)
-      when 'MODE'
+      when :MODE
         # MODE ([+-]<modes> (<params>)*)*
         # When a MODE message is received by a server,
         # Type C will have parameters too, so we must
@@ -1267,6 +1279,7 @@ module Irc
         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
@@ -1302,13 +1315,13 @@ module Irc
                   data[:modes] << [setting + m]
                   who_wants_params << data[:modes].length - 1
                 else
-                  warn "Unknown mode #{m} in #{serverstring}"
+                  warning "Unknown mode #{m} in #{serverstring.inspect}"
                 end
               }
             else
               idx = who_wants_params.shift
               if idx.nil?
-                warn "Oops, problems parsing #{serverstring}"
+                warning "Oops, problems parsing #{serverstring.inspect}"
                 break
               end
               data[:modes][idx] << arg
@@ -1332,6 +1345,7 @@ module Irc
 
         handle(:mode, data)
       else
+        warning "Unknown message #{serverstring.inspect}"
         handle(:unknown, data)
       end
     end