]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/commitdiff
First shot at the new Irc framework. Bot is usable (sort of), but not all functionali...
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>
Mon, 31 Jul 2006 15:33:15 +0000 (15:33 +0000)
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>
Mon, 31 Jul 2006 15:33:15 +0000 (15:33 +0000)
Rakefile
bin/rbot
data/rbot/plugins/nickserv.rb
lib/rbot/channel.rb [deleted file]
lib/rbot/irc.rb
lib/rbot/ircbot.rb
lib/rbot/message.rb
lib/rbot/rfc2812.rb

index 00355b064cfc7c095bf46ca6e7535816c7a879ea..6ad7f1bf64c6ad9c517633e63e2c4cc3f6763108 100644 (file)
--- a/Rakefile
+++ b/Rakefile
@@ -6,7 +6,7 @@ task :default => [:repackage]
 
 spec = Gem::Specification.new do |s|
   s.name = 'rbot'
-  s.version = '0.9.10'
+  s.version = '0.9.11'
   s.summary = <<-EOF
     A modular ruby IRC bot.
   EOF
index 45dba848256adbd3359d623a3c4080e512815658..8921eeb81e93f8e67428196048f641a8dec6ec79 100755 (executable)
--- a/bin/rbot
+++ b/bin/rbot
@@ -29,7 +29,7 @@ require 'etc'
 require 'getoptlong'
 require 'fileutils'
 
-$version="0.9.10-svn"
+$version="0.9.11-svn"
 $opts = Hash.new
 
 orig_opts = ARGV.dup
