]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/messagemapper.rb
42563d2305a2a3d131b57d1e8148e9de49e7bd90
[user/henk/code/ruby/rbot.git] / lib / rbot / messagemapper.rb
1 module Irc
2   class MessageMapper
3     attr_writer :fallback
4
5     def initialize(parent)
6       @parent = parent
7       @routes = Array.new
8       @fallback = 'usage'
9     end
10     
11     def map(*args)
12       @routes << Route.new(*args)
13     end
14     
15     def each
16       @routes.each {|route| yield route}
17     end
18     def last
19       @routes.last
20     end
21     
22     def handle(m)
23       return false if @routes.empty?
24       failures = []
25       @routes.each do |route|
26         options, failure = route.recognize(m)
27         if options.nil?
28           failures << [route, failure]
29         else
30           action = route.options[:action] ? route.options[:action] : route.items[0]
31           next unless @parent.respond_to?(action)
32           auth = route.options[:auth] ? route.options[:auth] : action
33           if m.bot.auth.allow?(auth, m.source, m.replyto)
34             debug "route found and auth'd: #{action.inspect} #{options.inspect}"
35             @parent.send(action, m, options)
36             return true
37           end
38           # if it's just an auth failure but otherwise the match is good,
39           # don't try any more handlers
40           break
41         end
42       end
43       debug failures.inspect
44       debug "no handler found, trying fallback"
45       if @fallback != nil && @parent.respond_to?(@fallback)
46         if m.bot.auth.allow?(@fallback, m.source, m.replyto)
47           @parent.send(@fallback, m, {})
48           return true
49         end
50       end
51       return false
52     end
53
54   end
55
56   class Route
57     attr_reader :defaults # The defaults hash
58     attr_reader :options  # The options hash
59     attr_reader :items
60     def initialize(template, hash={})
61       raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
62       @defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
63       @requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
64       self.items = template
65       @options = hash
66     end
67     def items=(str)
68       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
69       items.shift if items.first == ""
70       items.pop if items.last == ""
71       @items = items
72
73       if @items.first.kind_of? Symbol
74         raise ArgumentError, "Illegal template -- first component cannot be dynamic\n   #{str.inspect}"
75       end
76
77       # Verify uniqueness of each component.
78       @items.inject({}) do |seen, item|
79         if item.kind_of? Symbol
80           raise ArgumentError, "Illegal template -- duplicate item #{item}\n   #{str.inspect}" if seen.key? item
81           seen[item] = true
82         end
83         seen
84       end
85     end
86
87     # Recognize the provided string components, returning a hash of
88     # recognized values, or [nil, reason] if the string isn't recognized.
89     def recognize(m)
90       components = m.message.split(/\s+/)
91       options = {}
92
93       @items.each do |item|
94         if /^\*/ =~ item.to_s
95           if components.empty?
96             value = @defaults.has_key?(item) ? @defaults[item].clone : []
97           else
98             value = components.clone
99           end
100           components = []
101           def value.to_s() self.join(' ') end
102           options[item.to_s.sub(/^\*/,"").intern] = value
103         elsif item.kind_of? Symbol
104           value = components.shift || @defaults[item]
105           if passes_requirements?(item, value)
106             options[item] = value
107           else
108             if @defaults.has_key?(item)
109               debug "item #{item} doesn't pass reqs but has a default of #{@defaults[item]}"
110               options[item] = @defaults[item].clone
111               # push the test-failed component back on the stack
112               components.unshift value
113             else
114               return nil, requirements_for(item)
115             end
116           end
117         else
118           return nil, "No value available for component #{item.inspect}" if components.empty?
119           component = components.shift
120           return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
121         end
122       end
123
124       return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
125
126       return nil, "route is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
127       return nil, "route is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
128       
129       options.delete_if {|k, v| v.nil?} # Remove nil values.
130       return options, nil
131     end
132
133     def inspect
134       when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
135       default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
136       "<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join(' ').inspect}#{default_str}#{when_str}>"
137     end
138
139     # Verify that the given value passes this route's requirements
140     def passes_requirements?(name, value)
141       return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
142
143       case @requirements[name]
144         when nil then true
145         when Regexp then
146           value = value.to_s
147           match = @requirements[name].match(value)
148           match && match[0].length == value.length
149         else
150           @requirements[name] == value.to_s
151       end
152     end
153
154     def requirements_for(name)
155       name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
156       presence = (@defaults.key?(name) && @defaults[name].nil?)
157       requirement = case @requirements[name]
158         when nil then nil
159         when Regexp then "match #{@requirements[name].inspect}"
160         else "be equal to #{@requirements[name].inspect}"
161       end
162       if presence && requirement then "#{name} must be present and #{requirement}"
163       elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
164       else "#{name} has no requirements"
165       end
166     end
167   end
168 end