]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/irc.rb
Rework netmask/hostname detection code to work around non-RFC-compliant servers
[user/henk/code/ruby/rbot.git] / lib / rbot / irc.rb
index d5621b0fd587028096ae71381221b99a7826f7ac..1c9d4dcd5a9186054aa0a8ecbe81c080860ff84f 100644 (file)
@@ -3,7 +3,11 @@
 # * 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
+#   sent us privmsgs, we know we can remove him from the Server @users list\r
+# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?\r
+#   See items marked as TODO Ho.\r
+#   The framework to do this is now in place, thanks to the new [] method\r
+#   for NetmaskList, which allows retrieval by Netmask or String\r
 #++\r
 # :title: IRC module\r
 #\r
 # Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
 # License:: GPLv2\r
 \r
+require 'singleton'\r
+\r
+class Object\r
+\r
+  # We extend the Object class with a method that\r
+  # checks if the receiver is nil or empty\r
+  def nil_or_empty?\r
+    return true unless self\r
+    return true if self.respond_to? :empty and self.empty?\r
+    return false\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
+  # Due to its Scandinavian origins, IRC has strange case mappings, which\r
+  # consider the characters <tt>{}|^</tt> as the uppercase\r
+  # equivalents of # <tt>[]\~</tt>.\r
+  #\r
+  # This is however not the same on all IRC servers: some use standard ASCII\r
+  # casemapping, other do not consider <tt>^</tt> as the uppercase of\r
+  # <tt>~</tt>\r
+  #\r
+  class Casemap\r
+    @@casemaps = {}\r
+\r
+    # Create a new casemap with name _name_, uppercase characters _upper_ and\r
+    # lowercase characters _lower_\r
+    #\r
+    def initialize(name, upper, lower)\r
+      @key = name.to_sym\r
+      raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)\r
+      @@casemaps[@key] = {\r
+        :upper => upper,\r
+        :lower => lower,\r
+        :casemap => self\r
+      }\r
+    end\r
+\r
+    # Returns the Casemap with the given name\r
+    #\r
+    def Casemap.get(name)\r
+      @@casemaps[name.to_sym][:casemap]\r
+    end\r
+\r
+    # Retrieve the 'uppercase characters' of this Casemap\r
+    #\r
+    def upper\r
+      @@casemaps[@key][:upper]\r
+    end\r
+\r
+    # Retrieve the 'lowercase characters' of this Casemap\r
+    #\r
+    def lower\r
+      @@casemaps[@key][:lower]\r
+    end\r
+\r
+    # Return a Casemap based on the receiver\r
+    #\r
+    def to_irc_casemap\r
+      self\r
+    end\r
+\r
+    # A Casemap is represented by its lower/upper mappings\r
+    #\r
+    def inspect\r
+      "#<#{self.class}:#{'0x%x'% self.object_id}: #{upper.inspect} ~(#{self})~ #{lower.inspect}>"\r
+    end\r
+\r
+    # As a String we return our name\r
+    #\r
+    def to_s\r
+      @key.to_s\r
+    end\r
+\r
+    # Two Casemaps are equal if they have the same upper and lower ranges\r
+    #\r
+    def ==(arg)\r
+      other = arg.to_irc_casemap\r
+      return self.upper == other.upper && self.lower == other.lower\r
+    end\r
+\r
+    # Raise an error if _arg_ and self are not the same Casemap\r
+    #\r
+    def must_be(arg)\r
+      other = arg.to_irc_casemap\r
+      raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other\r
+      return true\r
+    end\r
+\r
+  end\r
+\r
+  # The rfc1459 casemap\r
+  #\r
+  class RfcCasemap < Casemap\r
+    include Singleton\r
+\r
+    def initialize\r
+      super('rfc1459', "\x41-\x5e", "\x61-\x7e")\r
+    end\r
+\r
+  end\r
+  RfcCasemap.instance\r
+\r
+  # The strict-rfc1459 Casemap\r
+  #\r
+  class StrictRfcCasemap < Casemap\r
+    include Singleton\r
+\r
+    def initialize\r
+      super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")\r
+    end\r
+\r
+  end\r
+  StrictRfcCasemap.instance\r
+\r
+  # The ascii Casemap\r
+  #\r
+  class AsciiCasemap < Casemap\r
+    include Singleton\r
+\r
+    def initialize\r
+      super('ascii', "\x41-\x5a", "\x61-\x7a")\r
+    end\r
+\r
+  end\r
+  AsciiCasemap.instance\r
+\r
+\r
+  # This module is included by all classes that are either bound to a server\r
+  # or should have a casemap.\r
+  #\r
+  module ServerOrCasemap\r
+\r
+    attr_reader :server\r
+\r
+    # This method initializes the instance variables @server and @casemap\r
+    # according to the values of the hash keys :server and :casemap in _opts_\r
+    #\r
+    def init_server_or_casemap(opts={})\r
+      @server = opts.fetch(:server, nil)\r
+      raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)\r
+\r
+      @casemap = opts.fetch(:casemap, nil)\r
+      if @server\r
+        if @casemap\r
+          @server.casemap.must_be(@casemap)\r
+          @casemap = nil\r
+        end\r
+      else\r
+        @casemap = (@casemap || 'rfc1459').to_irc_casemap\r
+      end\r
+    end\r
+\r
+    # This is an auxiliary method: it returns true if the receiver fits the\r
+    # server and casemap specified in _opts_, false otherwise.\r
+    #\r
+    def fits_with_server_and_casemap?(opts={})\r
+      srv = opts.fetch(:server, nil)\r
+      cmap = opts.fetch(:casemap, nil)\r
+      cmap = cmap.to_irc_casemap unless cmap.nil?\r
+\r
+      if srv.nil?\r
+        return true if cmap.nil? or cmap == casemap\r
+      else\r
+        return true if srv == @server and (cmap.nil? or cmap == casemap)\r
+      end\r
+      return false\r
+    end\r
+\r
+    # Returns the casemap of the receiver, by looking at the bound\r
+    # @server (if possible) or at the @casemap otherwise\r
+    #\r
+    def casemap\r
+      return @server.casemap if defined?(@server) and @server\r
+      return @casemap\r
+    end\r
+\r
+    # Returns a hash with the current @server and @casemap as values of\r
+    # :server and :casemap\r
+    #\r
+    def server_and_casemap\r
+      h = {}\r
+      h[:server] = @server if defined?(@server) and @server\r
+      h[:casemap] = @casemap if defined?(@casemap) and @casemap\r
+      return h\r
+    end\r
+\r
+    # We allow up/downcasing with a different casemap\r
+    #\r
+    def irc_downcase(cmap=casemap)\r
+      self.to_s.irc_downcase(cmap)\r
+    end\r
+\r
+    # Up/downcasing something that includes this module returns its\r
+    # Up/downcased to_s form\r
+    #\r
+    def downcase\r
+      self.irc_downcase\r
+    end\r
+\r
+    # We allow up/downcasing with a different casemap\r
+    #\r
+    def irc_upcase(cmap=casemap)\r
+      self.to_s.irc_upcase(cmap)\r
+    end\r
+\r
+    # Up/downcasing something that includes this module returns its\r
+    # Up/downcased to_s form\r
+    #\r
+    def upcase\r
+      self.irc_upcase\r
+    end\r
+\r
+  end\r
+\r
+end\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
+  # This method returns the Irc::Casemap whose name is the receiver\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
+  def to_irc_casemap\r
+    Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"\r
+  end\r
+\r
+  # This method returns a string which is the downcased version of the\r
+  # receiver, according to the given _casemap_\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
+    cmap = casemap.to_irc_casemap\r
+    self.tr(cmap.upper, cmap.lower)\r
   end\r
 \r
   # This is the same as the above, except that the string is altered in place\r
