X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Firc.rb;h=129f947e61d1d9d906e1b02d7b7e7deb486fd5f3;hb=7205060ebc35daf26a22ff6453b4faef477aaca7;hp=cc446bd4584500f6a2c5fa9cd00ae3d7f2680e28;hpb=0a40dcda89ff63dc10678add63a5621c2119f67e;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb index cc446bd4..129f947e 100644 --- a/lib/rbot/irc.rb +++ b/lib/rbot/irc.rb @@ -3,7 +3,11 @@ # * do we want to handle a Channel list for each User telling which # Channels is the User on (of those the client is on too)? # We may want this so that when a User leaves all Channels and he hasn't -# sent us privmsgs, we know remove him from the Server @users list +# sent us privmsgs, we know we can remove him from the Server @users list +# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf? +# See items marked as TODO Ho. +# The framework to do this is now in place, thanks to the new [] method +# for NetmaskList, which allows retrieval by Netmask or String #++ # :title: IRC module # @@ -14,11 +18,228 @@ # Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com) # Copyright:: Copyright (c) 2006 Giuseppe Bilotta # License:: GPLv2 + +require 'singleton' + +class Object + + # We extend the Object class with a method that + # checks if the receiver is nil or empty + def nil_or_empty? + return true unless self + return true if self.respond_to? :empty and self.empty? + return false + end +end + +# The Irc module is used to keep all IRC-related classes +# in the same namespace # -# TODO User should have associated Server too -# -# TODO rather than the complex init methods, we should provide a single one (having a String parameter) -# and then provide to_irc_netmask(casemap), to_irc_user(server), to_irc_channel(server) etc +module Irc + + + # Due to its Scandinavian origins, IRC has strange case mappings, which + # consider the characters {}|^ as the uppercase + # equivalents of # []\~. + # + # This is however not the same on all IRC servers: some use standard ASCII + # casemapping, other do not consider ^ as the uppercase of + # ~ + # + class Casemap + @@casemaps = {} + + # Create a new casemap with name _name_, uppercase characters _upper_ and + # lowercase characters _lower_ + # + def initialize(name, upper, lower) + @key = name.to_sym + raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key) + @@casemaps[@key] = { + :upper => upper, + :lower => lower, + :casemap => self + } + end + + # Returns the Casemap with the given name + # + def Casemap.get(name) + @@casemaps[name.to_sym][:casemap] + end + + # Retrieve the 'uppercase characters' of this Casemap + # + def upper + @@casemaps[@key][:upper] + end + + # Retrieve the 'lowercase characters' of this Casemap + # + def lower + @@casemaps[@key][:lower] + end + + # Return a Casemap based on the receiver + # + def to_irc_casemap + self + end + + # A Casemap is represented by its lower/upper mappings + # + def inspect + "#<#{self.class}:#{'0x%x'% self.object_id}: #{upper.inspect} ~(#{self})~ #{lower.inspect}>" + end + + # As a String we return our name + # + def to_s + @key.to_s + end + + # Two Casemaps are equal if they have the same upper and lower ranges + # + def ==(arg) + other = arg.to_irc_casemap + return self.upper == other.upper && self.lower == other.lower + end + + # Raise an error if _arg_ and self are not the same Casemap + # + def must_be(arg) + other = arg.to_irc_casemap + raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other + return true + end + + end + + # The rfc1459 casemap + # + class RfcCasemap < Casemap + include Singleton + + def initialize + super('rfc1459', "\x41-\x5e", "\x61-\x7e") + end + + end + RfcCasemap.instance + + # The strict-rfc1459 Casemap + # + class StrictRfcCasemap < Casemap + include Singleton + + def initialize + super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d") + end + + end + StrictRfcCasemap.instance + + # The ascii Casemap + # + class AsciiCasemap < Casemap + include Singleton + + def initialize + super('ascii', "\x41-\x5a", "\x61-\x7a") + end + + end + AsciiCasemap.instance + + + # This module is included by all classes that are either bound to a server + # or should have a casemap. + # + module ServerOrCasemap + + attr_reader :server + + # This method initializes the instance variables @server and @casemap + # according to the values of the hash keys :server and :casemap in _opts_ + # + def init_server_or_casemap(opts={}) + @server = opts.fetch(:server, nil) + raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server) + + @casemap = opts.fetch(:casemap, nil) + if @server + if @casemap + @server.casemap.must_be(@casemap) + @casemap = nil + end + else + @casemap = (@casemap || 'rfc1459').to_irc_casemap + end + end + + # This is an auxiliary method: it returns true if the receiver fits the + # server and casemap specified in _opts_, false otherwise. + # + def fits_with_server_and_casemap?(opts={}) + srv = opts.fetch(:server, nil) + cmap = opts.fetch(:casemap, nil) + cmap = cmap.to_irc_casemap unless cmap.nil? + + if srv.nil? + return true if cmap.nil? or cmap == casemap + else + return true if srv == @server and (cmap.nil? or cmap == casemap) + end + return false + end + + # Returns the casemap of the receiver, by looking at the bound + # @server (if possible) or at the @casemap otherwise + # + def casemap + return @server.casemap if defined?(@server) and @server + return @casemap + end + + # Returns a hash with the current @server and @casemap as values of + # :server and :casemap + # + def server_and_casemap + h = {} + h[:server] = @server if defined?(@server) and @server + h[:casemap] = @casemap if defined?(@casemap) and @casemap + return h + end + + # We allow up/downcasing with a different casemap + # + def irc_downcase(cmap=casemap) + self.to_s.irc_downcase(cmap) + end + + # Up/downcasing something that includes this module returns its + # Up/downcased to_s form + # + def downcase + self.irc_downcase + end + + # We allow up/downcasing with a different casemap + # + def irc_upcase(cmap=casemap) + self.to_s.irc_upcase(cmap) + end + + # Up/downcasing something that includes this module returns its + # Up/downcased to_s form + # + def upcase + self.irc_upcase + end + + end + +end # We start by extending the String class @@ -26,30 +247,19 @@ # class String - # This method returns a string which is the downcased version of the - # receiver, according to IRC rules: due to the Scandinavian origin of IRC, - # the characters {}|^ are considered the uppercase equivalent of - # []\~. + # This method returns the Irc::Casemap whose name is the receiver # - # Since IRC is mostly case-insensitive (the Windows way: case is preserved, - # but it's actually ignored to check equality), this method is rather - # important when checking if two strings refer to the same entity - # (User/Channel) + def to_irc_casemap + Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}" + end + + # This method returns a string which is the downcased version of the + # receiver, according to the given _casemap_ # - # Modern server allow different casemaps, too, in which some or all - # of the extra characters are not converted # def irc_downcase(casemap='rfc1459') - case casemap - when 'rfc1459' - self.tr("\x41-\x5e", "\x61-\x7e") - when 'strict-rfc1459' - self.tr("\x41-\x5d", "\x61-\x7d") - when 'ascii' - self.tr("\x41-\x5a", "\x61-\x7a") - else - raise TypeError, "Unknown casemap #{casemap}" - end + cmap = casemap.to_irc_casemap + self.tr(cmap.upper, cmap.lower) end # This is the same as the above, except that the string is altered in place @@ -57,16 +267,8 @@ class String # See also the discussion about irc_downcase # def irc_downcase!(casemap='rfc1459') - case casemap - when 'rfc1459' - self.tr!("\x41-\x5e", "\x61-\x7e") - when 'strict-rfc1459' - self.tr!("\x41-\x5d", "\x61-\x7d") - when 'ascii' - self.tr!("\x41-\x5a", "\x61-\x7a") - else - raise TypeError, "Unknown casemap #{casemap}" - end + cmap = casemap.to_irc_casemap + self.tr!(cmap.upper, cmap.lower) end # Upcasing functions are provided too @@ -74,16 +276,8 @@ class String # See also the discussion about irc_downcase # def irc_upcase(casemap='rfc1459') - case casemap - when 'rfc1459' - self.tr("\x61-\x7e", "\x41-\x5e") - when 'strict-rfc1459' - self.tr("\x61-\x7d", "\x41-\x5d") - when 'ascii' - self.tr("\x61-\x7a", "\x41-\x5a") - else - raise TypeError, "Unknown casemap #{casemap}" - end + cmap = casemap.to_irc_casemap + self.tr(cmap.lower, cmap.upper) end # In-place upcasing @@ -91,16 +285,8 @@ class String # See also the discussion about irc_downcase # def irc_upcase!(casemap='rfc1459') - case casemap - when 'rfc1459' - self.tr!("\x61-\x7e", "\x41-\x5e") - when 'strict-rfc1459' - self.tr!("\x61-\x7d", "\x41-\x5d") - when 'ascii' - self.tr!("\x61-\x7a", "\x41-\x5a") - else - raise TypeError, "Unknown casemap #{casemap}" - end + cmap = casemap.to_irc_casemap + self.tr!(cmap.lower, cmap.upper) end # This method checks if the receiver contains IRC glob characters @@ -135,8 +321,9 @@ class String raise "Unexpected match #{m} when converting #{self}" end } - Regexp.new(regmask) + Regexp.new("^#{regmask}$") end + end @@ -201,6 +388,7 @@ class ArrayOf < Array # This method is similar to the above, except that it raises an exception # if the receiver is not valid + # def validate raise TypeError unless valid? end @@ -273,6 +461,13 @@ class ArrayOf < Array } end + # We introduce the 'downcase' method, which maps downcase() to all the Array + # elements, properly failing when the elements don't have a downcase method + # + def downcase + self.map { |el| el.downcase } + end + # Modifying methods which we don't handle yet are made private # private :[]=, :collect!, :map!, :fill, :flatten! @@ -280,19 +475,112 @@ class ArrayOf < Array end -# The Irc module is used to keep all IRC-related classes -# in the same namespace +# We extend the Regexp class with an Irc module which will contain some +# Irc-specific regexps # +class Regexp + + # We start with some general-purpose ones which will be used in the + # Irc module too, but are useful regardless + DIGITS = /\d+/ + HEX_DIGIT = /[0-9A-Fa-f]/ + HEX_DIGITS = /#{HEX_DIGIT}+/ + HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/ + DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/ + DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/ + HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/ + IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/ + + # IPv6, from Resolv::IPv6, without the \A..\z anchors + HEX_16BIT = /#{HEX_DIGIT}{1,4}/ + IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/ + IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/ + IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/ + IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/ + IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/ + + # We start with some IRC related regular expressions, used to match + # Irc::User nicks and users and Irc::Channel names + # + # For each of them we define two versions of the regular expression: + # * a generic one, which should match for any server but may turn out to + # match more than a specific server would accept + # * an RFC-compliant matcher + # + module Irc + + # Channel-name-matching regexps + CHAN_FIRST = /[#&+]/ + CHAN_SAFE = /![A-Z0-9]{5}/ + CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/ + GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/ + RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/ + + # Nick-matching regexps + SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/ + NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/ + NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/ + GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/ + RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/ + + USER_CHAR = /[^\x00\x0a\x0d @]/ + GEN_USER = /#{USER_CHAR}+/ + + # Host-matching regexps + HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/ + HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/ + HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/ + + GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/ + + # # FreeNode network replaces the host of affiliated users with + # # 'virtual hosts' + # # FIXME we need the true syntax to match it properly ... + # PDPC_HOST_PART = /[0-9A-Za-z.-]+/ + # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/ + + # # NOTE: the final optional and non-greedy dot is needed because some + # # servers (e.g. FreeNode) send the hostname of the services as "services." + # # which is not RFC compliant, but sadly done. + # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/ + + # Sadly, different networks have different, RFC-breaking ways of cloaking + # the actualy host address: see above for an example to handle FreeNode. + # Another example would be Azzurra, wich also inserts a "=" in the + # cloacked host. So let's just not care about this and go with the simplest + # thing: + GEN_HOST_EXT = /\S+/ + + # User-matching Regexp + GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ + + # Things such has the BIP proxy send invalid nicks in a complete netmask, + # so we want to match this, rather: this matches either a compliant nick + # or a a string with a very generic nick, a very generic hostname after an + # @ sign, and an optional user after a ! + BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/ + + # # For Netmask, we want to allow wildcards * and ? in the nick + # # (they are already allowed in the user and host part + # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/ + + # # Netmask-matching Regexp + # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ + + end + +end + + module Irc # A Netmask identifies each user by collecting its nick, username and # hostname in the form nick!user@host # - # Netmasks can also contain glob patterns in any of their components; in this - # form they are used to refer to more than a user or to a user appearing - # under different - # forms. + # Netmasks can also contain glob patterns in any of their components; in + # this form they are used to refer to more than a user or to a user + # appearing under different forms. # # Example: # * *!*@* refers to everybody @@ -300,85 +588,124 @@ module Irc # regardless of the nick used. # class Netmask - attr_reader :nick, :user, :host - attr_reader :casemap - # call-seq: - # Netmask.new(netmask) => new_netmask - # Netmask.new(hash={}, casemap=nil) => new_netmask - # Netmask.new("nick!user@host", casemap=nil) => new_netmask - # - # Create a new Netmask in any of these forms - # 1. from another Netmask (does a .dup) - # 2. from a Hash with any of the keys :nick, :user and - # :host - # 3. from a String in the form nick!user@host - # - # In all but the first forms a casemap may be speficied, the default - # being 'rfc1459'. + # Netmasks have an associated casemap unless they are bound to a server # - # The nick is downcased following IRC rules and according to the given casemap. + include ServerOrCasemap + + attr_reader :nick, :user, :host + + # Create a new Netmask from string _str_, which must be in the form + # _nick_!_user_@_host_ # - # FIXME check if user and host need to be downcased too. + # It is possible to specify a server or a casemap in the optional Hash: + # these are used to associate the Netmask with the given server and to set + # its casemap: if a server is specified and a casemap is not, the server's + # casemap is used. If both a server and a casemap are specified, the + # casemap must match the server's casemap or an exception will be raised. # # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern # - def initialize(str={}, casemap=nil) - case str - when Netmask - raise ArgumentError, "Can't set casemap when initializing from other Netmask" if casemap - @casemap = str.casemap.dup - @nick = str.nick.dup - @user = str.user.dup - @host = str.host.dup - when Hash - @casemap = casemap || str[:casemap] || 'rfc1459' - @nick = str[:nick].to_s.irc_downcase(@casemap) - @user = str[:user].to_s - @host = str[:host].to_s - when String - case str - when "" - @casemap = casemap || 'rfc1459' - @nick = nil - @user = nil - @host = nil - when /^(\S+?)(?:!(\S+)@(?:(\S+))?)?$/ - @casemap = casemap || 'rfc1459' - @nick = $1.irc_downcase(@casemap) - @user = $2 - @host = $3 + def initialize(str="", opts={}) + # First of all, check for server/casemap option + # + init_server_or_casemap(opts) + + # Now we can see if the given string _str_ is an actual Netmask + if str.respond_to?(:to_str) + case str.to_str + # We match a pretty generic string, to work around non-compliant + # servers + when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/ + # We do assignment using our internal methods + self.nick = $1 + self.user = $2 + self.host = $3 else - raise ArgumentError, "#{str} is not a valid netmask" + raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}" end else - raise ArgumentError, "#{str} is not a valid netmask" + raise TypeError, "#{str} cannot be converted to a #{self.class}" end + end + + # A Netmask is easily converted to a String for the usual representation. + # We skip the user or host parts if they are "*", unless we've been asked + # for the full form + # + def to_s + ret = nick.dup + ret << "!" << user unless user == "*" + ret << "@" << host unless host == "*" + return ret + end + + def fullform + "#{nick}!#{user}@#{host}" + end - @nick = "*" if @nick.to_s.empty? - @user = "*" if @user.to_s.empty? - @host = "*" if @host.to_s.empty? + # This method downcases the fullform of the netmask. While this may not be + # significantly different from the #downcase() method provided by the + # ServerOrCasemap mixin, it's significantly different for Netmask + # subclasses such as User whose simple downcasing uses the nick only. + # + def full_irc_downcase(cmap=casemap) + self.fullform.irc_downcase(cmap) end + # full_downcase() will return the fullform downcased according to the + # User's own casemap + # + def full_downcase + self.full_irc_downcase + end + + # Converts the receiver into a Netmask with the given (optional) + # server/casemap association. We return self unless a conversion + # is needed (different casemap/server) + # + # Subclasses of Netmask will return a new Netmask, using full_downcase + # + def to_irc_netmask(opts={}) + if self.class == Netmask + return self if fits_with_server_and_casemap?(opts) + end + return self.full_downcase.to_irc_netmask(opts) + end + + # Converts the receiver into a User with the given (optional) + # server/casemap association. We return self unless a conversion + # is needed (different casemap/server) + # + def to_irc_user(opts={}) + self.fullform.to_irc_user(server_and_casemap.merge(opts)) + end + + # Inspection of a Netmask reveals the server it's bound to (if there is + # one), its casemap and the nick, user and host part + # def inspect str = "<#{self.class}:#{'0x%x' % self.object_id}:" + str << " @server=#{@server}" if defined?(@server) and @server str << " @nick=#{@nick.inspect} @user=#{@user.inspect}" - str << " @host=#{@host.inspect}>" - str + str << " @host=#{@host.inspect} casemap=#{casemap.inspect}" + str << ">" end - # Equality: two Netmasks are equal if they have the same @nick, @user, @host and @casemap + # Equality: two Netmasks are equal if they downcase to the same thing + # + # TODO we may want it to try other.to_irc_netmask # def ==(other) - self.class == other.class && @nick == other.nick && @user == other.user && @host == other.host && @casemap == other.casemap + return false unless other.kind_of?(self.class) + self.downcase == other.downcase end - # This method changes the nick of the Netmask, downcasing the argument - # following IRC rules and defaulting to the generic glob pattern if - # the result is the null string. + # This method changes the nick of the Netmask, defaulting to the generic + # glob pattern if the result is the null string. # def nick=(newnick) - @nick = newnick.to_s.irc_downcase(@casemap) + @nick = newnick.to_s @nick = "*" if @nick.empty? end @@ -398,12 +725,19 @@ module Irc @host = "*" if @host.empty? end - # This method changes the casemap of a Netmask, which is needed in some - # extreme circumstances. Please use sparingly + # We can replace everything at once with data from another Netmask # - def casemap=(newcmap) - @casemap = newcmap.to_s - @casemap = "rfc1459" if @casemap.empty? + def replace(other) + case other + when Netmask + nick = other.nick + user = other.user + host = other.host + @server = other.server + @casemap = other.casemap unless @server + else + replace(other.to_irc_netmask(server_and_casemap)) + end end # This method checks if a Netmask is definite or not, by seeing if @@ -413,18 +747,12 @@ module Irc return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob? end - # A Netmask is easily converted to a String for the usual representation - # - def fullform - return "#{nick}!#{user}@#{host}" - end - alias :to_s :fullform - # This method is used to match the current Netmask against another one # # The method returns true if each component of the receiver matches the - # corresponding component of the argument. By _matching_ here we mean that - # any netmask described by the receiver is also described by the argument. + # corresponding component of the argument. By _matching_ here we mean + # that any netmask described by the receiver is also described by the + # argument. # # In this sense, matching is rather simple to define in the case when the # receiver has no globs: it is just necessary to check if the argument @@ -436,15 +764,18 @@ module Irc # # The more complex case in which both the receiver and the argument have # globs is not handled yet. - # + # def matches?(arg) - cmp = Netmask.new(arg) - raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap - raise TypeError, "#{arg} is not a valid Netmask" unless cmp.kind_of?(Netmask) + cmp = arg.to_irc_netmask(:casemap => casemap) + debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})" [:nick, :user, :host].each { |component| - us = self.send(component) - them = cmp.send(component) - raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob? + us = self.send(component).irc_downcase(casemap) + them = cmp.send(component).irc_downcase(casemap) + if us.has_irc_glob? && them.has_irc_glob? + next if us == them + warn NotImplementedError + return false + end return false if us.has_irc_glob? && !them.has_irc_glob? return false unless us =~ them.to_irc_regexp } @@ -454,15 +785,17 @@ module Irc # Case equality. Checks if arg matches self # def ===(arg) - Netmask.new(arg).matches?(self) + arg.to_irc_netmask(:casemap => casemap).matches?(self) end + # Sorting is done via the fullform + # def <=>(arg) case arg when Netmask - self.fullform <=> arg.fullform + self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap) else - self.to_s <=> arg.to_s + self.downcase <=> arg.downcase end end @@ -475,19 +808,75 @@ module Irc # Create a new NetmaskList, optionally filling it with the elements from # the Array argument fed to it. + # def initialize(ar=[]) super(Netmask, ar) end + + # We enhance the [] method by allowing it to pick an element that matches + # a given Netmask, a String or a Regexp + # TODO take into consideration the opportunity to use select() instead of + # find(), and/or a way to let the user choose which one to take (second + # argument?) + # + def [](*args) + if args.length == 1 + case args[0] + when Netmask + self.find { |mask| + mask.matches?(args[0]) + } + when String + self.find { |mask| + mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap)) + } + when Regexp + self.find { |mask| + mask.fullform =~ args[0] + } + else + super(*args) + end + else + super(*args) + end + end + end +end + + +class String - # An IRC User is identified by his/her Netmask (which must not have - # globs). In fact, User is just a subclass of Netmask. However, - # a User will not allow one's host or user data to be changed. + # We keep extending String, this time adding a method that converts a + # String into an Irc::Netmask object # - # Due to the idiosincrasies of the IRC protocol, we allow - # the creation of a user with an unknown mask represented by the - # glob pattern *@*. Only in this case they may be set. + def to_irc_netmask(opts={}) + Irc::Netmask.new(self, opts) + end + +end + + +module Irc + + + # An IRC User is identified by his/her Netmask (which must not have globs). + # In fact, User is just a subclass of Netmask. + # + # Ideally, the user and host information of an IRC User should never + # change, and it shouldn't contain glob patterns. However, IRC is somewhat + # idiosincratic and it may be possible to know the nick of a User much before + # its user and host are known. Moreover, some networks (namely Freenode) may + # change the hostname of a User when (s)he identifies with Nickserv. + # + # As a consequence, we must allow changes to a User host and user attributes. + # We impose a restriction, though: they may not contain glob patterns, except + # for the special case of an unknown user/host which is represented by a *. + # + # It is possible to create a totally unknown User (e.g. for initializations) + # by setting the nick to * too. # # TODO list: # * see if it's worth to add the other USER data @@ -496,43 +885,49 @@ module Irc class User < Netmask alias :to_s :nick + attr_accessor :real_name + # Create a new IRC User from a given Netmask (or anything that can be converted # into a Netmask) provided that the given Netmask does not have globs. # - def initialize(str="", casemap=nil) + def initialize(str="", opts={}) super raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*" raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*" raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*" @away = false + @real_name = String.new + end + + # The nick of a User may be changed freely, but it must not contain glob patterns. + # + def nick=(newnick) + raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob? + super end - # We only allow the user to be changed if it was "*". Otherwise, - # we raise an exception if the new host is different from the old one + # We have to allow changing the user of an Irc User due to some networks + # (e.g. Freenode) changing hostmasks on the fly. We still check if the new + # user data has glob patterns though. # def user=(newuser) - if user == "*" - super - else - raise "Can't change the username of user #{self}" if user != newuser - end + raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob? + super end - # We only allow the host to be changed if it was "*". Otherwise, - # we raise an exception if the new host is different from the old one + # We have to allow changing the host of an Irc User due to some networks + # (e.g. Freenode) changing hostmasks on the fly. We still check if the new + # host data has glob patterns though. # def host=(newhost) - if host == "*" - super - else - raise "Can't change the hostname of user #{self}" if host != newhost - end + raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob? + super end # Checks if a User is well-known or not by looking at the hostname and user # def known? - return user!="*" && host!="*" + return nick != "*" && user != "*" && host != "*" end # Is the user away? @@ -551,165 +946,354 @@ module Irc @away = false end end + + # Since to_irc_user runs the same checks on server and channel as + # to_irc_netmask, we just try that and return self if it works. + # + # Subclasses of User will return self if possible. + # + def to_irc_user(opts={}) + return self if fits_with_server_and_casemap?(opts) + return self.full_downcase.to_irc_user(opts) + end + + # We can replace everything at once with data from another User + # + def replace(other) + case other + when User + self.nick = other.nick + self.user = other.user + self.host = other.host + @server = other.server + @casemap = other.casemap unless @server + @away = other.away? + else + self.replace(other.to_irc_user(server_and_casemap)) + end + end + + def modes_on(channel) + case channel + when Channel + channel.modes_of(self) + else + return @server.channel(channel).modes_of(self) if @server + raise "Can't resolve channel #{channel}" + end + end + + def is_op?(channel) + case channel + when Channel + channel.has_op?(self) + else + return @server.channel(channel).has_op?(self) if @server + raise "Can't resolve channel #{channel}" + end + end + + def is_voice?(channel) + case channel + when Channel + channel.has_voice?(self) + else + return @server.channel(channel).has_voice?(self) if @server + raise "Can't resolve channel #{channel}" + end + end end # A UserList is an ArrayOf Users + # We derive it from NetmaskList, which allows us to inherit any special + # NetmaskList method # - class UserList < ArrayOf + class UserList < NetmaskList # Create a new UserList, optionally filling it with the elements from # the Array argument fed to it. + # def initialize(ar=[]) - super(User, ar) + super(ar) + @element_class = User end - end - - # A ChannelTopic represents the topic of a channel. It consists of - # the topic itself, who set it and when - class ChannelTopic - attr_accessor :text, :set_by, :set_on - alias :to_s :text - - # Create a new ChannelTopic setting the text, the creator and - # the creation time - def initialize(text="", set_by="", set_on=Time.new) - @text = text - @set_by = set_by - @set_on = Time.new + # Convenience method: convert the UserList to a list of nicks. The indices + # are preserved + # + def nicks + self.map { |user| user.nick } end - # Replace a ChannelTopic with another one - def replace(topic) - raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.kind_of?(ChannelTopic) - @text = topic.text.dup - @set_by = topic.set_by.dup - @set_on = topic.set_on.dup - end end +end - # Mode on a channel - class ChannelMode - def initialize(ch) - @channel = ch - end +class String + + # We keep extending String, this time adding a method that converts a + # String into an Irc::User object + # + def to_irc_user(opts={}) + Irc::User.new(self, opts) end +end - # Channel modes of type A manipulate lists +module Irc + + # An IRC Channel is identified by its name, and it has a set of properties: + # * a Channel::Topic + # * a UserList + # * a set of Channel::Modes # - class ChannelModeTypeA < ChannelMode - def initialize(ch) - super - @list = NetmaskList.new - end + # The Channel::Topic and Channel::Mode classes are defined within the + # Channel namespace because they only make sense there + # + class Channel - def set(val) - nm = @channel.server.new_netmask(val) - @list << nm unless @list.include?(nm) - end - def reset(val) - nm = @channel.server.new_netmask(val) - @list.delete(nm) - end - end + # Mode on a Channel + # + class Mode + attr_reader :channel + def initialize(ch) + @channel = ch + end - # Channel modes of type B need an argument - # - class ChannelModeTypeB < ChannelMode - def initialize(ch) - super - @arg = nil end - def set(val) - @arg = val - end - def reset(val) - @arg = nil if @arg == val - end - end + # Channel modes of type A manipulate lists + # + # Example: b (banlist) + # + class ModeTypeA < Mode + attr_reader :list + def initialize(ch) + super + @list = NetmaskList.new + end + + def set(val) + nm = @channel.server.new_netmask(val) + @list << nm unless @list.include?(nm) + end + + def reset(val) + nm = @channel.server.new_netmask(val) + @list.delete(nm) + end - # Channel modes that change the User prefixes are like - # Channel modes of type B, except that they manipulate - # lists of Users, so they are somewhat similar to channel - # modes of type A - # - class ChannelUserMode < ChannelModeTypeB - def initialize(ch) - super - @list = UserList.new end - def set(val) - u = @channel.server.user(val) - @list << u unless @list.include?(u) + + # Channel modes of type B need an argument + # + # Example: k (key) + # + class ModeTypeB < Mode + def initialize(ch) + super + @arg = nil + end + + def status + @arg + end + alias :value :status + + def set(val) + @arg = val + end + + def reset(val) + @arg = nil if @arg == val + end + end - def reset(val) - u = @channel.server.user(val) - @list.delete(u) + + # Channel modes that change the User prefixes are like + # Channel modes of type B, except that they manipulate + # lists of Users, so they are somewhat similar to channel + # modes of type A + # + class UserMode < ModeTypeB + attr_reader :list + alias :users :list + def initialize(ch) + super + @list = UserList.new + end + + def set(val) + u = @channel.server.user(val) + @list << u unless @list.include?(u) + end + + def reset(val) + u = @channel.server.user(val) + @list.delete(u) + end + end - end - # Channel modes of type C need an argument when set, - # but not when they get reset - # - class ChannelModeTypeC < ChannelMode - def initialize(ch) - super - @arg = false + + # Channel modes of type C need an argument when set, + # but not when they get reset + # + # Example: l (limit) + # + class ModeTypeC < Mode + def initialize(ch) + super + @arg = nil + end + + def status + @arg + end + alias :value :status + + def set(val) + @arg = val + end + + def reset + @arg = nil + end + end - def set(val) - @arg = val + + # Channel modes of type D are basically booleans + # + # Example: m (moderate) + # + class ModeTypeD < Mode + def initialize(ch) + super + @set = false + end + + def set? + return @set + end + + def set + @set = true + end + + def reset + @set = false + end + end - def reset - @arg = false + + # A Topic represents the topic of a channel. It consists of + # the topic itself, who set it and when + # + class Topic + attr_accessor :text, :set_by, :set_on + alias :to_s :text + + # Create a new Topic setting the text, the creator and + # the creation time + # + def initialize(text="", set_by="", set_on=Time.new) + @text = text + @set_by = set_by.to_irc_netmask + @set_on = set_on + end + + # Replace a Topic with another one + # + def replace(topic) + raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class) + @text = topic.text.dup + @set_by = topic.set_by.dup + @set_on = topic.set_on.dup + end + + # Returns self + # + def to_irc_channel_topic + self + end + end + end - # Channel modes of type D are basically booleans - class ChannelModeTypeD < ChannelMode - def initialize(ch) - super - @set = false - end +end - def set? - return @set - end - def set - @set = true - end +class String - def reset - @set = false - end + # Returns an Irc::Channel::Topic with self as text + # + def to_irc_channel_topic + Irc::Channel::Topic.new(self) end +end - # An IRC Channel is identified by its name, and it has a set of properties: - # * a topic - # * a UserList - # * a set of modes + +module Irc + + + # Here we start with the actual Channel class # class Channel - attr_reader :name, :topic, :mode, :users, :server + + include ServerOrCasemap + attr_reader :name, :topic, :mode, :users alias :to_s :name def inspect str = "<#{self.class}:#{'0x%x' % self.object_id}:" - str << " on server #{server}" + str << " on server #{server}" if server str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}" - str << " @users=<#{@users.sort.join(', ')}>" - str + str << " @users=[#{user_nicks.sort.join(', ')}]" + str << ">" + end + + # Returns self + # + def to_irc_channel + self + end + + # TODO Ho + def user_nicks + @users.map { |u| u.downcase } + end + + # Checks if the receiver already has a user with the given _nick_ + # + def has_user?(nick) + @users.index(nick.to_irc_user(server_and_casemap)) + end + + # Returns the user with nick _nick_, if available + # + def get_user(nick) + idx = has_user?(nick) + @users[idx] if idx + end + + # Adds a user to the channel + # + def add_user(user, opts={}) + silent = opts.fetch(:silent, false) + if has_user?(user) + warn "Trying to add user #{user} to channel #{self} again" unless silent + else + @users << user.to_irc_user(server_and_casemap) + end end # Creates a new channel with the given name, optionally setting the topic @@ -718,43 +1302,32 @@ module Irc # No additional info is created here, because the channel flags and userlists # allowed depend on the server. # - # FIXME doesn't check if users have the same casemap as the channel yet - # - def initialize(server, name, topic=nil, users=[]) - raise TypeError, "First parameter must be an Irc::Server" unless server.kind_of?(Server) + def initialize(name, topic=nil, users=[], opts={}) raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty? - raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/ + warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/ raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/ - @server = server + init_server_or_casemap(opts) - @name = name.irc_downcase(casemap) + @name = name - @topic = topic || ChannelTopic.new + @topic = (topic.to_irc_channel_topic rescue Channel::Topic.new) - case users - when UserList - @users = users - when Array - @users = UserList.new(users) - else - raise ArgumentError, "Invalid user list #{users.inspect}" - end + @users = UserList.new + + users.each { |u| + add_user(u) + } # Flags @mode = {} end - # Returns the casemap of the originating server - def casemap - return @server.casemap - end - # Removes a user from the channel # def delete_user(user) @mode.each { |sym, mode| - mode.reset(user) if mode.kind_of?(ChannelUserMode) + mode.reset(user) if mode.kind_of?(UserMode) } @users.delete(user) end @@ -768,25 +1341,25 @@ module Irc # A channel is local to a server if it has the '&' prefix # def local? - name[0] = 0x26 + name[0] == 0x26 end # A channel is modeless if it has the '+' prefix # def modeless? - name[0] = 0x2b + name[0] == 0x2b end # A channel is safe if it has the '!' prefix # def safe? - name[0] = 0x21 + name[0] == 0x21 end - # A channel is safe if it has the '#' prefix + # A channel is normal if it has the '#' prefix # def normal? - name[0] = 0x23 + name[0] == 0x23 end # Create a new mode @@ -794,6 +1367,22 @@ module Irc def create_mode(sym, kl) @mode[sym.to_sym] = kl.new(self) end + + def modes_of(user) + l = [] + @mode.map { |s, m| + l << s if (m.class <= UserMode and m.list[user]) + } + l + end + + def has_op?(user) + @mode.has_key?(:o) and @mode[:o].list[user] + end + + def has_voice?(user) + @mode.has_key?(:v) and @mode[:v].list[user] + end end @@ -803,11 +1392,37 @@ module Irc # Create a new ChannelList, optionally filling it with the elements from # the Array argument fed to it. + # def initialize(ar=[]) super(Channel, ar) end + + # Convenience method: convert the ChannelList to a list of channel names. + # The indices are preserved + # + def names + self.map { |chan| chan.name } + end + + end + +end + + +class String + + # We keep extending String, this time adding a method that converts a + # String into an Irc::Channel object + # + def to_irc_channel(opts={}) + Irc::Channel.new(self, opts) end +end + + +module Irc + # An IRC Server represents the Server the client is connected to. # @@ -819,32 +1434,35 @@ module Irc attr_reader :channels, :users + # TODO Ho def channel_names - @channels.map { |ch| ch.name } + @channels.map { |ch| ch.downcase } end + # TODO Ho def user_nicks - @users.map { |u| u.nick } + @users.map { |u| u.downcase } end def inspect - chans = @channels.map { |ch| - ch.inspect + chans, users = [@channels, @users].map {|d| + d.sort { |a, b| + a.downcase <=> b.downcase + }.map { |x| + x.inspect + } } - users = @users.map { |u| - u.inspect - }.sort str = "<#{self.class}:#{'0x%x' % self.object_id}:" + str << " @hostname=#{hostname}" str << " @channels=#{chans}" - str << " @users=#{users}>" - str + str << " @users=#{users}" + str << ">" end - # Create a new Server, with all instance variables reset - # to nil (for scalar variables), the channel and user lists - # are empty, and @supports is initialized to the default values - # for all known supported features. + # Create a new Server, with all instance variables reset to nil (for + # scalar variables), empty channel and user lists and @supports + # initialized to the default values for all known supported features. # def initialize @hostname = @version = @usermodes = @chanmodes = nil @@ -860,7 +1478,7 @@ module Irc # def reset_capabilities @supports = { - :casemapping => 'rfc1459', + :casemapping => 'rfc1459'.to_irc_casemap, :chanlimit => {}, :chanmodes => { :typea => nil, # Type A: address lists @@ -868,8 +1486,8 @@ module Irc :typec => nil, # Type C: needs a parameter when set :typed => nil # Type D: must not have a parameter }, - :channellen => 200, - :chantypes => "#&", + :channellen => 50, + :chantypes => "#&!+", :excepts => nil, :idchan => {}, :invex => nil, @@ -879,8 +1497,8 @@ module Irc :network => nil, :nicklen => 9, :prefix => { - :modes => 'ov'.scan(/./), - :prefixes => '@+'.scan(/./) + :modes => [:o, :v], + :prefixes => [:"@", :+] }, :safelist => nil, :statusmsg => nil, @@ -894,10 +1512,10 @@ module Irc # Resets the Channel and User list # def reset_lists - @users.each { |u| + @users.reverse_each { |u| delete_user(u) } - @channels.each { |u| + @channels.reverse_each { |u| delete_channel(u) } end @@ -907,6 +1525,7 @@ module Irc def clear reset_lists reset_capabilities + @hostname = @version = @usermodes = @chanmodes = nil end # This method is used to parse a 004 RPL_MY_INFO line @@ -953,31 +1572,22 @@ module Irc key = prekey.downcase.to_sym end case key - when :casemapping, :network + when :casemapping noval_warn(key, val) { - @supports[key] = val - @users.each { |u| - debug "Resetting casemap of #{u} from #{u.casemap} to #{val}" - u.casemap = val - } + @supports[key] = val.to_irc_casemap } when :chanlimit, :idchan, :maxlist, :targmax noval_warn(key, val) { groups = val.split(',') groups.each { |g| k, v = g.split(':') - @supports[key][k] = v.to_i + @supports[key][k] = v.to_i || 0 + if @supports[key][k] == 0 + warn "Deleting #{key} limit of 0 for #{k}" + @supports[key].delete(k) + end } } - when :maxchannels - noval_warn(key, val) { - reparse += "CHANLIMIT=(chantypes):#{val} " - } - when :maxtargets - noval_warn(key, val) { - @supports[key]['PRIVMSG'] = val.to_i - @supports[key]['NOTICE'] = val.to_i - } when :chanmodes noval_warn(key, val) { groups = val.split(',') @@ -1000,6 +1610,19 @@ module Irc when :invex val ||= 'I' @supports[key] = val + when :maxchannels + noval_warn(key, val) { + reparse += "CHANLIMIT=(chantypes):#{val} " + } + when :maxtargets + noval_warn(key, val) { + @supports[:targmax]['PRIVMSG'] = val.to_i + @supports[:targmax]['NOTICE'] = val.to_i + } + when :network + noval_warn(key, val) { + @supports[key] = val + } when :nicklen noval_warn(key, val) { @supports[key] = val.to_i @@ -1037,7 +1660,7 @@ module Irc # Returns the casemap of the server. # def casemap - @supports[:casemapping] || 'rfc1459' + @supports[:casemapping] end # Returns User or Channel depending on what _name_ can be @@ -1064,26 +1687,32 @@ module Irc # Checks if the receiver already has a channel with the given _name_ # def has_channel?(name) - channel_names.index(name.to_s) + return false if name.nil_or_empty? + channel_names.index(name.irc_downcase(casemap)) end alias :has_chan? :has_channel? # Returns the channel with name _name_, if available # def get_channel(name) - idx = channel_names.index(name.to_s) + return nil if name.nil_or_empty? + idx = has_channel?(name) channels[idx] if idx end alias :get_chan :get_channel - # Create a new Channel object and add it to the list of - # Channels on the receiver, unless the channel - # was present already. In this case, the default action is - # to raise an exception, unless _fails_ is set to false - # - # The Channel is automatically created with the appropriate casemap + # Create a new Channel object bound to the receiver and add it to the + # list of Channels on the receiver, unless the channel was + # present already. In this case, the default action is to raise an + # exception, unless _fails_ is set to false. An exception can also be + # raised if _str_ is nil or empty, again only if _fails_ is set to true; + # otherwise, the method just returns nil # def new_channel(name, topic=nil, users=[], fails=true) + if name.nil_or_empty? + raise "Tried to look for empty or nil channel name #{name.inspect}" if fails + return nil + end ex = get_chan(name) if ex raise "Channel #{name} already exists on server #{self}" if fails @@ -1108,18 +1737,19 @@ module Irc channel_names.each { |n| count += 1 if k.include?(n[0]) } - raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k] + # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k] + warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k] } # So far, everything is fine. Now create the actual Channel # - chan = Channel.new(self, name, topic, users) + chan = Channel.new(name, topic, users, :server => self) # We wade through +prefix+ and +chanmodes+ to create appropriate # lists and flags for this channel @supports[:prefix][:modes].each { |mode| - chan.create_mode(mode, ChannelUserMode) + chan.create_mode(mode, Channel::UserMode) } if @supports[:prefix][:modes] @supports[:chanmodes].each { |k, val| @@ -1127,19 +1757,19 @@ module Irc case k when :typea val.each { |mode| - chan.create_mode(mode, ChannelModeTypeA) + chan.create_mode(mode, Channel::ModeTypeA) } when :typeb val.each { |mode| - chan.create_mode(mode, ChannelModeTypeB) + chan.create_mode(mode, Channel::ModeTypeB) } when :typec val.each { |mode| - chan.create_mode(mode, ChannelModeTypeC) + chan.create_mode(mode, Channel::ModeTypeC) } when :typed val.each { |mode| - chan.create_mode(mode, ChannelModeTypeD) + chan.create_mode(mode, Channel::ModeTypeD) } end end @@ -1170,44 +1800,45 @@ module Irc # Checks if the receiver already has a user with the given _nick_ # def has_user?(nick) - user_nicks.index(nick.to_s) + return false if nick.nil_or_empty? + user_nicks.index(nick.irc_downcase(casemap)) end # Returns the user with nick _nick_, if available # def get_user(nick) - idx = user_nicks.index(nick.to_s) + idx = has_user?(nick) @users[idx] if idx end - # Create a new User object and add it to the list of - # Users on the receiver, unless the User - # was present already. In this case, the default action is - # to raise an exception, unless _fails_ is set to false - # - # The User is automatically created with the appropriate casemap + # Create a new User object bound to the receiver and add it to the list + # of Users on the receiver, unless the User was present + # already. In this case, the default action is to raise an exception, + # unless _fails_ is set to false. An exception can also be raised + # if _str_ is nil or empty, again only if _fails_ is set to true; + # otherwise, the method just returns nil # def new_user(str, fails=true) - case str - when User - tmp = str - else - tmp = User.new(str, self.casemap) + if str.nil_or_empty? + raise "Tried to look for empty or nil user name #{str.inspect}" if fails + return nil end - # debug "Creating or selecting user #{tmp.inspect} from #{str.inspect}" + tmp = str.to_irc_user(:server => self) old = get_user(tmp.nick) + # debug "Tmp: #{tmp.inspect}" + # debug "Old: #{old.inspect}" if old # debug "User already existed as #{old.inspect}" if tmp.known? if old.known? + # debug "Both were known" # Do not raise an error: things like Freenode change the hostname after identification warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp raise "User #{tmp} already exists on server #{self}" if fails end - if old != tmp - old.user = tmp.user - old.host = tmp.host - # debug "User improved to #{old.inspect}" + if old.fullform.downcase != tmp.fullform.downcase + old.replace(tmp) + # debug "Known user now #{old.inspect}" end end return old @@ -1223,19 +1854,22 @@ module Irc # new_user(_str_, +false+) # def user(str) - u = new_user(str, false) - debug "Server user #{u.inspect} from #{str.inspect}" - u + new_user(str, false) + end + + # Deletes User _user_ from Channel _channel_ + # + def delete_user_from_channel(user, channel) + channel.delete_user(user) end # Remove User _someuser_ from the list of Users. # _someuser_ must be specified with the full Netmask. # def delete_user(someuser) - idx = has_user?(someuser.nick) + idx = has_user?(someuser) raise "Tried to remove unmanaged user #{user}" unless idx have = self.user(someuser) - raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser && have.user != "*" && have.host != "*" @channels.each { |ch| delete_user_from_channel(have, ch) } @@ -1245,11 +1879,7 @@ module Irc # Create a new Netmask object with the appropriate casemap # def new_netmask(str) - if str.kind_of?(Netmask ) - raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap - return str - end - Netmask.new(str, self.casemap) + str.to_irc_netmask(:server => self) end # Finds all Users on server whose Netmask matches _mask_ @@ -1259,7 +1889,7 @@ module Irc @users.inject(UserList.new) { |list, user| if user.user == "*" or user.host == "*" - list << user if user.nick =~ nm.nick.to_irc_regexp + list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp else list << user if user.matches?(nm) end @@ -1267,12 +1897,6 @@ module Irc } end - # Deletes User from Channel - # - def delete_user_from_channel(user, channel) - channel.delete_user(user) - end - end end