You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by jv...@apache.org on 2005/12/16 23:30:52 UTC

svn commit: r357245 [1/3] - in /maven/sandbox/issue/rbot/dist: ./ bin/ lib/ lib/site_ruby/ lib/site_ruby/1.8/ lib/site_ruby/1.8/rbot/ plugins/ share/ share/rbot/ share/rbot/languages/ share/rbot/templates/ share/rbot/templates/lart/

Author: jvanzyl
Date: Fri Dec 16 14:30:38 2005
New Revision: 357245

URL: http://svn.apache.org/viewcvs?rev=357245&view=rev
Log: (empty)

Added:
    maven/sandbox/issue/rbot/dist/
    maven/sandbox/issue/rbot/dist/bin/
    maven/sandbox/issue/rbot/dist/bin/rbot   (with props)
    maven/sandbox/issue/rbot/dist/conf.yaml
    maven/sandbox/issue/rbot/dist/filters.yaml   (with props)
    maven/sandbox/issue/rbot/dist/keyword.db   (with props)
    maven/sandbox/issue/rbot/dist/levels.rbot
    maven/sandbox/issue/rbot/dist/lib/
    maven/sandbox/issue/rbot/dist/lib/site_ruby/
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/keywords.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/language.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/message.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/messagemapper.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/pkgconfig.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/plugins.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/rbotconfig.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/registry.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/rfc2812.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/timer.rb   (with props)
    maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/utils.rb   (with props)
    maven/sandbox/issue/rbot/dist/plugin_registry.db   (with props)
    maven/sandbox/issue/rbot/dist/plugins/
    maven/sandbox/issue/rbot/dist/plugins/jira.rb   (with props)
    maven/sandbox/issue/rbot/dist/rbot.sh   (with props)
    maven/sandbox/issue/rbot/dist/share/
    maven/sandbox/issue/rbot/dist/share/rbot/
    maven/sandbox/issue/rbot/dist/share/rbot/languages/
    maven/sandbox/issue/rbot/dist/share/rbot/languages/dutch.lang
    maven/sandbox/issue/rbot/dist/share/rbot/languages/english.lang
    maven/sandbox/issue/rbot/dist/share/rbot/languages/french.lang
    maven/sandbox/issue/rbot/dist/share/rbot/languages/german.lang
    maven/sandbox/issue/rbot/dist/share/rbot/templates/
    maven/sandbox/issue/rbot/dist/share/rbot/templates/keywords.rbot
    maven/sandbox/issue/rbot/dist/share/rbot/templates/lart/
    maven/sandbox/issue/rbot/dist/share/rbot/templates/lart/larts
    maven/sandbox/issue/rbot/dist/share/rbot/templates/lart/praises
    maven/sandbox/issue/rbot/dist/share/rbot/templates/levels.rbot
    maven/sandbox/issue/rbot/dist/share/rbot/templates/users.rbot
    maven/sandbox/issue/rbot/dist/users.rbot

Added: maven/sandbox/issue/rbot/dist/bin/rbot
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/bin/rbot?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/bin/rbot (added)
+++ maven/sandbox/issue/rbot/dist/bin/rbot Fri Dec 16 14:30:38 2005
@@ -0,0 +1,81 @@
+#!/usr/bin/env ruby
+
+# Copyright (C) 2002 Tom Gilbert.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies of the Software and its documentation and acknowledgment shall be
+# given in the documentation and software packages that this Software was
+# used.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+$VERBOSE=true
+
+require 'etc'
+require 'getoptlong'
+require 'fileutils'
+
+$version="0.9.9"
+$opts = Hash.new
+
+orig_opts = ARGV.dup
+
+opts = GetoptLong.new(
+  ["--debug", "-d", GetoptLong::NO_ARGUMENT],
+  ["--help",  "-h", GetoptLong::NO_ARGUMENT],
+  ["--trace",  "-t", GetoptLong::REQUIRED_ARGUMENT],
+  ["--version", "-v", GetoptLong::NO_ARGUMENT]
+)
+
+$debug = false
+opts.each {|opt, arg|
+  $debug = true if(opt == "--debug")
+  $opts[opt.sub(/^-+/, "")] = arg
+}
+
+if ($opts["trace"])
+  set_trace_func proc { |event, file, line, id, binding, classname|
+    if classname.to_s == $opts["trace"]
+      printf "TRACE: %8s %s:%-2d %10s %8s\n", event, File.basename(file), line, id, classname
+    end
+  }
+end
+
+begin
+  require 'rbot/ircbot'
+rescue LoadError => e
+  puts "Error: couldn't find the rbot/ircbot module for loading\n - did you install rbot using setup.rb?"
+  exit 2
+end
+
+if ($opts["version"])
+  puts "rbot #{$version}"
+  exit 0
+end
+
+if ($opts["help"])
+  puts "usage: rbot [options] [config directory]"
+  puts "  -h, --help         this message"
+  puts "  -v, --version      version information"
+  puts "  -d, --debug        enable debug messages"
+  puts "config directory defaults to ~/.rbot"
+  exit 0
+end
+
+if(bot = Irc::IrcBot.new(ARGV.shift, ARGV.shift, :argv => orig_opts))
+  # just run the bot
+  bot.mainloop
+end
+

Propchange: maven/sandbox/issue/rbot/dist/bin/rbot
------------------------------------------------------------------------------
    svn:executable = *

Added: maven/sandbox/issue/rbot/dist/conf.yaml
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/conf.yaml?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/conf.yaml (added)
+++ maven/sandbox/issue/rbot/dist/conf.yaml Fri Dec 16 14:30:38 2005
@@ -0,0 +1,7 @@
+core.address_prefix: 
+- "!"
+irc.nick: faqbot
+server.name: irc.codehaus.org
+irc.join_channels: 
+- "#foo"
+datadir: /home/jvanzyl/js/org/apache/maven/sandbox/issue/rbot/dist/share/rbot

Added: maven/sandbox/issue/rbot/dist/filters.yaml
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/filters.yaml?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/filters.yaml (added)
+++ maven/sandbox/issue/rbot/dist/filters.yaml Fri Dec 16 14:30:38 2005
@@ -0,0 +1 @@
+link /home/jvanzyl/js/org/apache/maven/sandbox/issue/rissue/filters.yaml
\ No newline at end of file

Propchange: maven/sandbox/issue/rbot/dist/filters.yaml
------------------------------------------------------------------------------
    svn:special = *

Added: maven/sandbox/issue/rbot/dist/keyword.db
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/keyword.db?rev=357245&view=auto
==============================================================================
Binary file - no diff available.

Propchange: maven/sandbox/issue/rbot/dist/keyword.db
------------------------------------------------------------------------------
    svn:mime-type = application/octet-stream