@@ -52,16 +267,8 @@ class String
   # 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
+    cmap = casemap.to_irc_casemap\r
+    self.tr!(cmap.upper, cmap.lower)\r
   end\r
 \r
   # Upcasing functions are provided too\r
@@ -69,16 +276,8 @@ class String
   # 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
+    cmap = casemap.to_irc_casemap\r
+    self.tr(cmap.lower, cmap.upper)\r
   end\r
 \r
   # In-place upcasing\r
@@ -86,16 +285,8 @@ class String
   # 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
+    cmap = casemap.to_irc_casemap\r
+    self.tr!(cmap.lower, cmap.upper)\r
   end\r
 \r
   # This method checks if the receiver contains IRC glob characters\r
@@ -132,6 +323,7 @@ class String
     }\r
     Regexp.new(regmask)\r
   end\r
+\r
 end\r
 \r
 \r
@@ -151,17 +343,21 @@ class ArrayOf < Array
   # 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
+    raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)\r
     super()\r
     @element_class = kl\r
     case ar\r
     when Array\r
-      send(:+, ar)\r
+      insert(0, *ar)\r
     else\r
       raise TypeError, "#{self.class} can only be initialized from an Array"\r
     end\r
   end\r
 \r
+  def inspect\r
+    "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>"\r
+  end\r
+\r
   # Private method to check the validity of the elements passed to it\r
   # and optionally raise an error\r
   #\r
@@ -169,8 +365,8 @@ class ArrayOf < Array
   #\r
   def internal_will_accept?(raising, *els)\r
     els.each { |el|\r
-      unless el.class <= @element_class\r
-        raise TypeError if raising\r
+      unless el.kind_of?(@element_class)\r
+        raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising\r
         return false\r
       end\r
     }\r
@@ -192,6 +388,7 @@ class ArrayOf < Array
 \r
   # This method is similar to the above, except that it raises an exception\r
   # if the receiver is not valid\r
+  #\r
   def validate\r
     raise TypeError unless valid?\r
   end\r
@@ -202,6 +399,60 @@ class ArrayOf < Array
     super(el) if internal_will_accept?(true, el)\r
   end\r
 \r
+  # Overloaded from Array#&, checks for appropriate class of argument elements\r
+  #\r
+  def &(ar)\r
+    r = super(ar)\r
+    ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)\r
+  end\r
+\r
+  # Overloaded from Array#+, checks for appropriate class of argument elements\r
+  #\r
+  def +(ar)\r
+    ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#-, so that an ArrayOf is returned. There is no need\r
+  # to check the validity of the elements in the argument\r
+  #\r
+  def -(ar)\r
+    ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#|, checks for appropriate class of argument elements\r
+  #\r
+  def |(ar)\r
+    ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#concat, checks for appropriate class of argument\r
+  # elements\r
+  #\r
+  def concat(ar)\r
+    super(ar) if internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#insert, checks for appropriate class of argument\r
+  # elements\r
+  #\r
+  def insert(idx, *ar)\r
+    super(idx, *ar) if internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#replace, checks for appropriate class of argument\r
+  # elements\r
+  #\r
+  def replace(ar)\r
+    super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)\r
+  end\r
+\r
+  # Overloaded from Array#push, checks for appropriate class of argument\r
+  # elements\r
+  #\r
+  def push(*ar)\r
+    super(*ar) if internal_will_accept?(true, *ar)\r
+  end\r
+\r
   # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
   #\r
   def unshift(*els)\r
@@ -210,26 +461,105 @@ class ArrayOf < Array
     }\r
   end\r
 \r
-  # Overloaded from Array#+, checks for appropriate class of argument elements\r
+  # We introduce the 'downcase' method, which maps downcase() to all the Array\r
+  # elements, properly failing when the elements don't have a downcase method\r
   #\r
-  def +(ar)\r
-    super(ar) if internal_will_accept?(true, *ar)\r
+  def downcase\r
+    self.map { |el| el.downcase }\r
   end\r
