]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blobdiff - lib/rbot/irc.rb
message: add #thanks method, similar to okay
[user/henk/code/ruby/rbot.git] / lib / rbot / irc.rb
index 4ad57d9947c04b473e8c34315e27461e3058983c..17b7bc3e4b0cee684e9f44a37d1701b10666c744 100644 (file)
@@ -4,6 +4,8 @@
 #   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
@@ -31,7 +48,7 @@ class Object
 
   # 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
@@ -114,7 +131,7 @@ module Irc
       if self == other
         return true
       else
-        warn "Casemap mismatch (#{self.inspect} != #{other.inspect})"
+        warning "Casemap mismatch (#{self.inspect} != #{other.inspect})"
         return false
       end
     end
@@ -127,7 +144,7 @@ module Irc
     include Singleton
 
     def initialize
-      super('rfc1459', "\x41-\x5e", "\x61-\x7e")
+      super('rfc1459', "\x41-\x5a\x7b-\x7e", "\x61-\x7a\x5b-\x5e")
     end
 
   end
@@ -139,7 +156,7 @@ module Irc
     include Singleton
 
     def initialize
-      super('strict-rfc1459', "\x41-\x5d", "\x61-\x7d")
+      super('strict-rfc1459', "\x41-\x5a\x7b-\x7d", "\x61-\x7a\x5b-\x5d")
     end
 
   end
@@ -179,6 +196,7 @@ module Irc
           @casemap = nil
         end
       else
+        warning 'casemap fallback to rfc1459 without hints, correct?'
         @casemap = (@casemap || 'rfc1459').to_irc_casemap
       end
     end
@@ -214,7 +232,6 @@ module Irc
       h = {}
       h[:server] = @server if defined?(@server) and @server
       h[:casemap] = @casemap if defined?(@casemap) and @casemap
-      h[:casemap] ||= @server.casemap if defined?(@server) and @server
       return h
     end
 
@@ -257,7 +274,13 @@ class String
   # 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
@@ -494,8 +517,8 @@ class Regexp
   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}/
+  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
@@ -524,7 +547,7 @@ class Regexp
     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}+/
@@ -541,7 +564,7 @@ class Regexp
     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})+/
@@ -549,7 +572,7 @@ class Regexp
     # # 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.
@@ -617,6 +640,12 @@ module Irc
     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
@@ -683,7 +712,7 @@ module Irc
     # Subclasses of Netmask will return a new Netmask, using full_downcase
     #
     def to_irc_netmask(opts={})
-      if self.class == Netmask and not opts[:force]
+      if self.class == Netmask
         return self if fits_with_server_and_casemap?(opts)
       end
       return self.full_downcase.to_irc_netmask(server_and_casemap.merge(opts))
@@ -811,7 +840,7 @@ module Irc
         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?
@@ -923,7 +952,7 @@ module Irc
   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.
@@ -935,6 +964,8 @@ module Irc
       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.
@@ -1040,6 +1071,14 @@ module Irc
         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
 
 
@@ -1297,15 +1336,25 @@ module Irc
   #
   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
+    attr_accessor :creation_time, :url
 
     def inspect
       str = self.__to_s__[0..-2]
       str << " on server #{server}" if server
       str << " @name=#{@name.inspect} @topic=#{@topic.text.inspect}"
       str << " @users=[#{user_nicks.sort.join(', ')}]"
+      str << " (created on #{creation_time})" if creation_time
+      str << " (URL #{url})" if url
       str << ">"
     end
 
@@ -1336,9 +1385,9 @@ module Irc
     # 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
@@ -1352,7 +1401,7 @@ module Irc
     #
     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)
@@ -1369,6 +1418,12 @@ module Irc
 
       # Flags
       @mode = ModeHash.new
+
+      # creation time, only on some networks
+      @creation_time = nil
+
+      # URL, only on some networks
+      @url = nil
     end
 
     # Removes a user from the channel
@@ -1383,31 +1438,31 @@ module Irc
     # 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
@@ -1477,7 +1532,6 @@ module Irc
   class Server
 
     attr_reader :hostname, :version, :usermodes, :chanmodes
-    alias :to_s :hostname
     attr_reader :supports, :capabilities
 
     attr_reader :channels, :users
@@ -1508,6 +1562,10 @@ module Irc
       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.
@@ -1606,7 +1664,7 @@ module Irc
       if val
         yield if block_given?
       else
-        warn "No #{key.to_s.upcase} value"
+        warning "No #{key.to_s.upcase} value"
       end
     end
 
@@ -1614,7 +1672,7 @@ module Irc
       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
@@ -1626,7 +1684,7 @@ module Irc
     def parse_isupport(line)
       debug "Parsing ISUPPORT #{line.inspect}"
       ar = line.split(' ')
-      reparse = ""
+      reparse = []
       ar.each { |en|
         prekey, val = en.split('=', 2)
         if prekey =~ /^-(.*)/
@@ -1638,7 +1696,15 @@ module Irc
         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) {
@@ -1647,7 +1713,8 @@ module Irc
               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
             }
@@ -1676,7 +1743,7 @@ module Irc
           @supports[key] = val
         when :maxchannels
           noval_warn(key, val) {
-            reparse += "CHANLIMIT=(chantypes):#{val} "
+            reparse << "CHANLIMIT=(chantypes):#{val} "
           }
         when :maxtargets
           noval_warn(key, val) {
@@ -1717,8 +1784,12 @@ module Irc
           @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.
@@ -1783,14 +1854,14 @@ module Irc
         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+
@@ -1802,7 +1873,7 @@ module Irc
             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
@@ -1907,7 +1978,7 @@ module Irc
         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