diff options
author | Tom Gilbert <tom@linuxbrit.co.uk> | 2005-07-26 21:50:00 +0000 |
---|---|---|
committer | Tom Gilbert <tom@linuxbrit.co.uk> | 2005-07-26 21:50:00 +0000 |
commit | 5d5d9df1a4825fad5ef045cfc0b21b16e5e2bcc7 (patch) | |
tree | f3814abafc8f1d6c589a29f4ddc89f55e510d493 /rbot/messagemapper.rb | |
parent | 3ba6917c904f5e664ae78b146f4e394ad805eb96 (diff) |
* 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+$/}
Diffstat (limited to 'rbot/messagemapper.rb')
-rw-r--r-- | rbot/messagemapper.rb | 158 |
1 files changed, 158 insertions, 0 deletions
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 |