From 992ef2edbf3eed7cb1d7c4c22f72cd203aff2aa5 Mon Sep 17 00:00:00 2001 From: Tom Gilbert Date: Sat, 30 Jul 2005 00:22:21 +0000 Subject: Sat Jul 30 01:19:32 BST 2005 Tom Gilbert * config module for configuring the running bot via IRC * BotConfig.register method for various modules and any plugin to register bot configuration which the new config module will expose for them. * various other tweaks as I continue to refactor.. --- ChangeLog | 7 ++ data/rbot/plugins/url.rb | 5 +- lib/rbot/auth.rb | 2 + lib/rbot/config.rb | 206 +++++++++++++++++++++++++++++++++++++++-------- lib/rbot/httputil.rb | 68 +++++++++++++--- lib/rbot/ircbot.rb | 76 +++++++++++++---- lib/rbot/keywords.rb | 8 +- lib/rbot/language.rb | 7 ++ 8 files changed, 316 insertions(+), 63 deletions(-) diff --git a/ChangeLog b/ChangeLog index aca85919..68e85fca 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,10 @@ +Sat Jul 30 01:19:32 BST 2005 Tom Gilbert + + * config module for configuring the running bot via IRC + * BotConfig.register method for various modules and any plugin to register + bot configuration which the new config module will expose for them. + * various other tweaks as I continue to refactor.. + Fri Jul 29 13:07:56 BST 2005 Tom Gilbert * Moved some stuff out of util.rb into the plugins that actually need diff --git a/data/rbot/plugins/url.rb b/data/rbot/plugins/url.rb index ced92133..5629e30a 100644 --- a/data/rbot/plugins/url.rb +++ b/data/rbot/plugins/url.rb @@ -1,6 +1,9 @@ Url = Struct.new("Url", :channel, :nick, :time, :url) class UrlPlugin < Plugin + BotConfig.register('url.max_urls', :type => :integer, :default => 100, + :desc => "Maximum number of urls to store. New urls replace oldest ones.") + def initialize super @registry.set_default(Array.new) @@ -22,7 +25,7 @@ class UrlPlugin < Plugin } url = Url.new(m.target, m.sourcenick, Time.new, urlstr) debug "#{list.length} urls so far" - if list.length > 50 # TODO make this configurable + if list.length > @bot.config['url.max_urls'] list.pop end debug "storing url #{url.url}" diff --git a/lib/rbot/auth.rb b/lib/rbot/auth.rb index 7811d9e4..41890f7e 100644 --- a/lib/rbot/auth.rb +++ b/lib/rbot/auth.rb @@ -19,6 +19,8 @@ module Irc # User-level authentication to allow/disallow access to bot commands based # on hostmask and userlevel. class IrcAuth + BotConfig.register('auth.password', :type => BotConfig::Password, :default => "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) diff --git a/lib/rbot/config.rb b/lib/rbot/config.rb index 19506ab2..f6ff7691 100644 --- a/lib/rbot/config.rb +++ b/lib/rbot/config.rb @@ -1,45 +1,189 @@ module Irc require 'yaml' + require 'rbot/messagemapper' + + class BotConfigItem + attr_reader :type + attr_reader :desc + attr_reader :key + attr_reader :values + def initialize(key, params) + @key = key + if params.has_key? :default + @default = params[:default] + else + @default = false + end + @desc = params[:desc] + @type = params[:type] || String + @values = params[:values] + @on_change = params[:on_change] + end + def default + if @default.class == Proc + @default.call + else + @default + end + end + def on_change(newvalue) + return unless @on_change + @on_change.call(newvalue) + end + end # container for bot configuration class BotConfig + class Enum + end + class Password + end + class Boolean + end + + attr_reader :items + @@items = Hash.new + + def BotConfig.register(key, params) + unless params.nil? || params.instance_of?(Hash) + raise ArgumentError,"params must be a hash" + end + raise ArgumentError,"params must contain a period" unless key =~ /^.+\..+$/ + @@items[key] = BotConfigItem.new(key, params) + 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 @config[key] if @config.has_key?(key) + return @@items[key].default if @@items.has_key?(key) + return false + end + + # pass everything 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 name unless modules.include?(name) + end + if modules.empty? + m.reply "no such module #{params[:module]}" + else + m.reply "module #{params[:module]} contains: " + 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}" + end + value = self[key] + if @@items[key].type == :array + value = self[key].join(", ") + elsif @@items[key].type == :password && !m.private + value = "******" + end + 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 + 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 + @config.delete(key) + 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}" + end + item = @@items[key] + puts "item type is #{item.type}" + case item.type + when :string + @config[key] = value + when :password + @config[key] = value + when :integer + @config[key] = value.to_i + when :float + @config[key] = value.to_f + when :array + @config[key] = value.split(/,\s*/) + when :boolean + if value == "true" + @config[key] = true + else + @config[key] = false + end + when :enum + unless item.values.include?(value) + m.reply "invalid value #{value}, allowed values are: " + item.values.join(", ") + return + end + @config[key] = value + else + puts "ACK, unsupported type #{item.type}" + exit 2 + end + item.on_change(@config[key]) + m.okay + end + # bot:: parent bot class # create a new config hash from #{botclass}/conf.rbot def initialize(bot) @bot = bot - # some defaults @config = Hash.new(false) - - @config['server.name'] = "localhost" - @config['server.port'] = 6667 - @config['server.password'] = false - @config['server.bindhost'] = false - @config['server.reconnect_wait'] = 5 - @config['irc.nick'] = "rbot" - @config['irc.user'] = "rbot" - @config['irc.join_channels'] = "" - @config['core.language'] = "english" - @config['core.save_every'] = 60 - @config['keyword.listen'] = false - @config['auth.password'] = "" - @config['server.sendq_delay'] = 2.0 - @config['server.sendq_burst'] = 4 - @config['keyword.address'] = true - @config['keyword.listen'] = false - @config['http.proxy'] = false - @config['http.proxy_include'] = false - @config['http.proxy_exclude'] = false - @config['http.proxy_user'] = false - @config['http.proxy_pass'] = false + # respond to config messages, to provide runtime configuration + # management + # messages will be: + # get (implied) + # set + # unset + # and for arrays: + # add + # remove + @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' + # TODO # have this class persist key/values in hash using yaml as it kinda # already does. @@ -83,6 +227,10 @@ module Irc file.puts @config.to_yaml end end + + def privmsg(m) + @handler.handle(m) + end end # I don't see a nice way to avoid the first start wizard knowing way too @@ -104,37 +252,31 @@ module Irc @questions = [ { :question => "What server should the bot connect to?", - :prompt => "Hostname", :key => "server.name", :type => :string, }, { :question => "What port should the bot connect to?", - :prompt => "Port", :key => "server.port", :type => :number, }, { :question => "Does this IRC server require a password for access? Leave blank if not.", - :prompt => "Password", :key => "server.password", :type => :password, }, { :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.", - :prompt => "Local bind", :key => "server.bindhost", :type => :string, }, { :question => "What IRC nickname should the bot attempt to use?", - :prompt => "Nick", :key => "irc.nick", :type => :string, }, { :question => "What local user should the bot appear to be?", - :prompt => "User", :key => "irc.user", :type => :string, }, @@ -146,7 +288,6 @@ module Irc }, { :question => "Which language file should the bot use?", - :prompt => "Language", :key => "core.language", :type => :enum, :items => Dir.new(Config::DATADIR + "/languages").collect {|f| @@ -155,7 +296,6 @@ module Irc }, { :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)", - :prompt => "Password", :key => "auth.password", :type => :password, }, @@ -181,9 +321,9 @@ module Irc puts "valid values are: " + q[:items].join(", ") end if (defaults.has_key?(key)) - print q[:prompt] + " [#{defaults[key]}]: " + print q[:key] + " [#{defaults[key]}]: " else - print q[:prompt] + " []: " + print q[:key] + " []: " end response = STDIN.gets response.chop! diff --git a/lib/rbot/httputil.rb b/lib/rbot/httputil.rb index 85b56be4..56de7349 100644 --- a/lib/rbot/httputil.rb +++ b/lib/rbot/httputil.rb @@ -1,5 +1,6 @@ module Irc +require 'resolv' require 'net/http' Net::HTTP.version_1_2 @@ -7,6 +8,17 @@ Net::HTTP.version_1_2 # 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('http.proxy', :default => false, + :desc => "Proxy server to use for HTTP requests (URI, e.g http://proxy.host:port)") + BotConfig.register('http.proxy_user', :default => false, + :desc => "User for authenticating with the http proxy (if required)") + BotConfig.register('http.proxy_pass', :default => false, + :desc => "Password for authenticating with the http proxy (if required)") + BotConfig.register('http.proxy_include', :type => :array, :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('http.proxy_exclude', :type => :array, :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 = { @@ -14,6 +26,49 @@ class HttpUtil } 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.push 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 @@ -29,16 +84,9 @@ class HttpUtil proxy = URI.parse ENV['http_proxy'] end - # if http_proxy_include or http_proxy_exclude are set, then examine the - # uri to see if this is a proxied uri - # the excludes are a list of regexps, and each regexp is checked against - # the server name, and its IP addresses - if uri - if @bot.config["http.proxy_exclude"] - # TODO - end - if @bot.config["http.proxy_include"] - end + if proxy + debug "proxy is set to #{proxy.uri}" + proxy = nil unless proxy_required(uri) end proxy_host = nil diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb index 51f323b3..29debaaa 100644 --- a/lib/rbot/ircbot.rb +++ b/lib/rbot/ircbot.rb @@ -23,21 +23,23 @@ require 'thread' require 'etc' require 'fileutils' +# these first +require 'rbot/rbotconfig' +require 'rbot/config' +require 'rbot/utils' + require 'rbot/rfc2812' require 'rbot/keywords' -require 'rbot/config' require 'rbot/ircsocket' require 'rbot/auth' require 'rbot/timer' require 'rbot/plugins' require 'rbot/channel' -require 'rbot/utils' require 'rbot/message' require 'rbot/language' require 'rbot/dbhash' require 'rbot/registry' require 'rbot/httputil' -require 'rbot/rbotconfig' module Irc @@ -80,6 +82,44 @@ class IrcBot # create a new IrcBot with botclass +botclass+ def initialize(botclass) + # BotConfig for the core bot + BotConfig.register('server.name', + :default => "localhost", :requires_restart => true, + :desc => "What server should the bot connect to?", + :wizard => true) + BotConfig.register('server.port', + :default => 6667, :type => :integer, :requires_restart => true, + :desc => "What port should the bot connect to?", + :wizard => true) + BotConfig.register('server.password', + :default => false, :requires_restart => true, :type => :password, + :desc => "Password for connecting to this server (if required)", + :wizard => true) + BotConfig.register('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('server.reconnect_wait', + :default => 5, :type => :integer, + :desc => "Seconds to wait before attempting to reconnect, on disconnect") + BotConfig.register('irc.nick', :default => "rbot", + :desc => "IRC nickname the bot should attempt to use", :wizard => true, + :on_change => Proc.new{|v| sendq "NICK #{v}" }) + BotConfig.register('irc.user', :default => "rbot", + :requires_restart => true, + :desc => "local user the bot should appear to be", :wizard => true) + BotConfig.register('irc.join_channels', :default => [], :type => :array, + :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'", :wizard => true) + BotConfig.register('core.save_every', :default => 60, + # 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('server.sendq_delay', :default => 2.0, :type => :float, + :desc => "(flood prevention) the delay between sending messages to the server (in seconds)", + :on_change => Proc.new {|v| @socket.sendq_delay = v }) + BotConfig.register('server.sendq_burst', :default => 4, :type => :integer, + :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 {|v| @socket.sendq_burst = v }) + unless FileTest.directory? Config::DATADIR puts "data directory '#{Config::DATADIR}' not found, did you install.rb?" exit 2 @@ -199,20 +239,19 @@ class IrcBot @nick = data['NICK'] end if(@config['irc.quser']) - puts "authing with Q using #{@config['quakenet.user']} #{@config['quakenet.auth']}" + # TODO move this to a plugin + debug "authing with Q using #{@config['quakenet.user']} #{@config['quakenet.auth']}" @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config['quakenet.user']} #{@config['quakenet.auth']}" end - if(@config['irc.join_channels']) - @config['irc.join_channels'].split(", ").each {|c| - debug "autojoining channel #{c}" - if(c =~ /^(\S+)\s+(\S+)$/i) - join $1, $2 - else - join c if(c) - end - } - end + @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"]) @@ -656,12 +695,15 @@ class IrcBot 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?) + when (/^config\s+/) + @config.privmsg(m) else delegate_privmsg(m) end @@ -703,8 +745,8 @@ class IrcBot 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 - puts "joined channel #{m.channel}" else log "@ #{m.sourcenick} joined channel #{m.channel}", m.channel @channels[m.channel].users[m.sourcenick] = Hash.new @@ -717,9 +759,9 @@ class IrcBot def onpart(m) if(m.address?) + debug "left channel #{m.channel}" log "@ Left channel #{m.channel} (#{m.message})", m.channel @channels.delete(m.channel) - puts "left channel #{m.channel}" else log "@ #{m.sourcenick} left channel #{m.channel} (#{m.message})", m.channel @channels[m.channel].users.delete(m.sourcenick) @@ -733,9 +775,9 @@ class IrcBot # 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 - puts "kicked from channel #{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 diff --git a/lib/rbot/keywords.rb b/lib/rbot/keywords.rb index 3305af29..70802e35 100644 --- a/lib/rbot/keywords.rb +++ b/lib/rbot/keywords.rb @@ -8,7 +8,7 @@ module Irc # is, and has a single value of bar). # Keywords can have multiple values, to_s() will choose one at random class Keyword - + # type of keyword (e.g. "is" or "are") attr_reader :type @@ -81,6 +81,10 @@ module Irc # handle it, checks for a keyword command or lookup, otherwise the message # is delegated to plugins class Keywords + BotConfig.register('keyword.listen', :type => :boolean, :default => false, + :desc => "Should the bot listen to all chat and attempt to automatically detect keywords? (e.g. by spotting someone say 'foo is bar')") + BotConfig.register('keyword.address', :type => :boolean, :default => true, + :desc => "Should the bot require that keyword lookups are addressed to it? If not, the bot will attempt to lookup foo if someone says 'foo?' in channel") # create a new Keywords instance, associated to bot +bot+ def initialize(bot) @@ -415,7 +419,7 @@ module Irc end else # in channel message, not to me - if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.noaddress"] && m.message =~ /^(.*\S)\s*\?\s*$/)) + if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.address"] && m.message =~ /^(.*\S)\s*\?\s*$/)) keyword m, $1, false if(@bot.auth.allow?("keyword", m.source)) elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)) # TODO MUCH more selective on what's allowed here diff --git a/lib/rbot/language.rb b/lib/rbot/language.rb index 64555248..ee6746d6 100644 --- a/lib/rbot/language.rb +++ b/lib/rbot/language.rb @@ -1,6 +1,13 @@ module Irc class Language + BotConfig.register('core.language', + :default => "english", :type => :enum, + :values => Dir.new(Config::DATADIR + "/languages").collect {|f| + f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil + }.compact, + :desc => "Which language file the bot should use") + def initialize(language, file="") @language = language if file.empty? -- cgit v1.2.3