index 9ff79f084d8215f9bd1d2ccc50475cdbdc17c87a..a5280b1f3ca02deb43bf32c14266d6c59b4fe339 100644 (file)
@@ -7,8 +7,8 @@
 class NickServPlugin < Plugin
   
   BotConfig.register BotConfigStringValue.new('nickserv.name',
-    :default => "NickServ", :requires_restart => false,
-    :desc => "Name of the nick server")
+    :default => "nickserv", :requires_restart => false,
+    :desc => "Name of the nick server (all lowercase)")
   BotConfig.register BotConfigStringValue.new('nickserv.ident_request',
     :default => "IDENTIFY", :requires_restart => false,
     :on_change => Proc.new { |bot, v| bot.plugins.delegate "set_ident_request", v },
diff --git a/lib/rbot/channel.rb b/lib/rbot/channel.rb
deleted file mode 100644 (file)
index 34804c1..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-module Irc
-
-  # class to store IRC channel data (users, topic, per-channel configurations)
-  class IRCChannel
-    # name of channel
-    attr_reader :name
-
-    # current channel topic
-    attr_reader :topic
-
-    # hash containing users currently in the channel
-    attr_accessor :users
-
-    # if true, bot won't talk in this channel
-    attr_accessor :quiet
-
-    # name:: channel name
-    # create a new IRCChannel
-    def initialize(name)
-      @name = name
-      @users = Hash.new
-      @quiet = false
-      @topic = Topic.new
-    end
-
-    # eg @bot.channels[chan].topic = topic
-    def topic=(name)
-      @topic.name = name
-    end
-
-    # class to store IRC channel topic information
-    class Topic
-      # topic name
-      attr_accessor :name
-
-      # timestamp
-      attr_accessor :timestamp
-
-      # topic set by
-      attr_accessor :by
-
-      def initialize
-        @name = ""
-      end
-
-      # when called like "puts @bots.channels[chan].topic"
-      def to_s
-        @name
-      end
-    end
-
-  end
-
-end
index 31c4953ed66d127d579908744db8f375185dc0de..d5621b0fd587028096ae71381221b99a7826f7ac 100644 (file)
@@ -1,9 +1,9 @@
 #-- vim:sw=2:et\r
 # General TODO list\r
-# * when Users are deleted, we have to delete them from the appropriate\r
-#   channel lists too\r
 # * do we want to handle a Channel list for each User telling which\r
 #   Channels is the User on (of those the client is on too)?\r
+#   We may want this so that when a User leaves all Channels and he hasn't\r
+#   sent us privmsgs, we know remove him from the Server @users list\r
 #++\r
 # :title: IRC module\r
 #\r
@@ -274,7 +274,13 @@ module Irc
         @user = str[:user].to_s\r
         @host = str[:host].to_s\r
       when String\r
-        if str.match(/(\S+)(?:!(\S+)@(?:(\S+))?)?/)\r
+        case str\r
+        when ""\r
+          @casemap = casemap || 'rfc1459'\r
+          @nick = nil\r
+          @user = nil\r
+          @host = nil\r
+        when /(\S+)(?:!(\S+)@(?:(\S+))?)?/\r
           @casemap = casemap || 'rfc1459'\r
           @nick = $1.irc_downcase(@casemap)\r
           @user = $2\r
@@ -325,9 +331,10 @@ module Irc
 \r
     # A Netmask is easily converted to a String for the usual representation\r
     # \r
-    def to_s\r
+    def fullform\r
       return "#{nick}@#{user}!#{host}"\r
     end\r
+    alias :to_s :fullform\r
 \r
     # This method is used to match the current Netmask against another one\r
     #\r
@@ -382,23 +389,73 @@ module Irc
 \r
   # An IRC User is identified by his/her Netmask (which must not have\r
   # globs). In fact, User is just a subclass of Netmask. However,\r
-  # a User will not allow one's host or user data to be changed: only the\r
-  # nick can be dynamic\r
+  # a User will not allow one's host or user data to be changed.\r
+  #\r
+  # Due to the idiosincrasies of the IRC protocol, we allow\r
+  # the creation of a user with an unknown mask represented by the\r
+  # glob pattern *@*. Only in this case they may be set.\r
   #\r
   # TODO list:\r
   # * see if it's worth to add the other USER data\r
-  # * see if it's worth to add AWAY status\r
   # * see if it's worth to add NICKSERV status\r
   #\r
   class User < Netmask\r
-    private :host=, :user=\r
+    alias :to_s :nick\r
 \r
     # Create a new IRC User from a given Netmask (or anything that can be converted\r
     # into a Netmask) provided that the given Netmask does not have globs.\r
     #\r
-    def initialize(str, casemap=nil)\r
+    def initialize(str="", casemap=nil)\r
       super\r
-      raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if has_irc_glob?\r
+      raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"\r
+      raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"\r
+      raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
+      @away = false\r
+    end\r
+\r
+    # We only allow the user to be changed if it was "*". Otherwise,\r
+    # we raise an exception if the new host is different from the old one\r
+    #\r
+    def user=(newuser)\r
+      if user == "*"\r
+        super\r
+      else\r
+        raise "Can't change the username of user #{self}" if user != newuser\r
+      end\r
+    end\r
+\r
+    # We only allow the host to be changed if it was "*". Otherwise,\r
+    # we raise an exception if the new host is different from the old one\r
+    #\r
+    def host=(newhost)\r
+      if host == "*"\r
+        super\r
+      else\r
+        raise "Can't change the hostname of user #{self}" if host != newhost \r
+      end\r
+    end\r
+\r
+    # Checks if a User is well-known or not by looking at the hostname and user\r
+    #\r
+    def known?\r
+      return user!="*" && host!="*"\r
+    end\r
+\r
+    # Is the user away?\r
+    #\r
+    def away?\r
+      return @away\r
+    end\r
+\r
+    # Set the away status of the user. Use away=(nil) or away=(false)\r
+    # to unset away\r
+    #\r
+    def away=(msg="")\r
+      if msg\r
+        @away = msg\r
+      else\r
+        @away = false\r
+      end\r
     end\r
   end\r
 \r
@@ -415,67 +472,131 @@ module Irc
   end\r
 \r
 \r
-  # An IRC Channel is identified by its name, and it has a set of properties:\r
-  # * a topic\r
-  # * a UserList\r
-  # * a set of modes\r
+  # A ChannelTopic represents the topic of a channel. It consists of\r
+  # the topic itself, who set it and when\r
+  class ChannelTopic\r
+    attr_accessor :text, :set_by, :set_on\r
+    alias :to_s :text\r
+\r
+    # Create a new ChannelTopic setting the text, the creator and\r
+    # the creation time\r
+    def initialize(text="", set_by="", set_on=Time.new)\r
+      @text = text\r
+      @set_by = set_by\r
+      @set_on = Time.new\r
+    end\r
+  end\r
+\r
+\r
+  # Mode on a channel\r
+  class ChannelMode\r
+  end\r
+\r
+\r
+  # Channel modes of type A manipulate lists\r
   #\r
-  class Channel\r
-    attr_reader :name, :type, :casemap\r
+  class ChannelModeTypeA < ChannelMode\r
+    def initialize\r
+      @list = NetmaskList.new\r
+    end\r
 \r
-    # Create a new method. Auxiliary function for the following\r
-    # auxiliary functions ...\r
-    #\r
-    def create_method(name, &block)\r
-      self.class.send(:define_method, name, &block)\r
+    def set(val)\r
+      @list << val unless @list.include?(val)\r
     end\r
-    private :create_method\r
 \r
-    # Create a new channel boolean flag\r
-    #\r
-    def new_bool_flag(sym, acc=nil, default=false)\r
-      @flags[sym.to_sym] = default\r
-      racc = (acc||sym).to_s << "?"\r
-      wacc = (acc||sym).to_s << "="\r
-      create_method(racc.to_sym) { @flags[sym.to_sym] }\r
-      create_method(wacc.to_sym) { |val|\r
-        @flags[sym.to_sym] = val\r
-      }\r
+    def reset(val)\r
+      @list.delete_if(val) if @list.include?(val)\r
     end\r
+  end\r
 \r
-    # Create a new channel flag with data\r
-    #\r
-    def new_data_flag(sym, acc=nil, default=false)\r
-      @flags[sym.to_sym] = default\r
-      racc = (acc||sym).to_s\r
-      wacc = (acc||sym).to_s << "="\r
-      create_method(racc.to_sym) { @flags[sym.to_sym] }\r
-      create_method(wacc.to_sym) { |val|\r
-        @flags[sym.to_sym] = val\r
-      }\r
+  # Channel modes of type B need an argument\r
+  #\r
+  class ChannelModeTypeB < ChannelMode\r
+    def initialize\r
+      @arg = nil\r
     end\r
 \r
-    # Create a new variable with accessors\r
-    #\r
-    def new_variable(name, default=nil)\r
-      v = "@#{name}".to_sym\r
-      instance_variable_set(v, default)\r
-      create_method(name.to_sym) { instance_variable_get(v) }\r
-      create_method("#{name}=".to_sym) { |val|\r
-        instance_variable_set(v, val)\r
-      }\r
+    def set(val)\r
+      @arg = val\r
     end\r
 \r
-    # Create a new UserList\r
-    #\r
-    def new_userlist(name, default=UserList.new)\r
-      new_variable(name, default)\r
+    def reset(val)\r
+      @arg = nil if @arg == val\r
+    end\r
+  end\r
+\r
+  # Channel modes that change the User prefixes are like\r
+  # Channel modes of type B, except that they manipulate\r
+  # lists of Users, so they are somewhat similar to channel\r
+  # modes of type A\r
+  #\r
+  class ChannelUserMode < ChannelModeTypeB\r
+    def initialize\r
+      @list = UserList.new\r
+    end\r
+\r
+    def set(val)\r
+      @list << val unless @list.include?(val)\r
+    end\r
+\r
+    def reset(val)\r
+      @list.delete_if { |x| x == val }\r
+    end\r
+  end\r
+\r
+  # Channel modes of type C need an argument when set,\r
+  # but not when they get reset\r
+  #\r
+  class ChannelModeTypeC < ChannelMode\r
+    def initialize\r
+      @arg = false\r
+    end\r
+\r
+    def set(val)\r
+      @arg = val\r
+    end\r
+\r
+    def reset\r
+      @arg = false\r
+    end\r
+  end\r
+\r
+  # Channel modes of type D are basically booleans\r
+  class ChannelModeTypeD\r
+    def initialize\r
+      @set = false\r
+    end\r
+\r
+    def set?\r
+      return @set\r
+    end\r
+\r
+    def set\r
+      @set = true\r
+    end\r
+\r
+    def reset\r
+      @set = false\r
     end\r
+  end\r
+\r
 \r
-    # Create a new NetmaskList\r
+  # An IRC Channel is identified by its name, and it has a set of properties:\r
+  # * a topic\r
+  # * a UserList\r
+  # * a set of modes\r
+  #\r
+  class Channel\r
+    attr_reader :name, :topic, :casemap, :mode, :users\r
+    alias :to_s :name\r
+\r
+    # A String describing the Channel and (some of its) internals\r
     #\r
-    def new_netmasklist(name, default=NetmaskList.new)\r
-      new_variable(name, default)\r
+    def inspect\r
+      str = "<#{self.class}:#{'0x%08x' % self.object_id}:"\r
+      str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
+      str << " @users=<#{@users.join(', ')}>"\r
+      str\r
     end\r
 \r
     # Creates a new channel with the given name, optionally setting the topic\r
@@ -486,7 +607,7 @@ module Irc
     #\r
     # FIXME doesn't check if users have the same casemap as the channel yet\r
     #\r
-    def initialize(name, topic="", users=[], casemap=nil)\r
+    def initialize(name, topic=nil, users=[], casemap=nil)\r
       @casemap = casemap || 'rfc1459'\r
 \r
       raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
@@ -495,45 +616,34 @@ module Irc
 \r
       @name = name.irc_downcase(@casemap)\r
 \r
-      new_variable(:topic, topic)\r
+      @topic = topic || ChannelTopic.new\r
 \r
-      new_userlist(:users)\r
       case users\r
       when UserList\r
-        @users = users.dup\r
+        @users = users\r
       when Array\r
         @users = UserList.new(users)\r
       else\r
         raise ArgumentError, "Invalid user list #{users.inspect}"\r
       end\r
 \r
-      # new_variable(:creator)\r
-\r
-      # # Special users\r
-      # new_userlist(:super_ops)\r
-      # new_userlist(:ops)\r
-      # new_userlist(:half_ops)\r
-      # new_userlist(:voices)\r
-\r
-      # # Ban and invite lists\r
-      # new_netmasklist(:banlist)\r
-      # new_netmasklist(:exceptlist)\r
-      # new_netmasklist(:invitelist)\r
+      # Flags\r
+      @mode = {}\r
+    end\r
 \r
-      # # Flags\r
-      @flags = {}\r
-      # new_bool_flag(:a, :anonymous)\r
-      # new_bool_flag(:i, :invite_only)\r
-      # new_bool_flag(:m, :moderated)\r
-      # new_bool_flag(:n, :no_externals)\r
-      # new_bool_flag(:q, :quiet)\r
-      # new_bool_flag(:p, :private)\r
-      # new_bool_flag(:s, :secret)\r
-      # new_bool_flag(:r, :will_reop)\r
-      # new_bool_flag(:t, :free_topic)\r
+    # Removes a user from the channel\r
+    #\r
+    def delete_user(user)\r
+      @users.delete_if { |x| x == user }\r
+      @mode.each { |sym, mode|\r
+        mode.reset(user) if mode.class <= ChannelUserMode\r
+      }\r
+    end\r
 \r
-      # new_data_flag(:k, :key)\r
-      # new_data_flag(:l, :limit)\r
+    # The channel prefix\r
+    #\r
+    def prefix\r
+      name[0].chr\r
     end\r
 \r
     # A channel is local to a server if it has the '&' prefix\r
@@ -559,6 +669,12 @@ module Irc
     def normal?\r
       name[0] = 0x23\r
     end\r
+\r
+    # Create a new mode\r
+    #\r
+    def create_mode(sym, kl)\r
+      @mode[sym.to_sym] = kl.new\r
+    end\r
   end\r
 \r
 \r
@@ -579,7 +695,8 @@ module Irc
   class Server\r
 \r
     attr_reader :hostname, :version, :usermodes, :chanmodes\r
-    attr_reader :supports, :capab\r
+    alias :to_s :hostname\r
+    attr_reader :supports, :capabilities\r
 \r
     attr_reader :channels, :users\r
 \r
@@ -590,14 +707,27 @@ module Irc
     #\r
     def initialize\r
       @hostname = @version = @usermodes = @chanmodes = nil\r
+\r
+      @channels = ChannelList.new\r
+      @channel_names = Array.new\r
+\r
+      @users = UserList.new\r
+      @user_nicks = Array.new\r
+\r
+      reset_capabilities\r
+    end\r
+\r
+    # Resets the server capabilities\r
+    #\r
+    def reset_capabilities\r
       @supports = {\r
         :casemapping => 'rfc1459',\r
         :chanlimit => {},\r
         :chanmodes => {\r
-          :addr_list => nil, # Type A\r
-          :has_param => nil, # Type B\r
-          :set_param => nil, # Type C\r
-          :no_params => nil  # Type D\r
+          :typea => nil, # Type A: address lists\r
+          :typeb => nil, # Type B: needs a parameter\r
+          :typec => nil, # Type C: needs a parameter when set\r
+          :typed => nil  # Type D: must not have a parameter\r
         },\r
         :channellen => 200,\r
         :chantypes => "#&",\r
@@ -619,13 +749,25 @@ module Irc
         :targmax => {},\r
         :topiclen => nil\r
       }\r
-      @capab = {}\r
+      @capabilities = {}\r
+    end\r
 \r
-      @channels = ChannelList.new\r
-      @channel_names = Array.new\r
+    # Resets the Channel and User list\r
+    #\r
+    def reset_lists\r
+      @users.each { |u|\r
+        delete_user(u)\r
+      }\r
+      @channels.each { |u|\r
+        delete_channel(u)\r
+      }\r
+    end\r
 \r
-      @users = UserList.new\r
-      @user_nicks = Array.new\r
+    # Clears the server\r
+    #\r
+    def clear\r
+      reset_lists\r
+      reset_capabilities\r
     end\r
 \r
     # This method is used to parse a 004 RPL_MY_INFO line\r
@@ -659,10 +801,6 @@ module Irc
     #\r
     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
     #\r
-    # TODO this is just an initial draft that does nothing special.\r
-    # We want to properly parse most of the supported capabilities\r
-    # for later reuse.\r
-    #\r
     def parse_isupport(line)\r
       ar = line.split(' ')\r
       reparse = ""\r
@@ -699,10 +837,10 @@ module Irc
         when :chanmodes\r
           noval_warn(key, val) {\r
             groups = val.split(',')\r
-            @supports[key][:addr_list] = groups[0].scan(/./)\r
-            @supports[key][:has_param] = groups[1].scan(/./)\r
-            @supports[key][:set_param] = groups[2].scan(/./)\r
-            @supports[key][:no_params] = groups[3].scan(/./)\r
+            @supports[key][:typea] = groups[0].scan(/./)\r
+            @supports[key][:typeb] = groups[1].scan(/./)\r
+            @supports[key][:typec] = groups[2].scan(/./)\r
+            @supports[key][:typed] = groups[3].scan(/./)\r
           }\r
         when :channellen, :kicklen, :modes, :topiclen\r
           if val\r
@@ -758,17 +896,38 @@ module Irc
       @supports[:casemapping] || 'rfc1459'\r
     end\r
 \r
+    # Returns User or Channel depending on what _name_ can be\r
+    # a name of\r
+    #\r
+    def user_or_channel?(name)\r
+      if supports[:chantypes].include?(name[0].chr)\r
+        return Channel\r
+      else\r
+        return User\r
+      end\r
+    end\r
+\r
+    # Returns the actual User or Channel object matching _name_\r
+    #\r
+    def user_or_channel(name)\r
+      if supports[:chantypes].include?(name[0].chr)\r
+        return channel(name)\r
+      else\r
+        return user(name)\r
+      end\r
+    end\r
+\r
     # Checks if the receiver already has a channel with the given _name_\r
     #\r
     def has_channel?(name)\r
-      @channel_names.index(name)\r
+      @channel_names.index(name.to_s)\r
     end\r
     alias :has_chan? :has_channel?\r
 \r
     # Returns the channel with name _name_, if available\r
     #\r
     def get_channel(name)\r
-      idx = @channel_names.index(name)\r
+      idx = @channel_names.index(name.to_s)\r
       @channels[idx] if idx\r
     end\r
     alias :get_chan :get_channel\r
@@ -780,7 +939,7 @@ module Irc
     #\r
     # The Channel is automatically created with the appropriate casemap\r
     #\r
-    def new_channel(name, topic="", users=[], fails=true)\r
+    def new_channel(name, topic=nil, users=[], fails=true)\r
       if !has_chan?(name)\r
 \r
         prefix = name[0].chr\r
@@ -789,19 +948,19 @@ module Irc
         #\r
         # FIXME might need to raise an exception\r
         #\r
-        warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].includes?(prefix)\r
+        warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
         warn "#{self} doesn't support channel names this long (#{name.length} > #{@support[:channellen]}" unless name.length <= @supports[:channellen]\r
 \r
         # Next, we check if we hit the limit for channels of type +prefix+\r
         # if the server supports +chanlimit+\r
         #\r
         @supports[:chanlimit].keys.each { |k|\r
-          next unless k.includes?(prefix)\r
+          next unless k.include?(prefix)\r
           count = 0\r
           @channel_names.each { |n|\r
-            count += 1 if k.includes?(n[0].chr)\r
+            count += 1 if k.include?(n[0].chr)\r
           }\r
-          raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimits][k]\r
+          raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
         }\r
 \r
         # So far, everything is fine. Now create the actual Channel\r