+\r
+  # Modifying methods which we don't handle yet are made private\r
+  #\r
+  private :[]=, :collect!, :map!, :fill, :flatten!\r
+\r
 end\r
 \r
-# The Irc module is used to keep all IRC-related classes\r
-# in the same namespace\r
+\r
+# We extend the Regexp class with an Irc module which will contain some\r
+# Irc-specific regexps\r
 #\r
+class Regexp\r
+\r
+  # We start with some general-purpose ones which will be used in the\r
+  # Irc module too, but are useful regardless\r
+  DIGITS = /\d+/\r
+  HEX_DIGIT = /[0-9A-Fa-f]/\r
+  HEX_DIGITS = /#{HEX_DIGIT}+/\r
+  HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/\r
+  DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/\r
+  DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/\r
+  HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/\r
+  IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/\r
+\r
+  # IPv6, from Resolv::IPv6, without the \A..\z anchors\r
+  HEX_16BIT = /#{HEX_DIGIT}{1,4}/\r
+  IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/\r
+  IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/\r
+  IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/\r
+  IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/\r
+  IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/\r
+\r
+  # We start with some IRC related regular expressions, used to match\r
+  # Irc::User nicks and users and Irc::Channel names\r
+  #\r
+  # For each of them we define two versions of the regular expression:\r
+  #  * a generic one, which should match for any server but may turn out to\r
+  #    match more than a specific server would accept\r
+  #  * an RFC-compliant matcher\r
+  #\r
+  module Irc\r
+\r
+    # Channel-name-matching regexps\r
+    CHAN_FIRST = /[#&+]/\r
+    CHAN_SAFE = /![A-Z0-9]{5}/\r
+    CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/\r
+    GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/\r
+    RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/\r
+\r
+    # Nick-matching regexps\r
+    SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/\r
+    NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/\r
+    NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/\r
+    GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/\r
+    RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/\r
+\r
+    USER_CHAR = /[^\x00\x0a\x0d @]/\r
+    GEN_USER = /#{USER_CHAR}+/\r
+\r
+    # Host-matching regexps\r
+    HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/\r
+    HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/\r
+    HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/\r
+\r
+    GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/\r
+\r
+    # FreeNode network replaces the host of affiliated users with\r
+    # 'virtual hosts' \r
+    # FIXME we need the true syntax to match it properly ...\r
+    PDPC_HOST_PART = /[0-9A-Za-z.-]+/\r
+    PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/\r
+\r
+    # NOTE: the final optional and non-greedy dot is needed because some\r
+    # servers (e.g. FreeNode) send the hostname of the services as "services."\r
+    # which is not RFC compliant, but sadly done.\r
+    GEN_MASK_HOST = /#{PDPC_HOST}|#{GEN_HOST}\.??/ \r
+\r
+    # Netmask-matching Regexp\r
+    GEN_MASK = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_MASK_HOST}))?/\r
+  end\r
+\r
+end\r
+\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
+  # Netmasks can also contain glob patterns in any of their components; in\r
+  # this form they are used to refer to more than a user or to a user\r
+  # appearing under different forms.\r
   #\r
   # Example:\r
   # * <tt>*!*@*</tt> refers to everybody\r
@@ -237,72 +567,98 @@ module Irc
   #   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
+    # Netmasks have an associated casemap unless they are bound to a server\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
+    include ServerOrCasemap\r
+\r
+    attr_reader :nick, :user, :host\r
+\r
+    # Create a new Netmask from string _str_, which must be in the form\r
+    # _nick_!_user_@_host_\r
     #\r
