From d30940cb5ff75cf7eab81f6a588b3b5297a762ad Mon Sep 17 00:00:00 2001 From: Tom Gilbert Date: Sat, 30 Jul 2005 21:35:57 +0000 Subject: [PATCH] Sat Jul 30 22:33:36 BST 2005 Tom Gilbert * Config items are now objects, various types are available. * The config wizard will now use registered config items if :wizard is set to true for those items. It will ask questions in the order they were registered. * The config module now works for doing runtime configuration. * misc refactoring --- ChangeLog | 9 + data/rbot/plugins/nickserv.rb | 2 +- data/rbot/plugins/seen.rb | 19 +- data/rbot/plugins/url.rb | 3 +- lib/rbot/auth.rb | 3 +- lib/rbot/config.rb | 386 +++++++++++++++++----------------- lib/rbot/httputil.rb | 51 ++--- lib/rbot/ircbot.rb | 57 ++--- lib/rbot/keywords.rb | 6 +- lib/rbot/language.rb | 19 +- lib/rbot/utils.rb | 18 ++ 11 files changed, 302 insertions(+), 271 deletions(-) diff --git a/ChangeLog b/ChangeLog index 68e85fca..af545d9a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +Sat Jul 30 22:33:36 BST 2005 Tom Gilbert + + * Config items are now objects, various types are available. + * The config wizard will now use registered config items if :wizard is set + to true for those items. It will ask questions in the order they were + registered. + * The config module now works for doing runtime configuration. + * misc refactoring + Sat Jul 30 01:19:32 BST 2005 Tom Gilbert * config module for configuring the running bot via IRC diff --git a/data/rbot/plugins/nickserv.rb b/data/rbot/plugins/nickserv.rb index 246f253c..976bb8f8 100644 --- a/data/rbot/plugins/nickserv.rb +++ b/data/rbot/plugins/nickserv.rb @@ -73,7 +73,7 @@ class NickServPlugin < Plugin return unless(m.kind_of? NoticeMessage) if (m.sourcenick == "NickServ" && m.message =~ /This nickname is owned by someone else/) - puts "nickserv asked us to identify for nick #{@bot.nick}" + debug "nickserv asked us to identify for nick #{@bot.nick}" if @registry.has_key?(@bot.nick) @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick] end diff --git a/data/rbot/plugins/seen.rb b/data/rbot/plugins/seen.rb index 80d52f65..a8dc1af7 100644 --- a/data/rbot/plugins/seen.rb +++ b/data/rbot/plugins/seen.rb @@ -1,23 +1,6 @@ Saw = Struct.new("Saw", :nick, :time, :type, :where, :message) class SeenPlugin < Plugin - # turn a number of seconds into a human readable string, e.g - # 2 days, 3 hours, 18 minutes, 10 seconds - def secs_to_string(secs) - ret = "" - days = (secs / (60 * 60 * 24)).to_i - secs = secs % (60 * 60 * 24) - hours = (secs / (60 * 60)).to_i - secs = (secs % (60 * 60)) - mins = (secs / 60).to_i - secs = (secs % 60).to_i - ret += "#{days} days, " if days > 0 - ret += "#{hours} hours, " if hours > 0 || days > 0 - ret += "#{mins} minutes and " if mins > 0 || hours > 0 || days > 0 - ret += "#{secs} seconds" - return ret - end - def help(plugin, topic="") "seen => have you seen, or when did you last see " end @@ -80,7 +63,7 @@ class SeenPlugin < Plugin if (ago.to_i == 0) ret += "just now, " else - ret += secs_to_string(ago) + " ago, " + ret += Utils.secs_to_string(ago) + " ago, " end case saw.type diff --git a/data/rbot/plugins/url.rb b/data/rbot/plugins/url.rb index 5629e30a..7972037a 100644 --- a/data/rbot/plugins/url.rb +++ b/data/rbot/plugins/url.rb @@ -1,7 +1,8 @@ Url = Struct.new("Url", :channel, :nick, :time, :url) class UrlPlugin < Plugin - BotConfig.register('url.max_urls', :type => :integer, :default => 100, + BotConfig.register BotConfigIntegerValue.new('url.max_urls', + :default => 100, :validate => Proc.new{|v| v > 0}, :desc => "Maximum number of urls to store. New urls replace oldest ones.") def initialize diff --git a/lib/rbot/auth.rb b/lib/rbot/auth.rb index 41890f7e..ae7199e1 100644 --- a/lib/rbot/auth.rb +++ b/lib/rbot/auth.rb @@ -19,7 +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)") + BotConfig.register BotConfigStringValue.new('auth.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 diff --git a/lib/rbot/config.rb b/lib/rbot/config.rb index f6ff7691..a4522926 100644 --- a/lib/rbot/config.rb +++ b/lib/rbot/config.rb @@ -3,12 +3,23 @@ module Irc require 'yaml' require 'rbot/messagemapper' - class BotConfigItem + 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 :values + 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] @@ -17,40 +28,130 @@ module Irc end @desc = params[:desc] @type = params[:type] || String - @values = params[:values] @on_change = params[:on_change] + @validate = params[:validate] + @wizard = params[:wizard] + @requires_restart = params[:requires_restart] end def default - if @default.class == Proc + if @default.instance_of?(Proc) @default.call else @default end end - def on_change(newvalue) - return unless @on_change - @on_change.call(newvalue) + 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 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 - class Enum + # Array of registered BotConfigValues for defaults, types and help + @@items = Hash.new + def BotConfig.items + @@items end - class Password + # Hash containing key => value pairs for lookup and serialisation + @@config = Hash.new(false) + def BotConfig.config + @@config end - class Boolean + def BotConfig.bot + @@bot 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" + def BotConfig.register(item) + unless item.kind_of?(BotConfigValue) + raise ArgumentError,"item must be a BotConfigValue" end - raise ArgumentError,"params must contain a period" unless key =~ /^.+\..+$/ - @@items[key] = BotConfigItem.new(key, params) + @@items[item.key] = item end # currently we store values in a hash but this could be changed in the @@ -58,14 +159,19 @@ module Irc # 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 @@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 through to the hash + # pass everything else through to the hash def method_missing(method, *args, &block) - return @config.send(method, *args, &block) + return @@config.send(method, *args, &block) end def handle_list(m, params) @@ -74,12 +180,12 @@ module Irc @@items.each_key do |key| mod, name = key.split('.') next unless mod == params[:module] - modules.push name unless modules.include?(name) + modules.push key unless modules.include?(name) end if modules.empty? m.reply "no such module #{params[:module]}" else - m.reply "module #{params[:module]} contains: " + modules.join(", ") + m.reply modules.join(", ") end else @@items.each_key do |key| @@ -94,13 +200,9 @@ module Irc key = params[:key] unless @@items.has_key?(key) m.reply "no such config key #{key}" + return end - value = self[key] - if @@items[key].type == :array - value = self[key].join(", ") - elsif @@items[key].type == :password && !m.private - value = "******" - end + value = @@items[key].to_s m.reply "#{key}: #{value}" end @@ -109,6 +211,7 @@ module Irc unless @@items.has_key?(key) m.reply "no such config key #{key}" end + puts @@items[key].inspect m.reply "#{key}: #{@@items[key].desc}" end @@ -117,7 +220,7 @@ module Irc unless @@items.has_key?(key) m.reply "no such config key #{key}" end - @config.delete(key) + @@items[key].unset handle_get(m, params) end @@ -127,54 +230,57 @@ module Irc 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 + 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 - item.on_change(@config[key]) - m.okay + 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 => list configuration keys for module " + when "get" + m.reply "config get => get configuration value for key " + when "unset" + m.reply "reset key to the default" + when "set" + m.reply "config set => set configuration value for key to " + when "desc" + m.reply "config desc => describe what 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 - @config = Hash.new(false) + @@bot = bot # respond to config messages, to provide runtime configuration # management # messages will be: - # get (implied) + # get # set # unset + # desc # and for arrays: - # add - # remove + # add TODO + # remove TODO @handler = MessageMapper.new(self) @handler.map 'config list :module', :action => 'handle_list', :defaults => {:module => false} @@ -183,48 +289,26 @@ module Irc @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} - # TODO - # have this class persist key/values in hash using yaml as it kinda - # already does. - # have other users of the class describe config to it on init, like: - # @config.add(:key => 'server.name', :type => 'string', - # :default => 'localhost', :restart => true, - # :help => 'irc server to connect to') - # that way the config module doesn't have to know about all the other - # classes but can still provide help and defaults. - # Classes don't have to add keys, they can just use config as a - # persistent hash, but then they won't be presented by the config - # module for runtime display/changes. - # (:restart, if true, makes the bot reply to changes with "this change - # will take effect after the next restart) - # :proc => Proc.new {|newvalue| ...} - # (:proc, proc to run on change of setting) - # or maybe, @config.add_key(...) do |newvalue| .... end - # :validate => /regex/ - # (operates on received string before conversion) - # Special handling for arrays so the config module can be used to - # add/remove elements as well as changing the whole thing - # Allow config options to list possible valid values (if type is enum, - # for example). Then things like the language module can list the - # available languages for choosing. - - if(File.exist?("#{@bot.botclass}/conf.yaml")) - newconfig = YAML::load_file("#{@bot.botclass}/conf.yaml") - @config.update(newconfig) + if(File.exist?("#{@@bot.botclass}/conf.yaml")) + newconfig = YAML::load_file("#{@@bot.botclass}/conf.yaml") + @@config.update newconfig else # first-run wizard! - wiz = BotConfigWizard.new(@bot) - newconfig = wiz.run(@config) - @config.update(newconfig) + BotConfigWizard.new(@@bot).run + # save newly created config + save end end # write current configuration to #{botclass}/conf.rbot def save - Dir.mkdir("#{@bot.botclass}") if(!File.exist?("#{@bot.botclass}")) - File.open("#{@bot.botclass}/conf.yaml", "w") do |file| - file.puts @config.to_yaml + File.open("#{@@bot.botclass}/conf.yaml", "w") do |file| + file.puts @@config.to_yaml end end @@ -233,77 +317,13 @@ module Irc end end - # I don't see a nice way to avoid the first start wizard knowing way too - # much about other modules etc, because it runs early and stuff it - # configures is used to initialise the other modules... - # To minimise this we'll do as little as possible and leave the rest to - # online modification class BotConfigWizard - - # TODO things to configure.. - # config directory (botclass) - people don't realise they should set - # this. The default... isn't good. - # users? - default *!*@* to 10 - # levels? - need a way to specify a default level, methinks, for - # unconfigured items. - # def initialize(bot) @bot = bot - @questions = [ - { - :question => "What server should the bot connect to?", - :key => "server.name", - :type => :string, - }, - { - :question => "What port should the bot connect to?", - :key => "server.port", - :type => :number, - }, - { - :question => "Does this IRC server require a password for access? Leave blank if not.", - :key => "server.password", - :type => :password, - }, - { - :question => "Would you like rbot to bind to a specific local host or IP? Leave blank if not.", - :key => "server.bindhost", - :type => :string, - }, - { - :question => "What IRC nickname should the bot attempt to use?", - :key => "irc.nick", - :type => :string, - }, - { - :question => "What local user should the bot appear to be?", - :key => "irc.user", - :type => :string, - }, - { - :question => "What channels should the bot 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'", - :prompt => "Channels", - :key => "irc.join_channels", - :type => :string, - }, - { - :question => "Which language file should the bot use?", - :key => "core.language", - :type => :enum, - :items => Dir.new(Config::DATADIR + "/languages").collect {|f| - f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil - }.compact - }, - { - :question => "Enter your password for maxing your auth with the bot (used to associate new hostmasks with your owner-status etc)", - :key => "auth.password", - :type => :password, - }, - ] + @questions = BotConfig.items.values.find_all {|i| i.wizard } end - def run(defaults) - config = defaults.clone + def run() puts "First time rbot configuration wizard" puts "====================================" puts "This is the first time you have run rbot with a config directory of:" @@ -313,39 +333,23 @@ module Irc puts "rbot is connected and you are auth'd." puts "-----------------------------------" - @questions.each do |q| - puts q[:question] + return unless @questions + @questions.sort{|a,b| a.order <=> b.order }.each do |q| + puts q.desc begin - key = q[:key] - if q[:type] == :enum - puts "valid values are: " + q[:items].join(", ") - end - if (defaults.has_key?(key)) - print q[:key] + " [#{defaults[key]}]: " - else - print q[:key] + " []: " - end + print q.key + " [#{q.to_s}]: " response = STDIN.gets response.chop! - response = defaults[key] if response == "" && defaults.has_key?(key) - case q[:type] - when :string - when :number - raise "value '#{response}' is not a number" unless (response.class == Fixnum || response =~ /^\d+$/) - response = response.to_i - when :password - when :enum - raise "selected value '#{response}' is not one of the valid values" unless q[:items].include?(response) + unless response.empty? + q.set_string response, false end - config[key] = response - puts "configured #{key} => #{config[key]}" + puts "configured #{q.key} => #{q.to_s}" puts "-----------------------------------" - rescue RuntimeError => e - puts e.message + rescue ArgumentError => e + puts "failed to set #{q.key}: #{e.message}" retry end end - return config end end end diff --git a/lib/rbot/httputil.rb b/lib/rbot/httputil.rb index 56de7349..b49a42b1 100644 --- a/lib/rbot/httputil.rb +++ b/lib/rbot/httputil.rb @@ -8,15 +8,20 @@ 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, + 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('http.proxy_user', :default => false, + BotConfig.register BotConfigStringValue.new('http.proxy_user', + :default => nil, :desc => "User for authenticating with the http proxy (if required)") - BotConfig.register('http.proxy_pass', :default => false, + BotConfig.register BotConfigStringValue.new('http.proxy_pass', + :default => nil, :desc => "Password for authenticating with the http proxy (if required)") - BotConfig.register('http.proxy_include', :type => :array, :default => [], + 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('http.proxy_exclude', :type => :array, :default => [], + BotConfig.register BotConfigArrayValue.new('http.proxy_exclude', :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) @@ -77,31 +82,27 @@ class HttpUtil # +http_proxy_include/exclude+ options. def get_proxy(uri) proxy = nil - if (ENV['http_proxy']) - proxy = URI.parse ENV['http_proxy'] - end - if (@bot.config["http.proxy"]) - proxy = URI.parse ENV['http_proxy'] - end - - if proxy - debug "proxy is set to #{proxy.uri}" - proxy = nil unless proxy_required(uri) - end - proxy_host = nil proxy_port = nil proxy_user = nil proxy_pass = nil - if @bot.config["http.proxy_user"] - proxy_user = @bot.config["http.proxy_user"] - if @bot.config["http.proxy_pass"] - proxy_pass = @bot.config["http.proxy_pass"] + + if @bot.config["http.use_proxy"] + if (ENV['http_proxy']) + proxy = URI.parse ENV['http_proxy'] + end + if (@bot.config["http.proxy_uri"]) + proxy = URI.parse ENV['http_proxy_uri'] + 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 - if proxy - proxy_host = proxy.host - proxy_port = proxy.port end return Net::HTTP.new(uri.host, uri.port, proxy_host, proxy_port, proxy_user, proxy_port) diff --git a/lib/rbot/ircbot.rb b/lib/rbot/ircbot.rb index 29debaaa..d0010c2a 100644 --- a/lib/rbot/ircbot.rb +++ b/lib/rbot/ircbot.rb @@ -83,42 +83,46 @@ class IrcBot # create a new IrcBot with botclass +botclass+ def initialize(botclass) # BotConfig for the core bot - BotConfig.register('server.name', + BotConfig.register BotConfigStringValue.new('server.name', :default => "localhost", :requires_restart => true, :desc => "What server should the bot connect to?", :wizard => true) - BotConfig.register('server.port', + BotConfig.register BotConfigIntegerValue.new('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 => "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('server.bindhost', + 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('server.reconnect_wait', - :default => 5, :type => :integer, + 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('irc.nick', :default => "rbot", + BotConfig.register BotConfigStringValue.new('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", + :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('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, + 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('server.sendq_delay', :default => 2.0, :type => :float, + 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 {|v| @socket.sendq_delay = v }) - BotConfig.register('server.sendq_burst', :default => 4, :type => :integer, + :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 {|v| @socket.sendq_burst = v }) + :on_change => Proc.new {|bot, v| bot.socket.sendq_burst = v }) unless FileTest.directory? Config::DATADIR puts "data directory '#{Config::DATADIR}' not found, did you install.rb?" @@ -335,7 +339,7 @@ class IrcBot @client.process reply end end - rescue => e + rescue => e # TODO be selective, only grab Network errors puts "connection closed: #{e}" puts e.backtrace.join("\n") end @@ -490,6 +494,11 @@ class IrcBot end # attempt to change bot's nick to +name+ + # FIXME + # if rbot is already taken, this happens: + # rbot_, nick rbot + # --- rbot_ is now known as rbot__ + # he should of course just keep his existing nick and report the error :P def nickchg(name) sendq "NICK #{name}" end @@ -685,12 +694,14 @@ class IrcBot @config['irc.sendq_delay'] = freq m.okay end - when (/^status$/i) + 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) @@ -702,15 +713,13 @@ class IrcBot 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 else # stuff to handle when not addressed case m.message - when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))\s+#{@nick}$/i) + when (/^\s*(hello|howdy|hola|salut|bonjour|sup|niihau|hey|hi(\W|$)|yo(\W|$))[\s,-.]+#{@nick}$/i) say m.replyto, @lang.get("hello_X") % m.sourcenick when (/^#{@nick}!*$/) say m.replyto, @lang.get("hello_X") % m.sourcenick diff --git a/lib/rbot/keywords.rb b/lib/rbot/keywords.rb index 70802e35..daf3b40b 100644 --- a/lib/rbot/keywords.rb +++ b/lib/rbot/keywords.rb @@ -81,9 +81,11 @@ 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, + BotConfig.register BotConfigBooleanValue.new('keyword.listen', + :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, + BotConfig.register BotConfigBooleanValue.new('keyword.address', + :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+ diff --git a/lib/rbot/language.rb b/lib/rbot/language.rb index ee6746d6..d48607b8 100644 --- a/lib/rbot/language.rb +++ b/lib/rbot/language.rb @@ -1,21 +1,24 @@ module Irc class Language - BotConfig.register('core.language', - :default => "english", :type => :enum, + BotConfig.register BotConfigEnumValue.new('core.language', + :default => "english", :wizard => true, :values => Dir.new(Config::DATADIR + "/languages").collect {|f| f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil }.compact, + :on_change => Proc.new {|bot, v| bot.lang.set_language v}, :desc => "Which language file the bot should use") - def initialize(language, file="") - @language = language - if file.empty? - file = Config::DATADIR + "/languages/#{@language}.lang" - end + def initialize(language) + set_language language + end + + def set_language(language) + file = Config::DATADIR + "/languages/#{language}.lang" unless(FileTest.exist?(file)) - raise "no such language: #{@language} (no such file #{file})" + raise "no such language: #{language} (no such file #{file})" end + @language = language @file = file scan end diff --git a/lib/rbot/utils.rb b/lib/rbot/utils.rb index 4c474ae4..a1a8c484 100644 --- a/lib/rbot/utils.rb +++ b/lib/rbot/utils.rb @@ -6,6 +6,24 @@ module Irc # miscellaneous useful functions module Utils + # turn a number of seconds into a human readable string, e.g + # 2 days, 3 hours, 18 minutes, 10 seconds + def Utils.secs_to_string(secs) + ret = "" + days = (secs / (60 * 60 * 24)).to_i + secs = secs % (60 * 60 * 24) + hours = (secs / (60 * 60)).to_i + secs = (secs % (60 * 60)) + mins = (secs / 60).to_i + secs = (secs % 60).to_i + ret += "#{days} days, " if days > 0 + ret += "#{hours} hours, " if hours > 0 || days > 0 + ret += "#{mins} minutes and " if mins > 0 || hours > 0 || days > 0 + ret += "#{secs} seconds" + return ret + end + + def Utils.safe_exec(command, *args) IO.popen("-") {|p| if(p) -- 2.39.2