@@ -812,41 +971,51 @@ module Irc
         # lists and flags for this channel\r
 \r
         @supports[:prefix][:modes].each { |mode|\r
-          chan.new_userlist(mode)\r
+          chan.create_mode(mode, ChannelUserMode)\r
         } if @supports[:prefix][:modes]\r
 \r
         @supports[:chanmodes].each { |k, val|\r
           if val\r
             case k\r
-            when :addr_list\r
+            when :typea\r
               val.each { |mode|\r
-                chan.new_netmasklist(mode)\r
+                chan.create_mode(mode, ChannelModeTypeA)\r
               }\r
-            when :has_param, :set_param\r
+            when :typeb\r
               val.each { |mode|\r
-                chan.new_data_flag(mode)\r
+                chan.create_mode(mode, ChannelModeTypeB)\r
               }\r
-            when :no_params\r
+            when :typec\r
               val.each { |mode|\r
-                chan.new_bool_flag(mode)\r
+                chan.create_mode(mode, ChannelModeTypeC)\r
+              }\r
+            when :typed\r
+              val.each { |mode|\r
+                chan.create_mode(mode, ChannelModeTypeD)\r
               }\r
             end\r
           end\r
         }\r
 \r
-        # * appropriate @flags\r
-        # * a UserList for each @supports[:prefix]\r
-        # * a NetmaskList for each @supports[:chanmodes] of type A\r
-\r
-        @channels << newchan\r
+        @channels << chan\r
         @channel_names << name\r
