X-Git-Url: https://git.netwichtig.de/gitweb/?a=blobdiff_plain;f=lib%2Frbot%2Firc.rb;h=129f947e61d1d9d906e1b02d7b7e7deb486fd5f3;hb=4a86158144a13bc901222442ccd2db9c2bbd6bb0;hp=f2425d6a986b869a194722407297c2f05e35fac3;hpb=e74280756864bd2122442f5876a6f752b65c202d;p=user%2Fhenk%2Fcode%2Fruby%2Frbot.git diff --git a/lib/rbot/irc.rb b/lib/rbot/irc.rb index f2425d6a..129f947e 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 @@ -307,7 +321,7 @@ class String raise "Unexpected match #{m} when converting #{self}" end } - Regexp.new(regmask) + Regexp.new("^#{regmask}$") end end @@ -447,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! @@ -454,6 +475,103 @@ class ArrayOf < Array end +# We extend the Regexp class with an Irc module which will contain some +# Irc-specific regexps +# +class Regexp + + # We start with some general-purpose ones which will be used in the + # Irc module too, but are useful regardless + DIGITS = /\d+/ + HEX_DIGIT = /[0-9A-Fa-f]/ + HEX_DIGITS = /#{HEX_DIGIT}+/ + HEX_OCTET = /#{HEX_DIGIT}#{HEX_DIGIT}?/ + DEC_OCTET = /[01]?\d?\d|2[0-4]\d|25[0-5]/ + DEC_IP_ADDR = /#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}.#{DEC_OCTET}/ + HEX_IP_ADDR = /#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}.#{HEX_OCTET}/ + IP_ADDR = /#{DEC_IP_ADDR}|#{HEX_IP_ADDR}/ + + # IPv6, from Resolv::IPv6, without the \A..\z anchors + HEX_16BIT = /#{HEX_DIGIT}{1,4}/ + IP6_8Hex = /(?:#{HEX_16BIT}:){7}#{HEX_16BIT}/ + IP6_CompressedHex = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)/ + IP6_6Hex4Dec = /((?:#{HEX_16BIT}:){6,6})#{DEC_IP_ADDR}/ + IP6_CompressedHex4Dec = /((?:#{HEX_16BIT}(?::#{HEX_16BIT})*)?)::((?:#{HEX_16BIT}:)*)#{DEC_IP_ADDR}/ + IP6_ADDR = /(?:#{IP6_8Hex})|(?:#{IP6_CompressedHex})|(?:#{IP6_6Hex4Dec})|(?:#{IP6_CompressedHex4Dec})/ + + # We start with some IRC related regular expressions, used to match + # Irc::User nicks and users and Irc::Channel names + # + # For each of them we define two versions of the regular expression: + # * a generic one, which should match for any server but may turn out to + # match more than a specific server would accept + # * an RFC-compliant matcher + # + module Irc + + # Channel-name-matching regexps + CHAN_FIRST = /[#&+]/ + CHAN_SAFE = /![A-Z0-9]{5}/ + CHAN_ANY = /[^\x00\x07\x0A\x0D ,:]/ + GEN_CHAN = /(?:#{CHAN_FIRST}|#{CHAN_SAFE})#{CHAN_ANY}+/ + RFC_CHAN = /#{CHAN_FIRST}#{CHAN_ANY}{1,49}|#{CHAN_SAFE}#{CHAN_ANY}{1,44}/ + + # Nick-matching regexps + SPECIAL_CHAR = /[\x5b-\x60\x7b-\x7d]/ + NICK_FIRST = /#{SPECIAL_CHAR}|[[:alpha:]]/ + NICK_ANY = /#{SPECIAL_CHAR}|[[:alnum:]]|-/ + GEN_NICK = /#{NICK_FIRST}#{NICK_ANY}+/ + RFC_NICK = /#{NICK_FIRST}#{NICK_ANY}{0,8}/ + + USER_CHAR = /[^\x00\x0a\x0d @]/ + GEN_USER = /#{USER_CHAR}+/ + + # Host-matching regexps + HOSTNAME_COMPONENT = /[[:alnum:]](?:[[:alnum:]]|-)*[[:alnum:]]*/ + HOSTNAME = /#{HOSTNAME_COMPONENT}(?:\.#{HOSTNAME_COMPONENT})*/ + HOSTADDR = /#{IP_ADDR}|#{IP6_ADDR}/ + + GEN_HOST = /#{HOSTNAME}|#{HOSTADDR}/ + + # # FreeNode network replaces the host of affiliated users with + # # '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}\.??/ + + # Sadly, different networks have different, RFC-breaking ways of cloaking + # the actualy host address: see above for an example to handle FreeNode. + # Another example would be Azzurra, wich also inserts a "=" in the + # cloacked host. So let's just not care about this and go with the simplest + # thing: + GEN_HOST_EXT = /\S+/ + + # User-matching Regexp + GEN_USER_ID = /(#{GEN_NICK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ + + # Things such has the BIP proxy send invalid nicks in a complete netmask, + # so we want to match this, rather: this matches either a compliant nick + # or a a string with a very generic nick, a very generic hostname after an + # @ sign, and an optional user after a ! + BANG_AT = /#{GEN_NICK}|\S+?(?:!\S+?)?@\S+?/ + + # # For Netmask, we want to allow wildcards * and ? in the nick + # # (they are already allowed in the user and host part + # GEN_NICK_MASK = /(?:#{NICK_FIRST}|[?*])?(?:#{NICK_ANY}|[?*])+/ + + # # Netmask-matching Regexp + # GEN_MASK = /(#{GEN_NICK_MASK})(?:(?:!(#{GEN_USER}))?@(#{GEN_HOST_EXT}))?/ + + end + +end + + module Irc @@ -496,7 +614,9 @@ module Irc # Now we can see if the given string _str_ is an actual Netmask if str.respond_to?(:to_str) case str.to_str - when /^(?:(\S+?)(?:!(\S+)@(?:(\S+))?)?)?$/ + # We match a pretty generic string, to work around non-compliant + # servers + when /^(?:(\S+?)(?:(?:!(\S+?))?@(\S+))?)?$/ # We do assignment using our internal methods self.nick = $1 self.user = $2 @@ -509,24 +629,48 @@ module Irc end end - # A Netmask is easily converted to a String for the usual representation + # A Netmask is easily converted to a String for the usual representation. + # We skip the user or host parts if they are "*", unless we've been asked + # for the full form # + def to_s + ret = nick.dup + ret << "!" << user unless user == "*" + ret << "@" << host unless host == "*" + return ret + end + def fullform "#{nick}!#{user}@#{host}" end - alias :to_s :fullform + + # This method downcases the fullform of the netmask. While this may not be + # significantly different from the #downcase() method provided by the + # ServerOrCasemap mixin, it's significantly different for Netmask + # subclasses such as User whose simple downcasing uses the nick only. + # + 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 # Converts the receiver into a Netmask with the given (optional) # server/casemap association. We return self unless a conversion # is needed (different casemap/server) # - # Subclasses of Netmask will return a new Netmask + # Subclasses of Netmask will return a new Netmask, using full_downcase # def to_irc_netmask(opts={}) 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.full_downcase.to_irc_netmask(opts) end # Converts the receiver into a User with the given (optional) @@ -623,7 +767,7 @@ module Irc # def matches?(arg) cmp = arg.to_irc_netmask(:casemap => casemap) - debug "Matching #{self.fullform} against #{arg.fullform}" + 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) @@ -669,6 +813,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 @@ -712,6 +885,8 @@ module Irc class User < Netmask alias :to_s :nick + attr_accessor :real_name + # 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. # @@ -721,6 +896,7 @@ module Irc raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if user.has_irc_glob? && user != "*" raise ArgumentError, "#{str.inspect} must not have globs (unescaped * or ?)" if host.has_irc_glob? && host != "*" @away = false + @real_name = String.new end # The nick of a User may be changed freely, but it must not contain glob patterns. @@ -751,7 +927,7 @@ module Irc # Checks if a User is well-known or not by looking at the hostname and user # def known? - return nick!= "*" && user!="*" && host!="*" + return nick != "*" && user != "*" && host != "*" end # Is the user away? @@ -778,7 +954,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 @@ -797,18 +973,57 @@ module Irc end end + def modes_on(channel) + case channel + when Channel + channel.modes_of(self) + else + return @server.channel(channel).modes_of(self) if @server + raise "Can't resolve channel #{channel}" + end + end + + def is_op?(channel) + case channel + when Channel + channel.has_op?(self) + else + return @server.channel(channel).has_op?(self) if @server + raise "Can't resolve channel #{channel}" + end + end + + def is_voice?(channel) + case channel + when Channel + channel.has_voice?(self) + else + return @server.channel(channel).has_voice?(self) if @server + raise "Can't resolve channel #{channel}" + end + end end # 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 @@ -842,6 +1057,7 @@ module Irc # Mode on a Channel # class Mode + attr_reader :channel def initialize(ch) @channel = ch end @@ -851,7 +1067,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 @@ -872,12 +1091,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 @@ -895,6 +1121,8 @@ module Irc # modes of type A # class UserMode < ModeTypeB + attr_reader :list + alias :users :list def initialize(ch) super @list = UserList.new @@ -916,22 +1144,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 @@ -939,6 +1170,8 @@ module Irc # Channel modes of type D are basically booleans # + # Example: m (moderate) + # class ModeTypeD < Mode def initialize(ch) super @@ -972,7 +1205,7 @@ module Irc # def initialize(text="", set_by="", set_on=Time.new) @text = text - @set_by = set_by.to_irc_user + @set_by = set_by.to_irc_netmask @set_on = set_on end @@ -1024,7 +1257,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 @@ -1034,6 +1267,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) + @users.index(nick.to_irc_user(server_and_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) + warn "Trying to add user #{user} to channel #{self} again" unless silent + 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. # @@ -1054,7 +1316,7 @@ module Irc @users = UserList.new users.each { |u| - @users << u.to_irc_user(server_and_casemap) + add_user(u) } # Flags @@ -1079,25 +1341,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 @@ -1106,6 +1368,21 @@ module Irc @mode[sym.to_sym] = kl.new(self) end + def modes_of(user) + l = [] + @mode.map { |s, m| + l << s if (m.class <= UserMode and m.list[user]) + } + l + end + + def has_op?(user) + @mode.has_key?(:o) and @mode[:o].list[user] + end + + def has_voice?(user) + @mode.has_key?(:v) and @mode[:v].list[user] + end end @@ -1120,6 +1397,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 @@ -1150,10 +1434,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 @@ -1200,8 +1486,8 @@ module Irc :typec => nil, # Type C: needs a parameter when set :typed => nil # Type D: must not have a parameter }, - :channellen => 200, - :chantypes => "#&", + :channellen => 50, + :chantypes => "#&!+", :excepts => nil, :idchan => {}, :invex => nil, @@ -1211,8 +1497,8 @@ module Irc :network => nil, :nicklen => 9, :prefix => { - :modes => 'ov'.scan(/./), - :prefixes => '@+'.scan(/./) + :modes => [:o, :v], + :prefixes => [:"@", :+] }, :safelist => nil, :statusmsg => nil, @@ -1226,10 +1512,10 @@ module Irc # Resets the Channel and User list # def reset_lists - @users.each { |u| + @users.reverse_each { |u| delete_user(u) } - @channels.each { |u| + @channels.reverse_each { |u| delete_channel(u) } end @@ -1239,6 +1525,7 @@ module Irc def clear reset_lists reset_capabilities + @hostname = @version = @usermodes = @chanmodes = nil end # This method is used to parse a 004 RPL_MY_INFO line @@ -1295,6 +1582,10 @@ module Irc groups.each { |g| k, v = g.split(':') @supports[key][k] = v.to_i || 0 + if @supports[key][k] == 0 + warn "Deleting #{key} limit of 0 for #{k}" + @supports[key].delete(k) + end } } when :chanmodes @@ -1396,13 +1687,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 @@ -1411,9 +1704,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 @@ -1438,7 +1737,8 @@ module Irc channel_names.each { |n| count += 1 if k.include?(n[0]) } - raise IndexError, "Already joined #{count} channels with prefix #{k}" if count == @supports[:chanlimit][k] + # 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] } # So far, everything is fine. Now create the actual Channel @@ -1500,7 +1800,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 @@ -1513,9 +1814,15 @@ 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}" @@ -1582,7 +1889,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