-    # FIXME check if user and host need to be downcased too.\r
+    # It is possible to specify a server or a casemap in the optional Hash:\r
+    # these are used to associate the Netmask with the given server and to set\r
+    # its casemap: if a server is specified and a casemap is not, the server's\r
+    # casemap is used. If both a server and a casemap are specified, the\r
+    # casemap must match the server's casemap or an exception will be raised.\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
-        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
-          @host = $3\r
+    def initialize(str="", opts={})\r
+      # First of all, check for server/casemap option\r
+      #\r
+      init_server_or_casemap(opts)\r
+\r
+      # Now we can see if the given string _str_ is an actual Netmask\r
+      if str.respond_to?(:to_str)\r
+        case str.to_str\r
+        when /^(?:#{Regexp::Irc::GEN_MASK})?$/\r
+          # We do assignment using our internal methods\r
+          self.nick = $1\r
+          self.user = $2\r
+          self.host = $3\r
         else\r
-          raise ArgumentError, "#{str} is not a valid netmask"\r
+          raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"\r
         end\r
       else\r
-        raise ArgumentError, "#{str} is not a valid netmask"\r
+        raise TypeError, "#{str} cannot be converted to a #{self.class}"\r
       end\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
+    # A Netmask is easily converted to a String for the usual representation\r
+    #\r
+    def fullform\r
+      "#{nick}!#{user}@#{host}"\r
+    end\r
+    alias :to_s :fullform\r
+\r
+    # Converts the receiver into a Netmask with the given (optional)\r
+    # server/casemap association. We return self unless a conversion\r
+    # is needed (different casemap/server)\r
+    #\r
+    # Subclasses of Netmask will return a new Netmask\r
+    #\r
+    def to_irc_netmask(opts={})\r
+      if self.class == Netmask\r
+        return self if fits_with_server_and_casemap?(opts)\r
+      end\r
+      return self.downcase.to_irc_netmask(opts)\r
+    end\r
+\r
+    # Converts the receiver into a User with the given (optional)\r
+    # server/casemap association. We return self unless a conversion\r
+    # is needed (different casemap/server)\r
+    #\r
+    def to_irc_user(opts={})\r
+      self.fullform.to_irc_user(server_and_casemap.merge(opts))\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
+    # Inspection of a Netmask reveals the server it's bound to (if there is\r
+    # one), its casemap and the nick, user and host part\r
+    #\r
+    def inspect\r
+      str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
+      str << " @server=#{@server}" if defined?(@server) and @server\r
+      str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"\r
+      str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"\r
+      str << ">"\r
+    end\r
+\r
+    # Equality: two Netmasks are equal if they downcase to the same thing\r
+    #\r
+    # TODO we may want it to try other.to_irc_netmask\r
+    #\r
+    def ==(other)\r
+      return false unless other.kind_of?(self.class)\r
+      self.downcase == other.downcase\r
+    end\r
+\r
+    # This method changes the nick of the Netmask, defaulting to the generic\r
+    # glob pattern if the result is the null string.\r
     #\r
     def nick=(newnick)\r
-      @nick = newnick.to_s.irc_downcase(@casemap)\r
+      @nick = newnick.to_s\r
       @nick = "*" if @nick.empty?\r
     end\r
 \r
@@ -322,6 +678,21 @@ module Irc
       @host = "*" if @host.empty?\r
     end\r
 \r
+    # We can replace everything at once with data from another Netmask\r
+    #\r
+    def replace(other)\r
+      case other\r
+      when Netmask\r
+        nick = other.nick\r
+        user = other.user\r
+        host = other.host\r
+        @server = other.server\r
+        @casemap = other.casemap unless @server\r
+      else\r
+        replace(other.to_irc_netmask(server_and_casemap))\r
+      end\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
@@ -329,18 +700,12 @@ module Irc
       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 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
     # 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
+    # corresponding component of the argument. By _matching_ here we mean\r
+    # that any netmask described by the receiver is also described by the\r
+    # 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
@@ -352,15 +717,18 @@ module Irc
     #\r
     # The more complex case in which both the receiver and the argument have\r
     # globs is not handled yet.\r
-    # \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
+      cmp = arg.to_irc_netmask(:casemap => casemap)\r
+      debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"\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
+        us = self.send(component).irc_downcase(casemap)\r
+        them = cmp.send(component).irc_downcase(casemap)\r
+        if us.has_irc_glob? && them.has_irc_glob?\r
+          next if us == them\r
+          warn NotImplementedError\r
+          return false\r
+        end\r
         return false if us.has_irc_glob? && !them.has_irc_glob?\r
         return false unless us =~ them.to_irc_regexp\r
       }\r
@@ -370,8 +738,20 @@ module Irc
     # Case equality. Checks if arg matches self\r
     #\r
     def ===(arg)\r
-      Netmask(arg).matches?(self)\r
+      arg.to_irc_netmask(:casemap => casemap).matches?(self)\r
     end\r
+\r
+    # Sorting is done via the fullform\r
+    #\r
+    def <=>(arg)\r
+      case arg\r
+      when Netmask\r
+        self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)\r
+      else\r
+        self.downcase <=> arg.downcase\r
+      end\r
+    end\r
+\r
   end\r
 \r
 \r
@@ -381,19 +761,75 @@ module Irc
 \r
     # Create a new NetmaskList, optionally filling it with the elements from\r
     # the Array argument fed to it.\r
+    #\r
     def initialize(ar=[])\r
       super(Netmask, ar)\r
     end\r
+\r
+    # We enhance the [] method by allowing it to pick an element that matches\r
+    # a given Netmask, a String or a Regexp\r
+    # TODO take into consideration the opportunity to use select() instead of\r
+    # find(), and/or a way to let the user choose which one to take (second\r
+    # argument?)\r
+    #\r
+    def [](*args)\r
+      if args.length == 1\r
+        case args[0]\r
+        when Netmask\r
+          self.find { |mask|\r
+            mask.matches?(args[0])\r
+          }\r
+        when String\r
+          self.find { |mask|\r
+            mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))\r
+          }\r
+        when Regexp\r
+          self.find { |mask|\r
+            mask.fullform =~ args[0]\r
+          }\r
+        else\r
+          super(*args)\r
+        end\r
+      else\r
+        super(*args)\r
+      end\r
+    end\r
+\r
+  end\r
+\r
+end\r
+\r
+\r
+class String\r
+\r
+  # We keep extending String, this time adding a method that converts a\r
+  # String into an Irc::Netmask object\r
+  #\r
+  def to_irc_netmask(opts={})\r
+    Irc::Netmask.new(self, opts)\r
   end\r
 \r
+end\r
+\r
+\r
+module Irc\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.\r
+  # An IRC User is identified by his/her Netmask (which must not have globs).\r
+  # In fact, User is just a subclass of Netmask.\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
+  # Ideally, the user and host information of an IRC User should never\r
+  # change, and it shouldn't contain glob patterns. However, IRC is somewhat\r
+  # idiosincratic and it may be possible to know the nick of a User much before\r
+  # its user and host are known. Moreover, some networks (namely Freenode) may\r
+  # change the hostname of a User when (s)he identifies with Nickserv.\r
+  #\r
+  # As a consequence, we must allow changes to a User host and user attributes.\r
+  # We impose a restriction, though: they may not contain glob patterns, except\r
+  # for the special case of an unknown user/host which is represented by a *.\r
+  #\r
+  # It is possible to create a totally unknown User (e.g. for initializations)\r
+  # by setting the nick to * too.\r
   #\r
   # TODO list:\r
   # * see if it's worth to add the other USER data\r
@@ -405,7 +841,7 @@ module Irc
     # 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="", opts={})\r
       super\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
@@ -413,32 +849,35 @@ module Irc
       @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
+    # The nick of a User may be changed freely, but it must not contain glob patterns.\r
+    #\r
+    def nick=(newnick)\r
+      raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?\r
+      super\r
+    end\r
+\r
+    # We have to allow changing the user of an Irc User due to some networks\r
+    # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
+    # user data has glob patterns though.\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
+      raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?\r
+      super\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
+    # We have to allow changing the host of an Irc User due to some networks\r
+    # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
+    # host data has glob patterns though.\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
+      raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?\r
+      super\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
+      return nick!= "*" && user!="*" && host!="*"\r
     end\r
 \r
     # Is the user away?\r