-        return newchan\r
+        debug "Created channel #{chan.inspect}"\r
+        debug "Managing channels #{@channel_names.join(', ')}"\r
+        return chan\r
       end\r
 \r
       raise "Channel #{name} already exists on server #{self}" if fails\r
       return get_channel(name)\r
     end\r
 \r
+    # Returns the Channel with the given _name_ on the server,\r
+    # creating it if necessary. This is a short form for\r
+    # new_channel(_str_, nil, [], +false+)\r
+    #\r
+    def channel(str)\r
+      new_channel(str,nil,[],false)\r
+    end\r
+\r
     # Remove Channel _name_ from the list of <code>Channel</code>s\r
     #\r
     def delete_channel(name)\r
@@ -859,13 +1028,13 @@ module Irc
     # Checks if the receiver already has a user with the given _nick_\r
     #\r
     def has_user?(nick)\r
-      @user_nicks.index(nick)\r
+      @user_nicks.index(nick.to_s)\r
     end\r
 \r
     # Returns the user with nick _nick_, if available\r
     #\r
     def get_user(nick)\r
-      idx = @user_nicks.index(name)\r
+      idx = @user_nicks.index(nick.to_s)\r
       @users[idx] if idx\r
     end\r
 \r
@@ -877,7 +1046,12 @@ module Irc
     # The User is automatically created with the appropriate casemap\r
     #\r
     def new_user(str, fails=true)\r
-      tmp = User.new(str, self.casemap)\r
+      case str\r
+      when User\r
+        tmp = str\r
+      else\r
+        tmp = User.new(str, self.casemap)\r
+      end\r
       if !has_user?(tmp.nick)\r
         warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@support[:nicklen]}" unless tmp.nick.length <= @supports[:nicklen]\r
         @users << tmp\r
@@ -885,9 +1059,14 @@ module Irc
         return @users.last\r
       end\r
       old = get_user(tmp.nick)\r
-      raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp\r
-      raise "User #{tmp} already exists on server #{self}" if fails\r
-      return get_user(tmp)\r
+      if old.known?\r
+        raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp\r
+        raise "User #{tmp} already exists on server #{self}" if fails\r
+      else\r
+        old.user = tmp.user\r
+        old.host = tmp.host\r
+      end\r
+      return old\r
     end\r
 \r
     # Returns the User with the given Netmask on the server,\r
@@ -902,10 +1081,13 @@ module Irc
     # _someuser_ must be specified with the full Netmask.\r
     #\r
     def delete_user(someuser)\r
-      idx = has_user?(user.nick)\r
+      idx = has_user?(someuser.nick)\r
       raise "Tried to remove unmanaged user #{user}" unless idx\r
-      have = self.user(user)\r
-      raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser\r
+      have = self.user(someuser)\r
+      raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*"\r
+      @channels.each { |ch|\r
+        delete_user_from_channel(have, ch)\r
+      }\r
       @user_nicks.delete_at(idx)\r
       @users.delete_at(idx)\r
     end\r
@@ -926,10 +1108,21 @@ module Irc
       nm = new_netmask(mask)\r
       @users.inject(UserList.new) {\r
         |list, user|\r
-        list << user if user.matches?(nm)\r
+        if user.user == "*" or user.host == "*"\r
+          list << user if user.nick =~ nm.nick.to_irc_regexp\r
+        else\r
+          list << user if user.matches?(nm)\r
+        end\r
         list\r
       }\r
     end\r