Added: maven/sandbox/issue/rbot/dist/levels.rbot
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/levels.rbot?rev=357245&view=auto
==============================================================================
    (empty)

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1 @@
+link /home/jvanzyl/js/org/apache/maven/sandbox/issue/rissue/issue.rb
\ No newline at end of file

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/issue.rb
------------------------------------------------------------------------------
    svn:special = *

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1 @@
+link /home/jvanzyl/js/org/apache/maven/sandbox/issue/rissue/jira4r.rb
\ No newline at end of file

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/jira4r.rb
------------------------------------------------------------------------------
    svn:special = *

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,203 @@
+module Irc
+
+  # globmask:: glob to test with
+  # netmask::  netmask to test against
+  # Compare a netmask with a standard IRC glob, e.g foo!bar@baz.com would
+  # match *!*@baz.com, foo!*@*, *!bar@*, etc.
+  def Irc.netmaskmatch(globmask, netmask)
+    regmask = globmask.gsub(/\*/, ".*?")
+    return true if(netmask =~ /#{regmask}/i)
+    return false
+  end
+
+  # check if a string is an actual IRC hostmask
+  def Irc.ismask(mask)
+    mask =~ /^.+!.+@.+$/
+  end
+
+  
+  # User-level authentication to allow/disallow access to bot commands based
+  # on hostmask and userlevel.
+  class IrcAuth
+    BotConfig.register BotConfigStringValue.new('auth.password',
+      :default => "rbotauth", :wizard => true,
+      :desc => "Your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)")
+    
+    # create a new IrcAuth instance.
+    # bot:: associated bot class
+    def initialize(bot)
+      @bot = bot
+      @users = Hash.new(0)
+      @levels = Hash.new(0)
+      if(File.exist?("#{@bot.botclass}/users.rbot"))
+        IO.foreach("#{@bot.botclass}/users.rbot") do |line|
+          if(line =~ /\s*(\d+)\s*(\S+)/)
+            level = $1.to_i
+            mask = $2
+            @users[mask] = level
+          end
+        end
+      end
+      if(File.exist?("#{@bot.botclass}/levels.rbot"))
+        IO.foreach("#{@bot.botclass}/levels.rbot") do |line|
+          if(line =~ /\s*(\d+)\s*(\S+)/)
+            level = $1.to_i
+            command = $2
+            @levels[command] = level
+          end
+        end
+      end
+    end
+
+    # save current users and levels to files.
+    # levels are written to #{botclass}/levels.rbot
+    # users are written to #{botclass}/users.rbot
+    def save
+      Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}"))
+      File.open("#{@bot.botclass}/users.rbot", "w") do |file|
+        @users.each do |key, value|
+          file.puts "#{value} #{key}"
+        end
+      end
+      File.open("#{@bot.botclass}/levels.rbot", "w") do |file|
+        @levels.each do |key, value|
+          file.puts "#{value} #{key}"
+        end
+      end
+    end
+
+    # command:: command user wishes to perform
+    # mask::    hostmask of user
+    # tell::    optional recipient for "insufficient auth" message
+    #
+    # returns true if user with hostmask +mask+ is permitted to perform
+    # +command+ optionally pass tell as the target for the "insufficient auth"
+    # message, if the user is not authorised
+    def allow?(command, mask, tell=nil)
+      auth = userlevel(mask)
+      if(auth >= @levels[command])
+        return true
+      else
+        debug "#{mask} is not allowed to perform #{command}"
+        @bot.say tell, "insufficient \"#{command}\" auth (have #{auth}, need #{@levels[command]})" if tell
+        return false
+      end
+    end
+
+    # add user with hostmask matching +mask+ with initial auth level +level+
+    def useradd(mask, level)
+      if(Irc.ismask(mask))
+        @users[mask] = level
+      end
+    end
+    
+    # mask:: mask of user to remove
+    # remove user with mask +mask+
+    def userdel(mask)
+      if(Irc.ismask(mask))
+        @users.delete(mask)
+      end
+    end
+
+    # command:: command to adjust
+    # level::   new auth level for the command
+    # set required auth level of +command+ to +level+
+    def setlevel(command, level)
+      @levels[command] = level
+    end
+
+    # specific users.
+    # mask:: mask of user
+    # returns the authlevel of user with mask +mask+
+    # finds the matching user which has the highest authlevel (so you can have
+    # a default level of 5 for *!*@*, and yet still give higher levels to
+    def userlevel(mask)
+      # go through hostmask list, find match with _highest_ level (all users
+      # will match *!*@*)
+      level = 0
+      @users.each {|user,userlevel|
+        if(Irc.netmaskmatch(user, mask))
+          level = userlevel if userlevel > level
+        end
+      }
+      level
+    end
+
+    # return all currently defined commands (for which auth is required) and
+    # their required authlevels
+    def showlevels
+      reply = "Current levels are:"
+      @levels.sort.each {|a|
+        key = a[0]
+        value = a[1]
+        reply += " #{key}(#{value})"
+      }
+      reply
+    end
+
+    # return all currently defined users and their authlevels
+    def showusers
+      reply = "Current users are:"
+      @users.sort.each {|a|
+        key = a[0]
+        value = a[1]
+        reply += " #{key}(#{value})"
+      }
+      reply
+    end
+    
+    # module help
+    def help(topic="")
+      case topic
+        when "setlevel"
+          return "setlevel <command> <level> => Sets required level for <command> to <level> (private addressing only)"
+        when "useradd"
+          return "useradd <mask> <level> => Add user <mask> at level <level> (private addressing only)"
+        when "userdel"
+          return "userdel <mask> => Remove user <mask> (private addressing only)"
+        when "auth"
+          return "auth <masterpw> => Recognise your hostmask as bot master (private addressing only)"
+        when "levels"
+          return "levels => list commands and their required levels (private addressing only)"
+        when "users"
+          return "users => list users and their levels (private addressing only)"
+        else
+          return "Auth module (User authentication) topics: setlevel, useradd, userdel, auth, levels, users"
+      end
+    end
+
+    # privmsg handler
+    def privmsg(m)
+     if(m.address? && m.private?)
+      case m.message
+        when (/^setlevel\s+(\S+)\s+(\d+)$/)
+          if(@bot.auth.allow?("auth", m.source, m.replyto))
+            @bot.auth.setlevel($1, $2.to_i)
+            m.reply "level for #$1 set to #$2"
+          end
+        when (/^useradd\s+(\S+)\s+(\d+)/)
+          if(@bot.auth.allow?("auth", m.source, m.replyto))
+            @bot.auth.useradd($1, $2.to_i)
+            m.reply "added user #$1 at level #$2"
+          end
+        when (/^userdel\s+(\S+)/)
+          if(@bot.auth.allow?("auth", m.source, m.replyto))
+            @bot.auth.userdel($1)
+            m.reply "user #$1 is gone"
+          end
+        when (/^auth\s+(\S+)/)
+          if($1 == @bot.config["auth.password"])
+            @bot.auth.useradd(Regexp.escape(m.source), 1000)
+            m.reply "Identified, security level maxed out"
+          else
+            m.reply "incorrect password"
+          end
+        when ("levels")
+          m.reply @bot.auth.showlevels if(@bot.auth.allow?("config", m.source, m.replyto))
+        when ("users")
+          m.reply @bot.auth.showusers if(@bot.auth.allow?("config", m.source, m.replyto))
+      end
+     end
+    end
+  end
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/auth.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,54 @@
+module Irc
+
+  # class to store IRC channel data (users, topic, per-channel configurations)
+  class IRCChannel
+    # name of channel
+    attr_reader :name
+    
+    # current channel topic
+    attr_reader :topic
+    
+    # hash containing users currently in the channel
+    attr_accessor :users
+    
+    # if true, bot won't talk in this channel
+    attr_accessor :quiet
+    
+    # name:: channel name
+    # create a new IRCChannel
+    def initialize(name)
+      @name = name
+      @users = Hash.new
+      @quiet = false
+	  @topic = Topic.new
+    end
+
+    # eg @bot.channels[chan].topic = topic
+    def topic=(name)
+      @topic.name = name
+    end
+
+    # class to store IRC channel topic information
+    class Topic
+      # topic name
+      attr_accessor :name
+
+      # timestamp
+      attr_accessor :timestamp
+
+      # topic set by
+      attr_accessor :by
+
+      def initialize
+        @name = ""
+      end
+
+      # when called like "puts @bots.channels[chan].topic"
+      def to_s
+        @name
+      end
+    end
+
+  end
+
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/channel.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,381 @@
+module Irc
+
+  require 'yaml'
+  require 'rbot/messagemapper'
+
+  unless YAML.respond_to?(:load_file)
+      def YAML.load_file( filepath )
+        File.open( filepath ) do |f|
+          YAML::load( f )
+        end
+      end
+  end
+
+  class BotConfigValue
+    # allow the definition order to be preserved so that sorting by
+    # definition order is possible. The BotConfigWizard does this to allow
+    # the :wizard questions to be in a sensible order.
+    @@order = 0
+    attr_reader :type
+    attr_reader :desc
+    attr_reader :key
+    attr_reader :wizard
+    attr_reader :requires_restart
+    attr_reader :order
+    def initialize(key, params)
+      unless key =~ /^.+\..+$/
+        raise ArgumentError,"key must be of the form 'module.name'"
+      end
+      @order = @@order
+      @@order += 1
+      @key = key
+      if params.has_key? :default
+        @default = params[:default]
+      else
+        @default = false
+      end
+      @desc = params[:desc]
+      @type = params[:type] || String
+      @on_change = params[:on_change]
+      @validate = params[:validate]
+      @wizard = params[:wizard]
+      @requires_restart = params[:requires_restart]
+    end
+    def default
+      if @default.instance_of?(Proc)
+        @default.call
+      else
+        @default
+      end
+    end
+    def get
+      return BotConfig.config[@key] if BotConfig.config.has_key?(@key)
+      return @default
+    end
+    alias :value :get
+    def set(value, on_change = true)
+      BotConfig.config[@key] = value
+      @on_change.call(BotConfig.bot, value) if on_change && @on_change
+    end
+    def unset
+      BotConfig.config.delete(@key)
+    end
+
+    # set string will raise ArgumentErrors on failed parse/validate
+    def set_string(string, on_change = true)
+      value = parse string
+      if validate value
+        set value, on_change
+      else
+        raise ArgumentError, "invalid value: #{string}"
+      end
+    end
+    
+    # override this. the default will work for strings only
+    def parse(string)
+      string
+    end
+
+    def to_s
+      get.to_s
+    end
+
+    private
+    def validate(value)
+      return true unless @validate
+      if @validate.instance_of?(Proc)
+        return @validate.call(value)
+      elsif @validate.instance_of?(Regexp)
+        raise ArgumentError, "validation via Regexp only supported for strings!" unless value.instance_of? String
+        return @validate.match(value)
+      else
+        raise ArgumentError, "validation type #{@validate.class} not supported"
+      end
+    end
+  end
+
+  class BotConfigStringValue < BotConfigValue
+  end
+  class BotConfigBooleanValue < BotConfigValue
+    def parse(string)
+      return true if string == "true"
+      return false if string == "false"
+      raise ArgumentError, "#{string} does not match either 'true' or 'false'"
+    end
+  end
+  class BotConfigIntegerValue < BotConfigValue
+    def parse(string)
+      raise ArgumentError, "not an integer: #{string}" unless string =~ /^-?\d+$/
+      string.to_i
+    end
+  end
+  class BotConfigFloatValue < BotConfigValue
+    def parse(string)
+      raise ArgumentError, "not a float #{string}" unless string =~ /^-?[\d.]+$/
+      string.to_f
+    end
+  end
+  class BotConfigArrayValue < BotConfigValue
+    def parse(string)
+      string.split(/,\s+/)
+    end
+    def to_s
+      get.join(", ")
+    end
+  end
+  class BotConfigEnumValue < BotConfigValue
+    def initialize(key, params)
+      super
+      @values = params[:values]
+    end
+    def values
+      if @values.instance_of?(Proc)
+        return @values.call(BotConfig.bot)
+      else
+        return @values
+      end
+    end
+    def parse(string)
+      unless values.include?(string)
+        raise ArgumentError, "invalid value #{string}, allowed values are: " + @values.join(", ")
+      end
+      string
+    end
+    def desc
+      "#{@desc} [valid values are: " + values.join(", ") + "]"
+    end
+  end
+
+  # container for bot configuration
+  class BotConfig
+    # Array of registered BotConfigValues for defaults, types and help
+    @@items = Hash.new
+    def BotConfig.items
+      @@items
+    end
+    # Hash containing key => value pairs for lookup and serialisation
+    @@config = Hash.new(false)
+    def BotConfig.config
+      @@config
+    end
+    def BotConfig.bot
+      @@bot
+    end
+    
+    def BotConfig.register(item)
+      unless item.kind_of?(BotConfigValue)
+        raise ArgumentError,"item must be a BotConfigValue"
+      end
+      @@items[item.key] = item
+    end
+
+    # currently we store values in a hash but this could be changed in the
+    # future. We use hash semantics, however.
+    # components that register their config keys and setup defaults are
+    # supported via []
+    def [](key)
+      return @@items[key].value if @@items.has_key?(key)
+      # try to still support unregistered lookups
+      return @@config[key] if @@config.has_key?(key)
+      return false
+    end
+
+    # TODO should I implement this via BotConfigValue or leave it direct?
+    #    def []=(key, value)
+    #    end
+    
+    # pass everything else through to the hash
+    def method_missing(method, *args, &block)
+      return @@config.send(method, *args, &block)
+    end
+
+    def handle_list(m, params)
+      modules = []
+      if params[:module]
+        @@items.each_key do |key|
+          mod, name = key.split('.')
+          next unless mod == params[:module]
+          modules.push key unless modules.include?(name)
+        end
+        if modules.empty?
+          m.reply "no such module #{params[:module]}"
+        else
+          m.reply modules.join(", ")
+        end
+      else
+        @@items.each_key do |key|
+          name = key.split('.').first
+          modules.push name unless modules.include?(name)
+        end
+        m.reply "modules: " + modules.join(", ")
+      end
+    end
+
+    def handle_get(m, params)
+      key = params[:key]
+      unless @@items.has_key?(key)
+        m.reply "no such config key #{key}"
+        return
+      end
+      value = @@items[key].to_s
+      m.reply "#{key}: #{value}"
+    end
+
+    def handle_desc(m, params)
+      key = params[:key]
+      unless @@items.has_key?(key)
+        m.reply "no such config key #{key}"
+      end
+      puts @@items[key].inspect
+      m.reply "#{key}: #{@@items[key].desc}"
+    end
+
+    def handle_unset(m, params)
+      key = params[:key]
+      unless @@items.has_key?(key)
+        m.reply "no such config key #{key}"
+      end
+      @@items[key].unset
+      handle_get(m, params)
+    end
+
+    def handle_set(m, params)
+      key = params[:key]
+      value = params[:value].to_s
+      unless @@items.has_key?(key)
+        m.reply "no such config key #{key}"
+        return
+      end
+      begin
+        @@items[key].set_string(value)
+      rescue ArgumentError => e
+        m.reply "failed to set #{key}: #{e.message}"
+        return
+      end
+      if @@items[key].requires_restart
+        m.reply "this config change will take effect on the next restart"
+      else
+        m.okay
+      end
+    end
+
+    def handle_help(m, params)
+      topic = params[:topic]
+      case topic
+      when false
+        m.reply "config module - bot configuration. usage: list, desc, get, set, unset"
+      when "list"
+        m.reply "config list => list configuration modules, config list <module> => list configuration keys for module <module>"
+      when "get"
+        m.reply "config get <key> => get configuration value for key <key>"
+      when "unset"
+        m.reply "reset key <key> to the default"
+      when "set"
+        m.reply "config set <key> <value> => set configuration value for key <key> to <value>"
+      when "desc"
+        m.reply "config desc <key> => describe what key <key> configures"
+      else
+        m.reply "no help for config #{topic}"
+      end
+    end
+    def usage(m,params)
+      m.reply "incorrect usage, try '#{@@bot.nick}: help config'"
+    end
+
+    # bot:: parent bot class
+    # create a new config hash from #{botclass}/conf.rbot
+    def initialize(bot)
+      @@bot = bot
+
+      # respond to config messages, to provide runtime configuration
+      # management
+      # messages will be:
+      #  get
+      #  set
+      #  unset
+      #  desc
+      #  and for arrays:
+      #    add TODO
+      #    remove TODO
+      @handler = MessageMapper.new(self)
+      @handler.map 'config list :module', :action => 'handle_list',
+                   :defaults => {:module => false}
+      @handler.map 'config get :key', :action => 'handle_get'
+      @handler.map 'config desc :key', :action => 'handle_desc'
+      @handler.map 'config describe :key', :action => 'handle_desc'
+      @handler.map 'config set :key *value', :action => 'handle_set'
+      @handler.map 'config unset :key', :action => 'handle_unset'
+      @handler.map 'config help :topic', :action => 'handle_help',
+                   :defaults => {:topic => false}
+      @handler.map 'help config :topic', :action => 'handle_help',
+                   :defaults => {:topic => false}
+      
+      if(File.exist?("#{@@bot.botclass}/conf.yaml"))
+        begin
+          newconfig = YAML::load_file("#{@@bot.botclass}/conf.yaml")         
+          @@config.update newconfig
+          return
+        rescue
+          $stderr.puts "failed to read conf.yaml: #{$!}"
+        end
+      end
+      # if we got here, we need to run the first-run wizard
+      BotConfigWizard.new(@@bot).run
+      # save newly created config
+      save
+    end
+
+    # write current configuration to #{botclass}/conf.rbot
+    def save
+      begin
+        File.open("#{@@bot.botclass}/conf.yaml.new", "w") do |file|
+          file.puts @@config.to_yaml
+        end
+        File.rename("#{@@bot.botclass}/conf.yaml.new",
+                    "#{@@bot.botclass}/conf.yaml")
+      rescue
+        $stderr.puts "failed to write configuration file conf.yaml! #{$!}"
+      end
+    end
+
+    def privmsg(m)
+      @handler.handle(m)
+    end
+  end
+
+  class BotConfigWizard
+    def initialize(bot)
+      @bot = bot
+      @questions = BotConfig.items.values.find_all {|i| i.wizard }
+    end
+    
+    def run()
+      puts "First time rbot configuration wizard"
+      puts "===================================="
+      puts "This is the first time you have run rbot with a config directory of:"
+      puts @bot.botclass
+      puts "This wizard will ask you a few questions to get you started."
+      puts "The rest of rbot's configuration can be manipulated via IRC once"
+      puts "rbot is connected and you are auth'd."
+      puts "-----------------------------------"
+
+      return unless @questions
+      @questions.sort{|a,b| a.order <=> b.order }.each do |q|
+        puts q.desc
+        begin
+          print q.key + " [#{q.to_s}]: "
+          response = STDIN.gets
+          response.chop!
+          unless response.empty?
+            q.set_string response, false
+          end
+          puts "configured #{q.key} => #{q.to_s}"
+          puts "-----------------------------------"
+        rescue ArgumentError => e
+          puts "failed to set #{q.key}: #{e.message}"
+          retry
+        end
+      end
+    end
+  end
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/config.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,112 @@
+begin
+  require 'bdb'
+rescue Exception => e
+  puts "Got exception: "+e
+  puts "rbot couldn't load the bdb module, perhaps you need to install it? try: http://www.ruby-lang.org/en/raa-list.rhtml?name=bdb"
+  exit 2
+end
+
+# make BTree lookups case insensitive
+module BDB
+  class CIBtree < Btree
+    def bdb_bt_compare(a, b)
+      a.downcase <=> b.downcase
+    end
+  end
+end
+
+module Irc
+
+  # DBHash is for tying a hash to disk (using bdb).
+  # Call it with an identifier, for example "mydata". It'll look for
+  # mydata.db, if it exists, it will load and reference that db.
+  # Otherwise it'll create and empty db called mydata.db
+  class DBHash
+    
+    # absfilename:: use +key+ as an actual filename, don't prepend the bot's
+    #               config path and don't append ".db"
+    def initialize(bot, key, absfilename=false)
+      @bot = bot
+      @key = key
+      if absfilename && File.exist?(key)
+        # db already exists, use it
+        @db = DBHash.open_db(key)
+      elsif File.exist?(@bot.botclass + "/#{key}.db")
+        # db already exists, use it
+        @db = DBHash.open_db(@bot.botclass + "/#{key}.db")
+      elsif absfilename
+        # create empty db
+        @db = DBHash.create_db(key)
+      else
+        # create empty db
+        @db = DBHash.create_db(@bot.botclass + "/#{key}.db")
+      end
+    end
+
+    def method_missing(method, *args, &block)
+      return @db.send(method, *args, &block)
+    end
+
+    def DBHash.create_db(name)
+      debug "DBHash: creating empty db #{name}"
+      return BDB::Hash.open(name, nil, 
+                             BDB::CREATE | BDB::EXCL | BDB::TRUNCATE,
+                             0600, "set_pagesize" => 1024,
+                             "set_cachesize" => [(0), (32 * 1024), (0)])
+    end
+
+    def DBHash.open_db(name)
+      debug "DBHash: opening existing db #{name}"
+      return BDB::Hash.open(name, nil, 
+                             "r+", 0600, "set_pagesize" => 1024,
+                             "set_cachesize" => [(0), (32 * 1024), (0)])
+    end
+    
+  end
+
+  
+  # DBTree is a BTree equivalent of DBHash, with case insensitive lookups.
+  class DBTree
+    
+    # absfilename:: use +key+ as an actual filename, don't prepend the bot's
+    #               config path and don't append ".db"
+    def initialize(bot, key, absfilename=false)
+      @bot = bot
+      @key = key
+      if absfilename && File.exist?(key)
+        # db already exists, use it
+        @db = DBTree.open_db(key)
+      elsif absfilename
+        # create empty db
+        @db = DBTree.create_db(key)
+      elsif File.exist?(@bot.botclass + "/#{key}.db")
+        # db already exists, use it
+        @db = DBTree.open_db(@bot.botclass + "/#{key}.db")
+      else
+        # create empty db
+        @db = DBTree.create_db(@bot.botclass + "/#{key}.db")
+      end
+    end
+
+    def method_missing(method, *args, &block)
+      return @db.send(method, *args, &block)
+    end
+
+    def DBTree.create_db(name)
+      debug "DBTree: creating empty db #{name}"
+      return BDB::CIBtree.open(name, nil, 
+                             BDB::CREATE | BDB::EXCL | BDB::TRUNCATE,
+                             0600, "set_pagesize" => 1024,
+                             "set_cachesize" => [(0), (32 * 1024), (0)])
+    end
+
+    def DBTree.open_db(name)
+      debug "DBTree: opening existing db #{name}"
+      return BDB::CIBtree.open(name, nil, 
+                             "r+", 0600, "set_pagesize" => 1024,
+                             "set_cachesize" => [0, 32 * 1024, 0])
+    end
+    
+  end
+
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/dbhash.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,141 @@
+module Irc
+module Utils
+
+require 'resolv'
+require 'net/http'
+Net::HTTP.version_1_2
+  
+# class for making http requests easier (mainly for plugins to use)
+# this class can check the bot proxy configuration to determine if a proxy
+# needs to be used, which includes support for per-url proxy configuration.
+class HttpUtil
+    BotConfig.register BotConfigBooleanValue.new('http.use_proxy',
+      :default => false, :desc => "should a proxy be used for HTTP requests?")
+    BotConfig.register BotConfigStringValue.new('http.proxy_uri', :default => false,
+      :desc => "Proxy server to use for HTTP requests (URI, e.g http://proxy.host:port)")
+    BotConfig.register BotConfigStringValue.new('http.proxy_user',
+      :default => nil,
+      :desc => "User for authenticating with the http proxy (if required)")
+    BotConfig.register BotConfigStringValue.new('http.proxy_pass',
+      :default => nil,
+      :desc => "Password for authenticating with the http proxy (if required)")
+    BotConfig.register BotConfigArrayValue.new('http.proxy_include',
+      :default => [],
+      :desc => "List of regexps to check against a URI's hostname/ip to see if we should use the proxy to access this URI. All URIs are proxied by default if the proxy is set, so this is only required to re-include URIs that might have been excluded by the exclude list. e.g. exclude /.*\.foo\.com/, include bar\.foo\.com")
+    BotConfig.register BotConfigArrayValue.new('http.proxy_exclude',
+      :default => [],
+      :desc => "List of regexps to check against a URI's hostname/ip to see if we should use avoid the proxy to access this URI and access it directly")
+
+  def initialize(bot)
+    @bot = bot
+    @headers = {
+      'User-Agent' => "rbot http util #{$version} (http://linuxbrit.co.uk/rbot/)",
+    }
+  end
+
+  # if http_proxy_include or http_proxy_exclude are set, then examine the
+  # uri to see if this is a proxied uri
+  # the in/excludes are a list of regexps, and each regexp is checked against
+  # the server name, and its IP addresses
+  def proxy_required(uri)
+    use_proxy = true
+    if @bot.config["http.proxy_exclude"].empty? && @bot.config["http.proxy_include"].empty?
+      return use_proxy
+    end
+
+    list = [uri.host]
+    begin
+      list.concat Resolv.getaddresses(uri.host)
+    rescue StandardError => err
+      puts "warning: couldn't resolve host uri.host"
+    end
+
+    unless @bot.config["http.proxy_exclude"].empty?
+      re = @bot.config["http.proxy_exclude"].collect{|r| Regexp.new(r)}
+      re.each do |r|
+        list.each do |item|
+          if r.match(item)
+            use_proxy = false
+            break
+          end
+        end
+      end
+    end
+    unless @bot.config["http.proxy_include"].empty?
+      re = @bot.config["http.proxy_include"].collect{|r| Regexp.new(r)}
+      re.each do |r|
+        list.each do |item|
+          if r.match(item)
+            use_proxy = true
+            break
+          end
+        end
+      end
+    end
+    debug "using proxy for uri #{uri}?: #{use_proxy}"
+    return use_proxy
+  end
+
+  # uri:: Uri to create a proxy for
+  #
+  # return a net/http Proxy object, which is configured correctly for
+  # proxying based on the bot's proxy configuration. 
+  # This will include per-url proxy configuration based on the bot config
+  # +http_proxy_include/exclude+ options.
+  def get_proxy(uri)
+    proxy = nil
+    proxy_host = nil
+    proxy_port = nil
+    proxy_user = nil
+    proxy_pass = nil
+
+    if @bot.config["http.use_proxy"]
+      if (ENV['http_proxy'])
+        proxy = URI.parse ENV['http_proxy'] rescue nil
+      end
+      if (@bot.config["http.proxy_uri"])
+        proxy = URI.parse @bot.config["http.proxy_uri"] rescue nil
+      end
+      if proxy
+        debug "proxy is set to #{proxy.uri}"
+        if proxy_required(uri)
+          proxy_host = proxy.host
+          proxy_port = proxy.port
+          proxy_user = @bot.config["http.proxy_user"]
+          proxy_pass = @bot.config["http.proxy_pass"]
+        end
+      end
+    end
+    
+    return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port)
+  end
+
+  # uri::         uri to query (Uri object)
+  # readtimeout:: timeout for reading the response
+  # opentimeout:: timeout for opening the connection
+  #
+  # simple get request, returns response body if the status code is 200 and
+  # the request doesn't timeout.
+  def get(uri, readtimeout=10, opentimeout=5)
+    proxy = get_proxy(uri)
+    proxy.open_timeout = opentimeout
+    proxy.read_timeout = readtimeout
+   
+    begin
+      proxy.start() {|http|
+        resp = http.get(uri.request_uri(), @headers)
+        if resp.code == "200"
+          return resp.body
+        else
+          puts "HttpUtil.get return code #{resp.code} #{resp.body}"
+        end
+        return nil
+      }
+    rescue StandardError, Timeout::Error => e
+      $stderr.puts "HttpUtil.get exception: #{e}, while trying to get #{uri}"
+    end
+    return nil
+  end
+end
+end
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/httputil.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,864 @@
+require 'thread'
+require 'etc'
+require 'fileutils'
+
+$debug = false unless $debug
+# print +message+ if debugging is enabled
+def debug(message=nil)
+  stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
+  print "D: [#{stamp}] #{message}\n" if($debug && message)
+  #yield
+end
+
+# these first
+require 'rbot/rbotconfig'
+require 'rbot/config'
+require 'rbot/utils'
+
+require 'rbot/rfc2812'
+require 'rbot/keywords'
+require 'rbot/ircsocket'
+require 'rbot/auth'
+require 'rbot/timer'
+require 'rbot/plugins'
+require 'rbot/channel'
+require 'rbot/message'
+require 'rbot/language'
+require 'rbot/dbhash'
+require 'rbot/registry'
+require 'rbot/httputil'
+
+module Irc
+
+# Main bot class, which manages the various components, receives messages,
+# handles them or passes them to plugins, and contains core functionality.
+class IrcBot
+  # the bot's current nickname
+  attr_reader :nick
+  
+  # the bot's IrcAuth data
+  attr_reader :auth
+  
+  # the bot's BotConfig data
+  attr_reader :config
+  
+  # the botclass for this bot (determines configdir among other things)
+  attr_reader :botclass
+  
+  # used to perform actions periodically (saves configuration once per minute
+  # by default)
+  attr_reader :timer
+  
+  # bot's Language data
+  attr_reader :lang
+
+  # channel info for channels the bot is in
+  attr_reader :channels
+
+  # bot's irc socket
+  attr_reader :socket
+
+  # bot's object registry, plugins get an interface to this for persistant
+  # storage (hash interface tied to a bdb file, plugins use Accessors to store
+  # and restore objects in their own namespaces.)
+  attr_reader :registry
+
+  # bot's httputil help object, for fetching resources via http. Sets up
+  # proxies etc as defined by the bot configuration/environment
+  attr_reader :httputil
+
+  # create a new IrcBot with botclass +botclass+
+  def initialize(botclass, datadir, params = {})
+    # BotConfig for the core bot
+    BotConfig.register BotConfigStringValue.new('server.name',
+      :default => "localhost", :requires_restart => true,
+      :desc => "What server should the bot connect to?",
+      :wizard => true)
+    BotConfig.register BotConfigIntegerValue.new('server.port',
+      :default => 6667, :type => :integer, :requires_restart => true,
+      :desc => "What port should the bot connect to?", 
+      :validate => Proc.new {|v| v > 0}, :wizard => true)
+    BotConfig.register BotConfigStringValue.new('server.password',
+      :default => false, :requires_restart => true,
+      :desc => "Password for connecting to this server (if required)",
+      :wizard => true)
+    BotConfig.register BotConfigStringValue.new('server.bindhost',
+      :default => false, :requires_restart => true,
+      :desc => "Specific local host or IP for the bot to bind to (if required)",
+      :wizard => true)
+    BotConfig.register BotConfigIntegerValue.new('server.reconnect_wait',
+      :default => 5, :validate => Proc.new{|v| v >= 0},
+      :desc => "Seconds to wait before attempting to reconnect, on disconnect")
+    BotConfig.register BotConfigStringValue.new('irc.nick', :default => "rbot",
+      :desc => "IRC nickname the bot should attempt to use", :wizard => true,
+      :on_change => Proc.new{|bot, v| bot.sendq "NICK #{v}" })
+    BotConfig.register BotConfigStringValue.new('irc.user', :default => "rbot",
+      :requires_restart => true,
+      :desc => "local user the bot should appear to be", :wizard => true)
+    BotConfig.register BotConfigArrayValue.new('irc.join_channels',
+      :default => [], :wizard => true,
+      :desc => "What channels the bot should always join at startup. List multiple channels using commas to separate. If a channel requires a password, use a space after the channel name. e.g: '#chan1, #chan2, #secretchan secritpass, #chan3'")
+    BotConfig.register BotConfigIntegerValue.new('core.save_every',
+      :default => 60, :validate => Proc.new{|v| v >= 0},
+      # TODO change timer via on_change proc
+      :desc => "How often the bot should persist all configuration to disk (in case of a server crash, for example")
+    BotConfig.register BotConfigFloatValue.new('server.sendq_delay',
+      :default => 2.0, :validate => Proc.new{|v| v >= 0},
+      :desc => "(flood prevention) the delay between sending messages to the server (in seconds)",
+      :on_change => Proc.new {|bot, v| bot.socket.sendq_delay = v })
+    BotConfig.register BotConfigIntegerValue.new('server.sendq_burst',
+      :default => 4, :validate => Proc.new{|v| v >= 0},
+      :desc => "(flood prevention) max lines to burst to the server before throttling. Most ircd's allow bursts of up 5 lines, with non-burst limits of 512 bytes/2 seconds",
+      :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v })
+    BotConfig.register BotConfigIntegerValue.new('server.ping_timeout',
+      :default => 10, :validate => Proc.new{|v| v >= 0},
+      :on_change => Proc.new {|bot, v| bot.start_server_pings},
+      :desc => "reconnect if server doesn't respond to PING within this many seconds (set to 0 to disable)")
+
+    @argv = params[:argv]
+
+    #unless FileTest.directory? Config::datadir
+    #  puts "data directory '#{Config::datadir}' not found, did you setup.rb?"
+    #  exit 2
+    #end
+    
+    botclass = "#{Etc.getpwuid(Process::Sys.geteuid)[:dir]}/.rbot" unless botclass
+    #botclass = "#{ENV['HOME']}/.rbot" unless botclass
+    @botclass = botclass.gsub(/\/$/, "")
+
+    unless FileTest.directory? botclass
+      puts "no #{botclass} directory found, creating from templates.."
+      if FileTest.exist? botclass
+        puts "Error: file #{botclass} exists but isn't a directory"
+        exit 2
+      end
+      FileUtils.cp_r Config::datadir+'/templates', botclass
+    end
+    
+    Dir.mkdir("#{botclass}/logs") unless File.exist?("#{botclass}/logs")
+
+    @ping_timer = nil
+    @pong_timer = nil
+    @last_ping = nil
+    @startup_time = Time.new
+    @config = BotConfig.new(self)
+    @config['datadir'] = datadir
+    # TODO background self after botconfig has a chance to run wizard
+    @timer = Timer::Timer.new(1.0) # only need per-second granularity
+    @registry = BotRegistry.new self
+    @timer.add(@config['core.save_every']) { save } if @config['core.save_every']
+    @channels = Hash.new
+    @logs = Hash.new
+    @httputil = Utils::HttpUtil.new(self)
+    @lang = Language::Language.new(@config['core.language'],@config['datadir'])
+    @keywords = Keywords.new(self)
+    @auth = IrcAuth.new(self)
+
+    Dir.mkdir("#{botclass}/plugins") unless File.exist?("#{botclass}/plugins")
+    @plugins = Plugins::Plugins.new(self, ["#{botclass}/plugins"], @config['datadir'])
+
+    @socket = IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst'])
+    @nick = @config['irc.nick']
+
+    @client = IrcClient.new
+    @client[:privmsg] = proc { |data|
+      message = PrivMessage.new(self, data[:source], data[:target], data[:message])
+      onprivmsg(message)
+    }
+    @client[:notice] = proc { |data|
+      message = NoticeMessage.new(self, data[:source], data[:target], data[:message])
+      # pass it off to plugins that want to hear everything
+      @plugins.delegate "listen", message
+    }
+    @client[:motd] = proc { |data|
+      data[:motd].each_line { |line|
+        log "MOTD: #{line}", "server"
+      }
+    }
+    @client[:nicktaken] = proc { |data| 
+      nickchg "#{data[:nick]}_"
+    }
+    @client[:badnick] = proc {|data| 
+      puts "WARNING, bad nick (#{data[:nick]})"
+    }
+    @client[:ping] = proc {|data|
+      # (jump the queue for pongs)
+      @socket.puts "PONG #{data[:pingid]}"
+    }
+    @client[:pong] = proc {|data|
+      @last_ping = nil
+    }
+    @client[:nick] = proc {|data|
+      sourcenick = data[:sourcenick]
+      nick = data[:nick]
+      m = NickMessage.new(self, data[:source], data[:sourcenick], data[:nick])
+      if(sourcenick == @nick)
+        debug "my nick is now #{nick}"
+        @nick = nick
+      end
+      @channels.each {|k,v|
+        if(v.users.has_key?(sourcenick))
+          log "@ #{sourcenick} is now known as #{nick}", k
+          v.users[nick] = v.users[sourcenick]
+          v.users.delete(sourcenick)
+        end
+      }
+      @plugins.delegate("listen", m)
+      @plugins.delegate("nick", m)
+    }
+    @client[:quit] = proc {|data|
+      source = data[:source]
+      sourcenick = data[:sourcenick]
+      sourceurl = data[:sourceaddress]
+      message = data[:message]
+      m = QuitMessage.new(self, data[:source], data[:sourcenick], data[:message])
+      if(data[:sourcenick] =~ /#{Regexp.escape(@nick)}/i)
+      else
+        @channels.each {|k,v|
+          if(v.users.has_key?(sourcenick))
+            log "@ Quit: #{sourcenick}: #{message}", k
+            v.users.delete(sourcenick)
+          end
+        }
+      end
+      @plugins.delegate("listen", m)
+      @plugins.delegate("quit", m)
+    }
+    @client[:mode] = proc {|data|
+      source = data[:source]
+      sourcenick = data[:sourcenick]
+      sourceurl = data[:sourceaddress]
+      channel = data[:channel]
+      targets = data[:targets]
+      modestring = data[:modestring]
+      log "@ Mode #{modestring} #{targets} by #{sourcenick}", channel
+    }
+    @client[:welcome] = proc {|data|
+      log "joined server #{data[:source]} as #{data[:nick]}", "server"
+      debug "I think my nick is #{@nick}, server thinks #{data[:nick]}"
+      if data[:nick] && data[:nick].length > 0
+        @nick = data[:nick]
+      end
+
+      @plugins.delegate("connect")
+
+      @config['irc.join_channels'].each {|c|
+        debug "autojoining channel #{c}"
+        if(c =~ /^(\S+)\s+(\S+)$/i)
+          join $1, $2
+        else
+          join c if(c)
+        end
+      }
+    }
+    @client[:join] = proc {|data|
+      m = JoinMessage.new(self, data[:source], data[:channel], data[:message])
+      onjoin(m)
+    }
+    @client[:part] = proc {|data|
+      m = PartMessage.new(self, data[:source], data[:channel], data[:message])
+      onpart(m)
+    }
+    @client[:kick] = proc {|data|
+      m = KickMessage.new(self, data[:source], data[:target],data[:channel],data[:message]) 
+      onkick(m)
+    }
+    @client[:invite] = proc {|data|
+      if(data[:target] =~ /^#{Regexp.escape(@nick)}$/i)
+        join data[:channel] if (@auth.allow?("join", data[:source], data[:sourcenick]))
+      end
+    }
+    @client[:changetopic] = proc {|data|
+      channel = data[:channel]
+      sourcenick = data[:sourcenick]
+      topic = data[:topic]
+      timestamp = data[:unixtime] || Time.now.to_i
+      if(sourcenick == @nick)
+        log "@ I set topic \"#{topic}\"", channel
+      else
+        log "@ #{sourcenick} set topic \"#{topic}\"", channel
+      end
+      m = TopicMessage.new(self, data[:source], data[:channel], timestamp, data[:topic])
+
+      ontopic(m)
+      @plugins.delegate("listen", m)
+      @plugins.delegate("topic", m)
+    }
+    @client[:topic] = @client[:topicinfo] = proc {|data|
+      channel = data[:channel]
+      m = TopicMessage.new(self, data[:source], data[:channel], data[:unixtime], data[:topic])
+        ontopic(m)
+    }
+    @client[:names] = proc {|data|
+      channel = data[:channel]
+      users = data[:users]
+      unless(@channels[channel])
+        puts "bug: got names for channel '#{channel}' I didn't think I was in\n"
+        exit 2
+      end
+      @channels[channel].users.clear
+      users.each {|u|
+        @channels[channel].users[u[0].sub(/^[@&~+]/, '')] = ["mode", u[1]]
+      }
+    }
+    @client[:unknown] = proc {|data|
+      #debug "UNKNOWN: #{data[:serverstring]}"
+      log data[:serverstring], ".unknown"
+    }
+  end
+
+  # connect the bot to IRC
+  def connect
+    begin
+      trap("SIGINT") { quit }
+      trap("SIGTERM") { quit }
+      trap("SIGHUP") { quit }
+    rescue
+      debug "failed to trap signals, probably running on windows?"
+    end
+    begin
+      @socket.connect
+    rescue => e
+      raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e
+    end
+    @socket.puts "PASS " + @config['server.password'] if @config['server.password']
+    @socket.puts "NICK #{@nick}\nUSER #{@config['irc.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert"
+    start_server_pings
+  end
+
+  # begin event handling loop
+  def mainloop
+    while true
+      begin
+      connect
+      @timer.start
+      
+        while true
+          if @socket.select
+            break unless reply = @socket.gets
+            @client.process reply
+          end
+        end
+      # I despair of this. Some of my users get "connection reset by peer"
+      # exceptions that ARENT SocketError's. How am I supposed to handle
+      # that?
+      #rescue TimeoutError, SocketError => e
+      rescue SystemExit
+        exit 0
+      rescue Exception => e
+        puts "network exception: connection closed: #{e.inspect}"
+        puts e.backtrace.join("\n")
+        @socket.shutdown # now we reconnect
+      rescue => e
+        puts "unexpected exception: connection closed: #{e.inspect}"
+        puts e.backtrace.join("\n")
+        exit 2
+      end
+      
+      puts "disconnected"
+      @last_ping = nil
+      @channels.clear
+      @socket.clearq
+      
+      puts "waiting to reconnect"
+      sleep @config['server.reconnect_wait']
+    end
+  end
+  
+  # type:: message type
+  # where:: message target
+  # message:: message text
+  # send message +message+ of type +type+ to target +where+
+  # Type can be PRIVMSG, NOTICE, etc, but those you should really use the
+  # relevant say() or notice() methods. This one should be used for IRCd
+  # extensions you want to use in modules.
+  def sendmsg(type, where, message)
+    # limit it 440 chars + CRLF.. so we have to split long lines
+    left = 440 - type.length - where.length - 3
+    begin
+      if(left >= message.length)
+        sendq("#{type} #{where} :#{message}")
+        log_sent(type, where, message)
+        return
+      end
+      line = message.slice!(0, left)
+      lastspace = line.rindex(/\s+/)
+      if(lastspace)
+        message = line.slice!(lastspace, line.length) + message
+        message.gsub!(/^\s+/, "")
+      end
+      sendq("#{type} #{where} :#{line}")
+      log_sent(type, where, line)
+    end while(message.length > 0)
+  end
+
+  # queue an arbitraty message for the server
+  def sendq(message="")
+    # temporary
+    @socket.queue(message)
+  end
+
+  # send a notice message to channel/nick +where+
+  def notice(where, message)
+    message.each_line { |line|
+      line.chomp!
+      next unless(line.length > 0)
+      sendmsg("NOTICE", where, line)
+    }
+  end
+
+  # say something (PRIVMSG) to channel/nick +where+
+  def say(where, message)
+    message.to_s.gsub(/[\r\n]+/, "\n").each_line { |line|
+      line.chomp!
+      next unless(line.length > 0)
+      unless((where =~ /^#/) && (@channels.has_key?(where) && @channels[where].quiet))
+        sendmsg("PRIVMSG", where, line)
+      end
+    }
+  end
+
+  # perform a CTCP action with message +message+ to channel/nick +where+
+  def action(where, message)
+    sendq("PRIVMSG #{where} :\001ACTION #{message}\001")
+    if(where =~ /^#/)
+      log "* #{@nick} #{message}", where
+    elsif (where =~ /^(\S*)!.*$/)
+         log "* #{@nick}[#{where}] #{message}", $1
+    else
+         log "* #{@nick}[#{where}] #{message}", where
+    end
+  end
+
+  # quick way to say "okay" (or equivalent) to +where+
+  def okay(where)
+    say where, @lang.get("okay")
+  end
+
+  # log message +message+ to a file determined by +where+. +where+ can be a
+  # channel name, or a nick for private message logging
+  def log(message, where="server")
+    message.chomp!
+    stamp = Time.now.strftime("%Y/%m/%d %H:%M:%S")
+    where.gsub!(/[:!?$*()\/\\<>|"']/, "_")
+    unless(@logs.has_key?(where))
+      @logs[where] = File.new("#{@botclass}/logs/#{where}", "a")
+      @logs[where].sync = true
+    end
+    @logs[where].puts "[#{stamp}] #{message}"
+    #debug "[#{stamp}] <#{where}> #{message}"
+  end
+  
+  # set topic of channel +where+ to +topic+
+  def topic(where, topic)
+    sendq "TOPIC #{where} :#{topic}"
+  end
+
+  # disconnect from the server and cleanup all plugins and modules
+  def shutdown(message = nil)
+    begin
+      trap("SIGINT", "DEFAULT")
+      trap("SIGTERM", "DEFAULT")
+      trap("SIGHUP", "DEFAULT")
+    rescue
+      debug "failed to trap signals, probably running on windows?"
+    end
+    message = @lang.get("quit") if (message.nil? || message.empty?)
+    @socket.clearq
+    save
+    @plugins.cleanup
+    @channels.each_value {|v|
+      log "@ quit (#{message})", v.name
+    }
+    @registry.close
+    @socket.puts "QUIT :#{message}"
+    @socket.flush
+    @socket.shutdown
+    puts "rbot quit (#{message})"
+  end
+  
+  # message:: optional IRC quit message
+  # quit IRC, shutdown the bot
+  def quit(message=nil)
+    begin
+      shutdown(message)
+    ensure
+      exit 0
+    end
+  end
+
+  # totally shutdown and respawn the bot
+  def restart(message = false)
+    msg = message ? message : "restarting, back in #{@config['server.reconnect_wait']}..."
+    shutdown(msg)
+    sleep @config['server.reconnect_wait']
+    # now we re-exec
+    exec($0, *@argv)
+  end
+
+  # call the save method for bot's config, keywords, auth and all plugins
+  def save
+    @registry.flush
+    @config.save
+    @keywords.save
+    @auth.save
+    @plugins.save
+  end
+
+  # call the rescan method for the bot's lang, keywords and all plugins
+  def rescan
+    @lang.rescan
+    @plugins.rescan
+    @keywords.rescan
+  end
+  
+  # channel:: channel to join
+  # key::     optional channel key if channel is +s
+  # join a channel
+  def join(channel, key=nil)
+    if(key)
+      sendq "JOIN #{channel} :#{key}"
+    else
+      sendq "JOIN #{channel}"
+    end
+  end
+
+  # part a channel
+  def part(channel, message="")
+    sendq "PART #{channel} :#{message}"
+  end
+
+  # attempt to change bot's nick to +name+
+  def nickchg(name)
+      sendq "NICK #{name}"
+  end
+
+  # changing mode
+  def mode(channel, mode, target)
+      sendq "MODE #{channel} #{mode} #{target}"
+  end
+  
+  # m::     message asking for help
+  # topic:: optional topic help is requested for
+  # respond to online help requests
+  def help(topic=nil)
+    topic = nil if topic == ""
+    case topic
+    when nil
+      helpstr = "help topics: core, auth, keywords"
+      helpstr += @plugins.helptopics
+      helpstr += " (help <topic> for more info)"
+    when /^core$/i
+      helpstr = corehelp
+    when /^core\s+(.+)$/i
+      helpstr = corehelp $1
+    when /^auth$/i
+      helpstr = @auth.help
+    when /^auth\s+(.+)$/i
+      helpstr = @auth.help $1
+    when /^keywords$/i
+      helpstr = @keywords.help
+    when /^keywords\s+(.+)$/i
+      helpstr = @keywords.help $1
+    else
+      unless(helpstr = @plugins.help(topic))
+        helpstr = "no help for topic #{topic}"
+      end
+    end
+    return helpstr
+  end
+
+  # returns a string describing the current status of the bot (uptime etc)
+  def status
+    secs_up = Time.new - @startup_time
+    uptime = Utils.secs_to_string secs_up
+    return "Uptime #{uptime}, #{@plugins.length} plugins active, #{@registry.length} items stored in registry, #{@socket.lines_sent} lines sent, #{@socket.lines_received} received."
+  end
+
+  # we'll ping the server every 30 seconds or so, and expect a response
+  # before the next one come around..
+  def start_server_pings
+    @last_ping = nil
+    # stop existing timers if running
+    unless @ping_timer.nil?
+      @timer.remove @ping_timer
+      @ping_timer = nil
+    end
+    unless @pong_timer.nil?
+      @timer.remove @pong_timer
+      @pong_timer = nil
+    end
+    return unless @config['server.ping_timeout'] > 0
+    # we want to respond to a hung server within 30 secs or so
+    @ping_timer = @timer.add(30) {
+      @last_ping = Time.now
+      @socket.puts "PING :rbot"
+    }
+    @pong_timer = @timer.add(10) {
+      unless @last_ping.nil?
+        diff = Time.now - @last_ping
+        unless diff < @config['server.ping_timeout']
+          debug "no PONG from server for #{diff} seconds, reconnecting"
+          begin
+            @socket.shutdown
+            # TODO
+            # raise an exception to get back to the mainloop
+          rescue
+            debug "couldn't shutdown connection (already shutdown?)"
+          end
+          @last_ping = nil
+        end
+      end
+    }
+  end
+
+  private
+
+  # handle help requests for "core" topics
+  def corehelp(topic="")
+    case topic
+      when "quit"
+        return "quit [<message>] => quit IRC with message <message>"
+      when "restart"
+        return "restart => completely stop and restart the bot (including reconnect)"
+      when "join"
+        return "join <channel> [<key>] => join channel <channel> with secret key <key> if specified. #{@nick} also responds to invites if you have the required access level"
+      when "part"
+        return "part <channel> => part channel <channel>"
+      when "hide"
+        return "hide => part all channels"
+      when "save"
+        return "save => save current dynamic data and configuration"
+      when "rescan"
+        return "rescan => reload modules and static facts"
+      when "nick"
+        return "nick <nick> => attempt to change nick to <nick>"
+      when "say"
+        return "say <channel>|<nick> <message> => say <message> to <channel> or in private message to <nick>"
+      when "action"
+        return "action <channel>|<nick> <message> => does a /me <message> to <channel> or in private message to <nick>"
+      when "topic"
+        return "topic <channel> <message> => set topic of <channel> to <message>"
+      when "quiet"
+        return "quiet [in here|<channel>] => with no arguments, stop speaking in all channels, if \"in here\", stop speaking in this channel, or stop speaking in <channel>"
+      when "talk"
+        return "talk [in here|<channel>] => with no arguments, resume speaking in all channels, if \"in here\", resume speaking in this channel, or resume speaking in <channel>"
+      when "version"
+        return "version => describes software version"
+      when "botsnack"
+        return "botsnack => reward #{@nick} for being good"
+      when "hello"
+        return "hello|hi|hey|yo [#{@nick}] => greet the bot"
+      else
+        return "Core help topics: quit, restart, config, join, part, hide, save, rescan, nick, say, action, topic, quiet, talk, version, botsnack, hello"
+    end
+  end
+
+  # handle incoming IRC PRIVMSG +m+
+  def onprivmsg(m)
+    # log it first
+    if(m.action?)
+      if(m.private?)
+        log "* [#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
+      else
+        log "* #{m.sourcenick} #{m.message}", m.target
+      end
+    else
+      if(m.public?)
+        log "<#{m.sourcenick}> #{m.message}", m.target
+      else
+        log "[#{m.sourcenick}(#{m.sourceaddress})] #{m.message}", m.sourcenick
+      end
+    end
+
+    # pass it off to plugins that want to hear everything
+    @plugins.delegate "listen", m
+
+    if(m.private? && m.message =~ /^\001PING\s+(.+)\001/)
+      notice m.sourcenick, "\001PING #$1\001"
+      log "@ #{m.sourcenick} pinged me"
+      return
+    end
+
+    if(m.address?)
+      case m.message
+        when (/^join\s+(\S+)\s+(\S+)$/i)
+          join $1, $2 if(@auth.allow?("join", m.source, m.replyto))
+        when (/^join\s+(\S+)$/i)
+          join $1 if(@auth.allow?("join", m.source, m.replyto))
+        when (/^part$/i)
+          part m.target if(m.public? && @auth.allow?("join", m.source, m.replyto))
+        when (/^part\s+(\S+)$/i)
+          part $1 if(@auth.allow?("join", m.source, m.replyto))
+        when (/^quit(?:\s+(.*))?$/i)
+          quit $1 if(@auth.allow?("quit", m.source, m.replyto))
+        when (/^restart(?:\s+(.*))?$/i)
+          restart $1 if(@auth.allow?("quit", m.source, m.replyto))
+        when (/^hide$/i)
+          join 0 if(@auth.allow?("join", m.source, m.replyto))
+        when (/^save$/i)
+          if(@auth.allow?("config", m.source, m.replyto))
+            save
+            m.okay
+          end
+        when (/^nick\s+(\S+)$/i)
+          nickchg($1) if(@auth.allow?("nick", m.source, m.replyto))
+        when (/^say\s+(\S+)\s+(.*)$/i)
+          say $1, $2 if(@auth.allow?("say", m.source, m.replyto))
+        when (/^action\s+(\S+)\s+(.*)$/i)
+          action $1, $2 if(@auth.allow?("say", m.source, m.replyto))
+        when (/^topic\s+(\S+)\s+(.*)$/i)
+          topic $1, $2 if(@auth.allow?("topic", m.source, m.replyto))
+        when (/^mode\s+(\S+)\s+(\S+)\s+(.*)$/i)
+          mode $1, $2, $3 if(@auth.allow?("mode", m.source, m.replyto))
+        when (/^ping$/i)
+          say m.replyto, "pong"
+        when (/^rescan$/i)
+          if(@auth.allow?("config", m.source, m.replyto))
+            m.okay
+            rescan
+          end
+        when (/^quiet$/i)
+          if(auth.allow?("talk", m.source, m.replyto))
+            m.okay
+            @channels.each_value {|c| c.quiet = true }
+          end
+        when (/^quiet in (\S+)$/i)
+          where = $1
+          if(auth.allow?("talk", m.source, m.replyto))
+            m.okay
+            where.gsub!(/^here$/, m.target) if m.public?
+            @channels[where].quiet = true if(@channels.has_key?(where))
+          end
+        when (/^talk$/i)
+          if(auth.allow?("talk", m.source, m.replyto))
+            @channels.each_value {|c| c.quiet = false }
+            m.okay
+          end
+        when (/^talk in (\S+)$/i)
+          where = $1
+          if(auth.allow?("talk", m.source, m.replyto))
+            where.gsub!(/^here$/, m.target) if m.public?
+            @channels[where].quiet = false if(@channels.has_key?(where))
+            m.okay
+          end
+        when (/^status\??$/i)
+          m.reply status if auth.allow?("status", m.source, m.replyto)
+        when (/^registry stats$/i)
+          if auth.allow?("config", m.source, m.replyto)
+            m.reply @registry.stat.inspect
+          end
+        when (/^(help\s+)?config(\s+|$)/)
+          @config.privmsg(m)
+        when (/^(version)|(introduce yourself)$/i)
+          say m.replyto, "I'm a v. #{$version} rubybot, (c) Tom Gilbert - http://linuxbrit.co.uk/rbot/"
+        when (/^help(?:\s+(.*))?$/i)
+          say m.replyto, help($1)
+          #TODO move these to a "chatback" plugin
+        when (/^(botsnack|ciggie)$/i)
+          say m.replyto, @lang.get("thanks_X") % m.sourcenick if(m.public?)
+          say m.replyto, @lang.get("thanks") if(m.private?)
+        when (/^(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$)).*/i)
+          say m.replyto, @lang.get("hello_X") % m.sourcenick if(m.public?)
+          say m.replyto, @lang.get("hello") if(m.private?)
+        else
+          delegate_privmsg(m)
+      end
+    else
+      # stuff to handle when not addressed
+      case m.message
+        when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi|yo(\W|$))[\s,-.]+#{Regexp.escape(@nick)}$/i)
+          say m.replyto, @lang.get("hello_X") % m.sourcenick
+        when (/^#{Regexp.escape(@nick)}!*$/)
+          say m.replyto, @lang.get("hello_X") % m.sourcenick
+        else
+          @keywords.privmsg(m)
+      end
+    end
+  end
+
+  # log a message. Internal use only.
+  def log_sent(type, where, message)
+    case type
+      when "NOTICE"
+        if(where =~ /^#/)
+          log "-=#{@nick}=- #{message}", where
+        elsif (where =~ /(\S*)!.*/)
+             log "[-=#{where}=-] #{message}", $1
+        else
+             log "[-=#{where}=-] #{message}"
+        end
+      when "PRIVMSG"
+        if(where =~ /^#/)
+          log "<#{@nick}> #{message}", where
+        elsif (where =~ /^(\S*)!.*$/)
+          log "[msg(#{where})] #{message}", $1
+        else
+          log "[msg(#{where})] #{message}", where
+        end
+    end
+  end
+
+  def onjoin(m)
+    @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
+    if(m.address?)
+      debug "joined channel #{m.channel}"
+      log "@ Joined channel #{m.channel}", m.channel
+    else
+      log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel
+      @channels[m.channel].users[m.sourcenick] = Hash.new
+      @channels[m.channel].users[m.sourcenick]["mode"] = ""
+    end
+
+    @plugins.delegate("listen", m)
+    @plugins.delegate("join", m)
+  end
+
+  def onpart(m)
+    if(m.address?)
+      debug "left channel #{m.channel}"
+      log "@ Left channel #{m.channel} (#{m.message})", m.channel
+      @channels.delete(m.channel)
+    else
+      log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel
+      @channels[m.channel].users.delete(m.sourcenick)
+    end
+    
+    # delegate to plugins
+    @plugins.delegate("listen", m)
+    @plugins.delegate("part", m)
+  end
+
+  # respond to being kicked from a channel
+  def onkick(m)
+    if(m.address?)
+      debug "kicked from channel #{m.channel}"
+      @channels.delete(m.channel)
+      log "@ You have been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
+    else
+      @channels[m.channel].users.delete(m.sourcenick)
+      log "@ #{m.target} has been kicked from #{m.channel} by #{m.sourcenick} (#{m.message})", m.channel
+    end
+
+    @plugins.delegate("listen", m)
+    @plugins.delegate("kick", m)
+  end
+
+  def ontopic(m)
+    @channels[m.channel] = IRCChannel.new(m.channel) unless(@channels.has_key?(m.channel))
+    @channels[m.channel].topic = m.topic if !m.topic.nil?
+    @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil?
+    @channels[m.channel].topic.by = m.source if !m.source.nil?
+
+	  debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}"
+  end
+
+  # delegate a privmsg to auth, keyword or plugin handlers
+  def delegate_privmsg(message)
+    [@auth, @plugins, @keywords].each {|m|
+      break if m.privmsg(message)
+    }
+  end
+end
+
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircbot.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"

Added: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb
URL: http://svn.apache.org/viewcvs/maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb?rev=357245&view=auto
==============================================================================
--- maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb (added)
+++ maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb Fri Dec 16 14:30:38 2005
@@ -0,0 +1,188 @@
+module Irc
+
+  require 'socket'
+  require 'thread'
+  require 'rbot/timer'
+
+  # wrapped TCPSocket for communication with the server.
+  # emulates a subset of TCPSocket functionality
+  class IrcSocket
+    # total number of lines sent to the irc server
+    attr_reader :lines_sent
+    
+    # total number of lines received from the irc server
+    attr_reader :lines_received
+    
+    # delay between lines sent
+    attr_reader :sendq_delay
+    
+    # max lines to burst
+    attr_reader :sendq_burst
+    
+    # server:: server to connect to
+    # port::   IRCd port
+    # host::   optional local host to bind to (ruby 1.7+ required)
+    # create a new IrcSocket
+    def initialize(server, port, host, sendq_delay=2, sendq_burst=4)
+      @timer = Timer::Timer.new
+      @timer.add(0.2) do
+        spool
+      end
+      @server = server.dup
+      @port = port.to_i
+      @host = host
+      @sock = nil
+      @spooler = false
+      @lines_sent = 0
+      @lines_received = 0
+      if sendq_delay
+        @sendq_delay = sendq_delay.to_f
+      else
+        @sendq_delay = 2
+      end
+      @last_send = Time.new - @sendq_delay
+      @burst = 0
+      if sendq_burst
+        @sendq_burst = sendq_burst.to_i
+      else
+        @sendq_burst = 4
+      end
+    end
+    
+    # open a TCP connection to the server
+    def connect
+      @sock = nil
+      if(@host)
+        begin
+          @sock=TCPSocket.new(@server, @port, @host)
+        rescue ArgumentError => e
+          $stderr.puts "Your version of ruby does not support binding to a "
+          $stderr.puts "specific local address, please upgrade if you wish "
+          $stderr.puts "to use HOST = foo"
+          $stderr.puts "(this option has been disabled in order to continue)"
+          @sock=TCPSocket.new(@server, @port)
+        end
+      else
+        @sock=TCPSocket.new(@server, @port)
+      end 
+      @qthread = false
+      @qmutex = Mutex.new
+      @sendq = Array.new
+    end
+
+    def sendq_delay=(newfreq)
+      debug "changing sendq frequency to #{newfreq}"
+      @qmutex.synchronize do
+        @sendq_delay = newfreq
+        if newfreq == 0
+          clearq
+          @timer.stop
+        else
+          @timer.start
+        end
+      end
+    end
+
+    def sendq_burst=(newburst)
+      @qmutex.synchronize do
+        @sendq_burst = newburst
+      end
+    end
+
+    # used to send lines to the remote IRCd
+    # message: IRC message to send
+    def puts(message)
+      @qmutex.synchronize do
+        # debug "In puts - got mutex"
+        puts_critical(message)
+      end
+    end
+
+    # get the next line from the server (blocks)
+    def gets
+      reply = @sock.gets
+      @lines_received += 1
+      reply.strip! if reply
+      debug "RECV: #{reply.inspect}"
+      reply
+    end
+
+    def queue(msg)
+      if @sendq_delay > 0
+        @qmutex.synchronize do
+          @sendq.push msg
+        end
+        @timer.start
+      else
+        # just send it if queueing is disabled
+        self.puts(msg)
+      end
+    end
+
+    # pop a message off the queue, send it
+    def spool
+      if @sendq.empty?
+        @timer.stop
+        return
+      end
+      now = Time.new
+      if (now >= (@last_send + @sendq_delay))
+        # reset burst counter after @sendq_delay has passed
+        @burst = 0
+        debug "in spool, resetting @burst"
+      elsif (@burst >= @sendq_burst)
+        # nope. can't send anything, come back to us next tick...
+        @timer.start
+        return
+      end
+      @qmutex.synchronize do
+        debug "(can send #{@sendq_burst - @burst} lines, there are #{@sendq.length} to send)"
+        (@sendq_burst - @burst).times do
+          break if @sendq.empty?
+          puts_critical(@sendq.shift)
+        end
+      end
+      if @sendq.empty?
+        @timer.stop
+      end
+    end
+
+    def clearq
+      unless @sendq.empty?
+        @qmutex.synchronize do
+          @sendq.clear
+        end
+      end
+    end
+
+    # flush the TCPSocket
+    def flush
+      @sock.flush
+    end
+
+    # Wraps Kernel.select on the socket
+    def select(timeout=nil)
+      Kernel.select([@sock], nil, nil, timeout)
+    end
+
+    # shutdown the connection to the server
+    def shutdown(how=2)
+      @sock.shutdown(how) unless @sock.nil?
+      @sock = nil
+    end
+
+    private
+    
+    # same as puts, but expects to be called with a mutex held on @qmutex
+    def puts_critical(message)
+      # debug "in puts_critical"
+      debug "SEND: #{message.inspect}"
+      @sock.send(message + "\n",0)
+      @last_send = Time.new
+      @lines_sent += 1
+      @burst += 1
+    end
+
+  end
+
+end

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: maven/sandbox/issue/rbot/dist/lib/site_ruby/1.8/rbot/ircsocket.rb
------------------------------------------------------------------------------
    svn:keywords = "Author Date Id Revision"