@@ -457,146 +896,340 @@ module Irc
         @away = false\r
       end\r
     end\r
+\r
+    # Users can be either simply downcased (their nick only)\r
+    # or fully downcased: this will return the fullform downcased\r
+    # according to the given casemap.\r
+    #\r
+    def full_irc_downcase(cmap=casemap)\r
+      self.fullform.irc_downcase(cmap)\r
+    end\r
+\r
+    # full_downcase() will return the fullform downcased according to the\r
+    # User's own casemap\r
+    #\r
+    def full_downcase\r
+      self.full_irc_downcase\r
+    end\r
+\r
+    # Since to_irc_user runs the same checks on server and channel as\r
+    # to_irc_netmask, we just try that and return self if it works.\r
+    #\r
+    # Subclasses of User will return self if possible.\r
+    #\r
+    def to_irc_user(opts={})\r
+      return self if fits_with_server_and_casemap?(opts)\r
+      return self.full_downcase.to_irc_user(opts)\r
+    end\r
+\r
+    # We can replace everything at once with data from another User\r
+    #\r
+    def replace(other)\r
+      case other\r
+      when User\r
+        self.nick = other.nick\r
+        self.user = other.user\r
+        self.host = other.host\r
+        @server = other.server\r
+        @casemap = other.casemap unless @server\r
+        @away = other.away?\r
+      else\r
+        self.replace(other.to_irc_user(server_and_casemap))\r
+      end\r
+    end\r
+\r
   end\r
 \r
 \r
   # A UserList is an ArrayOf <code>User</code>s\r
+  # We derive it from NetmaskList, which allows us to inherit any special\r
+  # NetmaskList method\r
   #\r
-  class UserList < ArrayOf\r
+  class UserList < NetmaskList\r
 \r
     # Create a new UserList, optionally filling it with the elements from\r
     # the Array argument fed to it.\r
+    #\r
     def initialize(ar=[])\r
-      super(User, ar)\r
+      super(ar)\r
+      @element_class = User\r
     end\r
-  end\r
-\r
 \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
+    # Convenience method: convert the UserList to a list of nicks. The indices\r
+    # are preserved\r
+    #\r
+    def nicks\r
+      self.map { |user| user.nick }\r
     end\r
+\r
   end\r
 \r
+end\r
+\r
+class String\r
 \r
-  # Mode on a channel\r
-  class ChannelMode\r
+  # We keep extending String, this time adding a method that converts a\r
+  # String into an Irc::User object\r
+  #\r
+  def to_irc_user(opts={})\r
+    Irc::User.new(self, opts)\r
   end\r
 \r
+end\r
+\r
+module Irc\r
 \r
-  # Channel modes of type A manipulate lists\r
+  # An IRC Channel is identified by its name, and it has a set of properties:\r
+  # * a Channel::Topic\r
+  # * a UserList\r
+  # * a set of Channel::Modes\r
   #\r
-  class ChannelModeTypeA < ChannelMode\r
-    def initialize\r
-      @list = NetmaskList.new\r
-    end\r
+  # The Channel::Topic and Channel::Mode classes are defined within the\r
+  # Channel namespace because they only make sense there\r
+  #\r
+  class Channel\r
 \r
-    def set(val)\r
-      @list << val unless @list.include?(val)\r
-    end\r
 \r
-    def reset(val)\r
-      @list.delete_if(val) if @list.include?(val)\r
-    end\r
-  end\r
+    # Mode on a Channel\r
+    #\r
+    class Mode\r
+      attr_reader :channel\r
+      def initialize(ch)\r
+        @channel = ch\r
+      end\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
-    def set(val)\r
-      @arg = val\r
-    end\r
 \r
-    def reset(val)\r
-      @arg = nil if @arg == val\r
-    end\r
-  end\r
+    # Channel modes of type A manipulate lists\r
+    #\r
+    # Example: b (banlist)\r
+    #\r
+    class ModeTypeA < Mode\r
+      attr_reader :list\r
+      def initialize(ch)\r
+        super\r
+        @list = NetmaskList.new\r
+      end\r
+\r
+      def set(val)\r
+        nm = @channel.server.new_netmask(val)\r
+        @list << nm unless @list.include?(nm)\r
+      end\r
+\r
+      def reset(val)\r
+        nm = @channel.server.new_netmask(val)\r
+        @list.delete(nm)\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
+\r
+    # Channel modes of type B need an argument\r
+    #\r
+    # Example: k (key)\r
+    #\r
+    class ModeTypeB < Mode\r
+      def initialize(ch)\r
+        super\r
+        @arg = nil\r
+      end\r
+\r
+      def status\r
+        @arg\r
+      end\r
+      alias :value :status\r
+\r
+      def set(val)\r
+        @arg = val\r
+      end\r
+\r
+      def reset(val)\r
+        @arg = nil if @arg == val\r
+      end\r
+\r
     end\r
 \r
-    def reset(val)\r
-      @list.delete_if { |x| x == val }\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 UserMode < ModeTypeB\r
+      attr_reader :list\r
+      alias :users :list\r
+      def initialize(ch)\r
+        super\r
+        @list = UserList.new\r
+      end\r
+\r
+      def set(val)\r
+        u = @channel.server.user(val)\r
+        @list << u unless @list.include?(u)\r
+      end\r
+\r
+      def reset(val)\r
+        u = @channel.server.user(val)\r
+        @list.delete(u)\r
+      end\r
+\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
+\r
+    # Channel modes of type C need an argument when set,\r
+    # but not when they get reset\r
+    #\r
+    # Example: l (limit)\r
+    #\r
+    class ModeTypeC < Mode\r
+      def initialize(ch)\r
+        super\r
+        @arg = nil\r
+      end\r
+\r
+      def status\r
+        @arg\r
+      end\r
+      alias :value :status\r
+\r
+      def set(val)\r
+        @arg = val\r
+      end\r
+\r
+      def reset\r
+        @arg = nil\r
+      end\r
+\r
     end\r
 \r
