X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Firc.rb;h=0cf70d5a6253e7a086b2f8ad0ac868b7d3ac7cf2;hb=2d7853ce6e3683c8eb5e858ba7c5eb2dcaeba5eb;hp=ffc2be7182681862bf4dacc80e1c220b40cdce7d;hpb=158056c6f68bbe75e4c9a33433e1502c445c26b8;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb index ffc2be71..0cf70d5a 100644 --- a/lib/rbot/irc.rb +++ b/lib/rbot/irc.rb @@ -4,6 +4,8 @@ # 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 +# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf? +# See items marked as TODO Ho #++ # :title: IRC module # @@ -14,11 +16,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 +245,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 +265,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 +274,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 +283,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 @@ -137,6 +321,7 @@ class String } Regexp.new(regmask) end + end @@ -156,17 +341,21 @@ class ArrayOf < Array # optionally filling it with the elements from the Array argument. # def initialize(kl, ar=[]) - raise TypeError, "#{kl.inspect} must be a class name" unless kl.class <= Class + raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class) super() @element_class = kl case ar when Array - send(:+, ar) + insert(0, *ar) else raise TypeError, "#{self.class} can only be initialized from an Array" end end + def inspect + "#<#{self.class}[#{@element_class}]:#{'0x%x' % self.object_id}: #{super}>" + end + # Private method to check the validity of the elements passed to it # and optionally raise an error # @@ -174,7 +363,7 @@ class ArrayOf < Array # def internal_will_accept?(raising, *els) els.each { |el| - unless el.class <= @element_class + unless el.kind_of?(@element_class) raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising return false end @@ -197,6 +386,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 @@ -207,6 +397,60 @@ class ArrayOf < Array super(el) if internal_will_accept?(true, el) end + # Overloaded from Array#&, checks for appropriate class of argument elements + # + def &(ar) + r = super(ar) + ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r) + end + + # Overloaded from Array#+, checks for appropriate class of argument elements + # + def +(ar) + ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar) + end + + # Overloaded from Array#-, so that an ArrayOf is returned. There is no need + # to check the validity of the elements in the argument + # + def -(ar) + ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar) + end + + # Overloaded from Array#|, checks for appropriate class of argument elements + # + def |(ar) + ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar) + end + + # Overloaded from Array#concat, checks for appropriate class of argument + # elements + # + def concat(ar) + super(ar) if internal_will_accept?(true, *ar) + end + + # Overloaded from Array#insert, checks for appropriate class of argument + # elements + # + def insert(idx, *ar) + super(idx, *ar) if internal_will_accept?(true, *ar) + end + + # Overloaded from Array#replace, checks for appropriate class of argument + # elements + # + def replace(ar) + super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar) + end + + # Overloaded from Array#push, checks for appropriate class of argument + # elements + # + def push(*ar) + super(*ar) if internal_will_accept?(true, *ar) + end + # Overloaded from Array#unshift, checks for appropriate class of argument(s) # def unshift(*els) @@ -215,26 +459,22 @@ class ArrayOf < Array } end - # Overloaded from Array#+, checks for appropriate class of argument elements + # Modifying methods which we don't handle yet are made private # - def +(ar) - super(ar) if internal_will_accept?(true, *ar) - end + private :[]=, :collect!, :map!, :fill, :flatten! + end -# The Irc module is used to keep all IRC-related classes -# in the same namespace -# + 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 @@ -242,78 +482,98 @@ 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 + 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 + # + def fullform + "#{nick}!#{user}@#{host}" + end + alias :to_s :fullform + + # 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 + # + def to_irc_netmask(opts={}) + if self.class == Netmask + return self if fits_with_server_and_casemap?(opts) end + return self.fullform.to_irc_netmask(server_and_casemap.merge(opts)) + end - @nick = "*" if @nick.to_s.empty? - @user = "*" if @user.to_s.empty? - @host = "*" if @host.to_s.empty? + # 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 - # Equality: two Netmasks are equal if they have the same @nick, @user, @host and @casemap + # 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} casemap=#{casemap.inspect}" + str << ">" + end + + # 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 @@ -333,12 +593,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 @@ -348,18 +615,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 @@ -371,15 +632,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(arg) - raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap - raise TypeError, "#{arg} is not a valid Netmask" unless cmp.class <= Netmask + cmp = arg.to_irc_netmask(:casemap => casemap) + debug "Matching #{self.fullform} against #{arg.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 } @@ -389,8 +653,20 @@ module Irc # Case equality. Checks if arg matches self # def ===(arg) - Netmask(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.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap) + else + self.downcase <=> arg.downcase + end + end + end @@ -400,19 +676,46 @@ 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 + + end + +end + + +class String + + # We keep extending String, this time adding a method that converts a + # String into an Irc::Netmask object + # + def to_irc_netmask(opts={}) + Irc::Netmask.new(self, opts) end +end + - # 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. +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. # - # 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. + # 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 @@ -424,7 +727,7 @@ module Irc # 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 != "*" @@ -432,32 +735,35 @@ module Irc @away = false 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 + # 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 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? @@ -476,6 +782,33 @@ 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.fullform.to_irc_user(server_and_casemap(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 + end @@ -485,158 +818,269 @@ module Irc # Create a new UserList, optionally filling it with the elements from # the Array argument fed to it. + # def initialize(ar=[]) super(User, ar) end - 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 +end - # 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 - end +class String - # Replace a ChannelTopic with another one - def replace(topic) - raise TypeError, "#{topic.inspect} is not an Irc::ChannelTopic" unless topic.class <= ChannelTopic - @text = topic.text.dup - @set_by = topic.set_by.dup - @set_on = topic.set_on.dup - end + # 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 - # Mode on a channel - class ChannelMode - def initialize(ch) - @channel = ch - end - end - +module Irc - # Channel modes of type A manipulate lists + # 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 + 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 + 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 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 + 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 = false + end + + def status + @arg + end + + def set(val) + @arg = val + end + + def reset + @arg = false + 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_user + @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 - # A String describing the Channel and (some of its) internals - # def inspect - str = "<#{self.class}:#{'0x%08x' % self.object_id}:" - str << " on server #{server}" + str = "<#{self.class}:#{'0x%x' % self.object_id}:" + str << " on server #{server}" if server str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}" - str << " @users=<#{@users.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) + user_nicks.index(nick.irc_downcase(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) && !silent + warn "Trying to add user #{user} to channel #{self} again" + else + @users << user.to_irc_user(server_and_casemap) + end end # Creates a new channel with the given name, optionally setting the topic @@ -645,43 +1089,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.class <= 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.class <= ChannelUserMode + mode.reset(user) if mode.kind_of?(UserMode) } @users.delete(user) end @@ -695,25 +1128,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 @@ -721,6 +1154,7 @@ module Irc def create_mode(sym, kl) @mode[sym.to_sym] = kl.new(self) end + end @@ -730,11 +1164,30 @@ 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 + + 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. # @@ -746,19 +1199,42 @@ module Irc attr_reader :channels, :users - # 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. + # TODO Ho + def channel_names + @channels.map { |ch| ch.downcase } + end + + # TODO Ho + def user_nicks + @users.map { |u| u.downcase } + end + + def inspect + chans, users = [@channels, @users].map {|d| + d.sort { |a, b| + a.downcase <=> b.downcase + }.map { |x| + x.inspect + } + } + + str = "<#{self.class}:#{'0x%x' % self.object_id}:" + str << " @hostname=#{hostname}" + str << " @channels=#{chans}" + str << " @users=#{users}" + str << ">" + end + + # 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 @channels = ChannelList.new - @channel_names = Array.new @users = UserList.new - @user_nicks = Array.new reset_capabilities end @@ -767,7 +1243,7 @@ module Irc # def reset_capabilities @supports = { - :casemapping => 'rfc1459', + :casemapping => 'rfc1459'.to_irc_casemap, :chanlimit => {}, :chanmodes => { :typea => nil, # Type A: address lists @@ -860,31 +1336,18 @@ 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 } } - 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(',') @@ -907,6 +1370,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 @@ -944,14 +1420,14 @@ 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 # a name of # def user_or_channel?(name) - if supports[:chantypes].include?(name[0].chr) + if supports[:chantypes].include?(name[0]) return Channel else return User @@ -961,7 +1437,7 @@ module Irc # Returns the actual User or Channel object matching _name_ # def user_or_channel(name) - if supports[:chantypes].include?(name[0].chr) + if supports[:chantypes].include?(name[0]) return channel(name) else return user(name) @@ -971,26 +1447,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) - @channels[idx] if idx + 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 @@ -1012,21 +1494,21 @@ module Irc @supports[:chanlimit].keys.each { |k| next unless k.include?(prefix) count = 0 - @channel_names.each { |n| - count += 1 if k.include?(n[0].chr) + 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] } # 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| @@ -1034,28 +1516,26 @@ 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 } @channels << chan - @channel_names << name # debug "Created channel #{chan.inspect}" - # debug "Managing channels #{@channel_names.join(', ')}" return chan end end @@ -1073,56 +1553,57 @@ module Irc def delete_channel(name) idx = has_channel?(name) raise "Tried to remove unmanaged channel #{name}" unless idx - @channel_names.delete_at(idx) @channels.delete_at(idx) end # 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? - raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp + # 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 - else - old.user = tmp.user - old.host = tmp.host - # debug "User improved to #{old.inspect}" + end + if old.fullform.downcase != tmp.fullform.downcase + old.replace(tmp) + # debug "Known user now #{old.inspect}" end end return old else warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen] @users << tmp - @user_nicks << tmp.nick return @users.last end end @@ -1132,40 +1613,32 @@ module Irc # new_user(_str_, +false+) # def user(str) - # This method can get called before server has been initialized (e.g. on - # Freenode there is a NOTICE from AUTH on connect). In this case we just - # return the string - # - if defined?(@supports) - new_user(str, false) - else - str - end + 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) } - @user_nicks.delete_at(idx) @users.delete_at(idx) end # Create a new Netmask object with the appropriate casemap # def new_netmask(str) - if str.class <= 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_ @@ -1175,7 +1648,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 @@ -1183,12 +1656,7 @@ module Irc } end - # Deletes User from Channel - # - def delete_user_from_channel(user, channel) - channel.delete_user(user) - end - end + end