X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;ds=sidebyside;f=lib%2Frbot%2Firc.rb;h=129f947e61d1d9d906e1b02d7b7e7deb486fd5f3;hb=7205060ebc35daf26a22ff6453b4faef477aaca7;hp=74db8e85fd4d1ff750803760c2cce91fa6298b94;hpb=6bf3094ac4ad043f00a3ef8cc2af48a8c23d114a;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git
diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb
index 74db8e85..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
@@ -156,17 +343,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 +365,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 +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
@@ -207,6 +399,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 +461,126 @@ class ArrayOf < Array
}
end
- # Overloaded from Array#+, checks for appropriate class of argument elements
+ # 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 +(ar)
- super(ar) if internal_will_accept?(true, *ar)
+ def downcase
+ self.map { |el| el.downcase }
end
+
+ # Modifying methods which we don't handle yet are made private
+ #
+ private :[]=, :collect!, :map!, :fill, :flatten!
+
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
@@ -242,78 +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
+ # Netmasks have an associated casemap unless they are bound to a server
#
- # 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'.
- #
- # 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
+
+ # 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
- @nick = "*" if @nick.to_s.empty?
- @user = "*" if @user.to_s.empty?
- @host = "*" if @host.to_s.empty?
+ # 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 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
@@ -333,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
@@ -348,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
@@ -371,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.class <= 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
}
@@ -389,8 +785,20 @@ 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.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)
+ else
+ self.downcase <=> arg.downcase
+ end
+ end
+
end
@@ -400,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
@@ -421,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
- # 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,167 +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 User
s
+ # 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.class <= ChannelTopic
- @text = topic.text.dup
- @set_by = topic.set_by.dup
- @set_on = topic.set_on.dup
- end
end
+end
+
+class String
- # Mode on a channel
- class ChannelMode
- def initialize(ch)
- @channel = ch
- 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
+
+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
+ 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
- # 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)
+ @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
@@ -645,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.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 +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
@@ -721,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
@@ -730,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.
#
@@ -746,19 +1434,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 +1478,7 @@ module Irc
#
def reset_capabilities
@supports = {
- :casemapping => 'rfc1459',
+ :casemapping => 'rfc1459'.to_irc_casemap,
:chanlimit => {},
:chanmodes => {
:typea => nil, # Type A: address lists
@@ -775,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,
@@ -786,8 +1497,8 @@ module Irc
:network => nil,
:nicklen => 9,
:prefix => {
- :modes => 'ov'.scan(/./),
- :prefixes => '@+'.scan(/./)
+ :modes => [:o, :v],
+ :prefixes => [:"@", :+]
},
:safelist => nil,
:statusmsg => nil,
@@ -801,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
@@ -814,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
@@ -860,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(',')
@@ -907,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
@@ -944,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
@@ -971,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)
- @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
- # Channel
s 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 Channel
s 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 +1734,22 @@ 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]
+ # 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|
@@ -1034,28 +1757,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 +1794,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
- # User
s 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 User
s 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
@@ -1135,29 +1857,29 @@ module Irc
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 User
s.
# _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 User
s on server whose Netmask matches _mask_
@@ -1167,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
@@ -1175,12 +1897,7 @@ module Irc
}
end
- # Deletes User from Channel
- #
- def delete_user_from_channel(user, channel)
- channel.delete_user(user)
- end
-
end
+
end