# * 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
# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\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
-# TODO User should have associated Server too\r
-#\r
-# TODO rather than the complex init methods, we should provide a single one (having a String parameter)\r
-# and then provide to_irc_netmask(casemap), to_irc_user(server), to_irc_channel(server) etc\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
#\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
# 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
# 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
# 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
raise "Unexpected match #{m} when converting #{self}"\r
end\r
}\r
- Regexp.new(regmask)\r
+ Regexp.new("^#{regmask}$")\r
end\r
+\r
end\r
\r
\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
+ 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
#\r
def internal_will_accept?(raising, *els)\r
els.each { |el|\r
- unless el.class <= @element_class\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
# 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
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
}\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_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/ \r
+\r
+ # Sadly, different networks have different, RFC-breaking ways of cloaking\r
+ # the actualy host address: see above for an example to handle FreeNode.\r
+ # Another example would be Azzurra, wich also inserts a "=" in the\r
+ # cloacked host. So let's just not care about this and go with the simplest\r
+ # thing:\r
+ GEN_HOST_EXT = /\S+/\r
+\r
+ # User-matching Regexp\r
+ GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/\r
+\r
+ # Things such has the BIP proxy send invalid nicks in a complete netmask,\r
+ # so we want to match this, rather: this matches either a compliant nick\r
+ # or a a string with a very generic nick, a very generic hostname after an\r
+ # @ sign, and an optional user after a !\r
+ BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/\r
+\r
+ # # For Netmask, we want to allow wildcards * and ? in the nick\r
+ # # (they are already allowed in the user and host part\r
+ # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/\r
+\r
+ # # Netmask-matching Regexp\r
+ # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/\r
+\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
# 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
+ # We match a pretty generic string, to work around non-compliant\r
+ # servers\r
+ when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/\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
+ # A Netmask is easily converted to a String for the usual representation.\r
+ # We skip the user or host parts if they are "*", unless we've been asked\r
+ # for the full form\r
+ #\r
+ def to_s\r
+ ret = nick.dup\r
+ ret << "!" << user unless user == "*"\r
+ ret << "@" << host unless host == "*"\r
+ return ret\r
+ end\r
+\r
+ def fullform\r
+ "#{nick}!#{user}@#{host}"\r
+ end\r
+\r
+ # This method downcases the fullform of the netmask. While this may not be\r
+ # significantly different from the #downcase() method provided by the\r
+ # ServerOrCasemap mixin, it's significantly different for Netmask\r
+ # subclasses such as User whose simple downcasing uses the nick only.\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
+ # 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, using full_downcase\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.full_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
- @nick = "*" if @nick.to_s.empty?\r
- @user = "*" if @user.to_s.empty?\r
- @host = "*" if @host.to_s.empty?\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 have the same @nick, @user, @host and @casemap\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
- self.class == other.class && @nick == other.nick && @user == other.user && @host == other.host && @casemap == other.casemap\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, downcasing the argument\r
- # following IRC rules and defaulting to the generic glob pattern if\r
- # the result is the null string.\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
@host = "*" if @host.empty?\r
end\r
\r
- # This method changes the casemap of a Netmask, which is needed in some\r
- # extreme circumstances. Please use sparingly\r
+ # We can replace everything at once with data from another Netmask\r
#\r
- def casemap=(newcmap)\r
- @casemap = newcmap.to_s\r
- @casemap = "rfc1459" if @casemap.empty?\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
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
#\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.new(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
# Case equality. Checks if arg matches self\r
#\r
def ===(arg)\r
- Netmask.new(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
\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
- # 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
+ # We keep extending String, this time adding a method that converts a\r
+ # String into an Irc::Netmask object\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
+ 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 globs).\r
+ # In fact, User is just a subclass of Netmask.\r
+ #\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
class User < Netmask\r
alias :to_s :nick\r
\r
+ attr_accessor :real_name\r
+\r
# Create a new IRC User from a given Netmask (or anything that can be converted\r
# into a Netmask) provided that the given Netmask does not have globs.\r
#\r
- def initialize(str="", casemap=nil)\r
+ def initialize(str="", 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
raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
@away = false\r
+ @real_name = String.new\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
@away = false\r
end\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
+ def modes_on(channel)\r
+ case channel\r
+ when Channel\r
+ channel.modes_of(self)\r
+ else\r
+ return @server.channel(channel).modes_of(self) if @server\r
+ raise "Can't resolve channel #{channel}"\r
+ end\r
+ end\r
+\r
+ def is_op?(channel)\r
+ case channel\r
+ when Channel\r
+ channel.has_op?(self)\r
+ else\r
+ return @server.channel(channel).has_op?(self) if @server\r
+ raise "Can't resolve channel #{channel}"\r
+ end\r
+ end\r
+\r
+ def is_voice?(channel)\r
+ case channel\r
+ when Channel\r
+ channel.has_voice?(self)\r
+ else\r
+ return @server.channel(channel).has_voice?(self) if @server\r
+ raise "Can't resolve channel #{channel}"\r
+ end\r
+ end\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
- # Replace a ChannelTopic with another one\r
- def replace(topic)\r
- raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.class <= ChannelTopic\r
- @text = topic.text.dup\r
- @set_by = topic.set_by.dup\r
- @set_on = topic.set_on.dup\r
- end\r
end\r
\r
+end\r
+\r
+class String\r
\r
- # Mode on a channel\r
- class ChannelMode\r
- def initialize(ch)\r
- @channel = ch\r
- end\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(ch)\r
- super\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
- 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
- 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(ch)\r
- super\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(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
+\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
- u = @channel.server.user(val)\r
- @list.delete(u)\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(ch)\r
- super\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_netmask\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 < ChannelMode\r
- def initialize(ch)\r
- super\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, :mode, :users, :server\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 << " on server #{server}"\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
+ @users.index(nick.to_irc_user(server_and_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)\r
+ warn "Trying to add user #{user} to channel #{self} again" unless silent\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
# 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(server, name, topic=nil, users=[])\r
- raise TypeError, "First parameter must be an Irc::Server" unless server.class <= Server\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
- @server = server\r
+ init_server_or_casemap(opts)\r
\r
- @name = name.irc_downcase(casemap)\r
+ @name = name\r
\r
- @topic = topic || ChannelTopic.new\r
+ @topic = (topic.to_irc_channel_topic rescue Channel::Topic.new)\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
+ @users = UserList.new\r
+\r
+ users.each { |u|\r
+ add_user(u)\r
+ }\r
\r
# Flags\r
@mode = {}\r
end\r
\r
- # Returns the casemap of the originating server\r
- def casemap\r
- return @server.casemap\r
- end\r
-\r
# Removes a user from the channel\r
#\r
def delete_user(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
# 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
def create_mode(sym, kl)\r
@mode[sym.to_sym] = kl.new(self)\r
end\r
+\r
+ def modes_of(user)\r
+ l = []\r
+ @mode.map { |s, m|\r
+ l << s if (m.class <= UserMode and m.list[user])\r
+ }\r
+ l\r
+ end\r
+\r
+ def has_op?(user)\r
+ @mode.has_key?(:o) and @mode[:o].list[user]\r
+ end\r
+\r
+ def has_voice?(user)\r
+ @mode.has_key?(:v) and @mode[:v].list[user]\r
+ end\r
end\r
\r
\r
\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
\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
#\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
: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
: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
# Resets the Channel and User list\r
#\r
def reset_lists\r
- @users.each { |u|\r
+ @users.reverse_each { |u|\r
delete_user(u)\r
}\r
- @channels.each { |u|\r
+ @channels.reverse_each { |u|\r
delete_channel(u)\r
}\r
end\r
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
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
- @users.each { |u|\r
- debug "Resetting casemap of #{u} from #{u.casemap} to #{val}"\r
- u.casemap = val\r
- }\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
+ if @supports[key][k] == 0\r
+ warn "Deleting #{key} limit of 0 for #{k}"\r
+ @supports[key].delete(k)\r
+ end\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
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
# 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
# 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 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
@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
+ # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
+ warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]\r
}\r
\r
# So far, everything is fine. Now create the actual Channel\r
#\r
- chan = Channel.new(self, name, topic, users)\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
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
return chan\r
end\r
end\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.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
+ 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
- # debug "Creating or selecting user #{tmp.inspect} from #{str.inspect}"\r
+ tmp = str.to_irc_user(:server => self)\r
old = get_user(tmp.nick)\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
- raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\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
- else\r
- old.user = tmp.user\r
- old.host = tmp.host\r
- # debug "User improved to #{old.inspect}"\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
warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
@users << tmp\r
- @user_nicks << tmp.nick\r
return @users.last\r
end\r
end\r
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
@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
}\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
+\r
end\r
\r