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
168
|
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)
debug "item #{item} doesn't pass reqs but has a default of #{@defaults[item]}"
options[item] = @defaults[item].clone
# 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
|