X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Firc.rb;h=a4bd725ab22e41ff8142f4da9324458accfad42e;hb=e43dbcf88298d2273c9cca2b3e5c226484686289;hp=6ef84eb0d64e1ed8e0603b9ee1d6a7d8e16cbcae;hpb=9430b8ea5a8f36fed90879261fc4ffcdcdc65218;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb index 6ef84eb0..a4bd725a 100644 --- a/lib/rbot/irc.rb +++ b/lib/rbot/irc.rb @@ -3,7 +3,11 @@ # * 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 remove him from the Server @users list +# sent us privmsgs, we know we can remove him from the Server @users list +# * 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 # @@ -17,6 +21,16 @@ require 'singleton' +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 +end # The Irc module is used to keep all IRC-related classes # in the same namespace @@ -84,11 +98,18 @@ module Irc @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 + # Raise an error if _arg_ and self are not the same Casemap # def must_be(arg) other = arg.to_irc_casemap - raise "Casemap mismatch (#{self} != #{other})" unless self == other + raise "Casemap mismatch (#{self.inspect} != #{other.inspect})" unless self == other return true end @@ -176,14 +197,18 @@ module Irc # @server (if possible) or at the @casemap otherwise # def casemap - @server.casemap rescue @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 - {:server => @server, :casemap => @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 @@ -436,6 +461,13 @@ class ArrayOf < Array } 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! @@ -515,7 +547,7 @@ module Irc if self.class == Netmask return self if fits_with_server_and_casemap?(opts) end - return self.fullform.to_irc_netmask(server_and_casemap.merge(opts)) + return self.downcase.to_irc_netmask(opts) end # Converts the receiver into a User with the given (optional) @@ -531,7 +563,7 @@ module Irc # def inspect str = "<#{self.class}:#{'0x%x' % self.object_id}:" - str << " @server=#{@server}" if @server + str << " @server=#{@server}" if defined?(@server) and @server str << " @nick=#{@nick.inspect} @user=#{@user.inspect}" str << " @host=#{@host.inspect} casemap=#{casemap.inspect}" str << ">" @@ -612,10 +644,15 @@ module Irc # 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) - raise NotImplementedError if us.has_irc_glob? && them.has_irc_glob? + 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 } @@ -653,6 +690,35 @@ module Irc 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 @@ -755,6 +821,21 @@ module Irc end end + # Users can be either simply downcased (their nick only) + # or fully downcased: this will return the fullform downcased + # according to the given casemap. + # + 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 + # 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. # @@ -762,7 +843,7 @@ module Irc # def to_irc_user(opts={}) return self if fits_with_server_and_casemap?(opts) - return self.fullform.to_irc_user(server_and_casemap(opts)) + return self.full_downcase.to_irc_user(opts) end # We can replace everything at once with data from another User @@ -770,14 +851,14 @@ module Irc def replace(other) case other when User - nick = other.nick - user = other.user - host = other.host + self.nick = other.nick + self.user = other.user + self.host = other.host @server = other.server @casemap = other.casemap unless @server - @away = other.away + @away = other.away? else - replace(other.to_irc_user(server_and_casemap)) + self.replace(other.to_irc_user(server_and_casemap)) end end @@ -785,14 +866,24 @@ module Irc # A UserList is an ArrayOf Users + # We derive it from NetmaskList, which allows us to inherit any special + # NetmaskList method # - class UserList < ArrayOf + class UserList < NetmaskList # Create a new UserList, optionally filling it with the elements from # the Array argument fed to it. # def initialize(ar=[]) - super(User, 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 @@ -826,6 +917,7 @@ module Irc # Mode on a Channel # class Mode + attr_reader :channel def initialize(ch) @channel = ch end @@ -835,7 +927,10 @@ module Irc # Channel modes of type A manipulate lists # + # Example: b (banlist) + # class ModeTypeA < Mode + attr_reader :list def initialize(ch) super @list = NetmaskList.new @@ -856,12 +951,19 @@ module Irc # 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 @@ -879,6 +981,8 @@ module Irc # modes of type A # class UserMode < ModeTypeB + attr_reader :list + alias :users :list def initialize(ch) super @list = UserList.new @@ -900,22 +1004,25 @@ module Irc # 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 = false + @arg = nil end def status @arg end + alias :value :status def set(val) @arg = val end def reset - @arg = false + @arg = nil end end @@ -923,6 +1030,8 @@ module Irc # Channel modes of type D are basically booleans # + # Example: m (moderate) + # class ModeTypeD < Mode def initialize(ch) super @@ -1008,7 +1117,7 @@ module Irc str = "<#{self.class}:#{'0x%x' % self.object_id}:" str << " on server #{server}" if server str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}" - str << " @users=[#{@users.sort.join(', ')}]" + str << " @users=[#{user_nicks.sort.join(', ')}]" str << ">" end @@ -1018,6 +1127,35 @@ module Irc 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) + 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 + + # Adds a user to the channel + # + def add_user(user, opts={}) + silent = opts.fetch(:silent, false) + if has_user?(user) && !silent + warn "Trying to add user #{user} to channel #{self} again" + 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. # @@ -1038,7 +1176,7 @@ module Irc @users = UserList.new users.each { |u| - @users << u.to_irc_user(server_and_casemap) + add_user(u) } # Flags @@ -1063,25 +1201,25 @@ module Irc # A channel is local to a server if it has the '&' prefix # def local? - name[0] = 0x26 + name[0] == 0x26 end # A channel is modeless if it has the '+' prefix # def modeless? - name[0] = 0x2b + name[0] == 0x2b end # A channel is safe if it has the '!' prefix # def safe? - name[0] = 0x21 + name[0] == 0x21 end # A channel is normal if it has the '#' prefix # def normal? - name[0] = 0x23 + name[0] == 0x23 end # Create a new mode @@ -1104,6 +1242,13 @@ module Irc 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 @@ -1134,10 +1279,12 @@ module Irc 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 @@ -1176,7 +1323,7 @@ module Irc # def reset_capabilities @supports = { - :casemapping => 'rfc1459', + :casemapping => 'rfc1459'.to_irc_casemap, :chanlimit => {}, :chanmodes => { :typea => nil, # Type A: address lists @@ -1269,27 +1416,18 @@ module Irc key = prekey.downcase.to_sym end case key - when :casemapping, :network + when :casemapping noval_warn(key, val) { - @supports[key] = val + @supports[key] = val.to_irc_casemap } 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 + @supports[key][k] = v.to_i || 0 } } - when :maxchannels - noval_warn(key, val) { - reparse += "CHANLIMIT=(chantypes):#{val} " - } - when :maxtargets - noval_warn(key, val) { - @supports[key]['PRIVMSG'] = val.to_i - @supports[key]['NOTICE'] = val.to_i - } when :chanmodes noval_warn(key, val) { groups = val.split(',') @@ -1312,6 +1450,19 @@ module Irc 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 @@ -1376,13 +1527,15 @@ module Irc # Checks if the receiver already has a channel with the given _name_ # def has_channel?(name) - channel_names.index(name.downcase) + 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 @@ -1391,9 +1544,15 @@ module Irc # Create a new Channel object bound to the receiver and add it to the # list of Channels 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 + # 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 @@ -1480,7 +1639,8 @@ module Irc # Checks if the receiver already has a user with the given _nick_ # def has_user?(nick) - user_nicks.index(nick.downcase) + return false if nick.nil_or_empty? + user_nicks.index(nick.irc_downcase(casemap)) end # Returns the user with nick _nick_, if available @@ -1493,22 +1653,31 @@ module Irc # Create a new User object bound to the receiver and add it to the list # of Users 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 + # 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 != tmp + if old.fullform.downcase != tmp.fullform.downcase old.replace(tmp) - # debug "User improved to #{old.inspect}" + # debug "Known user now #{old.inspect}" end end return old @@ -1559,7 +1728,7 @@ module Irc @users.inject(UserList.new) { |list, user| if user.user == "*" or user.host == "*" - list << user if user.nick.downcase =~ nm.nick.downcase.to_irc_regexp + list << user if user.nick.irc_downcase(casemap) =~ nm.nick.irc_downcase(casemap).to_irc_regexp else list << user if user.matches?(nm) end