# * 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 remove him from the Server @users list\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
\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
+end\r
\r
# The Irc module is used to keep all IRC-related classes\r
# in the same namespace\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
# Raise an error if _arg_ and self are not the same Casemap\r
#\r
def must_be(arg)\r
other = arg.to_irc_casemap\r
- raise "Casemap mismatch (#{self} != #{other})" unless self == other\r
+ raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other\r
return true\r
end\r
\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
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
# 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
- when /^(?:(\S+?)(?:!(\S+)@(?:(\S+))?)?)?$/\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
end\r
end\r
\r
- # A Netmask is easily converted to a String for the usual representation\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
def fullform\r
"#{nick}!#{user}@#{host}"\r
end\r
- alias :to_s :fullform\r
\r
# Converts the receiver into a Netmask with the given (optional)\r
# server/casemap association. We return self unless a conversion\r
if self.class == Netmask\r
return self if fits_with_server_and_casemap?(opts)\r
end\r
- return self.fullform.to_irc_netmask(server_and_casemap.merge(opts))\r
+ return self.downcase.to_irc_netmask(opts)\r
end\r
\r
# Converts the receiver into a User with the given (optional)\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
- raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob?\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
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
# 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
+ return nick != "*" && user != "*" && host != "*"\r
end\r
\r
# Is the user away?\r
end\r
end\r
\r
+ # Users can be either simply downcased (their nick only)\r
+ # or fully downcased: this will return the fullform downcased\r
+ # according to the given casemap.\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
# 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
#\r
def to_irc_user(opts={})\r
return self if fits_with_server_and_casemap?(opts)\r
- return self.fullform.to_irc_user(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
def replace(other)\r
case other\r
when User\r
- nick = other.nick\r
- user = other.user\r
- host = other.host\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
+ @away = other.away?\r
else\r
- replace(other.to_irc_user(server_and_casemap))\r
+ self.replace(other.to_irc_user(server_and_casemap))\r
end\r
end\r
\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 < ArrayOf\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(User, 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
# Mode on a Channel\r
#\r
class Mode\r
+ attr_reader :channel\r
def initialize(ch)\r
@channel = ch\r
end\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
\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
# 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
# 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 = false\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 = false\r
+ @arg = nil\r
end\r
\r
end\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
#\r
def initialize(text="", set_by="", set_on=Time.new)\r
@text = text\r
- @set_by = set_by.to_irc_user\r
+ @set_by = set_by.to_irc_netmask\r
@set_on = set_on\r
end\r
\r
str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
str << " on server #{server}" if server\r
str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"\r
- str << " @users=[#{@users.sort.join(', ')}]"\r
+ str << " @users=[#{user_nicks.sort.join(', ')}]"\r
str << ">"\r
end\r
\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
+ 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
+ # Adds a user to the channel\r
+ #\r
+ def add_user(user, opts={})\r
+ silent = opts.fetch(:silent, false) \r
+ if has_user?(user) && !silent\r
+ warn "Trying to add user #{user} to channel #{self} again"\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
@users = UserList.new\r
\r
users.each { |u|\r
- @users << u.to_irc_user(server_and_casemap)\r
+ add_user(u)\r
}\r
\r
# Flags\r
# A channel is local to a server if it has the '&' prefix\r
#\r
def local?\r
- name[0] = 0x26\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
+ 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
+ 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
+ name[0] == 0x23\r
end\r
\r
# Create a new mode\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
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
:typec => nil, # Type C: needs a parameter when set\r
:typed => nil # Type D: must not have a parameter\r
},\r
- :channellen => 200,\r
- :chantypes => "#&",\r
+ :channellen => 50,\r
+ :chantypes => "#&!+",\r
:excepts => nil,\r
:idchan => {},\r
:invex => nil,\r
:network => nil,\r
:nicklen => 9,\r
:prefix => {\r
- :modes => 'ov'.scan(/./),\r
- :prefixes => '@+'.scan(/./)\r
+ :modes => [:o, :v],\r
+ :prefixes => [:"@", :+]\r
},\r
:safelist => nil,\r
:statusmsg => nil,\r
# Resets the Channel and User list\r
#\r
def reset_lists\r
- @users.each { |u|\r
+ @users.reverse_each { |u|\r
delete_user(u)\r
}\r
- @channels.each { |u|\r
+ @channels.reverse_each { |u|\r
delete_channel(u)\r
}\r
end\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
groups = val.split(',')\r
groups.each { |g|\r
k, v = g.split(':')\r
- @supports[key][k] = v.to_i\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
}\r
when :maxtargets\r
noval_warn(key, val) {\r
- @supports[key]['PRIVMSG'] = val.to_i\r
- @supports[key]['NOTICE'] = val.to_i\r
+ @supports[:targmax]['PRIVMSG'] = val.to_i\r
+ @supports[:targmax]['NOTICE'] = val.to_i\r
}\r
when :network\r
noval_warn(key, val) {\r
# Checks if the receiver already has a channel with the given _name_\r
#\r
def has_channel?(name)\r
- channel_names.index(name.downcase)\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
# 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\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
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
+ # 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
# Checks if the receiver already has a user with the given _nick_\r
#\r
def has_user?(nick)\r
- user_nicks.index(nick.downcase)\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
# 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\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 != tmp\r
+ if old.fullform.downcase != tmp.fullform.downcase\r
old.replace(tmp)\r
- # debug "User improved to #{old.inspect}"\r
+ # debug "Known user now #{old.inspect}"\r
end\r
end\r
return old\r
@users.inject(UserList.new) {\r
|list, user|\r
if user.user == "*" or user.host == "*"\r
- list << user if user.nick.downcase =~ nm.nick.downcase.to_irc_regexp\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