# 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
+# * Maybe ChannelList and UserList should be HashesOf instead of ArrayOf?\r
+# See items marked as TODO Ho\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
# @server (if possible) or at the @casemap otherwise\r
#\r
def casemap\r
- @server.casemap rescue @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
- {:server => @server, :casemap => @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 inspect\r
str = "<#{self.class}:#{'0x%x' % self.object_id}:"\r
- str << " @server=#{@server}" if @server\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
#\r
def matches?(arg)\r
cmp = arg.to_irc_netmask(:casemap => casemap)\r
+ debug "Matching #{self.fullform} against #{arg.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
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
# Channel modes of type A manipulate lists\r
#\r
+ # Example: b (banlist)\r
+ #\r
class ModeTypeA < Mode\r
def initialize(ch)\r
super\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
# 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
\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
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
\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 reset_capabilities\r
@supports = {\r
- :casemapping => 'rfc1459',\r
+ :casemapping => 'rfc1459'.to_irc_casemap,\r
:chanlimit => {},\r
:chanmodes => {\r
:typea => nil, # Type A: address lists\r
key = prekey.downcase.to_sym\r
end\r
case key\r
- when :casemapping, :network\r
+ when :casemapping\r
noval_warn(key, val) {\r
- @supports[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\r
+ @supports[key][k] = v.to_i || 0\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
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
# 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
# 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