-    def set(val)\r
-      @arg = val\r
+\r
+    # Channel modes of type D are basically booleans\r
+    #\r
+    # Example: m (moderate)\r
+    #\r
+    class ModeTypeD < Mode\r
+      def initialize(ch)\r
+        super\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
+\r
     end\r
 \r
-    def reset\r
-      @arg = false\r
+\r
+    # A Topic represents the topic of a channel. It consists of\r
+    # the topic itself, who set it and when\r
+    #\r
+    class Topic\r
+      attr_accessor :text, :set_by, :set_on\r
+      alias :to_s :text\r
+\r
+      # Create a new Topic setting the text, the creator and\r
+      # the creation time\r
+      #\r
+      def initialize(text="", set_by="", set_on=Time.new)\r
+        @text = text\r
+        @set_by = set_by.to_irc_user\r
+        @set_on = set_on\r
+      end\r
+\r
+      # Replace a Topic with another one\r
+      #\r
+      def replace(topic)\r
+        raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)\r
+        @text = topic.text.dup\r
+        @set_by = topic.set_by.dup\r
+        @set_on = topic.set_on.dup\r
+      end\r
+\r
+      # Returns self\r
+      #\r
+      def to_irc_channel_topic\r
+        self\r
+      end\r
+\r
     end\r
+\r
   end\r
 \r
-  # Channel modes of type D are basically booleans\r
-  class ChannelModeTypeD\r
-    def initialize\r
-      @set = false\r
-    end\r
+end\r
 \r
-    def set?\r
-      return @set\r
-    end\r
 \r
-    def set\r
-      @set = true\r
-    end\r
+class String\r
 \r
-    def reset\r
-      @set = false\r
-    end\r
+  # Returns an Irc::Channel::Topic with self as text\r
+  #\r
+  def to_irc_channel_topic\r
+    Irc::Channel::Topic.new(self)\r
   end\r
 \r
+end\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
+module Irc\r
+\r
+\r
+  # Here we start with the actual Channel class\r
   #\r
   class Channel\r
-    attr_reader :name, :topic, :casemap, :mode, :users\r
+\r
+    include ServerOrCasemap\r
+    attr_reader :name, :topic, :mode, :users\r
     alias :to_s :name\r
 \r
-    # A String describing the Channel and (some of its) internals\r
-    #\r
     def inspect\r
-      str = "<#{self.class}:#{'0x%08x' % self.object_id}:"\r
+      str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
+      str << " on server #{server}" if server\r
       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
-      str << " @users=<#{@users.join(', ')}>"\r
-      str\r
+      str << " @users=[#{user_nicks.sort.join(', ')}]"\r
+      str << ">"\r
+    end\r
+\r
+    # Returns self\r
+    #\r
+    def to_irc_channel\r
+      self\r
+    end\r
+\r
+    # TODO Ho\r
+    def user_nicks\r
+      @users.map { |u| u.downcase }\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.irc_downcase(casemap))\r
+    end\r
+\r
+    # Returns the user with nick _nick_, if available\r
+    #\r
+    def get_user(nick)\r
+      idx = has_user?(nick)\r
+      @users[idx] if idx\r
+    end\r
+\r
+    # Adds a user to the channel\r
+    #\r
+    def add_user(user, opts={})\r
+      silent = opts.fetch(:silent, false) \r
+      if has_user?(user) && !silent\r
+        warn "Trying to add user #{user} to channel #{self} again"\r
+      else\r
+        @users << user.to_irc_user(server_and_casemap)\r
+      end\r
     end\r
 \r
     # Creates a new channel with the given name, optionally setting the topic\r
@@ -605,27 +1238,22 @@ module Irc
     # 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=nil, users=[], casemap=nil)\r
-      @casemap = casemap || 'rfc1459'\r
-\r
+    def initialize(name, topic=nil, users=[], opts={})\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
+      warn "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
+      init_server_or_casemap(opts)\r
 \r
-      @topic = topic || ChannelTopic.new\r
+      @name = name\r
 \r
-      case users\r
-      when UserList\r
-        @users = users\r
-      when Array\r
-        @users = UserList.new(users)\r
-      else\r
-        raise ArgumentError, "Invalid user list #{users.inspect}"\r
-      end\r
+      @topic = (topic.to_irc_channel_topic rescue Channel::Topic.new)\r
+\r
+      @users = UserList.new\r
+\r
+      users.each { |u|\r
+        add_user(u)\r
+      }\r
 \r
       # Flags\r
       @mode = {}\r
@@ -634,10 +1262,10 @@ module Irc
     # 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
+        mode.reset(user) if mode.kind_of?(UserMode)\r
       }\r
+      @users.delete(user)\r
     end\r
 \r
     # The channel prefix\r
@@ -649,32 +1277,33 @@ module Irc
     # A channel is local to a server if it has the '&' prefix\r
     #\r
     def local?\r
-      name[0] = 0x26\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
+      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
+      name[0] == 0x21\r
     end\r
 \r
-    # A channel is safe if it has the '#' prefix\r
+    # A channel is normal if it has the '#' prefix\r
     #\r
     def normal?\r
-      name[0] = 0x23\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
+      @mode[sym.to_sym] = kl.new(self)\r
     end\r
+\r
   end\r
 \r
 \r
@@ -684,11 +1313,37 @@ module Irc
 \r
     # Create a new ChannelList, optionally filling it with the elements from\r
     # the Array argument fed to it.\r
+    #\r
     def initialize(ar=[])\r
       super(Channel, ar)\r
     end\r
