summaryrefslogtreecommitdiff
path: root/lib/rbot/messagemapper.rb
blob: f83fafb2902d5c7e36fd78c54f502cb1a4d24365 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
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]
          if passes_requirements?(item, value)
            options[item] = value
          else
            if @defaults.has_key?(item)
              options[item] = @defaults[item]
              # push the test-failed component back on the stack
              components.unshift value
            else
              return nil, requirements_for(item)
            end
          end
        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