--- /dev/null
+#-- vim:sw=2:et\r
+# General TODO list\r
+# * when Users are deleted, we have to delete them from the appropriate\r
+# channel lists too\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
+#++\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
+\r
+# We start by extending the String class\r
+# with some IRC-specific methods\r
+#\r
+class String\r
+\r
+ # This method returns a string which is the downcased version of the\r
+ # receiver, according to IRC rules: due to the Scandinavian origin of IRC,\r
+ # the characters <tt>{}|^</tt> are considered the uppercase equivalent of\r
+ # <tt>[]\~</tt>.\r
+ #\r
+ # Since IRC is mostly case-insensitive (the Windows way: case is preserved,\r
+ # but it's actually ignored to check equality), this method is rather\r
+ # important when checking if two strings refer to the same entity\r
+ # (User/Channel)\r
+ #\r
+ # Modern server allow different casemaps, too, in which some or all\r
+ # of the extra characters are not converted\r
+ #\r
+ def irc_downcase(casemap='rfc1459')\r
+ case casemap\r
+ when 'rfc1459'\r
+ self.tr("\x41-\x5e", "\x61-\x7e")\r
+ when 'strict-rfc1459'\r
+ self.tr("\x41-\x5d", "\x61-\x7d")\r
+ when 'ascii'\r
+ self.tr("\x41-\x5a", "\x61-\x7a")\r
+ else\r
+ raise TypeError, "Unknown casemap #{casemap}"\r
+ end\r
+ 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
+ case casemap\r
+ when 'rfc1459'\r
+ self.tr!("\x41-\x5e", "\x61-\x7e")\r
+ when 'strict-rfc1459'\r
+ self.tr!("\x41-\x5d", "\x61-\x7d")\r
+ when 'ascii'\r
+ self.tr!("\x41-\x5a", "\x61-\x7a")\r
+ else\r
+ raise TypeError, "Unknown casemap #{casemap}"\r
+ end\r
+ 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
+ case casemap\r
+ when 'rfc1459'\r
+ self.tr("\x61-\x7e", "\x41-\x5e")\r
+ when 'strict-rfc1459'\r
+ self.tr("\x61-\x7d", "\x41-\x5d")\r
+ when 'ascii'\r
+ self.tr("\x61-\x7a", "\x41-\x5a")\r
+ else\r
+ raise TypeError, "Unknown casemap #{casemap}"\r
+ end\r
+ end\r
+\r
+ # In-place upcasing\r
+ #\r
+ # See also the discussion about irc_downcase\r
+ #\r
+ def irc_upcase!(casemap='rfc1459')\r
+ case casemap\r
+ when 'rfc1459'\r
+ self.tr!("\x61-\x7e", "\x41-\x5e")\r
+ when 'strict-rfc1459'\r
+ self.tr!("\x61-\x7d", "\x41-\x5d")\r
+ when 'ascii'\r
+ self.tr!("\x61-\x7a", "\x41-\x5a")\r
+ else\r
+ raise TypeError, "Unknown casemap #{casemap}"\r
+ end\r
+ 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
+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.class <= Class\r
+ super()\r
+ @element_class = kl\r
+ case ar\r
+ when Array\r
+ send(:+, ar)\r
+ else\r
+ raise TypeError, "#{self.class} can only be initialized from an Array"\r
+ end\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.class <= @element_class\r
+ raise TypeError 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
+ 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#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
+ # Overloaded from Array#+, checks for appropriate class of argument elements\r
+ #\r
+ def +(ar)\r
+ super(ar) if internal_will_accept?(true, *ar)\r
+ end\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
+ # A Netmask identifies each user by collecting its nick, username and\r
+ # hostname in the form <tt>nick!user@host</tt>\r
+ #\r
+ # Netmasks can also contain glob patterns in any of their components; in this\r
+ # form they are used to refer to more than a user or to a user appearing\r
+ # under different\r
+ # forms.\r
+ #\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
+ attr_reader :nick, :user, :host\r
+ attr_reader :casemap\r
+\r
+ # call-seq:\r
+ # Netmask.new(netmask) => new_netmask\r
+ # Netmask.new(hash={}, casemap=nil) => new_netmask\r
+ # Netmask.new("nick!user@host", casemap=nil) => new_netmask\r
+ #\r
+ # Create a new Netmask in any of these forms\r
+ # 1. from another Netmask (does a .dup)\r
+ # 2. from a Hash with any of the keys <tt>:nick</tt>, <tt>:user</tt> and\r
+ # <tt>:host</tt>\r
+ # 3. from a String in the form <tt>nick!user@host</tt>\r
+ #\r
+ # In all but the first forms a casemap may be speficied, the default\r
+ # being 'rfc1459'.\r
+ #\r
+ # The nick is downcased following IRC rules and according to the given casemap.\r
+ #\r
+ # FIXME check if user and host need to be downcased too.\r
+ #\r
+ # Empty +nick+, +user+ or +host+ are converted to the generic glob pattern\r
+ #\r
+ def initialize(str={}, casemap=nil)\r
+ case str\r
+ when Netmask\r
+ raise ArgumentError, "Can't set casemap when initializing from other Netmask" if casemap\r
+ @casemap = str.casemap.dup\r
+ @nick = str.nick.dup\r
+ @user = str.user.dup\r
+ @host = str.host.dup\r
+ when Hash\r
+ @casemap = casemap || str[:casemap] || 'rfc1459'\r
+ @nick = str[:nick].to_s.irc_downcase(@casemap)\r
+ @user = str[:user].to_s\r
+ @host = str[:host].to_s\r
+ when String\r
+ if str.match(/(\S+)(?:!(\S+)@(?:(\S+))?)?/)\r
+ @casemap = casemap || 'rfc1459'\r
+ @nick = $1.irc_downcase(@casemap)\r
+ @user = $2\r
+ @host = $3\r
+ else\r
+ raise ArgumentError, "#{str} is not a valid netmask"\r
+ end\r
+ else\r
+ raise ArgumentError, "#{str} is not a valid netmask"\r
+ end\r
+\r
+ @nick = "*" if @nick.to_s.empty?\r
+ @user = "*" if @user.to_s.empty?\r
+ @host = "*" if @host.to_s.empty?\r
+ end\r
+\r
+ # This method changes the nick of the Netmask, downcasing the argument\r
+ # following IRC rules and defaulting to the generic glob pattern if\r
+ # the result is the null string.\r
+ #\r
+ def nick=(newnick)\r
+ @nick = newnick.to_s.irc_downcase(@casemap)\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
+\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
+ # 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
+ # A Netmask is easily converted to a String for the usual representation\r
+ # \r
+ def to_s\r
+ return "#{nick}@#{user}!#{host}"\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 that\r
+ # any netmask described by the receiver is also described by the 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 = Netmask(arg)\r
+ raise TypeError, "#{arg} and #{self} have different casemaps" if @casemap != cmp.casemap\r
+ raise TypeError, "#{arg} is not a valid Netmask" unless cmp.class <= Netmask\r
+ [:nick, :user, :host].each { |component|\r
+ us = self.send(:component)\r
+ them = cmp.send(:component)\r
+ raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?\r
+ 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
+ Netmask(arg).matches?(self)\r
+ end\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
+ def initialize(ar=[])\r
+ super(Netmask, ar)\r
+ end\r
+ end\r
+\r
+\r
+ # An IRC User is identified by his/her Netmask (which must not have\r
+ # globs). In fact, User is just a subclass of Netmask. However,\r
+ # a User will not allow one's host or user data to be changed: only the\r
+ # nick can be dynamic\r
+ #\r
+ # TODO list:\r
+ # * see if it's worth to add the other USER data\r
+ # * see if it's worth to add AWAY status\r
+ # * see if it's worth to add NICKSERV status\r
+ #\r
+ class User < Netmask\r
+ private :host=, :user=\r
+\r
+ # Create a new IRC User from a given Netmask (or anything that can be converted\r
+ # into a Netmask) provided that the given Netmask does not have globs.\r
+ #\r
+ def initialize(str, casemap=nil)\r
+ super\r
+ raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if has_irc_glob?\r
+ end\r
+ end\r
+\r
+\r
+ # A UserList is an ArrayOf <code>User</code>s\r
+ #\r
+ class UserList < ArrayOf\r
+\r
+ # Create a new UserList, optionally filling it with the elements from\r
+ # the Array argument fed to it.\r
+ def initialize(ar=[])\r
+ super(User, ar)\r
+ end\r
+ end\r
+\r
+\r
+ # An IRC Channel is identified by its name, and it has a set of properties:\r
+ # * a topic\r
+ # * a UserList\r
+ # * a set of modes\r
+ #\r
+ class Channel\r
+ attr_reader :name, :type, :casemap\r
+\r
+ # Create a new method. Auxiliary function for the following\r
+ # auxiliary functions ...\r
+ #\r
+ def create_method(name, &block)\r
+ self.class.send(:define_method, name, &block)\r
+ end\r
+ private :create_method\r
+\r
+ # Create a new channel boolean flag\r
+ #\r
+ def new_bool_flag(sym, acc=nil, default=false)\r
+ @flags[sym.to_sym] = default\r
+ racc = (acc||sym).to_s << "?"\r
+ wacc = (acc||sym).to_s << "="\r
+ create_method(racc.to_sym) { @flags[sym.to_sym] }\r
+ create_method(wacc.to_sym) { |val|\r
+ @flags[sym.to_sym] = val\r
+ }\r
+ end\r
+\r
+ # Create a new channel flag with data\r
+ #\r
+ def new_data_flag(sym, acc=nil, default=false)\r
+ @flags[sym.to_sym] = default\r
+ racc = (acc||sym).to_s\r
+ wacc = (acc||sym).to_s << "="\r
+ create_method(racc.to_sym) { @flags[sym.to_sym] }\r
+ create_method(wacc.to_sym) { |val|\r
+ @flags[sym.to_sym] = val\r
+ }\r
+ end\r
+\r
+ # Create a new variable with accessors\r
+ #\r
+ def new_variable(name, default=nil)\r
+ v = "@#{name}".to_sym\r
+ instance_variable_set(v, default)\r
+ create_method(name.to_sym) { instance_variable_get(v) }\r
+ create_method("#{name}=".to_sym) { |val|\r
+ instance_variable_set(v, val)\r
+ }\r
+ end\r
+\r
+ # Create a new UserList\r
+ #\r
+ def new_userlist(name, default=UserList.new)\r
+ new_variable(name, default)\r
+ end\r
+\r
+ # Create a new NetmaskList\r
+ #\r
+ def new_netmasklist(name, default=NetmaskList.new)\r
+ new_variable(name, default)\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
+ # FIXME doesn't check if users have the same casemap as the channel yet\r
+ #\r
+ def initialize(name, topic="", users=[], casemap=nil)\r
+ @casemap = casemap || 'rfc1459'\r
+\r
+ raise ArgumentError, "Channel name cannot be empty" if name.to_s.empty?\r
+ raise ArgumentError, "Unknown channel prefix #{name[0].chr}" if name !~ /^[&#+!]/\r
+ raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/\r
+\r
+ @name = name.irc_downcase(@casemap)\r
+\r
+ new_variable(:topic, topic)\r
+\r
+ new_userlist(:users)\r
+ case users\r
+ when UserList\r
+ @users = users.dup\r
+ when Array\r
+ @users = UserList.new(users)\r
+ else\r
+ raise ArgumentError, "Invalid user list #{users.inspect}"\r
+ end\r
+\r
+ # new_variable(:creator)\r
+\r
+ # # Special users\r
+ # new_userlist(:super_ops)\r
+ # new_userlist(:ops)\r
+ # new_userlist(:half_ops)\r
+ # new_userlist(:voices)\r
+\r
+ # # Ban and invite lists\r
+ # new_netmasklist(:banlist)\r
+ # new_netmasklist(:exceptlist)\r
+ # new_netmasklist(:invitelist)\r
+\r
+ # # Flags\r
+ @flags = {}\r
+ # new_bool_flag(:a, :anonymous)\r
+ # new_bool_flag(:i, :invite_only)\r
+ # new_bool_flag(:m, :moderated)\r
+ # new_bool_flag(:n, :no_externals)\r
+ # new_bool_flag(:q, :quiet)\r
+ # new_bool_flag(:p, :private)\r
+ # new_bool_flag(:s, :secret)\r
+ # new_bool_flag(:r, :will_reop)\r
+ # new_bool_flag(:t, :free_topic)\r
+\r
+ # new_data_flag(:k, :key)\r
+ # new_data_flag(:l, :limit)\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 safe if it has the '#' prefix\r
+ #\r
+ def normal?\r
+ name[0] = 0x23\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
+ def initialize(ar=[])\r
+ super(Channel, ar)\r
+ end\r
+ end\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
+ attr_reader :supports, :capab\r
+\r
+ attr_reader :channels, :users\r
+\r
+ # Create a new Server, with all instance variables reset\r
+ # to nil (for scalar variables), the channel and user lists\r
+ # are empty, and @supports is initialized to the default values\r
+ # for all known supported features.\r
+ #\r
+ def initialize\r
+ @hostname = @version = @usermodes = @chanmodes = nil\r
+ @supports = {\r
+ :casemapping => 'rfc1459',\r
+ :chanlimit => {},\r
+ :chanmodes => {\r
+ :addr_list => nil, # Type A\r
+ :has_param => nil, # Type B\r
+ :set_param => nil, # Type C\r
+ :no_params => nil # Type D\r
+ },\r
+ :channellen => 200,\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 => 'ov'.scan(/./),\r
+ :prefixes => '@+'.scan(/./)\r
+ },\r
+ :safelist => nil,\r
+ :statusmsg => nil,\r
+ :std => nil,\r
+ :targmax => {},\r
+ :topiclen => nil\r
+ }\r
+ @capab = {}\r
+\r
+ @channels = ChannelList.new\r
+ @channel_names = Array.new\r
+\r
+ @users = UserList.new\r
+ @user_nicks = Array.new\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
+ # TODO this is just an initial draft that does nothing special.\r
+ # We want to properly parse most of the supported capabilities\r
+ # for later reuse.\r
+ #\r
+ def parse_isupport(line)\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, :network\r
+ noval_warn(key, val) {\r
+ @supports[key] = val\r
+ }\r
+ when :chanlimit, :idchan, :maxlist, :targmax\r
+ noval_warn(key, val) {\r
+ groups = val.split(',')\r
+ groups.each { |g|\r
+ k, v = g.split(':')\r
+ @supports[key][k] = v.to_i\r
+ }\r
+ }\r
+ when :maxchannels\r
+ noval_warn(key, val) {\r
+ reparse += "CHANLIMIT=(chantypes):#{val} "\r
+ }\r
+ when :maxtargets\r
+ noval_warn(key, val) {\r
+ @supports[key]['PRIVMSG'] = val.to_i\r
+ @supports[key]['NOTICE'] = val.to_i\r
+ }\r
+ when :chanmodes\r
+ noval_warn(key, val) {\r
+ groups = val.split(',')\r
+ @supports[key][:addr_list] = groups[0].scan(/./)\r
+ @supports[key][:has_param] = groups[1].scan(/./)\r
+ @supports[key][:set_param] = groups[2].scan(/./)\r
+ @supports[key][:no_params] = groups[3].scan(/./)\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 :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(/./)\r
+ @supports[key][:prefixes] = p.scan(/./)\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] || 'rfc1459'\r
+ end\r
+\r
+ # Checks if the receiver already has a channel with the given _name_\r
+ #\r
+ def has_channel?(name)\r
+ @channel_names.index(name)\r
+ end\r
+ alias :has_chan? :has_channel?\r
+\r
+ # Returns the channel with name _name_, if available\r
+ #\r
+ def get_channel(name)\r
+ idx = @channel_names.index(name)\r
+ @channels[idx] if idx\r
+ end\r
+ alias :get_chan :get_channel\r
+\r
+ # Create a new Channel object and add it to the list of\r
+ # <code>Channel</code>s on the receiver, unless the channel\r
+ # was present already. In this case, the default action is\r
+ # to raise an exception, unless _fails_ is set to false\r
+ #\r
+ # The Channel is automatically created with the appropriate casemap\r
+ #\r
+ def new_channel(name, topic="", users=[], fails=true)\r
+ if !has_chan?(name)\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].includes?(prefix)\r
+ warn "#{self} doesn't support channel names this long (#{name.length} > #{@support[: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.includes?(prefix)\r
+ count = 0\r
+ @channel_names.each { |n|\r
+ count += 1 if k.includes?(n[0].chr)\r
+ }\r
+ raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimits][k]\r
+ }\r
+\r
+ # So far, everything is fine. Now create the actual Channel\r
+ #\r
+ chan = Channel.new(name, topic, users, self.casemap)\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.new_userlist(mode)\r
+ } if @supports[:prefix][:modes]\r
+\r
+ @supports[:chanmodes].each { |k, val|\r
+ if val\r
+ case k\r
+ when :addr_list\r
+ val.each { |mode|\r
+ chan.new_netmasklist(mode)\r
+ }\r
+ when :has_param, :set_param\r
+ val.each { |mode|\r
+ chan.new_data_flag(mode)\r
+ }\r
+ when :no_params\r
+ val.each { |mode|\r
+ chan.new_bool_flag(mode)\r
+ }\r
+ end\r
+ end\r
+ }\r
+\r
+ # * appropriate @flags\r
+ # * a UserList for each @supports[:prefix]\r
+ # * a NetmaskList for each @supports[:chanmodes] of type A\r
+\r
+ @channels << newchan\r
+ @channel_names << name\r
+ return newchan\r
+ end\r
+\r
+ raise "Channel #{name} already exists on server #{self}" if fails\r
+ return get_channel(name)\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
+ @channel_names.delete_at(idx)\r
+ @channels.delete_at(idx)\r
+ end\r
+\r
+ # Checks if the receiver already has a user with the given _nick_\r
+ #\r
+ def has_user?(nick)\r
+ @user_nicks.index(nick)\r
+ end\r
+\r
+ # Returns the user with nick _nick_, if available\r
+ #\r
+ def get_user(nick)\r
+ idx = @user_nicks.index(name)\r
+ @users[idx] if idx\r
+ end\r
+\r
+ # Create a new User object and add it to the list of\r
+ # <code>User</code>s on the receiver, unless the User\r
+ # was present already. In this case, the default action is\r
+ # to raise an exception, unless _fails_ is set to false\r
+ #\r
+ # The User is automatically created with the appropriate casemap\r
+ #\r
+ def new_user(str, fails=true)\r
+ tmp = User.new(str, self.casemap)\r
+ if !has_user?(tmp.nick)\r
+ warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@support[:nicklen]}" unless tmp.nick.length <= @supports[:nicklen]\r
+ @users << tmp\r
+ @user_nicks << tmp.nick\r
+ return @users.last\r
+ end\r
+ old = get_user(tmp.nick)\r
+ raise "User #{tmp.nick} has inconsistent Netmasks! #{self} knows #{old} but access was tried with #{tmp}" if old != tmp\r
+ raise "User #{tmp} already exists on server #{self}" if fails\r
+ return get_user(tmp)\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
+ # 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?(user.nick)\r
+ raise "Tried to remove unmanaged user #{user}" unless idx\r
+ have = self.user(user)\r
+ raise "User #{someuser.nick} has inconsistent Netmasks! #{self} knows #{have} but access was tried with #{someuser}" if have != someuser\r
+ @user_nicks.delete_at(idx)\r
+ @users.delete_at(idx)\r
+ end\r
+\r
+ # Create a new Netmask object with the appropriate casemap\r
+ #\r
+ def new_netmask(str)\r
+ if str.class <= Netmask \r
+ raise "Wrong casemap for Netmask #{str.inspect}" if str.casemap != self.casemap\r
+ return str\r
+ end\r
+ Netmask.new(str, self.casemap)\r
+ 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
+ list << user if user.matches?(nm)\r
+ list\r
+ }\r
+ end\r
+ end\r
+end\r
+\r
+# TODO test cases\r
+\r
+if __FILE__ == $0\r
+\r
+include Irc\r
+\r
+ # puts " -- irc_regexp tests"\r
+ # ["*", "a?b", "a*b", "a\\*b", "a\\?b", "a?\\*b", "*a*\\**b?"].each { |s|\r
+ # puts " --"\r
+ # puts s.inspect\r
+ # puts s.to_irc_regexp.inspect\r
+ # puts "aUb".match(s.to_irc_regexp)[0] if "aUb" =~ s.to_irc_regexp\r
+ # }\r
+\r
+ # puts " -- Netmasks"\r
+ # masks = []\r
+ # masks << Netmask.new("start")\r
+ # masks << masks[0].dup\r
+ # masks << Netmask.new(masks[0])\r
+ # puts masks.join("\n")\r
+ \r
+ # puts " -- Changing 1"\r
+ # masks[1].nick = "me"\r
+ # puts masks.join("\n")\r
+\r
+ # puts " -- Changing 2"\r
+ # masks[2].nick = "you"\r
+ # puts masks.join("\n")\r
+\r
+ # puts " -- Channel example"\r
+ # ch = Channel.new("#prova")\r
+ # p ch\r
+ # puts " -- Methods"\r
+ # puts ch.methods.sort.join("\n")\r
+ # puts " -- Instance variables"\r
+ # puts ch.instance_variables.join("\n")\r
+\r
+end\r