+\r
+    # Convenience method: convert the ChannelList to a list of channel names.\r
+    # The indices are preserved\r
+    #\r
+    def names\r
+      self.map { |chan| chan.name }\r
+    end\r
+\r
   end\r
 \r
+end\r
+\r
+\r
+class String\r
+\r
+  # We keep extending String, this time adding a method that converts a\r
+  # String into an Irc::Channel object\r
+  #\r
+  def to_irc_channel(opts={})\r
+    Irc::Channel.new(self, opts)\r
+  end\r
+\r
+end\r
+\r
+\r
+module Irc\r
+\r
 \r
   # An IRC Server represents the Server the client is connected to.\r
   #\r
@@ -700,19 +1355,42 @@ module Irc
 \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
+    # TODO Ho\r
+    def channel_names\r
+      @channels.map { |ch| ch.downcase }\r
+    end\r
+\r
+    # TODO Ho\r
+    def user_nicks\r
+      @users.map { |u| u.downcase }\r
+    end\r
+\r
+    def inspect\r
+      chans, users = [@channels, @users].map {|d|\r
+        d.sort { |a, b|\r
+          a.downcase <=> b.downcase\r
+        }.map { |x|\r
+          x.inspect\r
+        }\r
+      }\r
+\r
+      str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
+      str << " @hostname=#{hostname}"\r
+      str << " @channels=#{chans}"\r
+      str << " @users=#{users}"\r
+      str << ">"\r
+    end\r
+\r
+    # Create a new Server, with all instance variables reset to nil (for\r
+    # scalar variables), empty channel and user lists and @supports\r
+    # initialized to the default values for all known supported features.\r
     #\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