+\r
+    # Deletes User from Channel\r
+    #\r
+    def delete_user_from_channel(user, channel)\r
+      channel.delete_user(user)\r
+    end\r
+\r
   end\r
 end\r
 \r
index 6226e55e821b39cc93d94021dbc01841874d3bb0..65b9417269ce5ca31d488fc8e768ddf7832b4527 100644 (file)
@@ -71,13 +71,14 @@ require 'rbot/rbotconfig'
 require 'rbot/config'
 require 'rbot/utils'
 
+require 'rbot/irc'
 require 'rbot/rfc2812'
 require 'rbot/keywords'
 require 'rbot/ircsocket'
 require 'rbot/auth'
 require 'rbot/timer'
 require 'rbot/plugins'
-require 'rbot/channel'
+require 'rbot/channel'
 require 'rbot/message'
 require 'rbot/language'
 require 'rbot/dbhash'
@@ -89,9 +90,6 @@ module Irc
 # Main bot class, which manages the various components, receives messages,
 # handles them or passes them to plugins, and contains core functionality.
 class IrcBot
-  # the bot's current nickname
-  attr_reader :nick
-
   # the bot's IrcAuth data
   attr_reader :auth
 
@@ -108,13 +106,12 @@ 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
+  # server the bot is connected to
+  # TODO multiserver
+  attr_reader :server
 
   # bot's irc socket
+  # TODO multiserver
   attr_reader :socket
 
   # bot's object registry, plugins get an interface to this for persistant
@@ -129,6 +126,14 @@ class IrcBot
   # proxies etc as defined by the bot configuration/environment
   attr_reader :httputil
 
+  # bot User in the client/server connection
+  attr_reader :myself
+
+  # bot User in the client/server connection
+  def nick
+    myself.nick
+  end
+
   # create a new IrcBot with botclass +botclass+
   def initialize(botclass, params = {})
     # BotConfig for the core bot
@@ -308,14 +313,19 @@ class IrcBot
 
     log_session_start
 
-    @timer = Timer::Timer.new(1.0) # only need per-second granularity
     @registry = BotRegistry.new self
+
+    @timer = Timer::Timer.new(1.0) # only need per-second granularity
     @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)
+
     begin
       @auth = IrcAuth.new(self)
     rescue => e
@@ -329,28 +339,51 @@ class IrcBot
     @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"])
 
     @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
+    @server = @client.server
+    @myself = @client.client
+    @myself.nick = @config['irc.nick']
+
+    # Channels where we are quiet
+    # It's nil when we are not quiet, an empty list when we are quiet
+    # in all channels, a list of channels otherwise
+    @quiet = nil
+
+
+    @client[:welcome] = proc {|data|
+      irclog "joined server #{@client.server} as #{myself}", "server"
+
+      @plugins.delegate("connect")
+
+      @config['irc.join_channels'].each { |c|
+        debug "autojoining channel #{c}"
+        if(c =~ /^(\S+)\s+(\S+)$/i)
+          join $1, $2
+        else
+          join c if(c)
+        end
+      }
+    }
     @client[:isupport] = proc { |data|
-      if data[:capab]
-        sendq "CAPAB IDENTIFY-MSG"
-      end
+      # TODO this needs to go into rfc2812.rb
+      # Since capabs are two-steps processes, server.supports[:capab]
+      # should be a three-state: nil, [], [....]
+      sendq "CAPAB IDENTIFY-MSG" if @server.supports[:capab]
     }
     @client[:datastr] = proc { |data|
-      debug data.inspect
+      # TODO this needs to go into rfc2812.rb
       if data[:text] == "IDENTIFY-MSG"
-        @capabilities["identify-msg".to_sym] = true
+        @server.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])
+      message = PrivMessage.new(self, @server, data[:source], data[:target], data[:message])
       onprivmsg(message)
     }
     @client[:notice] = proc { |data|
-      message = NoticeMessage.new(self, data[:source], data[:target], data[:message])
+      message = NoticeMessage.new(self, @server, data[:source], data[:target], data[:message])
       # pass it off to plugins that want to hear everything
       @plugins.delegate "listen", message
     }
@@ -373,125 +406,99 @@ class IrcBot
       @last_ping = nil
     }
     @client[:nick] = proc {|data|
-      sourcenick = data[:sourcenick]
-      nick = data[:nick]
-      m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick])
-      if(sourcenick == @nick)
-        debug "my nick is now #{nick}"
-        @nick = nick
+      source = data[:source]
+      old = data[:oldnick]
+      new = data[:newnick]
+      m = NickMessage.new(self, @server, source, old, new)
+      if source == myself
+        debug "my nick is now #{new}"
       end
-      @channels.each {|k,v|
-        if(v.users.has_key?(sourcenick))
-          irclog "@ #{sourcenick} is now known as #{nick}", k
-          v.users[nick] = v.users[sourcenick]
-          v.users.delete(sourcenick)
-        end
+      data[:is_on].each { |ch|
+          irclog "@ #{data[:old]} is now known as #{data[:new]}", ch
       }
       @plugins.delegate("listen", m)
       @plugins.delegate("nick", m)
     }
     @client[:quit] = proc {|data|
-      source = data[:source]
-      sourcenick = data[:sourcenick]
-      sourceurl = data[:sourceaddress]
-      message = data[:message]
-      m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
-      if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
-      else
-        @channels.each {|k,v|
-          if(v.users.has_key?(sourcenick))
-            irclog "@ Quit: #{sourcenick}: #{message}", k
-            v.users.delete(sourcenick)
-          end
-        }
-      end
+      m = QuitMessage.new(self, @server, data[:source], data[:source], data[:message])
+      data[:was_on].each { |ch|
+        irclog "@ Quit: #{sourcenick}: #{message}", ch
+      }
       @plugins.delegate("listen", m)
       @plugins.delegate("quit", m)
     }
     @client[:mode] = proc {|data|
-      source = data[:source]
-      sourcenick = data[:sourcenick]
-      sourceurl = data[:sourceaddress]
-      channel = data[:channel]
-      targets = data[:targets]
-      modestring = data[:modestring]
-      irclog "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
-    }
-    @client[:welcome] = proc {|data|
-      irclog "joined server #{data[:source]} as #{data[:nick]}", "server"
-      debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
-      if data[:nick] && data[:nick].length > 0
-        @nick = data[:nick]
-      end
-
-      @plugins.delegate("connect")
-
-      @config['irc.join_channels'].each {|c|
-        debug "autojoining channel #{c}"
-        if(c =~ /^(\S+)\s+(\S+)$/i)
-          join $1, $2
-        else
-          join c if(c)
-        end
-      }
+      irclog "@ Mode #{data[:modestring]} by #{data[:sourcenick]}", data[:channel]
     }
     @client[:join] = proc {|data|
-      m = JoinMessage.new(self, data[:source], data[:channel], data[:message])
+      m = JoinMessage.new(self, @server, data[:source], data[:channel], data[:message])
       onjoin(m)
     }
     @client[:part] = proc {|data|
-      m = PartMessage.new(self, data[:source], data[:channel], data[:message])
+      m = PartMessage.new(self, @server, data[:source], data[:channel], data[:message])
       onpart(m)
     }
     @client[:kick] = proc {|data|
-      m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message])
+      m = KickMessage.new(self, @server, data[:source], data[:target], data[:channel],data[:message])
       onkick(m)
     }
     @client[:invite] = proc {|data|
-      if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
-        join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
+      if data[:target] == myself
+        join data[:channel] if @auth.allow?("join", data[:source], data[:source].nick)
       end
     }
     @client[:changetopic] = proc {|data|
       channel = data[:channel]
-      sourcenick = data[:sourcenick]
+      source = data[:source]
       topic = data[:topic]
-      timestamp = data[:unixtime] || Time.now.to_i
-      if(sourcenick == @nick)
+      if source == myself
         irclog "@ I set topic \"#{topic}\"", channel
       else
-        irclog "@ #{sourcenick} set topic \"#{topic}\"", channel
+        irclog "@ #{source} set topic \"#{topic}\"", channel
       end
-      m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
+      m = TopicMessage.new(self, @server, data[:source], data[:channel], data[:topic])
 
       ontopic(m)
       @plugins.delegate("listen", m)
       @plugins.delegate("topic", m)
     }
