diff options
-rw-r--r-- | ChangeLog | 45 | ||||
-rwxr-xr-x | rbot.rb | 4 | ||||
-rw-r--r-- | rbot/auth.rb | 2 | ||||
-rw-r--r-- | rbot/config.rb | 205 | ||||
-rw-r--r-- | rbot/ircbot.rb | 64 | ||||
-rw-r--r-- | rbot/ircsocket.rb | 56 | ||||
-rw-r--r-- | rbot/keywords.rb | 4 | ||||
-rw-r--r-- | rbot/message.rb | 9 | ||||
-rw-r--r-- | rbot/messagemapper.rb | 158 | ||||
-rw-r--r-- | rbot/plugins.rb | 66 | ||||
-rw-r--r-- | rbot/plugins/autoop.rb | 4 | ||||
-rw-r--r-- | rbot/plugins/cal.rb | 9 | ||||
-rw-r--r-- | rbot/plugins/eightball.rb | 5 | ||||
-rw-r--r-- | rbot/plugins/karma.rb | 93 | ||||
-rw-r--r-- | rbot/plugins/lart.rb | 8 | ||||
-rw-r--r-- | rbot/plugins/nickserv.rb | 10 | ||||
-rw-r--r-- | rbot/plugins/opmeh.rb | 1 | ||||
-rw-r--r-- | rbot/plugins/quotes.rb | 4 | ||||
-rw-r--r-- | rbot/plugins/remind.rb | 2 | ||||
-rw-r--r-- | rbot/plugins/roulette.rb | 2 |
20 files changed, 596 insertions, 155 deletions
@@ -1,3 +1,48 @@ +Tue Jul 26 14:41:34 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk> + + * Prevent multiple plugin registrations of the same name + * reworking the config system to use yaml for persistence + * reworking the config system key names + * on first startup, the bot will prompt for the essential startup config + * new config module for configuring the bot at runtime + * new config module includes new configurables, for example changing the + bot's language at runtime. + * various other fixes + * New way of mapping plugins to strings, using maps. These may be + familiar to rails users. This is to reduce the amount of regexps plugins + currently need to do to parse arguments. The old method (privmsg) is still + supported, of course. Example plugin now: + def MyPlugin < Plugin + def foo(m, params) + m.reply "bar" + end + + def complexfoo(m, params) + m.reply "qux! (#{params[:bar]} #{params[:baz]})" + end + end + plugin = MyPlugin.new + # simple map + plugin.map 'foo' + + # this will match "rbot: foo somestring otherstring" and pass the + # parameters as a hash using the names in the map. + plugin.map 'foo :bar :baz', :action => 'complexfoo' + # this means :foo is an optional parameter + plugin.map 'foo :foo', :defaults => {:foo => 'bar'} + # you can also gobble up into an array + plugin.map 'foo *bar' # params[:bar] will be an array of string elements + # and you can validate, here the first param must be a number + plugin.map 'foo :bar', :requirements => {:foo => /^\d+$/} + + +Sat Jul 23 01:39:08 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk> + + * Changed BotConfig to use yaml storage, method syntax instead of hash for + get/set, to allow more flexibility and encapsulation + * Added convenience method Message.okay (m.okay is the same as the + old-style @bot.okay m.replyto) + Wed Jul 20 23:30:01 BST 2005 Tom Gilbert <tom@linuxbrit.co.uk> * Move some core plugins to use the new httputil @@ -49,6 +49,10 @@ opts.each {|opt, arg| botclass = ARGV.shift botclass = "rbotconf" unless(botclass); +unless FileTest.directory? botclass + # TODO copy in samples/templates from install directory +end + if(bot = Irc::IrcBot.new(botclass)) if($opts["help"]) puts bot.help($opts["help"]) diff --git a/rbot/auth.rb b/rbot/auth.rb index 017745ab..7811d9e4 100644 --- a/rbot/auth.rb +++ b/rbot/auth.rb @@ -182,7 +182,7 @@ module Irc m.reply "user #$1 is gone" end when (/^auth\s+(\S+)/) - if($1 == @bot.config["PASSWD"]) + if($1 == @bot.config["auth.password"]) @bot.auth.useradd(Regexp.escape(m.source), 1000) m.reply "Identified, security level maxed out" else diff --git a/rbot/config.rb b/rbot/config.rb index 52899205..971a413c 100644 --- a/rbot/config.rb +++ b/rbot/config.rb @@ -1,40 +1,205 @@ module Irc + require 'yaml' + # container for bot configuration - # just treat it like a hash - class BotConfig < Hash + class BotConfig + + # currently we store values in a hash but this could be changed in the + # future. We use hash semantics, however. + def method_missing(method, *args, &block) + return @config.send(method, *args, &block) + end # bot:: parent bot class # create a new config hash from #{botclass}/conf.rbot def initialize(bot) - super(false) @bot = bot # some defaults - self["SERVER"] = "localhost" - self["PORT"] = "6667" - self["NICK"] = "rbot" - self["USER"] = "gilbertt" - self["LANGUAGE"] = "english" - self["SAVE_EVERY"] = "60" - self["KEYWORD_LISTEN"] = false - if(File.exist?("#{@bot.botclass}/conf.rbot")) - IO.foreach("#{@bot.botclass}/conf.rbot") do |line| - next if(line =~ /^\s*#/) - if(line =~ /(\S+)\s+=\s+(.*)$/) - self[$1] = $2 if($2) - end - end + @config = Hash.new(false) + + @config['server.name'] = "localhost" + @config['server.port'] = 6667 + @config['server.password'] = false + @config['server.bindhost'] = false + @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 + + # 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) + else + # first-run wizard! + wiz = BotConfigWizard.new(@bot) + newconfig = wiz.run(@config) + @config.update(newconfig) 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.rbot", "w") do |file| - self.each do |key, value| - file.puts "#{key} = #{value}" + File.open("#{@bot.botclass}/conf.yaml", "w") do |file| + file.puts @config.to_yaml + end + 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?", + :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, + }, + { + :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?", + :prompt => "Language", + :key => "core.language", + :type => :enum, + :items => Dir.new(File.dirname(__FILE__) + "/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)", + :prompt => "Password", + :key => "auth.password", + :type => :password, + }, + ] + end + + def run(defaults) + config = defaults.clone + 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 "-----------------------------------" + + @questions.each do |q| + puts q[:question] + begin + key = q[:key] + if q[:type] == :enum + puts "valid values are: " + q[:items].join(", ") + end + if (defaults.has_key?(key)) + print q[:prompt] + " [#{defaults[key]}]: " + else + print q[:prompt] + " []: " + end + 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) + end + config[key] = response + puts "configured #{key} => #{config[key]}" + puts "-----------------------------------" + rescue RuntimeError => e + puts e.message + retry end end + return config end end end diff --git a/rbot/ircbot.rb b/rbot/ircbot.rb index 7129c105..844231dd 100644 --- a/rbot/ircbot.rb +++ b/rbot/ircbot.rb @@ -86,20 +86,20 @@ class IrcBot @config = Irc::BotConfig.new(self) @timer = Timer::Timer.new @registry = BotRegistry.new self - @timer.add(@config["SAVE_EVERY"].to_i) { save } + @timer.add(@config['core.save_every']) { save } if @config['core.save_every'] @channels = Hash.new @logs = Hash.new @httputil = Irc::HttpUtil.new(self) - @lang = Irc::Language.new(@config["LANGUAGE"]) + @lang = Irc::Language.new(@config['core.language']) @keywords = Irc::Keywords.new(self) @auth = Irc::IrcAuth.new(self) @plugins = Irc::Plugins.new(self, ["#{botclass}/plugins"]) - @socket = Irc::IrcSocket.new(@config["SERVER"], @config["PORT"], @config["HOST"], @config["SENDQ_DELAY"], @config["SENDQ_BURST"]) - @nick = @config["NICK"] - @server_password = @config["SERVER_PASSWORD"] - if @config["ADDRESS_PREFIX"] - @addressing_prefixes = @config["ADDRESS_PREFIX"].split(" ") + + @socket = Irc::IrcSocket.new(@config['server.name'], @config['server.port'], @config['server.bindhost'], @config['server.sendq_delay'], @config['server.sendq_burst']) + @nick = @config['irc.nick'] + if @config['core.address_prefix'] + @addressing_prefixes = @config['core.address_prefix'].split(" ") else @addressing_prefixes = Array.new end @@ -179,13 +179,13 @@ class IrcBot if data['NICK'] && data['NICK'].length > 0 @nick = data['NICK'] end - if(@config["QUSER"]) - puts "authing with Q using #{@config["QUSER"]} #{@config["QAUTH"]}" - @socket.puts "PRIVMSG Q@CServe.quakenet.org :auth #{@config["QUSER"]} #{@config["QAUTH"]}" + if(@config['irc.quser']) + puts "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["JOIN_CHANNELS"]) - @config["JOIN_CHANNELS"].split(", ").each {|c| + if(@config['irc.join_channels']) + @config['irc.join_channels'].split(", ").each {|c| puts "autojoining channel #{c}" if(c =~ /^(\S+)\s+(\S+)$/i) join $1, $2 @@ -225,8 +225,8 @@ class IrcBot m = TopicMessage.new(self, data["SOURCE"], data["CHANNEL"], timestamp, data["TOPIC"]) ontopic(m) - @plugins.delegate("topic", m) @plugins.delegate("listen", m) + @plugins.delegate("topic", m) } @client["TOPIC"] = @client["TOPICINFO"] = proc {|data| channel = data["CHANNEL"] @@ -258,10 +258,10 @@ class IrcBot begin @socket.connect rescue => e - raise "failed to connect to IRC server at #{@config['SERVER']} #{@config['PORT']}: " + e + raise "failed to connect to IRC server at #{@config['server.name']} #{@config['server.port']}: " + e end - @socket.puts "PASS " + @server_password if @server_password - @socket.puts "NICK #{@nick}\nUSER #{@config['USER']} 4 #{@config['SERVER']} :Ruby bot. (c) Tom Gilbert" + @socket.puts "PASS " + @config['server.password'] if @config['server.password'] + @socket.puts "NICK #{@nick}\nUSER #{@config['server.user']} 4 #{@config['server.name']} :Ruby bot. (c) Tom Gilbert" end # begin event handling loop @@ -563,8 +563,8 @@ class IrcBot join 0 if(@auth.allow?("join", m.source, m.replyto)) when (/^save$/i) if(@auth.allow?("config", m.source, m.replyto)) - okay m.replyto save + m.okay end when (/^nick\s+(\S+)$/i) nickchg($1) if(@auth.allow?("nick", m.source, m.replyto)) @@ -580,55 +580,55 @@ class IrcBot say m.replyto, "pong" when (/^rescan$/i) if(@auth.allow?("config", m.source, m.replyto)) - okay m.replyto + m.okay rescan end when (/^quiet$/i) if(auth.allow?("talk", m.source, m.replyto)) - say m.replyto, @lang.get("okay") + 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)) - say m.replyto, @lang.get("okay") + 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 } - okay m.replyto + 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)) - okay m.replyto + m.okay end - # TODO break this out into an options module + # TODO break this out into a config module when (/^options get sendq_delay$/i) if auth.allow?("config", m.source, m.replyto) - m.reply "options->sendq_delay = #{@socket.get_sendq}" + m.reply "options->sendq_delay = #{@socket.sendq_delay}" end when (/^options get sendq_burst$/i) if auth.allow?("config", m.source, m.replyto) - m.reply "options->sendq_burst = #{@socket.get_maxburst}" + m.reply "options->sendq_burst = #{@socket.sendq_burst}" end when (/^options set sendq_burst (.*)$/i) num = $1.to_i if auth.allow?("config", m.source, m.replyto) - @socket.set_maxburst(num) - @config["SENDQ_BURST"] = num - okay m.replyto + @socket.sendq_burst = num + @config['irc.sendq_burst'] = num + m.okay end when (/^options set sendq_delay (.*)$/i) freq = $1.to_f if auth.allow?("config", m.source, m.replyto) - @socket.set_sendq(freq) - @config["SENDQ_DELAY"] = freq - okay m.replyto + @socket.sendq_delay = freq + @config['irc.sendq_delay'] = freq + m.okay end when (/^status$/i) m.reply status if auth.allow?("status", m.source, m.replyto) @@ -735,7 +735,7 @@ class IrcBot @channels[m.channel].topic.timestamp = m.timestamp if !m.timestamp.nil? @channels[m.channel].topic.by = m.source if !m.source.nil? - puts @channels[m.channel].topic + debug "topic of channel #{m.channel} is now #{@channels[m.channel].topic}" end # delegate a privmsg to auth, keyword or plugin handlers diff --git a/rbot/ircsocket.rb b/rbot/ircsocket.rb index 25895644..35857736 100644 --- a/rbot/ircsocket.rb +++ b/rbot/ircsocket.rb @@ -8,29 +8,37 @@ module Irc 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, sendfreq=2, maxburst=4) + def initialize(server, port, host, sendq_delay=2, sendq_burst=4) @server = server.dup @port = port.to_i @host = host @lines_sent = 0 @lines_received = 0 - if sendfreq - @sendfreq = sendfreq.to_f + if sendq_delay + @sendq_delay = sendq_delay.to_f else - @sendfreq = 2 + @sendq_delay = 2 end - @last_send = Time.new - @sendfreq + @last_send = Time.new - @sendq_delay @burst = 0 - if maxburst - @maxburst = maxburst.to_i + if sendq_burst + @sendq_burst = sendq_burst.to_i else - @maxburst = 4 + @sendq_burst = 4 end end @@ -52,15 +60,15 @@ module Irc @qthread = false @qmutex = Mutex.new @sendq = Array.new - if (@sendfreq > 0) + if (@sendq_delay > 0) @qthread = Thread.new { spooler } end end - def set_sendq(newfreq) + def sendq_delay=(newfreq) debug "changing sendq frequency to #{newfreq}" @qmutex.synchronize do - @sendfreq = newfreq + @sendq_delay = newfreq if newfreq == 0 && @qthread clearq Thread.kill(@qthread) @@ -71,20 +79,12 @@ module Irc end end - def set_maxburst(newburst) + def sendq_burst=(newburst) @qmutex.synchronize do - @maxburst = newburst + @sendq_burst = newburst end end - def get_maxburst - return @maxburst - end - - def get_sendq - return @sendfreq - end - # used to send lines to the remote IRCd # message: IRC message to send def puts(message) @@ -106,7 +106,7 @@ module Irc end def queue(msg) - if @sendfreq > 0 + if @sendq_delay > 0 @qmutex.synchronize do # debug "QUEUEING: #{msg}" @sendq.push msg @@ -120,7 +120,7 @@ module Irc def spooler while true spool - sleep 0.1 + sleep 0.2 end end @@ -128,17 +128,17 @@ module Irc def spool unless @sendq.empty? now = Time.new - if (now >= (@last_send + @sendfreq)) - # reset burst counter after @sendfreq has passed + if (now >= (@last_send + @sendq_delay)) + # reset burst counter after @sendq_delay has passed @burst = 0 debug "in spool, resetting @burst" - elsif (@burst >= @maxburst) + elsif (@burst >= @sendq_burst) # nope. can't send anything return end @qmutex.synchronize do - debug "(can send #{@maxburst - @burst} lines, there are #{@sendq.length} to send)" - (@maxburst - @burst).times 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 diff --git a/rbot/keywords.rb b/rbot/keywords.rb index f1997829..3305af29 100644 --- a/rbot/keywords.rb +++ b/rbot/keywords.rb @@ -415,9 +415,9 @@ module Irc end else # in channel message, not to me - if(m.message =~ /^'(.*)$/ || (@bot.config["NO_KEYWORD_ADDRESS"] == "true" && m.message =~ /^(.*\S)\s*\?\s*$/)) + if(m.message =~ /^'(.*)$/ || (!@bot.config["keyword.noaddress"] && 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+(.*)$/)) + elsif(@bot.config["keyword.listen"] == true && (m.message =~ /^(.*?)\s+(is|are)\s+(.*)$/)) # TODO MUCH more selective on what's allowed here keyword_command(m.sourcenick, m.replyto, $1, $2, $3, true) if(@bot.auth.allow?("keycmd", m.source)) end diff --git a/rbot/message.rb b/rbot/message.rb index c217b1da..d7f614ab 100644 --- a/rbot/message.rb +++ b/rbot/message.rb @@ -5,6 +5,9 @@ module Irc # nick/channel and a message part) class BasicUserMessage + # associated bot + attr_reader :bot + # when the message was received attr_reader :time @@ -175,6 +178,12 @@ module Irc @replied = true end + # convenience method to reply "okay" in the current language to the + # message + def okay + @bot.say @replyto, @bot.lang.get("okay") + end + end # class to manage IRC PRIVMSGs diff --git a/rbot/messagemapper.rb b/rbot/messagemapper.rb new file mode 100644 index 00000000..d03721c6 --- /dev/null +++ b/rbot/messagemapper.rb @@ -0,0 +1,158 @@ +module Irc + class MessageMapper + attr_writer :fallback + + def initialize(parent) + @parent = parent + @routes = Array.new + @fallback = 'usage' + end + + def map(*args) + @routes << Route.new(*args) + end + + def each + @routes.each {|route| yield route} + end + def last + @routes.last + end + + def handle(m) + return false if @routes.empty? + failures = [] + @routes.each do |route| + options, failure = route.recognize(m) + if options.nil? + failures << [route, failure] + else + action = route.options[:action] ? route.options[:action] : route.items[0] + next unless @parent.respond_to?(action) + auth = route.options[:auth] ? route.options[:auth] : action + if m.bot.auth.allow?(auth, m.source, m.replyto) + debug "route found and auth'd: #{action.inspect} #{options.inspect}" + @parent.send(action, m, options) + return true + end + # if it's just an auth failure but otherwise the match is good, + # don't try any more handlers + break + end + end + debug failures.inspect + debug "no handler found, trying fallback" + if @fallback != nil && @parent.respond_to?(@fallback) + if m.bot.auth.allow?(@fallback, m.source, m.replyto) + @parent.send(@fallback, m, {}) + return true + end + end + return false + end + + end + + class Route + attr_reader :defaults # The defaults hash + attr_reader :options # The options hash + attr_reader :items + def initialize(template, hash={}) + raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash) + @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {} + @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {} + self.items = template + @options = hash + end + def items=(str) + items = str.split(/\s+/).collect {|c| (/^(:|\*)(\w+)$/ =~ c) ? (($1 == ':' ) ? $2.intern : "*#{$2}".intern) : c} if str.kind_of?(String) # split and convert ':xyz' to symbols + items.shift if items.first == "" + items.pop if items.last == "" + @items = items + + if @items.first.kind_of? Symbol + raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}" + end + + # Verify uniqueness of each component. + @items.inject({}) do |seen, item| + if item.kind_of? Symbol + raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item + seen[item] = true + end + seen + end + end + + # Recognize the provided string components, returning a hash of + # recognized values, or [nil, reason] if the string isn't recognized. + def recognize(m) + components = m.message.split(/\s+/) + options = {} + + @items.each do |item| + if /^\*/ =~ item.to_s + if components.empty? + value = @defaults.has_key?(item) ? @defaults[item].clone : [] + else + value = components.clone + end + components = [] + def value.to_s() self.join(' ') end + options[item.to_s.sub(/^\*/,"").intern] = value + elsif item.kind_of? Symbol + value = components.shift || @defaults[item] + return nil, requirements_for(item) unless passes_requirements?(item, value) + options[item] = value + else + return nil, "No value available for component #{item.inspect}" if components.empty? + component = components.shift + return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item + end + end + + return nil, "Unused components were left: #{components.join '/'}" unless components.empty? + + return nil, "route is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private? + return nil, "route is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private? + + options.delete_if {|k, v| v.nil?} # Remove nil values. + return options, nil + end + + def inspect + when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}" + default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}" + "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join('/').inspect}#{default_str}#{when_str}>" + end + + # Verify that the given value passes this route's requirements + def passes_requirements?(name, value) + return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be + + case @requirements[name] + when nil then true + when Regexp then + value = value.to_s + match = @requirements[name].match(value) + match && match[0].length == value.length + else + @requirements[name] == value.to_s + end + end + + def requirements_for(name) + name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect) + presence = (@defaults.key?(name) && @defaults[name].nil?) + requirement = case @requirements[name] + when nil then nil + when Regexp then "match #{@requirements[name].inspect}" + else "be equal to #{@requirements[name].inspect}" + end + if presence && requirement then "#{name} must be present and #{requirement}" + elsif presence || requirement then "#{name} must #{requirement || 'be present'}" + else "#{name} has no requirements" + end + end + end +end diff --git a/rbot/plugins.rb b/rbot/plugins.rb index b99fe562..5db047fb 100644 --- a/rbot/plugins.rb +++ b/rbot/plugins.rb @@ -1,8 +1,50 @@ module Irc + require 'rbot/messagemapper' # base class for all rbot plugins # certain methods will be called if they are provided, if you define one of # the following methods, it will be called as appropriate: + # + # map(template, options):: + # map is the new, cleaner way to respond to specific message formats + # without littering your plugin code with regexps + # examples: + # plugin.map 'karmastats', :action => 'karma_stats' + # + # # while in the plugin... + # def karma_stats(m, params) + # m.reply "..." + # end + # + # # the default action is the first component + # plugin.map 'karma' + # + # # attributes can be pulled out of the match string + # plugin.map 'karma for :key' + # plugin.map 'karma :key' + # + # # while in the plugin... + # def karma(m, params) + # item = params[:key] + # m.reply 'karma for #{item}' + # end + # + # # you can setup defaults, to make parameters optional + # plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'} + # + # # the default auth check is also against the first component + # # but that can be changed + # plugin.map 'karmastats', :auth => 'karma' + # + # # maps can be restricted to public or private message: + # plugin.map 'karmastats', :private false, + # plugin.map 'karmastats', :public false, + # end + # + # To activate your maps, you simply register them + # plugin.register_maps + # This also sets the privmsg handler to use the map lookups for + # handling messages. You can still use listen(), kick() etc methods # # listen(UserMessage):: # Called for all messages of any type. To @@ -43,14 +85,28 @@ module Irc # plugin reload or bot quit - close any open # files/connections or flush caches here class Plugin + attr_reader :bot # the associated bot # initialise your plugin. Always call super if you override this method, # as important variables are set up for you def initialize @bot = Plugins.bot @names = Array.new + @handler = MessageMapper.new(self) @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, "")) end + def map(*args) + @handler.map(*args) + # register this map + name = @handler.last.items[0] + self.register name + unless self.respond_to?('privmsg') + def self.privmsg(m) + @handler.handle(m) + end + end + end + # return an identifier for this plugin, defaults to a list of the message # prefixes handled (used for error messages etc) def name @@ -70,15 +126,17 @@ module Irc # this can be called multiple times for a plugin to handle multiple # message prefixes def register(name) + return if Plugins.plugins.has_key?(name) Plugins.plugins[name] = self @names << name end - # is this plugin listening to all messages? - def listen? - @listen + # default usage method provided as a utility for simple plugins. The + # MessageMapper uses 'usage' as its default fallback method. + def usage(m, params) + m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'" end - + end # class to manage multiple plugins and delegate messages to them for diff --git a/rbot/plugins/autoop.rb b/rbot/plugins/autoop.rb index 094ee343..fdbcf6e0 100644 --- a/rbot/plugins/autoop.rb +++ b/rbot/plugins/autoop.rb @@ -38,7 +38,7 @@ class AutoOP < Plugin @registry[ma[1]] = channels.split(/,\s*/).collect { |x| x.strip } - @bot.okay m.replyto + m.okay else m.reply @bot.lang.get('dunno') end @@ -48,7 +48,7 @@ class AutoOP < Plugin if(!@registry.delete(params)) m.reply @bot.lang.get('dunno') else - @bot.okay m.replyto + m.okay end end diff --git a/rbot/plugins/cal.rb b/rbot/plugins/cal.rb index 1e823194..4f28310b 100644 --- a/rbot/plugins/cal.rb +++ b/rbot/plugins/cal.rb @@ -2,13 +2,14 @@ class CalPlugin < Plugin def help(plugin, topic="") "cal [options] => show current calendar [unix cal options]" end - def privmsg(m) - if m.params && m.params.length > 0 - m.reply Utils.safe_exec("cal", m.params) + def cal(m, params) + if params.has_key?(:month) + m.reply Utils.safe_exec("cal", params[:month], params[:year]) else m.reply Utils.safe_exec("cal") end end end plugin = CalPlugin.new -plugin.register("cal") +plugin.map 'cal :month :year', :requirements => {:month => /^\d+$/, :year => /^\d+$/} +plugin.map 'cal' diff --git a/rbot/plugins/eightball.rb b/rbot/plugins/eightball.rb index 6d123b34..64748490 100644 --- a/rbot/plugins/eightball.rb +++ b/rbot/plugins/eightball.rb @@ -8,11 +8,12 @@ class EightBallPlugin < Plugin def help(plugin, topic="") "magic 8-ball ruby bot module written by novex for nvinfo on #dumber@quakenet, usage:<botname> 8ball will i ever beat this cancer?" end - def privmsg(m) + def eightball(m, params) answers = @answers[rand(@answers.length)] action = "shakes the magic 8-ball... #{answers}" @bot.action m.replyto, action end end plugin = EightBallPlugin.new -plugin.register("8ball") +plugin.map '8ball', :action => 'usage' +plugin.map '8ball *params', :action => 'eightball' diff --git a/rbot/plugins/karma.rb b/rbot/plugins/karma.rb index 1bed175a..148427a5 100644 --- a/rbot/plugins/karma.rb +++ b/rbot/plugins/karma.rb @@ -27,60 +27,59 @@ class KarmaPlugin < Plugin end end + + def stats(m, params) + if (@registry.length) + max = @registry.values.max + min = @registry.values.min + best = @registry.to_hash.index(max) + worst = @registry.to_hash.index(min) + m.reply "#{@registry.length} items. Best: #{best} (#{max}); Worst: #{worst} (#{min})" + end + end + + def karma(m, params) + thing = params[:key] + thing = m.sourcenick unless thing + thing = thing.to_s + karma = @registry[thing] + if(karma != 0) + m.reply "karma for #{thing}: #{@registry[thing]}" + else + m.reply "#{thing} has neutral karma" + end + end + + def help(plugin, topic="") - "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own." + "karma module: <thing>++/<thing>-- => increase/decrease karma for <thing>, karma for <thing>? => show karma for <thing>, karmastats => show stats. Karma is a community rating system - only in-channel messages can affect karma and you cannot adjust your own." end def listen(m) - if(m.kind_of?(PrivMessage) && m.public?) - # in channel message, the kind we are interested in - if(m.message =~ /(\+\+|--)/) - string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1") - seen = Hash.new - while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, "")) - key = $1 - change = $2 - next if seen[key] - seen[key] = true + return unless m.kind_of?(PrivMessage) && m.public? + # in channel message, the kind we are interested in + if(m.message =~ /(\+\+|--)/) + string = m.message.sub(/\W(--|\+\+)(\(.*?\)|[^(++)(\-\-)\s]+)/, "\2\1") + seen = Hash.new + while(string.sub!(/(\(.*?\)|[^(++)(\-\-)\s]+)(\+\+|--)/, "")) + key = $1 + change = $2 + next if seen[key] + seen[key] = true - key.sub!(/^\((.*)\)$/, "\1") - key.gsub!(/\s+/, " ") - next unless(key.length > 0) - next if(key == m.sourcenick) - if(change == "++") - @registry[key] += 1 - elsif(change == "--") - @registry[key] -= 1 - end + key.sub!(/^\((.*)\)$/, "\1") + key.gsub!(/\s+/, " ") + next unless(key.length > 0) + next if(key == m.sourcenick) + if(change == "++") + @registry[key] += 1 + elsif(change == "--") + @registry[key] -= 1 end end end end - def privmsg(m) - if (m.plugin == "karmastats") - if (@registry.length) - max = @registry.values.max - min = @registry.values.min - best = @registry.to_hash.index(max) - worst = @registry.to_hash.index(min) - m.reply "#{@registry.length} votes. Best: #{best} (#{max}); Worst: #{worst} (#{min})" - return - end - end - unless(m.params) - m.reply "incorrect usage: " + m.plugin - return - end - if(m.params =~ /^(?:for\s+)?(\S+?)\??$/) - thing = $1 - karma = @registry[thing] - if(karma != 0) - m.reply "karma for #{thing}: #{@registry[thing]}" - else - m.reply "#{thing} has neutral karma" - end - end - end end plugin = KarmaPlugin.new -plugin.register("karma") -plugin.register("karmastats") +plugin.map 'karmastats', :action => 'stats' +plugin.map 'karma :key', :defaults => {:key => false} +plugin.map 'karma for :key' diff --git a/rbot/plugins/lart.rb b/rbot/plugins/lart.rb index 385b17c3..1c72c648 100644 --- a/rbot/plugins/lart.rb +++ b/rbot/plugins/lart.rb @@ -130,25 +130,25 @@ class LartPlugin < Plugin #{{{ def handle_addlart(m) @larts << m.params - @bot.okay m.replyto + m.okay end #}}} #{{{ def handle_rmlart(m) @larts.delete m.params - @bot.okay m.replyto + m.okay end #}}} #{{{ def handle_addpraise(m) @praises << m.params - @bot.okay m.replyto + m.okay end #}}} #{{{ def handle_rmpraise(m) @praises.delete m.params - @bot.okay m.replyto + m.okay end #}}} #}}} diff --git a/rbot/plugins/nickserv.rb b/rbot/plugins/nickserv.rb index 94c57e6d..1ef2baf7 100644 --- a/rbot/plugins/nickserv.rb +++ b/rbot/plugins/nickserv.rb @@ -38,23 +38,23 @@ class NickServPlugin < Plugin nick = $1 passwd = $2 @registry[nick] = passwd - @bot.okay m.replyto + m.okay when (/^register$/) passwd = genpasswd @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd @registry[@bot.nick] = passwd - @bot.okay m.replyto + m.okay when (/^register\s*(\S*)\s*(.*)$/) passwd = $1 email = $2 @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd + " " + email @registry[@bot.nick] = passwd - @bot.okay m.replyto + m.okay when (/^register\s*(.*)\s*$/) passwd = $1 @bot.sendmsg "PRIVMSG", "NickServ", "REGISTER " + passwd @registry[@bot.nick] = passwd - @bot.okay m.replyto + m.okay when (/^listnicks$/) if @bot.auth.allow?("config", m.source, m.replyto) if @registry.length > 0 @@ -68,7 +68,7 @@ class NickServPlugin < Plugin when (/^identify$/) if @registry.has_key?(@bot.nick) @bot.sendmsg "PRIVMSG", "NickServ", "IDENTIFY " + @registry[@bot.nick] - @bot.okay m.replyto + m.okay else m.reply "I dunno the nickserv password for the nickname #{@bot.nick} :(" end diff --git a/rbot/plugins/opmeh.rb b/rbot/plugins/opmeh.rb index eb392513..2776de60 100644 --- a/rbot/plugins/opmeh.rb +++ b/rbot/plugins/opmeh.rb @@ -12,6 +12,7 @@ class OpMehPlugin < Plugin end
target = m.sourcenick
@bot.sendq("MODE #{channel} +o #{target}")
+ m.okay
end
end
plugin = OpMehPlugin.new
diff --git a/rbot/plugins/quotes.rb b/rbot/plugins/quotes.rb index 0e46b495..674a9ed6 100644 --- a/rbot/plugins/quotes.rb +++ b/rbot/plugins/quotes.rb @@ -186,7 +186,7 @@ class QuotePlugin < Plugin num = $2.to_i if(@bot.auth.allow?("delquote", m.source, m.replyto)) if(delquote(channel, num)) - @bot.okay m.replyto + m.okay else m.reply "quote not found!" end @@ -288,7 +288,7 @@ class QuotePlugin < Plugin num = $1.to_i if(@bot.auth.allow?("delquote", m.source, m.replyto)) if(delquote(m.target, num)) - @bot.okay m.replyto + m.okay else m.reply "quote not found!" end diff --git a/rbot/plugins/remind.rb b/rbot/plugins/remind.rb index 402e2d08..5ad980ae 100644 --- a/rbot/plugins/remind.rb +++ b/rbot/plugins/remind.rb @@ -145,7 +145,7 @@ class RemindPlugin < Plugin m.reply "incorrect usage: " + help(m.plugin) return end - @bot.okay m.replyto + m.okay end end plugin = RemindPlugin.new diff --git a/rbot/plugins/roulette.rb b/rbot/plugins/roulette.rb index a3d102f3..c9d585ea 100644 --- a/rbot/plugins/roulette.rb +++ b/rbot/plugins/roulette.rb @@ -30,7 +30,7 @@ class RoulettePlugin < Plugin elsif m.params == "clearstats" if @bot.auth.allow?("config", m.source, m.replyto) @registry.clear - @bot.okay m.replyto + m.okay end return elsif m.params |