# 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
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
# We alias the to_s method to __to_s__ to make
# it accessible in all classes
- alias :__to_s__ :to_s
+ alias :__to_s__ :to_s
end
# The Irc module is used to keep all IRC-related classes
if self == other
return true
else
- warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
+ warning "Casemap mismatch (#{self.inspect} != #{other.inspect})"
return false
end
end
include Singleton
def initialize
- super('rfc1459', "\x41-\x5e", "\x61-\x7e")
+ super('rfc1459', "\x41-\x5a\x7b-\x7e", "\x61-\x7a\x5b-\x5e")
end
end
include Singleton
def initialize
- super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
+ super('strict-rfc1459', "\x41-\x5a\x7b-\x7d", "\x61-\x7a\x5b-\x5d")
end
end
@casemap = nil
end
else
+ warning 'casemap fallback to rfc1459 without hints, correct?'
@casemap = (@casemap || 'rfc1459').to_irc_casemap
end
end
# This method returns the Irc::Casemap whose name is the receiver
#
def to_irc_casemap
- Irc::Casemap.get(self) rescue raise TypeError, "Unkown Irc::Casemap #{self.inspect}"
+ 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
RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/
# Nick-matching regexps
- SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/
+ SPECIAL_CHAR = /[\[-\`\{-\}]/
NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/
NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/
GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/
GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/
# # FreeNode network replaces the host of affiliated users with
- # # 'virtual hosts'
+ # # '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}\.??/
+ # 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.
def initialize(str="", opts={})
# First of all, check for server/casemap option
#
+ debug 'new netmask "%s" casemap=%s server=%s server#casemap=%s' % [
+ str,
+ (opts[:casemap].class.to_s rescue 'null'),
+ (opts[:server].hostname.to_s rescue 'null'),
+ (opts[:server].casemap.class.to_s rescue 'null')
+ ]
init_server_or_casemap(opts)
# Now we can see if the given string _str_ is an actual Netmask
them = cmp.send(component).irc_downcase(casemap)
if us.has_irc_glob? && them.has_irc_glob?
next if us == them
- warn NotImplementedError
+ warning NotImplementedError
return false
end
return false if us.has_irc_glob? && !them.has_irc_glob?
class User < Netmask
alias :to_s :nick
- attr_accessor :real_name
+ 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.
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.
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
#
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
# Adds a user to the channel
#
def add_user(user, opts={})
- silent = opts.fetch(:silent, false)
+ silent = opts.fetch(:silent, false)
if has_user?(user)
- warn "Trying to add user #{user} to channel #{self} again" unless silent
+ warning "Trying to add user #{user} to channel #{self} again" unless silent
else
@users << user.to_irc_user(server_and_casemap)
end
#
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].chr}" if name !~ /^[&#+!]/
+ warning "Unknown channel prefix #{name[0,1]}" if name !~ /^[&#+!]/
raise ArgumentError, "Invalid character in #{name.inspect}" if name =~ /[ \x07,]/
init_server_or_casemap(opts)
# The channel prefix
#
def prefix
- name[0].chr
+ name[0,1]
end
# A channel is local to a server if it has the '&' prefix
#
def local?
- name[0] == 0x26
+ name[0,1] == '&'
end
# A channel is modeless if it has the '+' prefix
#
def modeless?
- name[0] == 0x2b
+ name[0,1] == '+'
end
# A channel is safe if it has the '!' prefix
#
def safe?
- name[0] == 0x21
+ name[0,1] == '!'
end
# A channel is normal if it has the '#' prefix
#
def normal?
- name[0] == 0x23
+ name[0,1] == '#'
end
# Create a new mode
class Server
attr_reader :hostname, :version, :usermodes, :chanmodes
- alias :to_s :hostname
attr_reader :supports, :capabilities
attr_reader :channels, :users
str << ">"
end
+ def to_s
+ hostname.nil? ? "<no hostname>" : hostname
+ 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.
if val
yield if block_given?
else
- warn "No #{key.to_s.upcase} value"
+ warning "No #{key.to_s.upcase} value"
end
end
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}"
+ warning "No #{key.to_s.upcase} value must be specified, got #{val}"
end
end
private :noval_warn, :val_warn
def parse_isupport(line)
debug "Parsing ISUPPORT #{line.inspect}"
ar = line.split(' ')
- reparse = ""
+ reparse = []
ar.each { |en|
prekey, val = en.split('=', 2)
if prekey =~ /^-(.*)/
case key
when :casemapping
noval_warn(key, val) {
- @supports[key] = val.to_irc_casemap
+ 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) {
k, v = g.split(':')
@supports[key][k] = v.to_i || 0
if @supports[key][k] == 0
- warn "Deleting #{key} limit of 0 for #{k}"
+ # If no argument is given for a particular command (e.g. "WHOIS:"),
+ # that command does not have a limit on the number of targets.)
@supports[key].delete(k)
end
}
@supports[key] = val
when :maxchannels
noval_warn(key, val) {
- reparse += "CHANLIMIT=(chantypes):#{val} "
+ reparse << "CHANLIMIT=(chantypes):#{val} "
}
when :maxtargets
noval_warn(key, val) {
@supports[key] = val.nil? ? true : val
end
}
- reparse.gsub!("(chantypes)",@supports[:chantypes])
- parse_isupport(reparse) unless reparse.empty?
+ 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.
return ex
else
- prefix = name[0].chr
+ 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]
+ warning "#{self} doesn't support channel prefix #{prefix}" unless @supports[:chantypes].include?(prefix)
+ warning "#{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+
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]
+ warning "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
end
return old
else
- warn "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
+ warning "#{self} doesn't support nicknames this long (#{tmp.nick.length} > #{@supports[:nicklen]})" unless tmp.nick.length <= @supports[:nicklen]
@users << tmp
return @users.last
end