-#-- vim:sw=2:et\r
-# General TODO list\r
-# * do we want to handle a Channel list for each User telling which\r
-# Channels is the User on (of those the client is on too)?\r
-# We may want this so that when a User leaves all Channels and he hasn't\r
-# sent us privmsgs, we know we can remove him from the Server @users list\r
-# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?\r
-# See items marked as TODO Ho.\r
-# The framework to do this is now in place, thanks to the new [] method\r
-# for NetmaskList, which allows retrieval by Netmask or String\r
-#++\r
-# :title: IRC module\r
-#\r
-# Basic IRC stuff\r
-#\r
-# This module defines the fundamental building blocks for IRC\r
-#\r
-# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)\r
-# Copyright:: Copyright (c) 2006 Giuseppe Bilotta\r
-# License:: GPLv2\r
-\r
-require 'singleton'\r
-\r
-class Object\r
-\r
- # We extend the Object class with a method that\r
- # checks if the receiver is nil or empty\r
- def nil_or_empty?\r
- return true unless self\r
- return true if self.respond_to? :empty and self.empty?\r
- return false\r
- end\r
-\r
- # We alias the to_s method to __to_s__ to make\r
- # it accessible in all classes\r
- alias :__to_s__ :to_s \r
-end\r
-\r
-# The Irc module is used to keep all IRC-related classes\r
-# in the same namespace\r
-#\r
-module Irc\r
-\r
-\r
- # Due to its Scandinavian origins, IRC has strange case mappings, which\r
- # consider the characters <tt>{}|^</tt> as the uppercase\r
- # equivalents of # <tt>[]\~</tt>.\r
- #\r
- # This is however not the same on all IRC servers: some use standard ASCII\r
- # casemapping, other do not consider <tt>^</tt> as the uppercase of\r
- # <tt>~</tt>\r
- #\r
- class Casemap\r
- @@casemaps = {}\r
-\r
- # Create a new casemap with name _name_, uppercase characters _upper_ and\r
- # lowercase characters _lower_\r
- #\r
- def initialize(name, upper, lower)\r
- @key = name.to_sym\r
- raise "Casemap #{name.inspect} already exists!" if @@casemaps.has_key?(@key)\r
- @@casemaps[@key] = {\r
- :upper => upper,\r
- :lower => lower,\r
- :casemap => self\r
- }\r
- end\r
-\r
- # Returns the Casemap with the given name\r
- #\r
- def Casemap.get(name)\r
- @@casemaps[name.to_sym][:casemap]\r
- end\r
-\r
- # Retrieve the 'uppercase characters' of this Casemap\r
- #\r
- def upper\r
- @@casemaps[@key][:upper]\r
- end\r
-\r
- # Retrieve the 'lowercase characters' of this Casemap\r
- #\r
- def lower\r
- @@casemaps[@key][:lower]\r
- end\r
-\r
- # Return a Casemap based on the receiver\r
- #\r
- def to_irc_casemap\r
- self\r
- end\r
-\r
- # A Casemap is represented by its lower/upper mappings\r
- #\r
- def inspect\r
- self.__to_s__[0..-2] + " #{upper.inspect} ~(#{self})~ #{lower.inspect}>"\r
- end\r
-\r
- # As a String we return our name\r
- #\r
- def to_s\r
- @key.to_s\r
- end\r
-\r
- # Two Casemaps are equal if they have the same upper and lower ranges\r
- #\r
- def ==(arg)\r
- other = arg.to_irc_casemap\r
- return self.upper == other.upper && self.lower == other.lower\r
- end\r
-\r
- # Give a warning if _arg_ and self are not the same Casemap\r
- #\r
- def must_be(arg)\r
- other = arg.to_irc_casemap\r
- if self == other\r
- return true\r
- else\r
- warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"\r
- return false\r
- end\r
- end\r
-\r
- end\r
-\r
- # The rfc1459 casemap\r
- #\r
- class RfcCasemap < Casemap\r
- include Singleton\r
-\r
- def initialize\r
- super('rfc1459', "\x41-\x5e", "\x61-\x7e")\r
- end\r
-\r
- end\r
- RfcCasemap.instance\r
-\r
- # The strict-rfc1459 Casemap\r
- #\r
- class StrictRfcCasemap < Casemap\r
- include Singleton\r
-\r
- def initialize\r
- super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")\r
- end\r
-\r
- end\r
- StrictRfcCasemap.instance\r
-\r
- # The ascii Casemap\r
- #\r
- class AsciiCasemap < Casemap\r
- include Singleton\r
-\r
- def initialize\r
- super('ascii', "\x41-\x5a", "\x61-\x7a")\r
- end\r
-\r
- end\r
- AsciiCasemap.instance\r
-\r
-\r
- # This module is included by all classes that are either bound to a server\r
- # or should have a casemap.\r
- #\r
- module ServerOrCasemap\r
-\r
- attr_reader :server\r
-\r
- # This method initializes the instance variables @server and @casemap\r
- # according to the values of the hash keys :server and :casemap in _opts_\r
- #\r
- def init_server_or_casemap(opts={})\r
- @server = opts.fetch(:server, nil)\r
- raise TypeError, "#{@server} is not a valid Irc::Server" if @server and not @server.kind_of?(Server)\r
-\r
- @casemap = opts.fetch(:casemap, nil)\r
- if @server\r
- if @casemap\r
- @server.casemap.must_be(@casemap)\r
- @casemap = nil\r
- end\r
- else\r
- @casemap = (@casemap || 'rfc1459').to_irc_casemap\r
- end\r
- end\r
-\r
- # This is an auxiliary method: it returns true if the receiver fits the\r
- # server and casemap specified in _opts_, false otherwise.\r
- #\r
- def fits_with_server_and_casemap?(opts={})\r
- srv = opts.fetch(:server, nil)\r
- cmap = opts.fetch(:casemap, nil)\r
- cmap = cmap.to_irc_casemap unless cmap.nil?\r
-\r
- if srv.nil?\r
- return true if cmap.nil? or cmap == casemap\r
- else\r
- return true if srv == @server and (cmap.nil? or cmap == casemap)\r
- end\r
- return false\r
- end\r
-\r
- # Returns the casemap of the receiver, by looking at the bound\r
- # @server (if possible) or at the @casemap otherwise\r
- #\r
- def casemap\r
- return @server.casemap if defined?(@server) and @server\r
- return @casemap\r
- end\r
-\r
- # Returns a hash with the current @server and @casemap as values of\r
- # :server and :casemap\r
- #\r
- def server_and_casemap\r
- h = {}\r
- h[:server] = @server if defined?(@server) and @server\r
- h[:casemap] = @casemap if defined?(@casemap) and @casemap\r
- return h\r
- end\r
-\r
- # We allow up/downcasing with a different casemap\r
- #\r
- def irc_downcase(cmap=casemap)\r
- self.to_s.irc_downcase(cmap)\r
- end\r
-\r
- # Up/downcasing something that includes this module returns its\r
- # Up/downcased to_s form\r
- #\r
- def downcase\r
- self.irc_downcase\r
- end\r
-\r
- # We allow up/downcasing with a different casemap\r
- #\r
- def irc_upcase(cmap=casemap)\r
- self.to_s.irc_upcase(cmap)\r
- end\r
-\r
- # Up/downcasing something that includes this module returns its\r
- # Up/downcased to_s form\r
- #\r
- def upcase\r
- self.irc_upcase\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
-\r
-# We start by extending the String class\r
-# with some IRC-specific methods\r
-#\r
-class String\r
-\r
- # This method returns the Irc::Casemap whose name is the receiver\r
- #\r
- def to_irc_casemap\r
- Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"\r
- end\r
-\r
- # This method returns a string which is the downcased version of the\r
- # receiver, according to the given _casemap_\r
- #\r
- #\r
- def irc_downcase(casemap='rfc1459')\r
- cmap = casemap.to_irc_casemap\r
- self.tr(cmap.upper, cmap.lower)\r
- end\r
-\r
- # This is the same as the above, except that the string is altered in place\r
- #\r
- # See also the discussion about irc_downcase\r
- #\r
- def irc_downcase!(casemap='rfc1459')\r
- cmap = casemap.to_irc_casemap\r
- self.tr!(cmap.upper, cmap.lower)\r
- end\r
-\r
- # Upcasing functions are provided too\r
- #\r
- # See also the discussion about irc_downcase\r
- #\r
- def irc_upcase(casemap='rfc1459')\r
- cmap = casemap.to_irc_casemap\r
- self.tr(cmap.lower, cmap.upper)\r
- end\r
-\r
- # In-place upcasing\r
- #\r
- # See also the discussion about irc_downcase\r
- #\r
- def irc_upcase!(casemap='rfc1459')\r
- cmap = casemap.to_irc_casemap\r
- self.tr!(cmap.lower, cmap.upper)\r
- end\r
-\r
- # This method checks if the receiver contains IRC glob characters\r
- #\r
- # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any\r
- # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly\r
- # one arbitrary character". These characters can be escaped by prefixing them\r
- # with a slash (<tt>\\</tt>).\r
- #\r
- # A known limitation of this glob syntax is that there is no way to escape\r
- # the escape character itself, so it's not possible to build a glob pattern\r
- # where the escape character precedes a glob.\r
- #\r
- def has_irc_glob?\r
- self =~ /^[*?]|[^\\][*?]/\r
- end\r
-\r
- # This method is used to convert the receiver into a Regular Expression\r
- # that matches according to the IRC glob syntax\r
- #\r
- def to_irc_regexp\r
- regmask = Regexp.escape(self)\r
- regmask.gsub!(/(\\\\)?\\[*?]/) { |m|\r
- case m\r
- when /\\(\\[*?])/\r
- $1\r
- when /\\\*/\r
- '.*'\r
- when /\\\?/\r
- '.'\r
- else\r
- raise "Unexpected match #{m} when converting #{self}"\r
- end\r
- }\r
- Regexp.new("^#{regmask}$")\r
- end\r
-\r
-end\r
-\r
-\r
-# ArrayOf is a subclass of Array whose elements are supposed to be all\r
-# of the same class. This is not intended to be used directly, but rather\r
-# to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)\r
-#\r
-# Presently, only very few selected methods from Array are overloaded to check\r
-# if the new elements are the correct class. An orthodox? method is provided\r
-# to check the entire ArrayOf against the appropriate class.\r
-#\r
-class ArrayOf < Array\r
-\r
- attr_reader :element_class\r
-\r
- # Create a new ArrayOf whose elements are supposed to be all of type _kl_,\r
- # optionally filling it with the elements from the Array argument.\r
- #\r
- def initialize(kl, ar=[])\r
- raise TypeError, "#{kl.inspect} must be a class name" unless kl.kind_of?(Class)\r
- super()\r
- @element_class = kl\r
- case ar\r
- when Array\r
- insert(0, *ar)\r
- else\r
- raise TypeError, "#{self.class} can only be initialized from an Array"\r
- end\r
- end\r
-\r
- def inspect\r
- self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"\r
- end\r
-\r
- # Private method to check the validity of the elements passed to it\r
- # and optionally raise an error\r
- #\r
- # TODO should it accept nils as valid?\r
- #\r
- def internal_will_accept?(raising, *els)\r
- els.each { |el|\r
- unless el.kind_of?(@element_class)\r
- raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising\r
- return false\r
- end\r
- }\r
- return true\r
- end\r
- private :internal_will_accept?\r
-\r
- # This method checks if the passed arguments are acceptable for our ArrayOf\r
- #\r
- def will_accept?(*els)\r
- internal_will_accept?(false, *els)\r
- end\r
-\r
- # This method checks that all elements are of the appropriate class\r
- #\r
- def valid?\r
- will_accept?(*self)\r
- end\r
-\r
- # This method is similar to the above, except that it raises an exception\r
- # if the receiver is not valid\r
- #\r
- def validate\r
- raise TypeError unless valid?\r
- end\r
-\r
- # Overloaded from Array#<<, checks for appropriate class of argument\r
- #\r
- def <<(el)\r
- super(el) if internal_will_accept?(true, el)\r
- end\r
-\r
- # Overloaded from Array#&, checks for appropriate class of argument elements\r
- #\r
- def &(ar)\r
- r = super(ar)\r
- ArrayOf.new(@element_class, r) if internal_will_accept?(true, *r)\r
- end\r
-\r
- # Overloaded from Array#+, checks for appropriate class of argument elements\r
- #\r
- def +(ar)\r
- ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#-, so that an ArrayOf is returned. There is no need\r
- # to check the validity of the elements in the argument\r
- #\r
- def -(ar)\r
- ArrayOf.new(@element_class, super(ar)) # if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#|, checks for appropriate class of argument elements\r
- #\r
- def |(ar)\r
- ArrayOf.new(@element_class, super(ar)) if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#concat, checks for appropriate class of argument\r
- # elements\r
- #\r
- def concat(ar)\r
- super(ar) if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#insert, checks for appropriate class of argument\r
- # elements\r
- #\r
- def insert(idx, *ar)\r
- super(idx, *ar) if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#replace, checks for appropriate class of argument\r
- # elements\r
- #\r
- def replace(ar)\r
- super(ar) if (ar.kind_of?(ArrayOf) && ar.element_class <= @element_class) or internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#push, checks for appropriate class of argument\r
- # elements\r
- #\r
- def push(*ar)\r
- super(*ar) if internal_will_accept?(true, *ar)\r
- end\r
-\r
- # Overloaded from Array#unshift, checks for appropriate class of argument(s)\r
- #\r
- def unshift(*els)\r
- els.each { |el|\r
- super(el) if internal_will_accept?(true, *els)\r
- }\r
- end\r
-\r
- # We introduce the 'downcase' method, which maps downcase() to all the Array\r
- # elements, properly failing when the elements don't have a downcase method\r
- #\r
- def downcase\r
- self.map { |el| el.downcase }\r
- end\r
-\r
- # Modifying methods which we don't handle yet are made private\r
- #\r
- private :[]=, :collect!, :map!, :fill, :flatten!\r
-\r
-end\r
-\r
-\r
-# We extend the Regexp class with an Irc module which will contain some\r
-# Irc-specific regexps\r
-#\r
-class Regexp\r
-\r
- # We start with some general-purpose ones which will be used in the\r
- # Irc module too, but are useful regardless\r
- DIGITS = /\d+/\r
- HEX_DIGIT = /[0-9A-Fa-f]/\r
- HEX_DIGITS = /#{HEX_DIGIT}+/\r
- HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/\r
- DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/\r
- DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/\r
- HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/\r
- IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/\r
-\r
- # IPv6, from Resolv::IPv6, without the \A..\z anchors\r
- HEX_16BIT = /#{HEX_DIGIT}{1,4}/\r
- IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/\r
- IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/\r
- IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/\r
- IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/\r
- IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/\r
-\r
- # We start with some IRC related regular expressions, used to match\r
- # Irc::User nicks and users and Irc::Channel names\r
- #\r
- # For each of them we define two versions of the regular expression:\r
- # * a generic one, which should match for any server but may turn out to\r
- # match more than a specific server would accept\r
- # * an RFC-compliant matcher\r
- #\r
- module Irc\r
-\r
- # Channel-name-matching regexps\r
- CHAN_FIRST = /[#&+]/\r
- CHAN_SAFE = /![A-Z0-9]{5}/\r
- CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/\r
- GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/\r
- RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/\r
-\r
- # Nick-matching regexps\r
- SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/\r
- NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/\r
- NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/\r
- GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/\r
- RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/\r
-\r
- USER_CHAR = /[^\x00\x0a\x0d @]/\r
- GEN_USER = /#{USER_CHAR}+/\r
-\r
- # Host-matching regexps\r
- HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/\r
- HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/\r
- HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/\r
-\r
- GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/\r
-\r
- # # FreeNode network replaces the host of affiliated users with\r
- # # 'virtual hosts' \r
- # # FIXME we need the true syntax to match it properly ...\r
- # PDPC_HOST_PART = /[0-9A-Za-z.-]+/\r
- # PDPC_HOST = /#{PDPC_HOST_PART}(?:\/#{PDPC_HOST_PART})+/\r
-\r
- # # NOTE: the final optional and non-greedy dot is needed because some\r
- # # servers (e.g. FreeNode) send the hostname of the services as "services."\r
- # # which is not RFC compliant, but sadly done.\r
- # GEN_HOST_EXT = /#{PDPC_HOST}|#{GEN_HOST}\.??/ \r
-\r
- # Sadly, different networks have different, RFC-breaking ways of cloaking\r
- # the actualy host address: see above for an example to handle FreeNode.\r
- # Another example would be Azzurra, wich also inserts a "=" in the\r
- # cloacked host. So let's just not care about this and go with the simplest\r
- # thing:\r
- GEN_HOST_EXT = /\S+/\r
-\r
- # User-matching Regexp\r
- GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/\r
-\r
- # Things such has the BIP proxy send invalid nicks in a complete netmask,\r
- # so we want to match this, rather: this matches either a compliant nick\r
- # or a a string with a very generic nick, a very generic hostname after an\r
- # @ sign, and an optional user after a !\r
- BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/\r
-\r
- # # For Netmask, we want to allow wildcards * and ? in the nick\r
- # # (they are already allowed in the user and host part\r
- # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/\r
-\r
- # # Netmask-matching Regexp\r
- # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/\r
-\r
- end\r
-\r
-end\r
-\r
-\r
-module Irc\r
-\r
-\r
- # A Netmask identifies each user by collecting its nick, username and\r
- # hostname in the form <tt>nick!user@host</tt>\r
- #\r
- # Netmasks can also contain glob patterns in any of their components; in\r
- # this form they are used to refer to more than a user or to a user\r
- # appearing under different forms.\r
- #\r
- # Example:\r
- # * <tt>*!*@*</tt> refers to everybody\r
- # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+\r
- # regardless of the nick used.\r
- #\r
- class Netmask\r
-\r
- # Netmasks have an associated casemap unless they are bound to a server\r
- #\r
- include ServerOrCasemap\r
-\r
- attr_reader :nick, :user, :host\r
- alias :ident :user\r
-\r
- # Create a new Netmask from string _str_, which must be in the form\r
- # _nick_!_user_@_host_\r
- #\r
- # It is possible to specify a server or a casemap in the optional Hash:\r
- # these are used to associate the Netmask with the given server and to set\r
- # its casemap: if a server is specified and a casemap is not, the server's\r
- # casemap is used. If both a server and a casemap are specified, the\r
- # casemap must match the server's casemap or an exception will be raised.\r
- #\r
- # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
- #\r
- def initialize(str="", opts={})\r
- # First of all, check for server/casemap option\r
- #\r
- init_server_or_casemap(opts)\r
-\r
- # Now we can see if the given string _str_ is an actual Netmask\r
- if str.respond_to?(:to_str)\r
- case str.to_str\r
- # We match a pretty generic string, to work around non-compliant\r
- # servers\r
- when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/\r
- # We do assignment using our internal methods\r
- self.nick = $1\r
- self.user = $2\r
- self.host = $3\r
- else\r
- raise ArgumentError, "#{str.to_str.inspect} does not represent a valid #{self.class}"\r
- end\r
- else\r
- raise TypeError, "#{str} cannot be converted to a #{self.class}"\r
- end\r
- end\r
-\r
- # A Netmask is easily converted to a String for the usual representation.\r
- # We skip the user or host parts if they are "*", unless we've been asked\r
- # for the full form\r
- #\r
- def to_s\r
- ret = nick.dup\r
- ret << "!" << user unless user == "*"\r
- ret << "@" << host unless host == "*"\r
- return ret\r
- end\r
-\r
- def fullform\r
- "#{nick}!#{user}@#{host}"\r
- end\r
-\r
- alias :to_str :fullform\r
-\r
- # This method downcases the fullform of the netmask. While this may not be\r
- # significantly different from the #downcase() method provided by the\r
- # ServerOrCasemap mixin, it's significantly different for Netmask\r
- # subclasses such as User whose simple downcasing uses the nick only.\r
- #\r
- def full_irc_downcase(cmap=casemap)\r
- self.fullform.irc_downcase(cmap)\r
- end\r
-\r
- # full_downcase() will return the fullform downcased according to the\r
- # User's own casemap\r
- #\r
- def full_downcase\r
- self.full_irc_downcase\r
- end\r
-\r
- # This method returns a new Netmask which is the fully downcased version\r
- # of the receiver\r
- def downcased\r
- return self.full_downcase.to_irc_netmask(server_and_casemap)\r
- end\r
-\r
- # Converts the receiver into a Netmask with the given (optional)\r
- # server/casemap association. We return self unless a conversion\r
- # is needed (different casemap/server)\r
- #\r
- # Subclasses of Netmask will return a new Netmask, using full_downcase\r
- #\r
- def to_irc_netmask(opts={})\r
- if self.class == Netmask\r
- return self if fits_with_server_and_casemap?(opts)\r
- end\r
- return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))\r
- end\r
-\r
- # Converts the receiver into a User with the given (optional)\r
- # server/casemap association. We return self unless a conversion\r
- # is needed (different casemap/server)\r
- #\r
- def to_irc_user(opts={})\r
- self.fullform.to_irc_user(server_and_casemap.merge(opts))\r
- end\r
-\r
- # Inspection of a Netmask reveals the server it's bound to (if there is\r
- # one), its casemap and the nick, user and host part\r
- #\r
- def inspect\r
- str = self.__to_s__[0..-2]\r
- str << " @server=#{@server}" if defined?(@server) and @server\r
- str << " @nick=#{@nick.inspect} @user=#{@user.inspect}"\r
- str << " @host=#{@host.inspect} casemap=#{casemap.inspect}"\r
- str << ">"\r
- end\r
-\r
- # Equality: two Netmasks are equal if they downcase to the same thing\r
- #\r
- # TODO we may want it to try other.to_irc_netmask\r
- #\r
- def ==(other)\r
- return false unless other.kind_of?(self.class)\r
- self.downcase == other.downcase\r
- end\r
-\r
- # This method changes the nick of the Netmask, defaulting to the generic\r
- # glob pattern if the result is the null string.\r
- #\r
- def nick=(newnick)\r
- @nick = newnick.to_s\r
- @nick = "*" if @nick.empty?\r
- end\r
-\r
- # This method changes the user of the Netmask, defaulting to the generic\r
- # glob pattern if the result is the null string.\r
- #\r
- def user=(newuser)\r
- @user = newuser.to_s\r
- @user = "*" if @user.empty?\r
- end\r
- alias :ident= :user=\r
-\r
- # This method changes the hostname of the Netmask, defaulting to the generic\r
- # glob pattern if the result is the null string.\r
- #\r
- def host=(newhost)\r
- @host = newhost.to_s\r
- @host = "*" if @host.empty?\r
- end\r
-\r
- # We can replace everything at once with data from another Netmask\r
- #\r
- def replace(other)\r
- case other\r
- when Netmask\r
- nick = other.nick\r
- user = other.user\r
- host = other.host\r
- @server = other.server\r
- @casemap = other.casemap unless @server\r
- else\r
- replace(other.to_irc_netmask(server_and_casemap))\r
- end\r
- end\r
-\r
- # This method checks if a Netmask is definite or not, by seeing if\r
- # any of its components are defined by globs\r
- #\r
- def has_irc_glob?\r
- return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?\r
- end\r
-\r
- def generalize\r
- u = user.dup\r
- unless u.has_irc_glob?\r
- u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')\r
- u = '*' + u\r
- end\r
-\r
- h = host.dup\r
- unless h.has_irc_glob?\r
- if h.include? '/'\r
- h.sub!(/x-\w+$/, 'x-*')\r
- else\r
- h.match(/^[^\.]+\.[^\.]+$/) or\r
- h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck!\r
- h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or\r
- h.sub!(/^[^\.]+\./, '*.')\r
- end\r
- end\r
- return Netmask.new("*!#{u}@#{h}", server_and_casemap)\r
- end\r
-\r
- # This method is used to match the current Netmask against another one\r
- #\r
- # The method returns true if each component of the receiver matches the\r
- # corresponding component of the argument. By _matching_ here we mean\r
- # that any netmask described by the receiver is also described by the\r
- # argument.\r
- #\r
- # In this sense, matching is rather simple to define in the case when the\r
- # receiver has no globs: it is just necessary to check if the argument\r
- # describes the receiver, which can be done by matching it against the\r
- # argument converted into an IRC Regexp (see String#to_irc_regexp).\r
- #\r
- # The situation is also easy when the receiver has globs and the argument\r
- # doesn't, since in this case the result is false.\r
- #\r
- # The more complex case in which both the receiver and the argument have\r
- # globs is not handled yet.\r
- #\r
- def matches?(arg)\r
- cmp = arg.to_irc_netmask(:casemap => casemap)\r
- debug "Matching #{self.fullform} against #{arg.inspect} (#{cmp.fullform})"\r
- [:nick, :user, :host].each { |component|\r
- us = self.send(component).irc_downcase(casemap)\r
- them = cmp.send(component).irc_downcase(casemap)\r
- if us.has_irc_glob? && them.has_irc_glob?\r
- next if us == them\r
- warn NotImplementedError\r
- return false\r
- end\r
- return false if us.has_irc_glob? && !them.has_irc_glob?\r
- return false unless us =~ them.to_irc_regexp\r
- }\r
- return true\r
- end\r
-\r
- # Case equality. Checks if arg matches self\r
- #\r
- def ===(arg)\r
- arg.to_irc_netmask(:casemap => casemap).matches?(self)\r
- end\r
-\r
- # Sorting is done via the fullform\r
- #\r
- def <=>(arg)\r
- case arg\r
- when Netmask\r
- self.fullform.irc_downcase(casemap) <=> arg.fullform.irc_downcase(casemap)\r
- else\r
- self.downcase <=> arg.downcase\r
- end\r
- end\r
-\r
- end\r
-\r
-\r
- # A NetmaskList is an ArrayOf <code>Netmask</code>s\r
- #\r
- class NetmaskList < ArrayOf\r
-\r
- # Create a new NetmaskList, optionally filling it with the elements from\r
- # the Array argument fed to it.\r
- #\r
- def initialize(ar=[])\r
- super(Netmask, ar)\r
- end\r
-\r
- # We enhance the [] method by allowing it to pick an element that matches\r
- # a given Netmask, a String or a Regexp\r
- # TODO take into consideration the opportunity to use select() instead of\r
- # find(), and/or a way to let the user choose which one to take (second\r
- # argument?)\r
- #\r
- def [](*args)\r
- if args.length == 1\r
- case args[0]\r
- when Netmask\r
- self.find { |mask|\r
- mask.matches?(args[0])\r
- }\r
- when String\r
- self.find { |mask|\r
- mask.matches?(args[0].to_irc_netmask(:casemap => mask.casemap))\r
- }\r
- when Regexp\r
- self.find { |mask|\r
- mask.fullform =~ args[0]\r
- }\r
- else\r
- super(*args)\r
- end\r
- else\r
- super(*args)\r
- end\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
-\r
-class String\r
-\r
- # We keep extending String, this time adding a method that converts a\r
- # String into an Irc::Netmask object\r
- #\r
- def to_irc_netmask(opts={})\r
- Irc::Netmask.new(self, opts)\r
- end\r
-\r
-end\r
-\r
-\r
-module Irc\r
-\r
-\r
- # An IRC User is identified by his/her Netmask (which must not have globs).\r
- # In fact, User is just a subclass of Netmask.\r
- #\r
- # Ideally, the user and host information of an IRC User should never\r
- # change, and it shouldn't contain glob patterns. However, IRC is somewhat\r
- # idiosincratic and it may be possible to know the nick of a User much before\r
- # its user and host are known. Moreover, some networks (namely Freenode) may\r
- # change the hostname of a User when (s)he identifies with Nickserv.\r
- #\r
- # As a consequence, we must allow changes to a User host and user attributes.\r
- # We impose a restriction, though: they may not contain glob patterns, except\r
- # for the special case of an unknown user/host which is represented by a *.\r
- #\r
- # It is possible to create a totally unknown User (e.g. for initializations)\r
- # by setting the nick to * too.\r
- #\r
- # TODO list:\r
- # * see if it's worth to add the other USER data\r
- # * see if it's worth to add NICKSERV status\r
- #\r
- class User < Netmask\r
- alias :to_s :nick\r
-\r
- attr_accessor :real_name\r
-\r
- # Create a new IRC User from a given Netmask (or anything that can be converted\r
- # into a Netmask) provided that the given Netmask does not have globs.\r
- #\r
- def initialize(str="", opts={})\r
- super\r
- raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if nick.has_irc_glob? && nick != "*"\r
- raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*"\r
- raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*"\r
- @away = false\r
- @real_name = String.new\r
- end\r
-\r
- # The nick of a User may be changed freely, but it must not contain glob patterns.\r
- #\r
- def nick=(newnick)\r
- raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?\r
- super\r
- end\r
-\r
- # We have to allow changing the user of an Irc User due to some networks\r
- # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
- # user data has glob patterns though.\r
- #\r
- def user=(newuser)\r
- raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?\r
- super\r
- end\r
-\r
- # We have to allow changing the host of an Irc User due to some networks\r
- # (e.g. Freenode) changing hostmasks on the fly. We still check if the new\r
- # host data has glob patterns though.\r
- #\r
- def host=(newhost)\r
- raise "Can't change the hostname to #{newhost}" if defined?(@host) and newhost.has_irc_glob?\r
- super\r
- end\r
-\r
- # Checks if a User is well-known or not by looking at the hostname and user\r
- #\r
- def known?\r
- return nick != "*" && user != "*" && host != "*"\r
- end\r
-\r
- # Is the user away?\r
- #\r
- def away?\r
- return @away\r
- end\r
-\r
- # Set the away status of the user. Use away=(nil) or away=(false)\r
- # to unset away\r
- #\r
- def away=(msg="")\r
- if msg\r
- @away = msg\r
- else\r
- @away = false\r
- end\r
- end\r
-\r
- # Since to_irc_user runs the same checks on server and channel as\r
- # to_irc_netmask, we just try that and return self if it works.\r
- #\r
- # Subclasses of User will return self if possible.\r
- #\r
- def to_irc_user(opts={})\r
- return self if fits_with_server_and_casemap?(opts)\r
- return self.full_downcase.to_irc_user(opts)\r
- end\r
-\r
- # We can replace everything at once with data from another User\r
- #\r
- def replace(other)\r
- case other\r
- when User\r
- self.nick = other.nick\r
- self.user = other.user\r
- self.host = other.host\r
- @server = other.server\r
- @casemap = other.casemap unless @server\r
- @away = other.away?\r
- else\r
- self.replace(other.to_irc_user(server_and_casemap))\r
- end\r
- end\r
-\r
- def modes_on(channel)\r
- case channel\r
- when Channel\r
- channel.modes_of(self)\r
- else\r
- return @server.channel(channel).modes_of(self) if @server\r
- raise "Can't resolve channel #{channel}"\r
- end\r
- end\r
-\r
- def is_op?(channel)\r
- case channel\r
- when Channel\r
- channel.has_op?(self)\r
- else\r
- return @server.channel(channel).has_op?(self) if @server\r
- raise "Can't resolve channel #{channel}"\r
- end\r
- end\r
-\r
- def is_voice?(channel)\r
- case channel\r
- when Channel\r
- channel.has_voice?(self)\r
- else\r
- return @server.channel(channel).has_voice?(self) if @server\r
- raise "Can't resolve channel #{channel}"\r
- end\r
- end\r
- end\r
-\r
-\r
- # A UserList is an ArrayOf <code>User</code>s\r
- # We derive it from NetmaskList, which allows us to inherit any special\r
- # NetmaskList method\r
- #\r
- class UserList < NetmaskList\r
-\r
- # Create a new UserList, optionally filling it with the elements from\r
- # the Array argument fed to it.\r
- #\r
- def initialize(ar=[])\r
- super(ar)\r
- @element_class = User\r
- end\r
-\r
- # Convenience method: convert the UserList to a list of nicks. The indices\r
- # are preserved\r
- #\r
- def nicks\r
- self.map { |user| user.nick }\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
-class String\r
-\r
- # We keep extending String, this time adding a method that converts a\r
- # String into an Irc::User object\r
- #\r
- def to_irc_user(opts={})\r
- Irc::User.new(self, opts)\r
- end\r
-\r
-end\r
-\r
-module Irc\r
-\r
- # An IRC Channel is identified by its name, and it has a set of properties:\r
- # * a Channel::Topic\r
- # * a UserList\r
- # * a set of Channel::Modes\r
- #\r
- # The Channel::Topic and Channel::Mode classes are defined within the\r
- # Channel namespace because they only make sense there\r
- #\r
- class Channel\r
-\r
-\r
- # Mode on a Channel\r
- #\r
- class Mode\r
- attr_reader :channel\r
- def initialize(ch)\r
- @channel = ch\r
- end\r
-\r
- end\r
-\r
-\r
- # Channel modes of type A manipulate lists\r
- #\r
- # Example: b (banlist)\r
- #\r
- class ModeTypeA < Mode\r
- attr_reader :list\r
- def initialize(ch)\r
- super\r
- @list = NetmaskList.new\r
- end\r
-\r
- def set(val)\r
- nm = @channel.server.new_netmask(val)\r
- @list << nm unless @list.include?(nm)\r
- end\r
-\r
- def reset(val)\r
- nm = @channel.server.new_netmask(val)\r
- @list.delete(nm)\r
- end\r
-\r
- end\r
-\r
-\r
- # Channel modes of type B need an argument\r
- #\r
- # Example: k (key)\r
- #\r
- class ModeTypeB < Mode\r
- def initialize(ch)\r
- super\r
- @arg = nil\r
- end\r
-\r
- def status\r
- @arg\r
- end\r
- alias :value :status\r
-\r
- def set(val)\r
- @arg = val\r
- end\r
-\r
- def reset(val)\r
- @arg = nil if @arg == val\r
- end\r
-\r
- end\r
-\r
-\r
- # Channel modes that change the User prefixes are like\r
- # Channel modes of type B, except that they manipulate\r
- # lists of Users, so they are somewhat similar to channel\r
- # modes of type A\r
- #\r
- class UserMode < ModeTypeB\r
- attr_reader :list\r
- alias :users :list\r
- def initialize(ch)\r
- super\r
- @list = UserList.new\r
- end\r
-\r
- def set(val)\r
- u = @channel.server.user(val)\r
- @list << u unless @list.include?(u)\r
- end\r
-\r
- def reset(val)\r
- u = @channel.server.user(val)\r
- @list.delete(u)\r
- end\r
-\r
- end\r
-\r
-\r
- # Channel modes of type C need an argument when set,\r
- # but not when they get reset\r
- #\r
- # Example: l (limit)\r
- #\r
- class ModeTypeC < Mode\r
- def initialize(ch)\r
- super\r
- @arg = nil\r
- end\r
-\r
- def status\r
- @arg\r
- end\r
- alias :value :status\r
-\r
- def set(val)\r
- @arg = val\r
- end\r
-\r
- def reset\r
- @arg = nil\r
- end\r
-\r
- end\r
-\r
-\r
- # Channel modes of type D are basically booleans\r
- #\r
- # Example: m (moderate)\r
- #\r
- class ModeTypeD < Mode\r
- def initialize(ch)\r
- super\r
- @set = false\r
- end\r
-\r
- def set?\r
- return @set\r
- end\r
-\r
- def set\r
- @set = true\r
- end\r
-\r
- def reset\r
- @set = false\r
- end\r
-\r
- end\r
-\r
-\r
- # A Topic represents the topic of a channel. It consists of\r
- # the topic itself, who set it and when\r
- #\r
- class Topic\r
- attr_accessor :text, :set_by, :set_on\r
- alias :to_s :text\r
-\r
- # Create a new Topic setting the text, the creator and\r
- # the creation time\r
- #\r
- def initialize(text="", set_by="", set_on=Time.new)\r
- @text = text\r
- @set_by = set_by.to_irc_netmask\r
- @set_on = set_on\r
- end\r
-\r
- # Replace a Topic with another one\r
- #\r
- def replace(topic)\r
- raise TypeError, "#{topic.inspect} is not of class #{self.class}" unless topic.kind_of?(self.class)\r
- @text = topic.text.dup\r
- @set_by = topic.set_by.dup\r
- @set_on = topic.set_on.dup\r
- end\r
-\r
- # Returns self\r
- #\r
- def to_irc_channel_topic\r
- self\r
- end\r
-\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
-\r
-class String\r
-\r
- # Returns an Irc::Channel::Topic with self as text\r
- #\r
- def to_irc_channel_topic\r
- Irc::Channel::Topic.new(self)\r
- end\r
-\r
-end\r
-\r
-\r
-module Irc\r
-\r
-\r
- # Here we start with the actual Channel class\r
- #\r
- class Channel\r
-\r
- include ServerOrCasemap\r
- attr_reader :name, :topic, :mode, :users\r
- alias :to_s :name\r
-\r
- def inspect\r
- str = self.__to_s__[0..-2]\r
- str << " on server #{server}" if server\r
- str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
- str << " @users=[#{user_nicks.sort.join(', ')}]"\r
- str << ">"\r
- end\r
-\r
- # Returns self\r
- #\r
- def to_irc_channel\r
- self\r
- end\r
-\r
- # TODO Ho\r
- def user_nicks\r
- @users.map { |u| u.downcase }\r
- end\r
-\r
- # Checks if the receiver already has a user with the given _nick_\r
- #\r
- def has_user?(nick)\r
- @users.index(nick.to_irc_user(server_and_casemap))\r
- end\r
-\r
- # Returns the user with nick _nick_, if available\r
- #\r
- def get_user(nick)\r
- idx = has_user?(nick)\r
- @users[idx] if idx\r
- end\r
-\r
- # Adds a user to the channel\r
- #\r
- def add_user(user, opts={})\r
- silent = opts.fetch(:silent, false) \r
- if has_user?(user)\r
- warn "Trying to add user #{user} to channel #{self} again" unless silent\r
- else\r
- @users << user.to_irc_user(server_and_casemap)\r
- end\r
- end\r
-\r
- # Creates a new channel with the given name, optionally setting the topic\r
- # and an initial users list.\r
- #\r
- # No additional info is created here, because the channel flags and userlists\r
- # allowed depend on the server.\r
- #\r
- def initialize(name, topic=nil, users=[], opts={})\r
- raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
- warn "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
- raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
-\r
- init_server_or_casemap(opts)\r
-\r
- @name = name\r
-\r
- @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new\r
-\r
- @users = UserList.new\r
-\r
- users.each { |u|\r
- add_user(u)\r
- }\r
-\r
- # Flags\r
- @mode = {}\r
- end\r
-\r
- # Removes a user from the channel\r
- #\r
- def delete_user(user)\r
- @mode.each { |sym, mode|\r
- mode.reset(user) if mode.kind_of?(UserMode)\r
- }\r
- @users.delete(user)\r
- end\r
-\r
- # The channel prefix\r
- #\r
- def prefix\r
- name[0].chr\r
- end\r
-\r
- # A channel is local to a server if it has the '&' prefix\r
- #\r
- def local?\r
- name[0] == 0x26\r
- end\r
-\r
- # A channel is modeless if it has the '+' prefix\r
- #\r
- def modeless?\r
- name[0] == 0x2b\r
- end\r
-\r
- # A channel is safe if it has the '!' prefix\r
- #\r
- def safe?\r
- name[0] == 0x21\r
- end\r
-\r
- # A channel is normal if it has the '#' prefix\r
- #\r
- def normal?\r
- name[0] == 0x23\r
- end\r
-\r
- # Create a new mode\r
- #\r
- def create_mode(sym, kl)\r
- @mode[sym.to_sym] = kl.new(self)\r
- end\r
-\r
- def modes_of(user)\r
- l = []\r
- @mode.map { |s, m|\r
- l << s if (m.class <= UserMode and m.list[user])\r
- }\r
- l\r
- end\r
-\r
- def has_op?(user)\r
- @mode.has_key?(:o) and @mode[:o].list[user]\r
- end\r
-\r
- def has_voice?(user)\r
- @mode.has_key?(:v) and @mode[:v].list[user]\r
- end\r
- end\r
-\r
-\r
- # A ChannelList is an ArrayOf <code>Channel</code>s\r
- #\r
- class ChannelList < ArrayOf\r
-\r
- # Create a new ChannelList, optionally filling it with the elements from\r
- # the Array argument fed to it.\r
- #\r
- def initialize(ar=[])\r
- super(Channel, ar)\r
- end\r
-\r
- # Convenience method: convert the ChannelList to a list of channel names.\r
- # The indices are preserved\r
- #\r
- def names\r
- self.map { |chan| chan.name }\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
-\r
-class String\r
-\r
- # We keep extending String, this time adding a method that converts a\r
- # String into an Irc::Channel object\r
- #\r
- def to_irc_channel(opts={})\r
- Irc::Channel.new(self, opts)\r
- end\r
-\r
-end\r
-\r
-\r
-module Irc\r
-\r
-\r
- # An IRC Server represents the Server the client is connected to.\r
- #\r
- class Server\r
-\r
- attr_reader :hostname, :version, :usermodes, :chanmodes\r
- alias :to_s :hostname\r
- attr_reader :supports, :capabilities\r
-\r
- attr_reader :channels, :users\r
-\r
- # TODO Ho\r
- def channel_names\r
- @channels.map { |ch| ch.downcase }\r
- end\r
-\r
- # TODO Ho\r
- def user_nicks\r
- @users.map { |u| u.downcase }\r
- end\r
-\r
- def inspect\r
- chans, users = [@channels, @users].map {|d|\r
- d.sort { |a, b|\r
- a.downcase <=> b.downcase\r
- }.map { |x|\r
- x.inspect\r
- }\r
- }\r
-\r
- str = self.__to_s__[0..-2]\r
- str << " @hostname=#{hostname}"\r
- str << " @channels=#{chans}"\r
- str << " @users=#{users}"\r
- str << ">"\r
- end\r
-\r
- # Create a new Server, with all instance variables reset to nil (for\r
- # scalar variables), empty channel and user lists and @supports\r
- # initialized to the default values for all known supported features.\r
- #\r
- def initialize\r
- @hostname = @version = @usermodes = @chanmodes = nil\r
-\r
- @channels = ChannelList.new\r
-\r
- @users = UserList.new\r
-\r
- reset_capabilities\r
- end\r
-\r
- # Resets the server capabilities\r
- #\r
- def reset_capabilities\r
- @supports = {\r
- :casemapping => 'rfc1459'.to_irc_casemap,\r
- :chanlimit => {},\r
- :chanmodes => {\r
- :typea => nil, # Type A: address lists\r
- :typeb => nil, # Type B: needs a parameter\r
- :typec => nil, # Type C: needs a parameter when set\r
- :typed => nil # Type D: must not have a parameter\r
- },\r
- :channellen => 50,\r
- :chantypes => "#&!+",\r
- :excepts => nil,\r
- :idchan => {},\r
- :invex => nil,\r
- :kicklen => nil,\r
- :maxlist => {},\r
- :modes => 3,\r
- :network => nil,\r
- :nicklen => 9,\r
- :prefix => {\r
- :modes => [:o, :v],\r
- :prefixes => [:"@", :+]\r
- },\r
- :safelist => nil,\r
- :statusmsg => nil,\r
- :std => nil,\r
- :targmax => {},\r
- :topiclen => nil\r
- }\r
- @capabilities = {}\r
- end\r
-\r
- # Convert a mode (o, v, h, ...) to the corresponding\r
- # prefix (@, +, %, ...). See also mode_for_prefix\r
- def prefix_for_mode(mode)\r
- return @supports[:prefix][:prefixes][\r
- @supports[:prefix][:modes].index(mode.to_sym)\r
- ]\r
- end\r
-\r
- # Convert a prefix (@, +, %, ...) to the corresponding\r
- # mode (o, v, h, ...). See also prefix_for_mode\r
- def mode_for_prefix(pfx)\r
- return @supports[:prefix][:modes][\r
- @supports[:prefix][:prefixes].index(pfx.to_sym)\r
- ]\r
- end\r
-\r
- # Resets the Channel and User list\r
- #\r
- def reset_lists\r
- @users.reverse_each { |u|\r
- delete_user(u)\r
- }\r
- @channels.reverse_each { |u|\r
- delete_channel(u)\r
- }\r
- end\r
-\r
- # Clears the server\r
- #\r
- def clear\r
- reset_lists\r
- reset_capabilities\r
- @hostname = @version = @usermodes = @chanmodes = nil\r
- end\r
-\r
- # This method is used to parse a 004 RPL_MY_INFO line\r
- #\r
- def parse_my_info(line)\r
- ar = line.split(' ')\r
- @hostname = ar[0]\r
- @version = ar[1]\r
- @usermodes = ar[2]\r
- @chanmodes = ar[3]\r
- end\r
-\r
- def noval_warn(key, val, &block)\r
- if val\r
- yield if block_given?\r
- else\r
- warn "No #{key.to_s.upcase} value"\r
- end\r
- end\r
-\r
- def val_warn(key, val, &block)\r
- if val == true or val == false or val.nil?\r
- yield if block_given?\r
- else\r
- warn "No #{key.to_s.upcase} value must be specified, got #{val}"\r
- end\r
- end\r
- private :noval_warn, :val_warn\r
-\r
- # This method is used to parse a 005 RPL_ISUPPORT line\r
- #\r
- # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]\r
- #\r
- def parse_isupport(line)\r
- debug "Parsing ISUPPORT #{line.inspect}"\r
- ar = line.split(' ')\r
- reparse = ""\r
- ar.each { |en|\r
- prekey, val = en.split('=', 2)\r
- if prekey =~ /^-(.*)/\r
- key = $1.downcase.to_sym\r
- val = false\r
- else\r
- key = prekey.downcase.to_sym\r
- end\r
- case key\r
- when :casemapping\r
- noval_warn(key, val) {\r
- @supports[key] = val.to_irc_casemap\r
- }\r
- when :chanlimit, :idchan, :maxlist, :targmax\r
- noval_warn(key, val) {\r
- groups = val.split(',')\r
- groups.each { |g|\r
- k, v = g.split(':')\r
- @supports[key][k] = v.to_i || 0\r
- if @supports[key][k] == 0\r
- warn "Deleting #{key} limit of 0 for #{k}"\r
- @supports[key].delete(k)\r
- end\r
- }\r
- }\r
- when :chanmodes\r
- noval_warn(key, val) {\r
- groups = val.split(',')\r
- @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}\r
- @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}\r
- @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}\r
- @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}\r
- }\r
- when :channellen, :kicklen, :modes, :topiclen\r
- if val\r
- @supports[key] = val.to_i\r
- else\r
- @supports[key] = nil\r
- end\r
- when :chantypes\r
- @supports[key] = val # can also be nil\r
- when :excepts\r
- val ||= 'e'\r
- @supports[key] = val\r
- when :invex\r
- val ||= 'I'\r
- @supports[key] = val\r
- when :maxchannels\r
- noval_warn(key, val) {\r
- reparse += "CHANLIMIT=(chantypes):#{val} "\r
- }\r
- when :maxtargets\r
- noval_warn(key, val) {\r
- @supports[:targmax]['PRIVMSG'] = val.to_i\r
- @supports[:targmax]['NOTICE'] = val.to_i\r
- }\r
- when :network\r
- noval_warn(key, val) {\r
- @supports[key] = val\r
- }\r
- when :nicklen\r
- noval_warn(key, val) {\r
- @supports[key] = val.to_i\r
- }\r
- when :prefix\r
- if val\r
- val.scan(/\((.*)\)(.*)/) { |m, p|\r
- @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}\r
- @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}\r
- }\r
- else\r
- @supports[key][:modes] = nil\r
- @supports[key][:prefixes] = nil\r
- end\r
- when :safelist\r
- val_warn(key, val) {\r
- @supports[key] = val.nil? ? true : val\r
- }\r
- when :statusmsg\r
- noval_warn(key, val) {\r
- @supports[key] = val.scan(/./)\r
- }\r
- when :std\r
- noval_warn(key, val) {\r
- @supports[key] = val.split(',')\r
- }\r
- else\r
- @supports[key] = val.nil? ? true : val\r
- end\r
- }\r
- reparse.gsub!("(chantypes)",@supports[:chantypes])\r
- parse_isupport(reparse) unless reparse.empty?\r
- end\r
-\r
- # Returns the casemap of the server.\r
- #\r
- def casemap\r
- @supports[:casemapping]\r
- end\r
-\r
- # Returns User or Channel depending on what _name_ can be\r
- # a name of\r
- #\r
- def user_or_channel?(name)\r
- if supports[:chantypes].include?(name[0])\r
- return Channel\r
- else\r
- return User\r
- end\r
- end\r
-\r
- # Returns the actual User or Channel object matching _name_\r
- #\r
- def user_or_channel(name)\r
- if supports[:chantypes].include?(name[0])\r
- return channel(name)\r
- else\r
- return user(name)\r
- end\r
- end\r
-\r
- # Checks if the receiver already has a channel with the given _name_\r
- #\r
- def has_channel?(name)\r
- return false if name.nil_or_empty?\r
- channel_names.index(name.irc_downcase(casemap))\r
- end\r
- alias :has_chan? :has_channel?\r
-\r
- # Returns the channel with name _name_, if available\r
- #\r
- def get_channel(name)\r
- return nil if name.nil_or_empty?\r
- idx = has_channel?(name)\r
- channels[idx] if idx\r
- end\r
- alias :get_chan :get_channel\r
-\r
- # Create a new Channel object bound to the receiver and add it to the\r
- # list of <code>Channel</code>s on the receiver, unless the channel was\r
- # present already. In this case, the default action is to raise an\r
- # exception, unless _fails_ is set to false. An exception can also be\r
- # raised if _str_ is nil or empty, again only if _fails_ is set to true;\r
- # otherwise, the method just returns nil\r
- #\r
- def new_channel(name, topic=nil, users=[], fails=true)\r
- if name.nil_or_empty?\r
- raise "Tried to look for empty or nil channel name #{name.inspect}" if fails\r
- return nil\r
- end\r
- ex = get_chan(name)\r
- if ex\r
- raise "Channel #{name} already exists on server #{self}" if fails\r
- return ex\r
- else\r
-\r
- prefix = name[0].chr\r
-\r
- # Give a warning if the new Channel goes over some server limits.\r
- #\r
- # FIXME might need to raise an exception\r
- #\r
- warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)\r
- warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]\r
-\r
- # Next, we check if we hit the limit for channels of type +prefix+\r
- # if the server supports +chanlimit+\r
- #\r
- @supports[:chanlimit].keys.each { |k|\r
- next unless k.include?(prefix)\r
- count = 0\r
- channel_names.each { |n|\r
- count += 1 if k.include?(n[0])\r
- }\r
- # raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k]\r
- warn "Already joined #{count}/#{@supports[:chanlimit][k]} channels with prefix #{k}, we may be going over server limits" if count >= @supports[:chanlimit][k]\r
- }\r
-\r
- # So far, everything is fine. Now create the actual Channel\r
- #\r
- chan = Channel.new(name, topic, users, :server => self)\r
-\r
- # We wade through +prefix+ and +chanmodes+ to create appropriate\r
- # lists and flags for this channel\r
-\r
- @supports[:prefix][:modes].each { |mode|\r
- chan.create_mode(mode, Channel::UserMode)\r
- } if @supports[:prefix][:modes]\r
-\r
- @supports[:chanmodes].each { |k, val|\r
- if val\r
- case k\r
- when :typea\r
- val.each { |mode|\r
- chan.create_mode(mode, Channel::ModeTypeA)\r
- }\r
- when :typeb\r
- val.each { |mode|\r
- chan.create_mode(mode, Channel::ModeTypeB)\r
- }\r
- when :typec\r
- val.each { |mode|\r
- chan.create_mode(mode, Channel::ModeTypeC)\r
- }\r
- when :typed\r
- val.each { |mode|\r
- chan.create_mode(mode, Channel::ModeTypeD)\r
- }\r
- end\r
- end\r
- }\r
-\r
- @channels << chan\r
- # debug "Created channel #{chan.inspect}"\r
- return chan\r
- end\r
- end\r
-\r
- # Returns the Channel with the given _name_ on the server,\r
- # creating it if necessary. This is a short form for\r
- # new_channel(_str_, nil, [], +false+)\r
- #\r
- def channel(str)\r
- new_channel(str,nil,[],false)\r
- end\r
-\r
- # Remove Channel _name_ from the list of <code>Channel</code>s\r
- #\r
- def delete_channel(name)\r
- idx = has_channel?(name)\r
- raise "Tried to remove unmanaged channel #{name}" unless idx\r
- @channels.delete_at(idx)\r
- end\r
-\r
- # Checks if the receiver already has a user with the given _nick_\r
- #\r
- def has_user?(nick)\r
- return false if nick.nil_or_empty?\r
- user_nicks.index(nick.irc_downcase(casemap))\r
- end\r
-\r
- # Returns the user with nick _nick_, if available\r
- #\r
- def get_user(nick)\r
- idx = has_user?(nick)\r
- @users[idx] if idx\r
- end\r
-\r
- # Create a new User object bound to the receiver and add it to the list\r
- # of <code>User</code>s on the receiver, unless the User was present\r
- # already. In this case, the default action is to raise an exception,\r
- # unless _fails_ is set to false. An exception can also be raised\r
- # if _str_ is nil or empty, again only if _fails_ is set to true;\r
- # otherwise, the method just returns nil\r
- #\r
- def new_user(str, fails=true)\r
- if str.nil_or_empty?\r
- raise "Tried to look for empty or nil user name #{str.inspect}" if fails\r
- return nil\r
- end\r
- tmp = str.to_irc_user(:server => self)\r
- old = get_user(tmp.nick)\r
- # debug "Tmp: #{tmp.inspect}"\r
- # debug "Old: #{old.inspect}"\r
- if old\r
- # debug "User already existed as #{old.inspect}"\r
- if tmp.known?\r
- if old.known?\r
- # debug "Both were known"\r
- # Do not raise an error: things like Freenode change the hostname after identification\r
- warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp\r
- raise "User #{tmp} already exists on server #{self}" if fails\r
- end\r
- if old.fullform.downcase != tmp.fullform.downcase\r
- old.replace(tmp)\r
- # debug "Known user now #{old.inspect}"\r
- end\r
- end\r
- return old\r
- else\r
- warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]\r
- @users << tmp\r
- return @users.last\r
- end\r
- end\r
-\r
- # Returns the User with the given Netmask on the server,\r
- # creating it if necessary. This is a short form for\r
- # new_user(_str_, +false+)\r
- #\r
- def user(str)\r
- new_user(str, false)\r
- end\r
-\r
- # Deletes User _user_ from Channel _channel_\r
- #\r
- def delete_user_from_channel(user, channel)\r
- channel.delete_user(user)\r
- end\r
-\r
- # Remove User _someuser_ from the list of <code>User</code>s.\r
- # _someuser_ must be specified with the full Netmask.\r
- #\r
- def delete_user(someuser)\r
- idx = has_user?(someuser)\r
- raise "Tried to remove unmanaged user #{user}" unless idx\r
- have = self.user(someuser)\r
- @channels.each { |ch|\r
- delete_user_from_channel(have, ch)\r
- }\r
- @users.delete_at(idx)\r
- end\r
-\r
- # Create a new Netmask object with the appropriate casemap\r
- #\r
- def new_netmask(str)\r
- str.to_irc_netmask(:server => self)\r
- end\r
-\r
- # Finds all <code>User</code>s on server whose Netmask matches _mask_\r
- #\r
- def find_users(mask)\r
- nm = new_netmask(mask)\r
- @users.inject(UserList.new) {\r
- |list, user|\r
- if user.user == "*" or user.host == "*"\r
- list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp\r
- else\r
- list << user if user.matches?(nm)\r
- end\r
- list\r
- }\r
- end\r
-\r
- end\r
-\r
-end\r
-\r
+#-- vim:sw=2:et
+# General TODO list
+# * 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 we can remove him from the Server @users list
+# FIXME for the time being, we do it with a method that scans the server
+# (if defined), so the method is slow and should not be used frequently.
+# * 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
+#
+# Basic IRC stuff
+#
+# This module defines the fundamental building blocks for IRC
+#
+# Author:: Giuseppe Bilotta (giuseppe.bilotta@gmail.com)
+
+require 'singleton'
+
+# The following monkeypatch is to fix a bug in Singleton where marshaling would
+# fail when trying to restore a marshaled Singleton due to _load being declared
+# private.
+if RUBY_VERSION < '1.9'
+module ::Singleton
+ public :_dump
+end
+
+class << Singleton
+ module SingletonClassMethods
+ public :_load
+ end
+end
+end
+
+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
+
+ # We alias the to_s method to __to_s__ to make
+ # it accessible in all classes
+ alias :__to_s__ :to_s
+end
+
+# The Irc module is used to keep all IRC-related classes
+# in the same namespace
+#
+module Irc
+
+
+ # Due to its Scandinavian origins, IRC has strange case mappings, which
+ # consider the characters <tt>{}|^</tt> as the uppercase
+ # equivalents of # <tt>[]\~</tt>.
+ #
+ # This is however not the same on all IRC servers: some use standard ASCII
+ # casemapping, other do not consider <tt>^</tt> as the uppercase of
+ # <tt>~</tt>
+ #
+ 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.__to_s__[0..-2] + " #{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
+
+ # Give a warning if _arg_ and self are not the same Casemap
+ #
+ def must_be(arg)
+ other = arg.to_irc_casemap
+ if self == other
+ return true
+ else
+ warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
+ return false
+ end
+ end
+
+ end
+
+ # The rfc1459 casemap
+ #
+ class RfcCasemap < Casemap
+ include Singleton
+
+ def initialize
+ super('rfc1459', "\x41-\x5a\x7b-\x7e", "\x61-\x7a\x5b-\x5e")
+ end
+
+ end
+ RfcCasemap.instance
+
+ # The strict-rfc1459 Casemap
+ #
+ class StrictRfcCasemap < Casemap
+ include Singleton
+
+ def initialize
+ super('strict-rfc1459', "\x41-\x5a\x7b-\x7d", "\x61-\x7a\x5b-\x5d")
+ 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
+# with some IRC-specific methods
+#
+class String
+
+ # This method returns the Irc::Casemap whose name is the receiver
+ #
+ def to_irc_casemap
+ begin
+ Irc::Casemap.get(self)
+ rescue
+ # raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
+ error "Unkown Irc::Casemap #{self.inspect} requested, defaulting to rfc1459"
+ Irc::Casemap.get('rfc1459')
+ end
+ end
+
+ # This method returns a string which is the downcased version of the
+ # receiver, according to the given _casemap_
+ #
+ #
+ def irc_downcase(casemap='rfc1459')
+ 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
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_downcase!(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr!(cmap.upper, cmap.lower)
+ end
+
+ # Upcasing functions are provided too
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_upcase(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr(cmap.lower, cmap.upper)
+ end
+
+ # In-place upcasing
+ #
+ # See also the discussion about irc_downcase
+ #
+ def irc_upcase!(casemap='rfc1459')
+ cmap = casemap.to_irc_casemap
+ self.tr!(cmap.lower, cmap.upper)
+ end
+
+ # This method checks if the receiver contains IRC glob characters
+ #
+ # IRC has a very primitive concept of globs: a <tt>*</tt> stands for "any
+ # number of arbitrary characters", a <tt>?</tt> stands for "one and exactly
+ # one arbitrary character". These characters can be escaped by prefixing them
+ # with a slash (<tt>\\</tt>).
+ #
+ # A known limitation of this glob syntax is that there is no way to escape
+ # the escape character itself, so it's not possible to build a glob pattern
+ # where the escape character precedes a glob.
+ #
+ def has_irc_glob?
+ self =~ /^[*?]|[^\\][*?]/
+ end
+
+ # This method is used to convert the receiver into a Regular Expression
+ # that matches according to the IRC glob syntax
+ #
+ def to_irc_regexp
+ regmask = Regexp.escape(self)
+ regmask.gsub!(/(\\\\)?\\[*?]/) { |m|
+ case m
+ when /\\(\\[*?])/
+ $1
+ when /\\\*/
+ '.*'
+ when /\\\?/
+ '.'
+ else
+ raise "Unexpected match #{m} when converting #{self}"
+ end
+ }
+ Regexp.new("^#{regmask}$")
+ end
+
+end
+
+
+# ArrayOf is a subclass of Array whose elements are supposed to be all
+# of the same class. This is not intended to be used directly, but rather
+# to be subclassed as needed (see for example Irc::UserList and Irc::NetmaskList)
+#
+# Presently, only very few selected methods from Array are overloaded to check
+# if the new elements are the correct class. An orthodox? method is provided
+# to check the entire ArrayOf against the appropriate class.
+#
+class ArrayOf < Array
+
+ attr_reader :element_class
+
+ # Create a new ArrayOf whose elements are supposed to be all of type _kl_,
+ # 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.kind_of?(Class)
+ super()
+ @element_class = kl
+ case ar
+ when Array
+ insert(0, *ar)
+ else
+ raise TypeError, "#{self.class} can only be initialized from an Array"
+ end
+ end
+
+ def inspect
+ self.__to_s__[0..-2].sub(/:[^:]+$/,"[#{@element_class}]\\0") + " #{super}>"
+ end
+
+ # Private method to check the validity of the elements passed to it
+ # and optionally raise an error
+ #
+ # TODO should it accept nils as valid?
+ #
+ def internal_will_accept?(raising, *els)
+ els.each { |el|
+ unless el.kind_of?(@element_class)
+ raise TypeError, "#{el.inspect} is not of class #{@element_class}" if raising
+ return false
+ end
+ }
+ return true
+ end
+ private :internal_will_accept?
+
+ # This method checks if the passed arguments are acceptable for our ArrayOf
+ #
+ def will_accept?(*els)
+ internal_will_accept?(false, *els)
+ end
+
+ # This method checks that all elements are of the appropriate class
+ #
+ def valid?
+ will_accept?(*self)
+ end
+
+ # 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
+
+ # Overloaded from Array#<<, checks for appropriate class of argument
+ #
+ def <<(el)
+ 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)
+ els.each { |el|
+ super(el) if internal_will_accept?(true, *els)
+ }
+ end
+
+ # We introduce the 'downcase' method, which maps downcase() to all the Array
+ # elements, properly failing when the elements don't have a downcase method
+ #
+ def downcase
+ self.map { |el| el.downcase }
+ end
+
+ # Modifying methods which we don't handle yet are made private
+ #
+ private :[]=, :collect!, :map!, :fill, :flatten!
+
+end
+
+
+# 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 <tt>nick!user@host</tt>
+ #
+ # 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:
+ # * <tt>*!*@*</tt> refers to everybody
+ # * <tt>*!someuser@somehost</tt> refers to user +someuser+ on host +somehost+
+ # regardless of the nick used.
+ #
+ class Netmask
+
+ # Netmasks have an associated casemap unless they are bound to a server
+ #
+ include ServerOrCasemap
+
+ attr_reader :nick, :user, :host
+ alias :ident :user
+
+ # Create a new Netmask from string _str_, which must be in the form
+ # _nick_!_user_@_host_
+ #
+ # 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="", 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.to_str.inspect} does not represent a valid #{self.class}"
+ end
+ else
+ 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
+
+ alias :to_str :fullform
+
+ # 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
+
+ # This method returns a new Netmask which is the fully downcased version
+ # of the receiver
+ def downcased
+ return self.full_downcase.to_irc_netmask(server_and_casemap)
+ 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(server_and_casemap.merge(opts))
+ end
+
+ # Converts the receiver into a User with the given (optional)
+ # server/casemap association. We return self unless a conversion
+ # is needed (different casemap/server)
+ #
+ def to_irc_user(opts={})
+ self.fullform.to_irc_user(server_and_casemap.merge(opts))
+ end
+
+ # Inspection of a Netmask reveals the server it's bound to (if there is
+ # one), its casemap and the nick, user and host part
+ #
+ def inspect
+ str = self.__to_s__[0..-2]
+ 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)
+ return false unless other.kind_of?(self.class)
+ self.downcase == other.downcase
+ end
+
+ # 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
+ @nick = "*" if @nick.empty?
+ end
+
+ # This method changes the user of the Netmask, defaulting to the generic
+ # glob pattern if the result is the null string.
+ #
+ def user=(newuser)
+ @user = newuser.to_s
+ @user = "*" if @user.empty?
+ end
+ alias :ident= :user=
+
+ # This method changes the hostname of the Netmask, defaulting to the generic
+ # glob pattern if the result is the null string.
+ #
+ def host=(newhost)
+ @host = newhost.to_s
+ @host = "*" if @host.empty?
+ end
+
+ # We can replace everything at once with data from another Netmask
+ #
+ 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
+ # any of its components are defined by globs
+ #
+ def has_irc_glob?
+ return @nick.has_irc_glob? || @user.has_irc_glob? || @host.has_irc_glob?
+ end
+
+ def generalize
+ u = user.dup
+ unless u.has_irc_glob?
+ u.sub!(/^[in]=/, '=') or u.sub!(/^\W(\w+)/, '\1')
+ u = '*' + u
+ end
+
+ h = host.dup
+ unless h.has_irc_glob?
+ if h.include? '/'
+ h.sub!(/x-\w+$/, 'x-*')
+ else
+ h.match(/^[^\.]+\.[^\.]+$/) or
+ h.sub!(/azzurra[=-][0-9a-f]+/i, '*') or # hello, azzurra, you suck!
+ h.sub!(/^(\d+\.\d+\.\d+\.)\d+$/, '\1*') or
+ h.sub!(/^[^\.]+\./, '*.')
+ end
+ end
+ return Netmask.new("*!#{u}@#{h}", server_and_casemap)
+ end
+
+ # 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.
+ #
+ # 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
+ # describes the receiver, which can be done by matching it against the
+ # argument converted into an IRC Regexp (see String#to_irc_regexp).
+ #
+ # The situation is also easy when the receiver has globs and the argument
+ # doesn't, since in this case the result is false.
+ #
+ # The more complex case in which both the receiver and the argument have
+ # globs is not handled yet.
+ #
+ def matches?(arg)
+ 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).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
+ }
+ return true
+ end
+
+ # Case equality. Checks if arg matches self
+ #
+ def ===(arg)
+ 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
+
+
+ # A NetmaskList is an ArrayOf <code>Netmask</code>s
+ #
+ class NetmaskList < ArrayOf
+
+ # 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
+
+ # 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
+
+
+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
+ # * see if it's worth to add NICKSERV status
+ #
+ class User < Netmask
+ alias :to_s :nick
+
+ attr_accessor :real_name, :idle_since, :signon
+
+ # 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="", 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
+ @idle_since = nil
+ @signon = nil
+ end
+
+ # The nick of a User may be changed freely, but it must not contain glob patterns.
+ #
+ def nick=(newnick)
+ raise "Can't change the nick to #{newnick}" if defined?(@nick) and newnick.has_irc_glob?
+ super
+ end
+
+ # We 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)
+ raise "Can't change the username to #{newuser}" if defined?(@user) and newuser.has_irc_glob?
+ super
+ end
+
+ # 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)
+ 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 nick != "*" && user != "*" && host != "*"
+ end
+
+ # Is the user away?
+ #
+ def away?
+ return @away
+ end
+
+ # Set the away status of the user. Use away=(nil) or away=(false)
+ # to unset away
+ #
+ def away=(msg="")
+ if msg
+ @away = msg
+ else
+ @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
+
+ def channels
+ if @server
+ @server.channels.select { |ch| ch.has_user?(self) }
+ else
+ Array.new
+ end
+ end
+ end
+
+
+ # A UserList is an ArrayOf <code>User</code>s
+ # We derive it from NetmaskList, which allows us to inherit any special
+ # NetmaskList method
+ #
+ class UserList < NetmaskList
+
+ # Create a new UserList, optionally filling it with the elements from
+ # the Array argument fed to it.
+ #
+ def initialize(ar=[])
+ super(ar)
+ @element_class = User
+ end
+
+ # Convenience method: convert the UserList to a list of nicks. The indices
+ # are preserved
+ #
+ def nicks
+ self.map { |user| user.nick }
+ end
+
+ end
+
+end
+
+class String
+
+ # We keep extending String, this time adding a method that converts a
+ # String into an Irc::User object
+ #
+ def to_irc_user(opts={})
+ Irc::User.new(self, opts)
+ end
+
+end
+
+module Irc
+
+ # An IRC Channel is identified by its name, and it has a set of properties:
+ # * a Channel::Topic
+ # * a UserList
+ # * a set of Channel::Modes
+ #
+ # The Channel::Topic and Channel::Mode classes are defined within the
+ # Channel namespace because they only make sense there
+ #
+ class Channel
+
+
+ # Mode on a Channel
+ #
+ class Mode
+ attr_reader :channel
+ def initialize(ch)
+ @channel = ch
+ end
+
+ end
+
+ # Hash of modes. Subclass of Hash that defines any? and all?
+ # to check if boolean modes (Type D) are set
+ class ModeHash < Hash
+ def any?(*ar)
+ !!ar.find { |m| s = m.to_sym ; self[s] && self[s].set? }
+ end
+ def all?(*ar)
+ !ar.find { |m| s = m.to_sym ; !(self[s] && self[s].set?) }
+ 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
+
+ end
+
+
+ # 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
+
+
+ # 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
+
+
+ # 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
+
+
+ # 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
+
+
+ # 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
+
+end
+
+
+class String
+
+ # Returns an Irc::Channel::Topic with self as text
+ #
+ def to_irc_channel_topic
+ Irc::Channel::Topic.new(self)
+ end
+
+end
+
+
+module Irc
+
+
+ # Here we start with the actual Channel class
+ #
+ class Channel
+
+ # Return the non-prefixed part of a channel name.
+ # Also works with ## channels found on some networks
+ # (e.g. FreeNode)
+ def self.npname(str)
+ return str.to_s.sub(/^[&#+!]+/,'')
+ end
+
+ include ServerOrCasemap
+ attr_reader :name, :topic, :mode, :users
+ alias :to_s :name
+ attr_accessor :creation_time, :url
+
+ def inspect
+ str = self.__to_s__[0..-2]
+ str << " on server #{server}" if server
+ str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
+ str << " @users=[#{user_nicks.sort.join(', ')}]"
+ str << " (created on #{creation_time})" if creation_time
+ str << " (URL #{url})" if url
+ 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
+ # and an initial users list.
+ #
+ # No additional info is created here, because the channel flags and userlists
+ # allowed depend on the server.
+ #
+ def initialize(name, topic=nil, users=[], opts={})
+ raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?
+ warn "Unknown channel prefix #{name[0,1]}" if name !~ /^[&#+!]/
+ raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
+
+ init_server_or_casemap(opts)
+
+ @name = name
+
+ @topic = topic ? topic.to_irc_channel_topic : Channel::Topic.new
+
+ @users = UserList.new
+
+ users.each { |u|
+ add_user(u)
+ }
+
+ # Flags
+ @mode = ModeHash.new
+
+ # creation time, only on some networks
+ @creation_time = nil
+
+ # URL, only on some networks
+ @url = nil
+ end
+
+ # Removes a user from the channel
+ #
+ def delete_user(user)
+ @mode.each { |sym, mode|
+ mode.reset(user) if mode.kind_of?(UserMode)
+ }
+ @users.delete(user)
+ end
+
+ # The channel prefix
+ #
+ def prefix
+ name[0,1]
+ end
+
+ # A channel is local to a server if it has the '&' prefix
+ #
+ def local?
+ name[0,1] == '&'
+ end
+
+ # A channel is modeless if it has the '+' prefix
+ #
+ def modeless?
+ name[0,1] == '+'
+ end
+
+ # A channel is safe if it has the '!' prefix
+ #
+ def safe?
+ name[0,1] == '!'
+ end
+
+ # A channel is normal if it has the '#' prefix
+ #
+ def normal?
+ name[0,1] == '#'
+ end
+
+ # Create a new mode
+ #
+ 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
+
+
+ # A ChannelList is an ArrayOf <code>Channel</code>s
+ #
+ class ChannelList < ArrayOf
+
+ # 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.
+ #
+ class Server
+
+ attr_reader :hostname, :version, :usermodes, :chanmodes
+ alias :to_s :hostname
+ attr_reader :supports, :capabilities
+
+ attr_reader :channels, :users
+
+ # 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.__to_s__[0..-2]
+ 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
+
+ @users = UserList.new
+
+ reset_capabilities
+ end
+
+ # Resets the server capabilities
+ #
+ def reset_capabilities
+ @supports = {
+ :casemapping => 'rfc1459'.to_irc_casemap,
+ :chanlimit => {},
+ :chanmodes => {
+ :typea => nil, # Type A: address lists
+ :typeb => nil, # Type B: needs a parameter
+ :typec => nil, # Type C: needs a parameter when set
+ :typed => nil # Type D: must not have a parameter
+ },
+ :channellen => 50,
+ :chantypes => "#&!+",
+ :excepts => nil,
+ :idchan => {},
+ :invex => nil,
+ :kicklen => nil,
+ :maxlist => {},
+ :modes => 3,
+ :network => nil,
+ :nicklen => 9,
+ :prefix => {
+ :modes => [:o, :v],
+ :prefixes => [:"@", :+]
+ },
+ :safelist => nil,
+ :statusmsg => nil,
+ :std => nil,
+ :targmax => {},
+ :topiclen => nil
+ }
+ @capabilities = {}
+ end
+
+ # Convert a mode (o, v, h, ...) to the corresponding
+ # prefix (@, +, %, ...). See also mode_for_prefix
+ def prefix_for_mode(mode)
+ return @supports[:prefix][:prefixes][
+ @supports[:prefix][:modes].index(mode.to_sym)
+ ]
+ end
+
+ # Convert a prefix (@, +, %, ...) to the corresponding
+ # mode (o, v, h, ...). See also prefix_for_mode
+ def mode_for_prefix(pfx)
+ return @supports[:prefix][:modes][
+ @supports[:prefix][:prefixes].index(pfx.to_sym)
+ ]
+ end
+
+ # Resets the Channel and User list
+ #
+ def reset_lists
+ @users.reverse_each { |u|
+ delete_user(u)
+ }
+ @channels.reverse_each { |u|
+ delete_channel(u)
+ }
+ end
+
+ # Clears the server
+ #
+ def clear
+ reset_lists
+ reset_capabilities
+ @hostname = @version = @usermodes = @chanmodes = nil
+ end
+
+ # This method is used to parse a 004 RPL_MY_INFO line
+ #
+ def parse_my_info(line)
+ ar = line.split(' ')
+ @hostname = ar[0]
+ @version = ar[1]
+ @usermodes = ar[2]
+ @chanmodes = ar[3]
+ end
+
+ def noval_warn(key, val, &block)
+ if val
+ yield if block_given?
+ else
+ warn "No #{key.to_s.upcase} value"
+ end
+ end
+
+ def val_warn(key, val, &block)
+ if val == true or val == false or val.nil?
+ yield if block_given?
+ else
+ warn "No #{key.to_s.upcase} value must be specified, got #{val}"
+ end
+ end
+ private :noval_warn, :val_warn
+
+ # This method is used to parse a 005 RPL_ISUPPORT line
+ #
+ # See the RPL_ISUPPORT draft[http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt]
+ #
+ def parse_isupport(line)
+ debug "Parsing ISUPPORT #{line.inspect}"
+ ar = line.split(' ')
+ reparse = []
+ ar.each { |en|
+ prekey, val = en.split('=', 2)
+ if prekey =~ /^-(.*)/
+ key = $1.downcase.to_sym
+ val = false
+ else
+ key = prekey.downcase.to_sym
+ end
+ case key
+ when :casemapping
+ noval_warn(key, val) {
+ if val == 'charset'
+ reparse << "CASEMAPPING=(charset)"
+ else
+ # TODO some servers offer non-standard CASEMAPPINGs in the form
+ # locale.charset[-options], which indicate an extended set of
+ # allowed characters (mostly for nicks). This might be supported
+ # with hooks for the unicode core module
+ @supports[key] = val.to_irc_casemap
+ end
+ }
+ 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 || 0
+ if @supports[key][k] == 0
+ warn "Deleting #{key} limit of 0 for #{k}"
+ @supports[key].delete(k)
+ end
+ }
+ }
+ when :chanmodes
+ noval_warn(key, val) {
+ groups = val.split(',')
+ @supports[key][:typea] = groups[0].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typeb] = groups[1].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typec] = groups[2].scan(/./).map { |x| x.to_sym}
+ @supports[key][:typed] = groups[3].scan(/./).map { |x| x.to_sym}
+ }
+ when :channellen, :kicklen, :modes, :topiclen
+ if val
+ @supports[key] = val.to_i
+ else
+ @supports[key] = nil
+ end
+ when :chantypes
+ @supports[key] = val # can also be nil
+ when :excepts
+ val ||= 'e'
+ @supports[key] = val
+ 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
+ }
+ when :prefix
+ if val
+ val.scan(/\((.*)\)(.*)/) { |m, p|
+ @supports[key][:modes] = m.scan(/./).map { |x| x.to_sym}
+ @supports[key][:prefixes] = p.scan(/./).map { |x| x.to_sym}
+ }
+ else
+ @supports[key][:modes] = nil
+ @supports[key][:prefixes] = nil
+ end
+ when :safelist
+ val_warn(key, val) {
+ @supports[key] = val.nil? ? true : val
+ }
+ when :statusmsg
+ noval_warn(key, val) {
+ @supports[key] = val.scan(/./)
+ }
+ when :std
+ noval_warn(key, val) {
+ @supports[key] = val.split(',')
+ }
+ else
+ @supports[key] = val.nil? ? true : val
+ end
+ }
+ unless reparse.empty?
+ reparse_str = reparse.join(" ")
+ reparse_str.gsub!("(chantypes)",@supports[:chantypes])
+ reparse_str.gsub!("(charset)",@supports[:charset] || 'rfc1459')
+ parse_isupport(reparse_str)
+ end
+ end
+
+ # Returns the casemap of the server.
+ #
+ def casemap
+ @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])
+ return Channel
+ else
+ return User
+ end
+ end
+
+ # Returns the actual User or Channel object matching _name_
+ #
+ def user_or_channel(name)
+ if supports[:chantypes].include?(name[0])
+ return channel(name)
+ else
+ return user(name)
+ end
+ end
+
+ # Checks if the receiver already has a channel with the given _name_
+ #
+ def has_channel?(name)
+ 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)
+ 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 bound to the receiver and add it to the
+ # list of <code>Channel</code>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
+ return ex
+ else
+
+ prefix = name[0,1]
+
+ # Give a warning if the new Channel goes over some server limits.
+ #
+ # FIXME might need to raise an exception
+ #
+ warn "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
+ warn "#{self} doesn't support channel names this long (#{name.length} > #{@supports[:channellen]})" unless name.length <= @supports[:channellen]
+
+ # Next, we check if we hit the limit for channels of type +prefix+
+ # if the server supports +chanlimit+
+ #
+ @supports[:chanlimit].keys.each { |k|
+ next unless k.include?(prefix)
+ count = 0
+ 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]
+ 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(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, Channel::UserMode)
+ } if @supports[:prefix][:modes]
+
+ @supports[:chanmodes].each { |k, val|
+ if val
+ case k
+ when :typea
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeA)
+ }
+ when :typeb
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeB)
+ }
+ when :typec
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeC)
+ }
+ when :typed
+ val.each { |mode|
+ chan.create_mode(mode, Channel::ModeTypeD)
+ }
+ end
+ end
+ }
+
+ @channels << chan
+ # debug "Created channel #{chan.inspect}"
+ return chan
+ end
+ end
+
+ # Returns the Channel with the given _name_ on the server,
+ # creating it if necessary. This is a short form for
+ # new_channel(_str_, nil, [], +false+)
+ #
+ def channel(str)
+ new_channel(str,nil,[],false)
+ end
+
+ # Remove Channel _name_ from the list of <code>Channel</code>s
+ #
+ def delete_channel(name)
+ idx = has_channel?(name)
+ raise "Tried to remove unmanaged channel #{name}" unless idx
+ @channels.delete_at(idx)
+ end
+
+ # Checks if the receiver already has a user with the given _nick_
+ #
+ def has_user?(nick)
+ 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 = has_user?(nick)
+ @users[idx] if idx
+ end
+
+ # Create a new User object bound to the receiver and add it to the list
+ # of <code>User</code>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)
+ if str.nil_or_empty?
+ raise "Tried to look for empty or nil user name #{str.inspect}" if fails
+ return nil
+ end
+ tmp = str.to_irc_user(:server => self)
+ old = get_user(tmp.nick)
+ # debug "Tmp: #{tmp.inspect}"
+ # debug "Old: #{old.inspect}"
+ if old
+ # debug "User already existed as #{old.inspect}"
+ if tmp.known?
+ if old.known?
+ # debug "Both were known"
+ # Do not raise an error: things like Freenode change the hostname after identification
+ warning "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old.inspect} but access was tried with #{tmp.inspect}" if old != tmp
+ raise "User #{tmp} already exists on server #{self}" if fails
+ end
+ if old.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
+ return @users.last
+ end
+ end
+
+ # Returns the User with the given Netmask on the server,
+ # creating it if necessary. This is a short form for
+ # new_user(_str_, +false+)
+ #
+ def user(str)
+ 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 <code>User</code>s.
+ # _someuser_ must be specified with the full Netmask.
+ #
+ def delete_user(someuser)
+ idx = has_user?(someuser)
+ raise "Tried to remove unmanaged user #{user}" unless idx
+ have = self.user(someuser)
+ @channels.each { |ch|
+ delete_user_from_channel(have, ch)
+ }
+ @users.delete_at(idx)
+ end
+
+ # Create a new Netmask object with the appropriate casemap
+ #
+ def new_netmask(str)
+ str.to_irc_netmask(:server => self)
+ end
+
+ # Finds all <code>User</code>s on server whose Netmask matches _mask_
+ #
+ def find_users(mask)
+ nm = new_netmask(mask)
+ @users.inject(UserList.new) {
+ |list, user|
+ if user.user == "*" or user.host == "*"
+ list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp
+ else
+ list << user if user.matches?(nm)
+ end
+ list
+ }
+ end
+
+ end
+
+end
+