-    @client[:topic] = @client[:topicinfo] = proc {|data|
-      channel = data[:channel]
-      m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic])
-        ontopic(m)
+    @client[:topic] = @client[:topicinfo] = proc { |data|
+      m = TopicMessage.new(self, @server, data[:source], data[:channel], data[:channel].topic)
+      ontopic(m)
     }
-    @client[:names] = proc {|data|
-      channel = data[:channel]
-      users = data[:users]
-      unless(@channels[channel])
-        warning "got names for channel '#{channel}' I didn't think I was in\n"
-        # exit 2
-      end
-      @channels[channel].users.clear
-      users.each {|u|
-        @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
-      }
+    @client[:names] = proc { |data|
       @plugins.delegate "names", data[:channel], data[:users]
     }
-    @client[:unknown] = proc {|data|
+    @client[:unknown] = proc { |data|
       #debug "UNKNOWN: #{data[:serverstring]}"
       irclog data[:serverstring], ".unknown"
     }
   end
 
+  # checks if we should be quiet on a channel
+  def quiet_on?(channel)
+    return false unless @quiet
+    return true if @quiet.empty?
+    return @quiet.include?(channel.to_s)
+  end
+
+  def set_quiet(channel=nil)
+    if channel
+      @quiet << channel.to_s unless @quiet.include?(channel.to_s)
+    else
+      @quiet = []
+    end
+  end
+
+  def reset_quiet(channel=nil)
+    if channel
+      @quiet.delete_if { |x| x == channel.to_s }
+    else
+      @quiet = nil
+    end
+  end
+
+  # things to do when we receive a signal
   def got_sig(sig)
     debug "received #{sig}, queueing quit"
     $interrupted += 1
@@ -524,8 +531,7 @@ class IrcBot
       raise e.class, "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
     end
     @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
+    @socket.emergency_puts "NICK #{@config['irc.nick']}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
     start_server_pings
   end
 
@@ -573,7 +579,7 @@ class IrcBot
       end
 
       stop_server_pings
-      @channels.clear
+      @server.clear
       if @socket.connected?
         @socket.clearq
         @socket.shutdown
@@ -601,7 +607,7 @@ class IrcBot
     # and all the extra stuff
     # TODO allow something to do for commands that produce too many messages
     # TODO example: math 10**10000
-    left = @socket.bytes_per - type.length - where.length - 4
+    left = @socket.bytes_per - type.length - where.to_s.length - 4
     begin
       if(left >= message.length)
         sendq "#{type} #{where} :#{message}", chan, ring
@@ -626,17 +632,18 @@ class IrcBot
   end
 
   # send a notice message to channel/nick +where+
-  def notice(where, message, mchan=nil, mring=-1)
+  def notice(where, message, mchan="", mring=-1)
     if mchan == ""
       chan = where
     else
       chan = mchan
     end
     if mring < 0
-      if where =~ /^#/
-        ring = 2
-      else
+      case where
+      when User
         ring = 1
+      else
+        ring = 2
       end
     else
       ring = mring
@@ -656,10 +663,11 @@ class IrcBot
       chan = mchan
     end
     if mring < 0
-      if where =~ /^#/
-        ring = 2
-      else
+      case where
+      when User
         ring = 1
+      else
+        ring = 2
       end
     else
       ring = mring
@@ -667,7 +675,7 @@ class IrcBot
     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))
+      unless quiet_on?(where)
         sendmsg "PRIVMSG", where, line, chan, ring 
       end
     }
@@ -681,7 +689,8 @@ class IrcBot
       chan = mchan
     end
     if mring < 0
-      if where =~ /^#/
+      case where
+      when Channel
         ring = 2
       else
         ring = 1
@@ -690,12 +699,13 @@ class IrcBot
       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
+    case where
+    when Channel
+      irclog "* #{myself} #{message}", where
+    when User
+      irclog "* #{myself}[#{where}] #{message}", $1
     else
-      irclog "* #{@nick}[#{where}] #{message}", where
+      irclog "* #{myself}[#{where}] #{message}", where
     end
   end
 
@@ -709,7 +719,7 @@ class IrcBot
   def irclog(message, where="server")
     message = message.chomp
     stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