@@ -721,7 +1399,7 @@ module Irc
     #\r
     def reset_capabilities\r
       @supports = {\r
-        :casemapping => 'rfc1459',\r
+        :casemapping => 'rfc1459'.to_irc_casemap,\r
         :chanlimit => {},\r
         :chanmodes => {\r
           :typea => nil, # Type A: address lists\r
@@ -729,8 +1407,8 @@ module Irc
           :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
+        :channellen => 50,\r
+        :chantypes => "#&!+",\r
         :excepts => nil,\r
         :idchan => {},\r
         :invex => nil,\r
@@ -740,8 +1418,8 @@ module Irc
         :network => nil,\r
         :nicklen => 9,\r
         :prefix => {\r
-          :modes => 'ov'.scan(/./),\r
-          :prefixes => '@+'.scan(/./)\r
+          :modes => [:o, :v],\r
+          :prefixes => [:"@", :+]\r
         },\r
         :safelist => nil,\r
         :statusmsg => nil,\r
@@ -768,6 +1446,7 @@ module Irc
     def clear\r
       reset_lists\r
       reset_capabilities\r
+      @hostname = @version = @usermodes = @chanmodes = nil\r
     end\r
 \r
     # This method is used to parse a 004 RPL_MY_INFO line\r
@@ -802,6 +1481,7 @@ module Irc
     # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
     #\r
     def parse_isupport(line)\r
+      debug "Parsing ISUPPORT #{line.inspect}"\r
       ar = line.split(' ')\r
       reparse = ""\r
       ar.each { |en|\r
@@ -813,34 +1493,25 @@ module Irc
           key = prekey.downcase.to_sym\r
         end\r
         case key\r
-        when :casemapping, :network\r
+        when :casemapping\r
           noval_warn(key, val) {\r
-            @supports[key] = val\r
+            @supports[key] = val.to_irc_casemap\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
+              @supports[key][k] = v.to_i || 0\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][: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
+            @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
+            @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
+            @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
+            @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
           }\r
         when :channellen, :kicklen, :modes, :topiclen\r
           if val\r
@@ -856,6 +1527,19 @@ module Irc
         when :invex\r
           val ||= 'I'\r
           @supports[key] = val\r
+        when :maxchannels\r
+          noval_warn(key, val) {\r
+            reparse += "CHANLIMIT=(chantypes):#{val} "\r
+          }\r
+        when :maxtargets\r
+          noval_warn(key, val) {\r
+            @supports[:targmax]['PRIVMSG'] = val.to_i\r
+            @supports[:targmax]['NOTICE'] = val.to_i\r
+          }\r
+        when :network\r
+          noval_warn(key, val) {\r
+            @supports[key] = val\r
+          }\r
         when :nicklen\r
           noval_warn(key, val) {\r
             @supports[key] = val.to_i\r
@@ -863,8 +1547,8 @@ module Irc
         when :prefix\r
           if val\r
             val.scan(/\((.*)\)(.*)/) { |m, p|\r
-              @supports[key][:modes] = m.scan(/./)\r
-              @supports[key][:prefixes] = p.scan(/./)\r
+              @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
+              @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
             }\r
           else\r
             @supports[key][:modes] = nil\r
@@ -893,14 +1577,14 @@ module Irc
     # Returns the casemap of the server.\r
     #\r
     def casemap\r
-      @supports[:casemapping] || 'rfc1459'\r
+      @supports[:casemapping]\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
+      if supports[:chantypes].include?(name[0])\r
         return Channel\r
       else\r
         return User\r
@@ -910,7 +1594,7 @@ module Irc
     # 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
+      if supports[:chantypes].include?(name[0])\r
         return channel(name)\r
       else\r
         return user(name)\r
@@ -920,27 +1604,37 @@ module Irc
     # Checks if the receiver already has a channel with the given _name_\r
     #\r
     def has_channel?(name)\r
-      @channel_names.index(name.to_s)\r
+      return false if name.nil_or_empty?\r
+      channel_names.index(name.irc_downcase(casemap))\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.to_s)\r
-      @channels[idx] if idx\r
+      return nil if name.nil_or_empty?\r
+      idx = has_channel?(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
+    # Create a new Channel object bound to the receiver and add it to the\r
+    # list of <code>Channel</code>s on the receiver, unless the channel was\r
+    # present already. In this case, the default action is to raise an\r
+    # exception, unless _fails_ is set to false.  An exception can also be\r
+    # raised if _str_ is nil or empty, again only if _fails_ is set to true;\r
+    # otherwise, the method just returns nil\r
     #\r
     def new_channel(name, topic=nil, users=[], fails=true)\r
-      if !has_chan?(name)\r
+      if name.nil_or_empty?\r
+        raise "Tried to look for empty or nil channel name #{name.inspect}" if fails\r
+        return nil\r
+      end\r
+      ex = get_chan(name)\r
+      if ex\r
+        raise "Channel #{name} already exists on server #{self}" if fails\r
+        return ex\r
+      else\r
 \r
         prefix = name[0].chr\r
 \r
@@ -949,7 +1643,7 @@ module Irc
         # FIXME might need to raise an exception\r
         #\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
+        warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[: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
@@ -957,21 +1651,21 @@ module Irc
         @supports[:chanlimit].keys.each { |k|\r
           next unless k.include?(prefix)\r
           count = 0\r
-          @channel_names.each { |n|\r
-            count += 1 if k.include?(n[0].chr)\r
+          channel_names.each { |n|\r
+            count += 1 if k.include?(n[0])\r
           }\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
         #\r
-        chan = Channel.new(name, topic, users, self.casemap)\r
+        chan = Channel.new(name, topic, users, :server => self)\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.create_mode(mode, ChannelUserMode)\r
+          chan.create_mode(mode, Channel::UserMode)\r
         } if @supports[:prefix][:modes]\r
 \r
         @supports[:chanmodes].each { |k, val|\r
@@ -979,33 +1673,28 @@ module Irc
             case k\r
             when :typea\r
               val.each { |mode|\r
-                chan.create_mode(mode, ChannelModeTypeA)\r
+                chan.create_mode(mode, Channel::ModeTypeA)\r
               }\r
             when :typeb\r
               val.each { |mode|\r
-                chan.create_mode(mode, ChannelModeTypeB)\r
+                chan.create_mode(mode, Channel::ModeTypeB)\r
               }\r
             when :typec\r
               val.each { |mode|\r
-                chan.create_mode(mode, ChannelModeTypeC)\r
+                chan.create_mode(mode, Channel::ModeTypeC)\r
               }\r
             when :typed\r
               val.each { |mode|\r
-                chan.create_mode(mode, ChannelModeTypeD)\r
+                chan.create_mode(mode, Channel::ModeTypeD)\r
               }\r
             end\r
           end\r
         }\r
 \r
         @channels << chan\r
-        @channel_names << name\r
-        debug "Created channel #{chan.inspect}"\r
-        debug "Managing channels #{@channel_names.join(', ')}"\r
+        # debug "Created channel #{chan.inspect}"\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
@@ -1021,52 +1710,59 @@ module Irc
     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.to_s)\r
+      return false if nick.nil_or_empty?\r
+      user_nicks.index(nick.irc_downcase(casemap))\r
     end\r
 \r
     # Returns the user with nick _nick_, if available\r
     #\r
     def get_user(nick)\r
-      idx = @user_nicks.index(nick.to_s)\r
+      idx = has_user?(nick)\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
+    # Create a new User object bound to the receiver and add it to the list\r
+    # of <code>User</code>s on the receiver, unless the User was present\r
+    # already. In this case, the default action is to raise an exception,\r
+    # unless _fails_ is set to false. An exception can also be raised\r
+    # if _str_ is nil or empty, again only if _fails_ is set to true;\r
+    # otherwise, the method just returns nil\r
     #\r
     def new_user(str, fails=true)\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
-        @user_nicks << tmp.nick\r
-        return @users.last\r
+      if str.nil_or_empty?\r
+        raise "Tried to look for empty or nil user name #{str.inspect}" if fails\r
+        return nil\r
       end\r
+      tmp = str.to_irc_user(:server => self)\r
       old = get_user(tmp.nick)\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
+      # debug "Tmp: #{tmp.inspect}"\r
+      # debug "Old: #{old.inspect}"\r
+      if old\r
+        # debug "User already existed as #{old.inspect}"\r
+        if tmp.known?\r
+          if old.known?\r
+            # debug "Both were known"\r
+            # Do not raise an error: things like Freenode change the hostname after identification\r
+            warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\r
+            raise "User #{tmp} already exists on server #{self}" if fails\r
+          end\r
+          if old.fullform.downcase != tmp.fullform.downcase\r
+            old.replace(tmp)\r
+            # debug "Known user now #{old.inspect}"\r
+          end\r
+        end\r
+        return old\r
       else\r
-        old.user = tmp.user\r
-        old.host = tmp.host\r
+        warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
+        @users << tmp\r
+        return @users.last\r
       end\r
-      return old\r
     end\r
 \r
     # Returns the User with the given Netmask on the server,\r
@@ -1077,29 +1773,29 @@ module Irc
       new_user(str, false)\r
     end\r
 \r
+    # Deletes User _user_ from Channel _channel_\r
+    #\r
+    def delete_user_from_channel(user, channel)\r
+      channel.delete_user(user)\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?(someuser.nick)\r
+      idx = has_user?(someuser)\r
       raise "Tried to remove unmanaged user #{user}" unless idx\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
 \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
+      str.to_irc_netmask(:server => self)\r
     end\r
 \r
     # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
@@ -1109,7 +1805,7 @@ module Irc
       @users.inject(UserList.new) {\r
         |list, user|\r
         if user.user == "*" or user.host == "*"\r
-          list << user if user.nick =~ nm.nick.to_irc_regexp\r
+          list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp\r
         else\r
           list << user if user.matches?(nm)\r
         end\r
@@ -1117,50 +1813,7 @@ module Irc
       }\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
-# 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
+\r