]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/commitdiff
Initial commit of the new Irc framework. Only add the file, no changes to the actual...
authorGiuseppe Bilotta <giuseppe.bilotta@gmail.com>
Sun, 30 Jul 2006 12:58:32 +0000 (12:58 +0000)
committerGiuseppe Bilotta <giuseppe.bilotta@gmail.com>
Sun, 30 Jul 2006 12:58:32 +0000 (12:58 +0000)
lib/rbot/irc.rb [new file with mode: 0644]

diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb
new file mode 100644 (file)
index 0000000..31c4953
--- /dev/null
@@ -0,0 +1,973 @@
+#-- 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
+#++\r
+# :title: IRC module\r
+#\r
+# Basic IRC stuff\r
+#\r
+# This module defines the fundamental building blocks for IRC\r
+#\r
+# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
+# Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
+# License:: GPLv2\r
+\r
+\r
+# We start by extending the String class\r
+# with some IRC-specific methods\r
+#\r
+class String\r
+\r
+  # This method returns a string which is the downcased version of the\r
+  # receiver, according to IRC rules: due to the Scandinavian origin of IRC,\r
+  # the characters <tt>{}|^</tt> are considered the uppercase equivalent of\r
+  # <tt>[]\~</tt>.\r
+  #\r
+  # Since IRC is mostly case-insensitive (the Windows way: case is preserved,\r
+  # but it's actually ignored to check equality), this method is rather\r
+  # important when checking if two strings refer to the same entity\r
+  # (User/Channel)\r
+  #\r
+  # Modern server allow different casemaps, too, in which some or all\r
+  # of the extra characters are not converted\r
+  #\r
+  def irc_downcase(casemap='rfc1459')\r
+    case casemap\r
+    when 'rfc1459'\r
+      self.tr("\x41-\x5e", "\x61-\x7e")\r
+    when 'strict-rfc1459'\r
+      self.tr("\x41-\x5d", "\x61-\x7d")\r
+    when 'ascii'\r
+      self.tr("\x41-\x5a", "\x61-\x7a")\r
+    else\r
+      raise TypeError, "Unknown casemap #{casemap}"\r
+    end\r
+  end\r
+\r
+  # This is the same as the above, except that the string is altered in place\r
+  #\r
+  # See also the discussion about irc_downcase\r
+  #\r
+  def irc_downcase!(casemap='rfc1459')\r
+    case casemap\r
+    when 'rfc1459'\r
+      self.tr!("\x41-\x5e", "\x61-\x7e")\r
+    when 'strict-rfc1459'\r
+      self.tr!("\x41-\x5d", "\x61-\x7d")\r
+    when 'ascii'\r
+      self.tr!("\x41-\x5a", "\x61-\x7a")\r
+    else\r
+      raise TypeError, "Unknown casemap #{casemap}"\r
+    end\r
+  end\r
+\r
+  # Upcasing functions are provided too\r
+  #\r
+  # See also the discussion about irc_downcase\r
+  #\r
+  def irc_upcase(casemap='rfc1459')\r
+    case casemap\r
+    when 'rfc1459'\r
+      self.tr("\x61-\x7e", "\x41-\x5e")\r
+    when 'strict-rfc1459'\r
+      self.tr("\x61-\x7d", "\x41-\x5d")\r
+    when 'ascii'\r
+      self.tr("\x61-\x7a", "\x41-\x5a")\r
+    else\r
+      raise TypeError, "Unknown casemap #{casemap}"\r
+    end\r
+  end\r
+\r
+  # In-place upcasing\r
+  #\r
+  # See also the discussion about irc_downcase\r
+  #\r
+  def irc_upcase!(casemap='rfc1459')\r
+    case casemap\r
+    when 'rfc1459'\r
+      self.tr!("\x61-\x7e", "\x41-\x5e")\r
+    when 'strict-rfc1459'\r
+      self.tr!("\x61-\x7d", "\x41-\x5d")\r
+    when 'ascii'\r
+      self.tr!("\x61-\x7a", "\x41-\x5a")\r
+    else\r
+      raise TypeError, "Unknown casemap #{casemap}"\r
+    end\r
+  end\r
+\r
+  # This method checks if the receiver contains IRC glob characters\r
+  #\r
+  # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any\r
+  # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly\r
+  # one arbitrary character". These characters can be escaped by prefixing them\r
+  # with a slash (<tt>\\</tt>).\r
+  #\r
+  # A known limitation of this glob syntax is that there is no way to escape\r
+  # the escape character itself, so it's not possible to build a glob pattern\r
+  # where the escape character precedes a glob.\r
+  #\r
+  def has_irc_glob?\r
+    self =~ /^[*?]|[^\\][*?]/\r
+  end\r
+\r
+  # This method is used to convert the receiver into a Regular Expression\r
+  # that matches according to the IRC glob syntax\r
+  #\r
+  def to_irc_regexp\r
+    regmask = Regexp.escape(self)\r
+    regmask.gsub!(/(\\\\)?\\[*?]/) { |m|\r
+      case m\r
+      when /\\(\\[*?])/\r
+        $1\r
+      when /\\\*/\r
+        '.*'\r
+      when /\\\?/\r
+        '.'\r
+      else\r
+        raise "Unexpected match #{m} when converting #{self}"\r
+      end\r
+    }\r
+    Regexp.new(regmask)\r
+  end\r
+end\r
+\r
+\r
+# ArrayOf is a subclass of Array whose elements are supposed to be all\r
+# of the same class. This is not intended to be used directly, but rather\r
+# to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)\r
+#\r
+# Presently, only very few selected methods from Array are overloaded to check\r
+# if the new elements are the correct class. An orthodox? method is provided\r
+# to check the entire ArrayOf against the appropriate class.\r
+#\r
+class ArrayOf < Array\r
+\r
+  attr_reader :element_class\r
+\r
+  # Create a new ArrayOf whose elements are supposed to be all of type _kl_,\r
+  # optionally filling it with the elements from the Array argument.\r
+  #\r
+  def initialize(kl, ar=[])\r
+    raise TypeError, "#{kl.inspect} must be a class name" unless kl.class <= Class\r
+    super()\r
+    @element_class = kl\r
+    case ar\r
+    when Array\r
+      send(:+, ar)\r
+    else\r
+      raise TypeError, "#{self.class} can only be initialized from an Array"\r
+    end\r
+  end\r
+\r
+  # Private method to check the validity of the elements passed to it\r
+  # and optionally raise an error\r
+  #\r
+  # TODO should it accept nils as valid?\r
+  #\r
+  def internal_will_accept?(raising, *els)\r
+    els.each { |el|\r
+      unless el.class <= @element_class\r
+        raise TypeError if raising\r
+        return false\r
+      end\r
+    }\r
+    return true\r
+  end\r
+  private :internal_will_accept?\r
+\r
+  # This method checks if the passed arguments are acceptable for our ArrayOf\r
+  #\r
+  def will_accept?(*els)\r
+    internal_will_accept?(false, *els)\r
+  end\r
+\r
+  # This method checks that all elements are of the appropriate class\r
+  #\r
+  def valid?\r
+    will_accept?(*self)\r
+  end\r
+\r
+  # This method is similar to the above, except that it raises an exception\r
+  # if the receiver is not valid\r
+  def validate\r
+    raise TypeError unless valid?\r
+  end\r
+\r
+  # Overloaded from Array#<<, checks for appropriate class of argument\r
+  #\r
+  def <<(el)\r
+    super(el) if internal_will_accept?(true, el)\r
+  end\r
+\r
+  # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
+  #\r
+  def unshift(*els)\r
+    els.each { |el|\r
+      super(el) if internal_will_accept?(true, *els)\r
+    }\r
+  end\r
+\r
+  # Overloaded from Array#+, checks for appropriate class of argument elements\r
+  #\r
+  def +(ar)\r
+    super(ar) if internal_will_accept?(true, *ar)\r
+  end\r
+end\r
+\r
+# The Irc module is used to keep all IRC-related classes\r
+# in the same namespace\r
+#\r
+module Irc\r
+\r
+\r
+  # A Netmask identifies each user by collecting its nick, username and\r
+  # hostname in the form <tt>nick!user@host</tt>\r
+  #\r
+  # Netmasks can also contain glob patterns in any of their components; in this\r
+  # form they are used to refer to more than a user or to a user appearing\r
+  # under different\r
+  # forms.\r
+  #\r
+  # Example:\r
+  # * <tt>*!*@*</tt> refers to everybody\r
+  # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+\r
+  #   regardless of the nick used.\r
+  #\r
+  class Netmask\r
+    attr_reader :nick, :user, :host\r
+    attr_reader :casemap\r
+\r
+    # call-seq:\r
+    #   Netmask.new(netmask) => new_netmask\r
+    #   Netmask.new(hash={}, casemap=nil) => new_netmask\r
+    #   Netmask.new("nick!user@host", casemap=nil) => new_netmask\r
+    #\r
+    # Create a new Netmask in any of these forms\r
+    # 1. from another Netmask (does a .dup)\r
+    # 2. from a Hash with any of the keys <tt>:nick</tt>, <tt>:user</tt> and\r
+    #    <tt>:host</tt>\r
+    # 3. from a String in the form <tt>nick!user@host</tt>\r
+    #\r
+    # In all but the first forms a casemap may be speficied, the default\r
+    # being 'rfc1459'.\r
+    #\r
+    # The nick is downcased following IRC rules and according to the given casemap.\r
+    #\r
+    # FIXME check if user and host need to be downcased too.\r
+    #\r
+    # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
+    #\r
+    def initialize(str={}, casemap=nil)\r
+      case str\r
+      when Netmask\r
+        raise ArgumentError, "Can't set casemap when initializing from other Netmask" if casemap\r
+        @casemap = str.casemap.dup\r
+        @nick = str.nick.dup\r
+        @user = str.user.dup\r
+        @host = str.host.dup\r
+      when Hash\r
+        @casemap = casemap || str[:casemap] || 'rfc1459'\r
+        @nick = str[:nick].to_s.irc_downcase(@casemap)\r
+        @user = str[:user].to_s\r
+        @host = str[:host].to_s\r
+      when String\r
+        if str.match(/(\S+)(?:!(\S+)@(?:(\S+))?)?/)\r
+          @casemap = casemap || 'rfc1459'\r
+          @nick = $1.irc_downcase(@casemap)\r
+          @user = $2\r
+          @host = $3\r
+        else\r
+          raise ArgumentError, "#{str} is not a valid netmask"\r
+        end\r
+      else\r
+        raise ArgumentError, "#{str} is not a valid netmask"\r
+      end\r
+\r
+      @nick = "*" if @nick.to_s.empty?\r
+      @user = "*" if @user.to_s.empty?\r
+      @host = "*" if @host.to_s.empty?\r
+    end\r
+\r
+    # This method changes the nick of the Netmask, downcasing the argument\r
+    # following IRC rules and defaulting to the generic glob pattern if\r
+    # the result is the null string.\r
+    #\r
+    def nick=(newnick)\r
+      @nick = newnick.to_s.irc_downcase(@casemap)\r
+      @nick = "*" if @nick.empty?\r
+    end\r
+\r
+    # This method changes the user of the Netmask, defaulting to the generic\r
+    # glob pattern if the result is the null string.\r
+    #\r
+    def user=(newuser)\r
+      @user = newuser.to_s\r
+      @user = "*" if @user.empty?\r
+    end\r
+\r
+    # This method changes the hostname of the Netmask, defaulting to the generic\r
+    # glob pattern if the result is the null string.\r
+    #\r
+    def host=(newhost)\r
+      @host = newhost.to_s\r
+      @host = "*" if @host.empty?\r
+    end\r
+\r
+    # This method checks if a Netmask is definite or not, by seeing if\r
+    # any of its components are defined by globs\r
+    #\r
+    def has_irc_glob?\r
+      return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?\r
+    end\r
+\r
+    # A Netmask is easily converted to a String for the usual representation\r
+    # \r
+    def to_s\r
+      return "#{nick}@#{user}!#{host}"\r
+    end\r
+\r
+    # This method is used to match the current Netmask against another one\r
+    #\r
+    # The method returns true if each component of the receiver matches the\r
+    # corresponding component of the argument. By _matching_ here we mean that\r
+    # any netmask described by the receiver is also described by the argument.\r
+    #\r
+    # In this sense, matching is rather simple to define in the case when the\r
+    # receiver has no globs: it is just necessary to check if the argument\r
+    # describes the receiver, which can be done by matching it against the\r
+    # argument converted into an IRC Regexp (see String#to_irc_regexp).\r
+    #\r
+    # The situation is also easy when the receiver has globs and the argument\r
+    # doesn't, since in this case the result is false.\r
+    #\r
+    # The more complex case in which both the receiver and the argument have\r
+    # globs is not handled yet.\r
+    # \r
+    def matches?(arg)\r
+      cmp = Netmask(arg)\r
+      raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap\r
+      raise TypeError, "#{arg} is not a valid Netmask" unless cmp.class <= Netmask\r
+      [:nick, :user, :host].each { |component|\r
+        us = self.send(:component)\r
+        them = cmp.send(:component)\r
+        raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?\r
+        return false if us.has_irc_glob? && !them.has_irc_glob?\r
+        return false unless us =~ them.to_irc_regexp\r
+      }\r
+      return true\r
+    end\r
+\r
+    # Case equality. Checks if arg matches self\r
+    #\r
+    def ===(arg)\r
+      Netmask(arg).matches?(self)\r
+    end\r
+  end\r
+\r
+\r
+  # A NetmaskList is an ArrayOf <code>Netmask</code>s\r
+  #\r
+  class NetmaskList < ArrayOf\r
+\r
+    # Create a new NetmaskList, optionally filling it with the elements from\r
+    # the Array argument fed to it.\r
+    def initialize(ar=[])\r
+      super(Netmask, ar)\r
+    end\r
+  end\r
+\r
+\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
+  #\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
+\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
+      super\r
+      raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if has_irc_glob?\r
+    end\r
+  end\r
+\r
+\r
+  # A UserList is an ArrayOf <code>User</code>s\r
+  #\r
+  class UserList < ArrayOf\r
+\r
+    # Create a new UserList, optionally filling it with the elements from\r
+    # the Array argument fed to it.\r
+    def initialize(ar=[])\r
+      super(User, ar)\r
+    end\r
+  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
+  #\r
+  class Channel\r
+    attr_reader :name, :type, :casemap\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
+    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
+    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
+    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
+    end\r
+\r
+    # Create a new UserList\r
+    #\r
+    def new_userlist(name, default=UserList.new)\r
+      new_variable(name, default)\r
+    end\r
+\r
+    # Create a new NetmaskList\r
+    #\r
+    def new_netmasklist(name, default=NetmaskList.new)\r
+      new_variable(name, default)\r
+    end\r
+\r
+    # Creates a new channel with the given name, optionally setting the topic\r
+    # and an initial users list.\r
+    #\r
+    # No additional info is created here, because the channel flags and userlists\r
+    # allowed depend on the server.\r
+    #\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
+      @casemap = casemap || 'rfc1459'\r
+\r
+      raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
+      raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
+      raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
+\r
+      @name = name.irc_downcase(@casemap)\r
+\r
+      new_variable(:topic, topic)\r
+\r
+      new_userlist(:users)\r
+      case users\r
+      when UserList\r
+        @users = users.dup\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
+\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
+\r
+      # new_data_flag(:k, :key)\r
+      # new_data_flag(:l, :limit)\r
+    end\r
+\r
+    # A channel is local to a server if it has the '&' prefix\r
+    #\r
+    def local?\r
+      name[0] = 0x26\r
+    end\r
+\r
+    # A channel is modeless if it has the '+' prefix\r
+    #\r
+    def modeless?\r
+      name[0] = 0x2b\r
+    end\r
+\r
+    # A channel is safe if it has the '!' prefix\r
+    #\r
+    def safe?\r
+      name[0] = 0x21\r
+    end\r
+\r
+    # A channel is safe if it has the '#' prefix\r
+    #\r
+    def normal?\r
+      name[0] = 0x23\r
+    end\r
+  end\r
+\r
+\r
+  # A ChannelList is an ArrayOf <code>Channel</code>s\r
+  #\r
+  class ChannelList < ArrayOf\r
+\r
+    # Create a new ChannelList, optionally filling it with the elements from\r
+    # the Array argument fed to it.\r
+    def initialize(ar=[])\r
+      super(Channel, ar)\r
+    end\r
+  end\r
+\r
+\r
+  # An IRC Server represents the Server the client is connected to.\r
+  #\r
+  class Server\r
+\r
+    attr_reader :hostname, :version, :usermodes, :chanmodes\r
+    attr_reader :supports, :capab\r
+\r
+    attr_reader :channels, :users\r
+\r
+    # Create a new Server, with all instance variables reset\r
+    # to nil (for scalar variables), the channel and user lists\r
+    # are empty, and @supports is initialized to the default values\r
+    # for all known supported features.\r
+    #\r
+    def initialize\r
+      @hostname = @version = @usermodes = @chanmodes = nil\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
+        },\r
+        :channellen => 200,\r
+        :chantypes => "#&",\r
+        :excepts => nil,\r
+        :idchan => {},\r
+        :invex => nil,\r
+        :kicklen => nil,\r
+        :maxlist => {},\r
+        :modes => 3,\r
+        :network => nil,\r
+        :nicklen => 9,\r
+        :prefix => {\r
+          :modes => 'ov'.scan(/./),\r
+          :prefixes => '@+'.scan(/./)\r
+        },\r
+        :safelist => nil,\r
+        :statusmsg => nil,\r
+        :std => nil,\r
+        :targmax => {},\r
+        :topiclen => nil\r
+      }\r
+      @capab = {}\r
+\r
+      @channels = ChannelList.new\r
+      @channel_names = Array.new\r
+\r
+      @users = UserList.new\r
+      @user_nicks = Array.new\r
+    end\r
+\r
+    # This method is used to parse a 004 RPL_MY_INFO line\r
+    #\r
+    def parse_my_info(line)\r
+      ar = line.split(' ')\r
+      @hostname = ar[0]\r
+      @version = ar[1]\r
+      @usermodes = ar[2]\r
+      @chanmodes = ar[3]\r
+    end\r
+\r
+    def noval_warn(key, val, &block)\r
+      if val\r
+        yield if block_given?\r
+      else\r
+        warn "No #{key.to_s.upcase} value"\r
+      end\r
+    end\r
+\r
+    def val_warn(key, val, &block)\r
+      if val == true or val == false or val.nil?\r
+        yield if block_given?\r
+      else\r
+        warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
+      end\r
+    end\r
+    private :noval_warn, :val_warn\r
+\r
+    # This method is used to parse a 005 RPL_ISUPPORT line\r
+    #\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
+      ar.each { |en|\r
+        prekey, val = en.split('=', 2)\r
+        if prekey =~ /^-(.*)/\r
+          key = $1.downcase.to_sym\r
+          val = false\r
+        else\r
+          key = prekey.downcase.to_sym\r
+        end\r
+        case key\r
+        when :casemapping, :network\r
+          noval_warn(key, val) {\r
+            @supports[key] = val\r
+          }\r
+        when :chanlimit, :idchan, :maxlist, :targmax\r
+          noval_warn(key, val) {\r
+            groups = val.split(',')\r
+            groups.each { |g|\r
+              k, v = g.split(':')\r
+              @supports[key][k] = v.to_i\r
+            }\r
+          }\r
+        when :maxchannels\r
+          noval_warn(key, val) {\r
+            reparse += "CHANLIMIT=(chantypes):#{val} "\r
+          }\r
+        when :maxtargets\r
+          noval_warn(key, val) {\r
+            @supports[key]['PRIVMSG'] = val.to_i\r
+            @supports[key]['NOTICE'] = val.to_i\r
+          }\r
+        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
+          }\r
+        when :channellen, :kicklen, :modes, :topiclen\r
+          if val\r
+            @supports[key] = val.to_i\r
+          else\r
+            @supports[key] = nil\r
+          end\r
+        when :chantypes\r
+          @supports[key] = val # can also be nil\r
+        when :excepts\r
+          val ||= 'e'\r
+          @supports[key] = val\r
+        when :invex\r
+          val ||= 'I'\r
+          @supports[key] = val\r
+        when :nicklen\r
+          noval_warn(key, val) {\r
+            @supports[key] = val.to_i\r
+          }\r
+        when :prefix\r
+          if val\r
+            val.scan(/\((.*)\)(.*)/) { |m, p|\r
+              @supports[key][:modes] = m.scan(/./)\r
+              @supports[key][:prefixes] = p.scan(/./)\r
+            }\r
+          else\r
+            @supports[key][:modes] = nil\r
+            @supports[key][:prefixes] = nil\r
+          end\r
+        when :safelist\r
+          val_warn(key, val) {\r
+            @supports[key] = val.nil? ? true : val\r
+          }\r
+        when :statusmsg\r
+          noval_warn(key, val) {\r
+            @supports[key] = val.scan(/./)\r
+          }\r
+        when :std\r
+          noval_warn(key, val) {\r
+            @supports[key] = val.split(',')\r
+          }\r
+        else\r
+          @supports[key] =  val.nil? ? true : val\r
+        end\r
+      }\r
+      reparse.gsub!("(chantypes)",@supports[:chantypes])\r
+      parse_isupport(reparse) unless reparse.empty?\r
+    end\r
+\r
+    # Returns the casemap of the server.\r
+    #\r
+    def casemap\r
+      @supports[:casemapping] || 'rfc1459'\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
+    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
+      @channels[idx] if idx\r
+    end\r
+    alias :get_chan :get_channel\r
+\r
+    # Create a new Channel object and add it to the list of\r
+    # <code>Channel</code>s on the receiver, unless the channel\r
+    # was present already. In this case, the default action is\r
+    # to raise an exception, unless _fails_ is set to false\r
+    #\r
+    # The Channel is automatically created with the appropriate casemap\r
+    #\r
+    def new_channel(name, topic="", users=[], fails=true)\r
+      if !has_chan?(name)\r
+\r
+        prefix = name[0].chr\r
+\r
+        # Give a warning if the new Channel goes over some server limits.\r
+        #\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 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
+          count = 0\r
+          @channel_names.each { |n|\r
+            count += 1 if k.includes?(n[0].chr)\r
+          }\r
+          raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimits][k]\r
+        }\r
+\r
+        # So far, everything is fine. Now create the actual Channel\r
+        #\r
+        chan = Channel.new(name, topic, users, self.casemap)\r
+\r
+        # We wade through +prefix+ and +chanmodes+ to create appropriate\r
+        # lists and flags for this channel\r
+\r
+        @supports[:prefix][:modes].each { |mode|\r
+          chan.new_userlist(mode)\r
+        } if @supports[:prefix][:modes]\r
+\r
+        @supports[:chanmodes].each { |k, val|\r
+          if val\r
+            case k\r
+            when :addr_list\r
+              val.each { |mode|\r
+                chan.new_netmasklist(mode)\r
+              }\r
+            when :has_param, :set_param\r
+              val.each { |mode|\r
+                chan.new_data_flag(mode)\r
+              }\r
+            when :no_params\r
+              val.each { |mode|\r
+                chan.new_bool_flag(mode)\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
+        @channel_names << name\r
+        return newchan\r
+      end\r
+\r
+      raise "Channel #{name} already exists on server #{self}" if fails\r
+      return get_channel(name)\r
+    end\r
+\r
+    # Remove Channel _name_ from the list of <code>Channel</code>s\r
+    #\r
+    def delete_channel(name)\r
+      idx = has_channel?(name)\r
+      raise "Tried to remove unmanaged channel #{name}" unless idx\r
+      @channel_names.delete_at(idx)\r
+      @channels.delete_at(idx)\r
+    end\r
+\r
+    # Checks if the receiver already has a user with the given _nick_\r
+    #\r
+    def has_user?(nick)\r
+      @user_nicks.index(nick)\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
+      @users[idx] if idx\r
+    end\r
+\r
+    # Create a new User object and add it to the list of\r
+    # <code>User</code>s on the receiver, unless the User\r
+    # was present already. In this case, the default action is\r
+    # to raise an exception, unless _fails_ is set to false\r
+    #\r
+    # 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
+      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
+        @user_nicks << tmp.nick\r
+        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
+    end\r
+\r
+    # Returns the User with the given Netmask on the server,\r
+    # creating it if necessary. This is a short form for\r
+    # new_user(_str_, +false+)\r
+    #\r
+    def user(str)\r
+      new_user(str, false)\r
+    end\r
+\r
+    # Remove User _someuser_ from the list of <code>User</code>s.\r
+    # _someuser_ must be specified with the full Netmask.\r
+    #\r
+    def delete_user(someuser)\r
+      idx = has_user?(user.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
+      @user_nicks.delete_at(idx)\r
+      @users.delete_at(idx)\r
+    end\r
+\r
+    # Create a new Netmask object with the appropriate casemap\r
+    #\r
+    def new_netmask(str)\r
+      if str.class <= Netmask \r
+        raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap\r
+        return str\r
+      end\r
+      Netmask.new(str, self.casemap)\r
+    end\r
+\r
+    # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
+    #\r
+    def find_users(mask)\r
+      nm = new_netmask(mask)\r
+      @users.inject(UserList.new) {\r
+        |list, user|\r
+        list << user if user.matches?(nm)\r
+        list\r
+      }\r
+    end\r
+  end\r
+end\r
+\r
+# TODO test cases\r
+\r
+if __FILE__ == $0\r
+\r
+include Irc\r
+\r
+  # puts " -- irc_regexp tests"\r
+  # ["*", "a?b", "a*b", "a\\*b", "a\\?b", "a?\\*b", "*a*\\**b?"].each { |s|\r
+  #   puts " --"\r
+  #   puts s.inspect\r
+  #   puts s.to_irc_regexp.inspect\r
+  #   puts "aUb".match(s.to_irc_regexp)[0] if "aUb" =~ s.to_irc_regexp\r
+  # }\r
+\r
+  # puts " -- Netmasks"\r
+  # masks = []\r
+  # masks << Netmask.new("start")\r
+  # masks << masks[0].dup\r
+  # masks << Netmask.new(masks[0])\r
+  # puts masks.join("\n")\r
\r
+  # puts " -- Changing 1"\r
+  # masks[1].nick = "me"\r
+  # puts masks.join("\n")\r
+\r
+  # puts " -- Changing 2"\r
+  # masks[2].nick = "you"\r
+  # puts masks.join("\n")\r
+\r
+  # puts " -- Channel example"\r
+  # ch = Channel.new("#prova")\r
+  # p ch\r
+  # puts " -- Methods"\r
+  # puts ch.methods.sort.join("\n")\r
+  # puts " -- Instance variables"\r
+  # puts ch.instance_variables.join("\n")\r
+\r
+end\r