-    where = where.gsub(/[:!?$*()\/\\<>|"']/, "_")
+    where = where.to_s.gsub(/[:!?$*()\/\\<>|"']/, "_")
     unless(@logs.has_key?(where))
       @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
       @logs[where].sync = true
@@ -746,8 +756,8 @@ class IrcBot
       @socket.shutdown
     end
     debug "Logging quits"
-    @channels.each_value {|v|
-      irclog "@ quit (#{message})", v.name
+    @server.channels.each { |ch|
+      irclog "@ quit (#{message})", ch
     }
     debug "Saving"
     save
@@ -910,7 +920,7 @@ class IrcBot
       when "restart"
         return "restart => completely stop and restart the bot (including reconnect)"
       when "join"
-        return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level"
+        return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{myself} also responds to invites if you have the required access level"
       when "part"
         return "part <channel> => part channel <channel>"
       when "hide"
@@ -934,9 +944,9 @@ class IrcBot
       when "version"
         return "version => describes software version"
       when "botsnack"
-        return "botsnack => reward #{@nick} for being good"
+        return "botsnack => reward #{myself} for being good"
       when "hello"
-        return "hello|hi|hey|yo [#{@nick}] => greet the bot"
+        return "hello|hi|hey|yo [#{myself}] => greet the bot"
       else
         return "Core help topics: quit, restart, config, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
     end
@@ -1015,25 +1025,25 @@ class IrcBot
         when (/^quiet$/i)
           if(auth.allow?("talk", m.source, m.replyto))
             m.okay
-            @channels.each_value {|c| c.quiet = true }
+            set_quiet
           end
         when (/^quiet in (\S+)$/i)
           where = $1
           if(auth.allow?("talk", m.source, m.replyto))
             m.okay
             where.gsub!(/^here$/, m.target) if m.public?
-            @channels[where].quiet = true if(@channels.has_key?(where))
+            set_quiet(where)
           end
         when (/^talk$/i)
           if(auth.allow?("talk", m.source, m.replyto))
-            @channels.each_value {|c| c.quiet = false }
+            reset_quiet
             m.okay
           end
         when (/^talk in (\S+)$/i)
           where = $1
           if(auth.allow?("talk", m.source, m.replyto))
             where.gsub!(/^here$/, m.target) if m.public?
-            @channels[where].quiet = false if(@channels.has_key?(where))
+            reset_quiet(where)
             m.okay
           end
         when (/^status\??$/i)
@@ -1059,9 +1069,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,-.]+#{Regexp.escape(@nick)}$/i)
+        when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(self.nick)}$/i)
           say m.replyto, @lang.get("hello_X") % m.sourcenick
-        when (/^#{Regexp.escape(@nick)}!*$/)
+        when (/^#{Regexp.escape(self.nick)}!*$/)
           say m.replyto, @lang.get("hello_X") % m.sourcenick
         else
           @keywords.privmsg(m)
@@ -1073,17 +1083,19 @@ class IrcBot
   def log_sent(type, where, message)
     case type
       when "NOTICE"
-        if(where =~ /^#/)
-          irclog "-=#{@nick}=- #{message}", where
-        elsif (where =~ /(\S*)!.*/)
+        case where
+        when Channel
+          irclog "-=#{myself}=- #{message}", where
+        when User
              irclog "[-=#{where}=-] #{message}", $1
         else
-             irclog "[-=#{where}=-] #{message}"
+             irclog "[-=#{where}=-] #{message}", where
         end
       when "PRIVMSG"
-        if(where =~ /^#/)
-          irclog "<#{@nick}> #{message}", where
-        elsif (where =~ /^(\S*)!.*$/)
+        case where
+        when Channel
+          irclog "<#{myself}> #{message}", where
+        when User
           irclog "[msg(#{where})] #{message}", $1
         else
           irclog "[msg(#{where})] #{message}", where
@@ -1092,14 +1104,11 @@ class IrcBot
   end
 
   def onjoin(m)
-    @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
-    if(m.address?)
+    if m.address?
       debug "joined channel #{m.channel}"
       irclog "@ Joined channel #{m.channel}", m.channel
     else
       irclog "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
-      @channels[m.channel].users[m.sourcenick] = Hash.new
-      @channels[m.channel].users[m.sourcenick]["mode"] = ""
     end
 
     @plugins.delegate("listen", m)
@@ -1110,15 +1119,8 @@ class IrcBot
     if(m.address?)
       debug "left channel #{m.channel}"
       irclog "@ Left channel #{m.channel} (#{m.message})", m.channel
-      @channels.delete(m.channel)
     else
       irclog "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
-      if @channels.has_key?(m.channel)
-        @channels[m.channel].users.delete(m.sourcenick)
-      else
-        warning "got part for channel '#{channel}' I didn't think I was in\n"
-        # exit 2
-      end
     end
 
     # delegate to plugins
@@ -1130,10 +1132,8 @@ class IrcBot
   def onkick(m)
     if(m.address?)
       debug "kicked from channel #{m.channel}"
-      @channels.delete(m.channel)
       irclog "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
     else
-      @channels[m.channel].users.delete(m.sourcenick)
       irclog "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
     end
 
@@ -1142,12 +1142,7 @@ class IrcBot
   end
 
   def ontopic(m)
-    @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
-    @channels[m.channel].topic = m.topic if !m.topic.nil?
-    @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
-    @channels[m.channel].topic.by = m.source if !m.source.nil?
-
-    debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
+    debug "topic of channel #{m.channel} is now #{m.topic}"
   end
 
   # delegate a privmsg to auth, keyword or plugin handlers
index 66b6175c21def0de1579de023f207881cc01268d..fff121944b33f055606565e86d97201ec7abcf80 100644 (file)
@@ -17,19 +17,16 @@ module Irc
     # associated bot
     attr_reader :bot
 
+    # associated server
+    attr_reader :server
+
     # when the message was received
     attr_reader :time
 
-    # hostmask of message source
+    # User that originated the message
     attr_reader :source
 
-    # nick of message source
-    attr_reader :sourcenick
-
-    # url part of message source
-    attr_reader :sourceaddress
-
-    # nick/channel message was sent to
+    # User/Channel message was sent to
     attr_reader :target
 
     # contents of the message
@@ -40,10 +37,11 @@ module Irc
 
     # instantiate a new Message
     # bot::      associated bot class
-    # source::   hostmask of the message source
-    # target::   nick/channel message is destined for
-    # message::  message part
-    def initialize(bot, source, target, message)
+    # server::   Server where the message took place
+    # source::   User that sent the message
+    # target::   User/Channel is destined for
+    # message::  actual message
+    def initialize(bot, server, source, target, message)
       @msg_wants_id = false unless defined? @msg_wants_id
 
       @time = Time.now
@@ -53,9 +51,10 @@ module Irc
       @target = target
       @message = BasicUserMessage.stripcolour message
       @replied = false
+      @server = server
 
       @identified = false
-      if @msg_wants_id && @bot.capabilities["identify-msg".to_sym]
+      if @msg_wants_id && @server.capabilities["identify-msg".to_sym]
         if @message =~ /([-+])(.*)/
           @identified = ($1=="+")
           @message = $2
@@ -64,18 +63,25 @@ module Irc
         end
       end
 
-      # split source into consituent parts
-      if source =~ /^((\S+)!(\S+))$/
-        @sourcenick = $2
-        @sourceaddress = $3
-      end
-
-      if target && target.downcase == @bot.nick.downcase
+      if target && target == @bot.myself
         @address = true
       end
 
     end
 
+    # Access the nick of the source
+    #
+    def sourcenick
+      @source.nick
+    end
+
+    # Access the user@host of the source
+    #
+    def sourceaddress
+      "#{@source.user}@#{@source.host}"
+    end
+
+    # Was the message from an identified user?
     def identified?
       return @identified
     end
@@ -133,18 +139,18 @@ module Irc
     # source::   hostmask of the message source
     # target::   nick/channel message is destined for
     # message::  message part
-    def initialize(bot, source, target, message)
-      super(bot, source, target, message)
+    def initialize(bot, server, source, target, message)
+      super(bot, server, source, target, message)
       @target = target
       @private = false
       @plugin = nil
       @action = false
 
-      if target.downcase == @bot.nick.downcase
+      if target == @bot.myself
         @private = true
         @address = true
         @channel = nil
-        @replyto = @sourcenick
+        @replyto = source
       else
         @replyto = @target
         @channel = @target
@@ -223,7 +229,7 @@ module Irc
 
   # class to manage IRC PRIVMSGs
   class PrivMessage < UserMessage
-    def initialize(bot, source, target, message)
+    def initialize(bot, server, source, target, message)
       @msg_wants_id = true
       super
     end
@@ -231,7 +237,7 @@ module Irc
 
   # class to manage IRC NOTICEs
   class NoticeMessage < UserMessage
-    def initialize(bot, source, target, message)
+    def initialize(bot, server, source, target, message)
       @msg_wants_id = true
       super
     end
@@ -244,8 +250,8 @@ module Irc
     # channel user was kicked from
     attr_reader :channel
 
-    def initialize(bot, source, target, channel, message="")
-      super(bot, source, target, message)
+    def initialize(bot, server, source, target, channel, message="")
+      super(bot, server, source, target, message)
       @channel = channel
     end
   end
@@ -253,14 +259,22 @@ module Irc
   # class to pass IRC Nick changes in. @message contains the old nickame,
   # @sourcenick contains the new one.
   class NickMessage < BasicUserMessage
-    def initialize(bot, source, oldnick, newnick)
-      super(bot, source, oldnick, newnick)
+    def initialize(bot, server, source, oldnick, newnick)
+      super(bot, server, source, oldnick, newnick)
+    end
+
+    def oldnick
+      return @target
+    end
+
+    def newnick
+      return @message
     end
   end
 
   class QuitMessage < BasicUserMessage
-    def initialize(bot, source, target, message="")
-      super(bot, source, target, message)
+    def initialize(bot, server, source, target, message="")
+      super(bot, server, source, target, message)
     end
   end
 
@@ -272,10 +286,10 @@ module Irc
     # topic set on channel
     attr_reader :channel
 
-    def initialize(bot, source, channel, timestamp, topic="")
-      super(bot, source, channel, topic)
+    def initialize(bot, server, source, channel, topic=ChannelTopic.new)
+      super(bot, server, source, channel, topic.text)
       @topic = topic
-      @timestamp = timestamp
+      @timestamp = topic.set_on
       @channel = channel
     end
   end
@@ -284,11 +298,11 @@ module Irc
   class JoinMessage < BasicUserMessage
     # channel joined
     attr_reader :channel
-    def initialize(bot, source, channel, message="")
-      super(bot, source, channel, message)
+    def initialize(bot, server, source, channel, message="")
+      super(bot, server, source, channel, message)
       @channel = channel
       # in this case sourcenick is the nick that could be the bot
-      @address = (sourcenick.downcase == @bot.nick.downcase)
+      @address = (source == @bot.myself)
     end
   end
 
index 965da0a1f884fcb7b362c1540fe6c7ab77756166..dee2920f3db9d6f3699b4b9dca4e05235ebb13c8 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 = User.new        # 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
@@ -878,8 +888,14 @@ module Irc
       if prefix != nil
         data[:source] = prefix
         if prefix =~ /^(\S+)!(\S+)$/
-          data[:sourcenick] = $1
-          data[:sourceaddress] = $2
+          data[:source] = @server.user($1)
+        else
+          if @server.hostname && @server.hostname != data[:source]
+            warning "Unknown origin #{data[:source]} for message\n#{serverstring.inspect}"
+          else
+            @server.instance_variable_set(:@hostname, data[:source])
+          end
+          data[:source] = @server
         end
       end
 
@@ -894,13 +910,29 @@ 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])
+          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
+          @user ||= @server.user(data[:nick])
+          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 +941,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(params.split(' ', 2).last)
+          handle(:isupport, data)
         when ERR_NICKNAMEINUSE
           # "* <nick> :Nickname is already in use"
           data[:nick] = argv[1]
@@ -924,49 +967,68 @@ 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 topic #{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 || ""
+            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.users << u
+            if ar[1]
+              m = @server.supports[:prefix][:prefixes].index(ar[1])
+              m = @server.supports[:prefix][:modes][m]
+              chan.mode[m.to_sym].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 +1067,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,56 +1092,158 @@ 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]
+
+        data[:target] = @server.user_or_channel(argv[0])
         data[:message] = argv[1]
         handle(:privmsg, data)
 
         # Now we split it
-        if(data[:target] =~ /^[#&!+].*/)
+        if(data[:target].class <= Channel)
           handle(:public, data)
         else
           handle(:msg, data)
         end
+      when 'NOTICE'
+        data[:target] = @server.user_or_channel(argv[0])
+        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.users.include?(data[:source])
+        }
+
+        @server.delete_user(data[:source])
+
         handle(:quit, data)
       when 'JOIN'
-        data[:channel] = argv[0]
+        data[:channel] = @server.channel(argv[0])
+        data[:channel].users << data[:source]
+
         handle(:join, data)
       when 'TOPIC'
-        data[:channel] = argv[0]
-        data[:topic] = argv[1]
+        data[:channel] = @server.channel(argv[0])
+        data[:topic] = ChannelTopic.new(argv[1], data[:source], Time.new)
+        data[:channel].topic = 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.users.include?(data[:source])
+        }
+
+        data[:newnick] = argv[0]
+        data[:oldnick] = data[:source].nick.dup
+        data[:source].nick = data[:nick]
+
         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
         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_want_params = []
+
+          argv[1..-1].each { |arg|
+            setting = arg[0].chr
+            if "+-".include?(setting)
+              arg[1..-1].each_byte { |m|
+                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}"
+                end
+              }
+            else
+              idx = who_wants_params.shift
+              if idx.nil?
+                warn "Oops, problems parsing #{serverstring}"
+                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
         handle(:unknown, data)
       end