rbot 0.9.9
Sign up to get free protection for your applications and to get access to all the features.
- data/AUTHORS +16 -0
- data/COPYING +21 -0
- data/ChangeLog +418 -0
- data/INSTALL +8 -0
- data/README +44 -0
- data/REQUIREMENTS +34 -0
- data/TODO +5 -0
- data/Usage_en.txt +129 -0
- data/bin/rbot +81 -0
- data/data/rbot/contrib/plugins/figlet.rb +20 -0
- data/data/rbot/contrib/plugins/ri.rb +83 -0
- data/data/rbot/contrib/plugins/stats.rb +232 -0
- data/data/rbot/contrib/plugins/vandale.rb +49 -0
- data/data/rbot/languages/dutch.lang +73 -0
- data/data/rbot/languages/english.lang +75 -0
- data/data/rbot/languages/french.lang +39 -0
- data/data/rbot/languages/german.lang +67 -0
- data/data/rbot/plugins/autoop.rb +68 -0
- data/data/rbot/plugins/autorejoin.rb +16 -0
- data/data/rbot/plugins/cal.rb +15 -0
- data/data/rbot/plugins/dice.rb +81 -0
- data/data/rbot/plugins/eightball.rb +19 -0
- data/data/rbot/plugins/excuse.rb +470 -0
- data/data/rbot/plugins/fish.rb +61 -0
- data/data/rbot/plugins/fortune.rb +22 -0
- data/data/rbot/plugins/freshmeat.rb +98 -0
- data/data/rbot/plugins/google.rb +51 -0
- data/data/rbot/plugins/host.rb +14 -0
- data/data/rbot/plugins/httpd.rb.disabled +35 -0
- data/data/rbot/plugins/insult.rb +258 -0
- data/data/rbot/plugins/karma.rb +85 -0
- data/data/rbot/plugins/lart.rb +181 -0
- data/data/rbot/plugins/math.rb +122 -0
- data/data/rbot/plugins/nickserv.rb +89 -0
- data/data/rbot/plugins/nslookup.rb +43 -0
- data/data/rbot/plugins/opme.rb +19 -0
- data/data/rbot/plugins/quakeauth.rb +51 -0
- data/data/rbot/plugins/quotes.rb +321 -0
- data/data/rbot/plugins/remind.rb +228 -0
- data/data/rbot/plugins/roshambo.rb +54 -0
- data/data/rbot/plugins/rot13.rb +10 -0
- data/data/rbot/plugins/roulette.rb +147 -0
- data/data/rbot/plugins/rss.rb.disabled +414 -0
- data/data/rbot/plugins/seen.rb +89 -0
- data/data/rbot/plugins/slashdot.rb +94 -0
- data/data/rbot/plugins/spell.rb +36 -0
- data/data/rbot/plugins/tube.rb +71 -0
- data/data/rbot/plugins/url.rb +88 -0
- data/data/rbot/plugins/weather.rb +649 -0
- data/data/rbot/plugins/wserver.rb +71 -0
- data/data/rbot/plugins/xmlrpc.rb.disabled +52 -0
- data/data/rbot/templates/keywords.rbot +4 -0
- data/data/rbot/templates/lart/larts +98 -0
- data/data/rbot/templates/lart/praises +5 -0
- data/data/rbot/templates/levels.rbot +30 -0
- data/data/rbot/templates/users.rbot +1 -0
- data/lib/rbot/auth.rb +203 -0
- data/lib/rbot/channel.rb +54 -0
- data/lib/rbot/config.rb +363 -0
- data/lib/rbot/dbhash.rb +112 -0
- data/lib/rbot/httputil.rb +141 -0
- data/lib/rbot/ircbot.rb +808 -0
- data/lib/rbot/ircsocket.rb +185 -0
- data/lib/rbot/keywords.rb +433 -0
- data/lib/rbot/language.rb +69 -0
- data/lib/rbot/message.rb +256 -0
- data/lib/rbot/messagemapper.rb +262 -0
- data/lib/rbot/plugins.rb +291 -0
- data/lib/rbot/post-install.rb +8 -0
- data/lib/rbot/rbotconfig.rb +36 -0
- data/lib/rbot/registry.rb +271 -0
- data/lib/rbot/rfc2812.rb +1104 -0
- data/lib/rbot/timer.rb +201 -0
- data/lib/rbot/utils.rb +83 -0
- data/setup.rb +1360 -0
- metadata +129 -0
@@ -0,0 +1,69 @@
|
|
1
|
+
module Irc
|
2
|
+
module Language
|
3
|
+
|
4
|
+
class Language
|
5
|
+
BotConfig.register BotConfigEnumValue.new('core.language',
|
6
|
+
:default => "english", :wizard => true,
|
7
|
+
:values => Proc.new{|bot|
|
8
|
+
Dir.new(Config::datadir + "/languages").collect {|f|
|
9
|
+
f =~ /\.lang$/ ? f.gsub(/\.lang$/, "") : nil
|
10
|
+
}.compact
|
11
|
+
},
|
12
|
+
:on_change => Proc.new {|bot, v| bot.lang.set_language v},
|
13
|
+
:desc => "Which language file the bot should use")
|
14
|
+
|
15
|
+
def initialize(language)
|
16
|
+
set_language language
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_language(language)
|
20
|
+
file = Config::datadir + "/languages/#{language}.lang"
|
21
|
+
unless(FileTest.exist?(file))
|
22
|
+
raise "no such language: #{language} (no such file #{file})"
|
23
|
+
end
|
24
|
+
@language = language
|
25
|
+
@file = file
|
26
|
+
scan
|
27
|
+
end
|
28
|
+
|
29
|
+
def scan
|
30
|
+
@strings = Hash.new
|
31
|
+
current_key = nil
|
32
|
+
IO.foreach(@file) {|l|
|
33
|
+
next if l =~ /^$/
|
34
|
+
next if l =~ /^\s*#/
|
35
|
+
if(l =~ /^(\S+):$/)
|
36
|
+
@strings[$1] = Array.new
|
37
|
+
current_key = $1
|
38
|
+
elsif(l =~ /^\s*(.*)$/)
|
39
|
+
@strings[current_key] << $1
|
40
|
+
end
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def rescan
|
45
|
+
scan
|
46
|
+
end
|
47
|
+
|
48
|
+
def get(key)
|
49
|
+
if(@strings.has_key?(key))
|
50
|
+
return @strings[key][rand(@strings[key].length)]
|
51
|
+
else
|
52
|
+
raise "undefined language key"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def save
|
57
|
+
File.open(@file, "w") {|file|
|
58
|
+
@strings.each {|key,val|
|
59
|
+
file.puts "#{key}:"
|
60
|
+
val.each_value {|v|
|
61
|
+
file.puts " #{v}"
|
62
|
+
}
|
63
|
+
}
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/lib/rbot/message.rb
ADDED
@@ -0,0 +1,256 @@
|
|
1
|
+
module Irc
|
2
|
+
|
3
|
+
# base user message class, all user messages derive from this
|
4
|
+
# (a user message is defined as having a source hostmask, a target
|
5
|
+
# nick/channel and a message part)
|
6
|
+
class BasicUserMessage
|
7
|
+
|
8
|
+
# associated bot
|
9
|
+
attr_reader :bot
|
10
|
+
|
11
|
+
# when the message was received
|
12
|
+
attr_reader :time
|
13
|
+
|
14
|
+
# hostmask of message source
|
15
|
+
attr_reader :source
|
16
|
+
|
17
|
+
# nick of message source
|
18
|
+
attr_reader :sourcenick
|
19
|
+
|
20
|
+
# url part of message source
|
21
|
+
attr_reader :sourceaddress
|
22
|
+
|
23
|
+
# nick/channel message was sent to
|
24
|
+
attr_reader :target
|
25
|
+
|
26
|
+
# contents of the message
|
27
|
+
attr_accessor :message
|
28
|
+
|
29
|
+
# has the message been replied to/handled by a plugin?
|
30
|
+
attr_accessor :replied
|
31
|
+
|
32
|
+
# instantiate a new Message
|
33
|
+
# bot:: associated bot class
|
34
|
+
# source:: hostmask of the message source
|
35
|
+
# target:: nick/channel message is destined for
|
36
|
+
# message:: message part
|
37
|
+
def initialize(bot, source, target, message)
|
38
|
+
@time = Time.now
|
39
|
+
@bot = bot
|
40
|
+
@source = source
|
41
|
+
@address = false
|
42
|
+
@target = target
|
43
|
+
@message = BasicUserMessage.stripcolour message
|
44
|
+
@replied = false
|
45
|
+
|
46
|
+
# split source into consituent parts
|
47
|
+
if source =~ /^((\S+)!(\S+))$/
|
48
|
+
@sourcenick = $2
|
49
|
+
@sourceaddress = $3
|
50
|
+
end
|
51
|
+
|
52
|
+
if target && target.downcase == @bot.nick.downcase
|
53
|
+
@address = true
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
# returns true if the message was addressed to the bot.
|
59
|
+
# This includes any private message to the bot, or any public message
|
60
|
+
# which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
|
61
|
+
# a kick message when bot was kicked etc.
|
62
|
+
def address?
|
63
|
+
return @address
|
64
|
+
end
|
65
|
+
|
66
|
+
# has this message been replied to by a plugin?
|
67
|
+
def replied?
|
68
|
+
return @replied
|
69
|
+
end
|
70
|
+
|
71
|
+
# strip mIRC colour escapes from a string
|
72
|
+
def BasicUserMessage.stripcolour(string)
|
73
|
+
return "" unless string
|
74
|
+
ret = string.gsub(/\cC\d\d?(?:,\d\d?)?/, "")
|
75
|
+
#ret.tr!("\x00-\x1f", "")
|
76
|
+
ret
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
# class for handling IRC user messages. Includes some utilities for handling
|
82
|
+
# the message, for example in plugins.
|
83
|
+
# The +message+ member will have any bot addressing "^bot: " removed
|
84
|
+
# (address? will return true in this case)
|
85
|
+
class UserMessage < BasicUserMessage
|
86
|
+
|
87
|
+
# for plugin messages, the name of the plugin invoked by the message
|
88
|
+
attr_reader :plugin
|
89
|
+
|
90
|
+
# for plugin messages, the rest of the message, with the plugin name
|
91
|
+
# removed
|
92
|
+
attr_reader :params
|
93
|
+
|
94
|
+
# convenience member. Who to reply to (i.e. would be sourcenick for a
|
95
|
+
# privately addressed message, or target (the channel) for a publicly
|
96
|
+
# addressed message
|
97
|
+
attr_reader :replyto
|
98
|
+
|
99
|
+
# channel the message was in, nil for privately addressed messages
|
100
|
+
attr_reader :channel
|
101
|
+
|
102
|
+
# for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
|
103
|
+
# will be stripped from the message)
|
104
|
+
attr_reader :action
|
105
|
+
|
106
|
+
# instantiate a new UserMessage
|
107
|
+
# bot:: associated bot class
|
108
|
+
# source:: hostmask of the message source
|
109
|
+
# target:: nick/channel message is destined for
|
110
|
+
# message:: message part
|
111
|
+
def initialize(bot, source, target, message)
|
112
|
+
super(bot, source, target, message)
|
113
|
+
@target = target
|
114
|
+
@private = false
|
115
|
+
@plugin = nil
|
116
|
+
@action = false
|
117
|
+
|
118
|
+
if target.downcase == @bot.nick.downcase
|
119
|
+
@private = true
|
120
|
+
@address = true
|
121
|
+
@channel = nil
|
122
|
+
@replyto = @sourcenick
|
123
|
+
else
|
124
|
+
@replyto = @target
|
125
|
+
@channel = @target
|
126
|
+
end
|
127
|
+
|
128
|
+
# check for option extra addressing prefixes, e.g "|search foo", or
|
129
|
+
# "!version" - first match wins
|
130
|
+
bot.addressing_prefixes.each {|mprefix|
|
131
|
+
if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
|
132
|
+
@address = true
|
133
|
+
break
|
134
|
+
end
|
135
|
+
}
|
136
|
+
|
137
|
+
# even if they used above prefixes, we allow for silly people who
|
138
|
+
# combine all possible types, e.g. "|rbot: hello", or
|
139
|
+
# "/msg rbot rbot: hello", etc
|
140
|
+
if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s*/, "")
|
141
|
+
@address = true
|
142
|
+
end
|
143
|
+
|
144
|
+
if(@message =~ /^\001ACTION\s(.+)\001/)
|
145
|
+
@message = $1
|
146
|
+
@action = true
|
147
|
+
end
|
148
|
+
|
149
|
+
# free splitting for plugins
|
150
|
+
@params = @message.dup
|
151
|
+
if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
|
152
|
+
@plugin = $1.downcase
|
153
|
+
@params = nil unless @params.length > 0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
# returns true for private messages, e.g. "/msg bot hello"
|
158
|
+
def private?
|
159
|
+
return @private
|
160
|
+
end
|
161
|
+
|
162
|
+
# returns true if the message was in a channel
|
163
|
+
def public?
|
164
|
+
return !@private
|
165
|
+
end
|
166
|
+
|
167
|
+
def action?
|
168
|
+
return @action
|
169
|
+
end
|
170
|
+
|
171
|
+
# convenience method to reply to a message, useful in plugins. It's the
|
172
|
+
# same as doing:
|
173
|
+
# <tt>@bot.say m.replyto, string</tt>
|
174
|
+
# So if the message is private, it will reply to the user. If it was
|
175
|
+
# in a channel, it will reply in the channel.
|
176
|
+
def reply(string)
|
177
|
+
@bot.say @replyto, string
|
178
|
+
@replied = true
|
179
|
+
end
|
180
|
+
|
181
|
+
# convenience method to reply "okay" in the current language to the
|
182
|
+
# message
|
183
|
+
def okay
|
184
|
+
@bot.say @replyto, @bot.lang.get("okay")
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
# class to manage IRC PRIVMSGs
|
190
|
+
class PrivMessage < UserMessage
|
191
|
+
end
|
192
|
+
|
193
|
+
# class to manage IRC NOTICEs
|
194
|
+
class NoticeMessage < UserMessage
|
195
|
+
end
|
196
|
+
|
197
|
+
# class to manage IRC KICKs
|
198
|
+
# +address?+ can be used as a shortcut to see if the bot was kicked,
|
199
|
+
# basically, +target+ was kicked from +channel+ by +source+ with +message+
|
200
|
+
class KickMessage < BasicUserMessage
|
201
|
+
# channel user was kicked from
|
202
|
+
attr_reader :channel
|
203
|
+
|
204
|
+
def initialize(bot, source, target, channel, message="")
|
205
|
+
super(bot, source, target, message)
|
206
|
+
@channel = channel
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# class to pass IRC Nick changes in. @message contains the old nickame,
|
211
|
+
# @sourcenick contains the new one.
|
212
|
+
class NickMessage < BasicUserMessage
|
213
|
+
def initialize(bot, source, oldnick, newnick)
|
214
|
+
super(bot, source, oldnick, newnick)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
class QuitMessage < BasicUserMessage
|
219
|
+
def initialize(bot, source, target, message="")
|
220
|
+
super(bot, source, target, message)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class TopicMessage < BasicUserMessage
|
225
|
+
# channel topic
|
226
|
+
attr_reader :topic
|
227
|
+
# topic set at (unixtime)
|
228
|
+
attr_reader :timestamp
|
229
|
+
# topic set on channel
|
230
|
+
attr_reader :channel
|
231
|
+
|
232
|
+
def initialize(bot, source, channel, timestamp, topic="")
|
233
|
+
super(bot, source, channel, topic)
|
234
|
+
@topic = topic
|
235
|
+
@timestamp = timestamp
|
236
|
+
@channel = channel
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# class to manage channel joins
|
241
|
+
class JoinMessage < BasicUserMessage
|
242
|
+
# channel joined
|
243
|
+
attr_reader :channel
|
244
|
+
def initialize(bot, source, channel, message="")
|
245
|
+
super(bot, source, channel, message)
|
246
|
+
@channel = channel
|
247
|
+
# in this case sourcenick is the nick that could be the bot
|
248
|
+
@address = (sourcenick.downcase == @bot.nick.downcase)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
# class to manage channel parts
|
253
|
+
# same as a join, but can have a message too
|
254
|
+
class PartMessage < JoinMessage
|
255
|
+
end
|
256
|
+
end
|
@@ -0,0 +1,262 @@
|
|
1
|
+
module Irc
|
2
|
+
|
3
|
+
# +MessageMapper+ is a class designed to reduce the amount of regexps and
|
4
|
+
# string parsing plugins and bot modules need to do, in order to process
|
5
|
+
# and respond to messages.
|
6
|
+
#
|
7
|
+
# You add templates to the MessageMapper which are examined by the handle
|
8
|
+
# method when handling a message. The templates tell the mapper which
|
9
|
+
# method in its parent class (your class) to invoke for that message. The
|
10
|
+
# string is split, optionally defaulted and validated before being passed
|
11
|
+
# to the matched method.
|
12
|
+
#
|
13
|
+
# A template such as "foo :option :otheroption" will match the string "foo
|
14
|
+
# bar baz" and, by default, result in method +foo+ being called, if
|
15
|
+
# present, in the parent class. It will receive two parameters, the
|
16
|
+
# Message (derived from BasicUserMessage) and a Hash containing
|
17
|
+
# {:option => "bar", :otheroption => "baz"}
|
18
|
+
# See the #map method for more details.
|
19
|
+
class MessageMapper
|
20
|
+
# used to set the method name used as a fallback for unmatched messages.
|
21
|
+
# The default fallback is a method called "usage".
|
22
|
+
attr_writer :fallback
|
23
|
+
|
24
|
+
# parent:: parent class which will receive mapped messages
|
25
|
+
#
|
26
|
+
# create a new MessageMapper with parent class +parent+. This class will
|
27
|
+
# receive messages from the mapper via the handle() method.
|
28
|
+
def initialize(parent)
|
29
|
+
@parent = parent
|
30
|
+
@templates = Array.new
|
31
|
+
@fallback = 'usage'
|
32
|
+
end
|
33
|
+
|
34
|
+
# args:: hash format containing arguments for this template
|
35
|
+
#
|
36
|
+
# map a template string to an action. example:
|
37
|
+
# map 'myplugin :parameter1 :parameter2'
|
38
|
+
# (other examples follow). By default, maps a matched string to an
|
39
|
+
# action with the name of the first word in the template. The action is
|
40
|
+
# a method which takes a message and a parameter hash for arguments.
|
41
|
+
#
|
42
|
+
# The :action => 'method_name' option can be used to override this
|
43
|
+
# default behaviour. Example:
|
44
|
+
# map 'myplugin :parameter1 :parameter2', :action => 'mymethod'
|
45
|
+
#
|
46
|
+
# By default whether a handler is fired depends on an auth check. The
|
47
|
+
# first component of the string is used for the auth check, unless
|
48
|
+
# overridden via the :auth => 'auth_name' option.
|
49
|
+
#
|
50
|
+
# Static parameters (not prefixed with ':' or '*') must match the
|
51
|
+
# respective component of the message exactly. Example:
|
52
|
+
# map 'myplugin :foo is :bar'
|
53
|
+
# will only match messages of the form "myplugin something is
|
54
|
+
# somethingelse"
|
55
|
+
#
|
56
|
+
# Dynamic parameters can be specified by a colon ':' to match a single
|
57
|
+
# component (whitespace seperated), or a * to such up all following
|
58
|
+
# parameters into an array. Example:
|
59
|
+
# map 'myplugin :parameter1 *rest'
|
60
|
+
#
|
61
|
+
# You can provide defaults for dynamic components using the :defaults
|
62
|
+
# parameter. If a component has a default, then it is optional. e.g:
|
63
|
+
# map 'myplugin :foo :bar', :defaults => {:bar => 'qux'}
|
64
|
+
# would match 'myplugin param param2' and also 'myplugin param'. In the
|
65
|
+
# latter case, :bar would be provided from the default.
|
66
|
+
#
|
67
|
+
# Components can be validated before being allowed to match, for
|
68
|
+
# example if you need a component to be a number:
|
69
|
+
# map 'myplugin :param', :requirements => {:param => /^\d+$/}
|
70
|
+
# will only match strings of the form 'myplugin 1234' or some other
|
71
|
+
# number.
|
72
|
+
#
|
73
|
+
# Templates can be set not to match public or private messages using the
|
74
|
+
# :public or :private boolean options.
|
75
|
+
#
|
76
|
+
# Further examples:
|
77
|
+
#
|
78
|
+
# # match 'karmastats' and call my stats() method
|
79
|
+
# map 'karmastats', :action => 'stats'
|
80
|
+
# # match 'karma' with an optional 'key' and call my karma() method
|
81
|
+
# map 'karma :key', :defaults => {:key => false}
|
82
|
+
# # match 'karma for something' and call my karma() method
|
83
|
+
# map 'karma for :key'
|
84
|
+
#
|
85
|
+
# # two matches, one for public messages in a channel, one for
|
86
|
+
# # private messages which therefore require a channel argument
|
87
|
+
# map 'urls search :channel :limit :string', :action => 'search',
|
88
|
+
# :defaults => {:limit => 4},
|
89
|
+
# :requirements => {:limit => /^\d+$/},
|
90
|
+
# :public => false
|
91
|
+
# plugin.map 'urls search :limit :string', :action => 'search',
|
92
|
+
# :defaults => {:limit => 4},
|
93
|
+
# :requirements => {:limit => /^\d+$/},
|
94
|
+
# :private => false
|
95
|
+
#
|
96
|
+
def map(*args)
|
97
|
+
@templates << Template.new(*args)
|
98
|
+
end
|
99
|
+
|
100
|
+
def each
|
101
|
+
@templates.each {|tmpl| yield tmpl}
|
102
|
+
end
|
103
|
+
def last
|
104
|
+
@templates.last
|
105
|
+
end
|
106
|
+
|
107
|
+
# m:: derived from BasicUserMessage
|
108
|
+
#
|
109
|
+
# examine the message +m+, comparing it with each map()'d template to
|
110
|
+
# find and process a match. Templates are examined in the order they
|
111
|
+
# were map()'d - first match wins.
|
112
|
+
#
|
113
|
+
# returns +true+ if a match is found including fallbacks, +false+
|
114
|
+
# otherwise.
|
115
|
+
def handle(m)
|
116
|
+
return false if @templates.empty?
|
117
|
+
failures = []
|
118
|
+
@templates.each do |tmpl|
|
119
|
+
options, failure = tmpl.recognize(m)
|
120
|
+
if options.nil?
|
121
|
+
failures << [tmpl, failure]
|
122
|
+
else
|
123
|
+
action = tmpl.options[:action] ? tmpl.options[:action] : tmpl.items[0]
|
124
|
+
next unless @parent.respond_to?(action)
|
125
|
+
auth = tmpl.options[:auth] ? tmpl.options[:auth] : tmpl.items[0]
|
126
|
+
debug "checking auth for #{auth}"
|
127
|
+
if m.bot.auth.allow?(auth, m.source, m.replyto)
|
128
|
+
debug "template match found and auth'd: #{action.inspect} #{options.inspect}"
|
129
|
+
@parent.send(action, m, options)
|
130
|
+
return true
|
131
|
+
end
|
132
|
+
debug "auth failed for #{auth}"
|
133
|
+
# if it's just an auth failure but otherwise the match is good,
|
134
|
+
# don't try any more handlers
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
end
|
138
|
+
debug failures.inspect
|
139
|
+
debug "no handler found, trying fallback"
|
140
|
+
if @fallback != nil && @parent.respond_to?(@fallback)
|
141
|
+
if m.bot.auth.allow?(@fallback, m.source, m.replyto)
|
142
|
+
@parent.send(@fallback, m, {})
|
143
|
+
return true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
return false
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
class Template
|
152
|
+
attr_reader :defaults # The defaults hash
|
153
|
+
attr_reader :options # The options hash
|
154
|
+
attr_reader :items
|
155
|
+
def initialize(template, hash={})
|
156
|
+
raise ArgumentError, "Second argument must be a hash!" unless hash.kind_of?(Hash)
|
157
|
+
@defaults = hash[:defaults].kind_of?(Hash) ? hash.delete(:defaults) : {}
|
158
|
+
@requirements = hash[:requirements].kind_of?(Hash) ? hash.delete(:requirements) : {}
|
159
|
+
self.items = template
|
160
|
+
@options = hash
|
161
|
+
end
|
162
|
+
def items=(str)
|
163
|
+
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
|
164
|
+
items.shift if items.first == ""
|
165
|
+
items.pop if items.last == ""
|
166
|
+
@items = items
|
167
|
+
|
168
|
+
if @items.first.kind_of? Symbol
|
169
|
+
raise ArgumentError, "Illegal template -- first component cannot be dynamic\n #{str.inspect}"
|
170
|
+
end
|
171
|
+
|
172
|
+
# Verify uniqueness of each component.
|
173
|
+
@items.inject({}) do |seen, item|
|
174
|
+
if item.kind_of? Symbol
|
175
|
+
raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
|
176
|
+
seen[item] = true
|
177
|
+
end
|
178
|
+
seen
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
# Recognize the provided string components, returning a hash of
|
183
|
+
# recognized values, or [nil, reason] if the string isn't recognized.
|
184
|
+
def recognize(m)
|
185
|
+
components = m.message.split(/\s+/)
|
186
|
+
options = {}
|
187
|
+
|
188
|
+
@items.each do |item|
|
189
|
+
if /^\*/ =~ item.to_s
|
190
|
+
if components.empty?
|
191
|
+
value = @defaults.has_key?(item) ? @defaults[item].clone : []
|
192
|
+
else
|
193
|
+
value = components.clone
|
194
|
+
end
|
195
|
+
components = []
|
196
|
+
def value.to_s() self.join(' ') end
|
197
|
+
options[item.to_s.sub(/^\*/,"").intern] = value
|
198
|
+
elsif item.kind_of? Symbol
|
199
|
+
value = components.shift || @defaults[item]
|
200
|
+
if passes_requirements?(item, value)
|
201
|
+
options[item] = value
|
202
|
+
else
|
203
|
+
if @defaults.has_key?(item)
|
204
|
+
options[item] = @defaults[item]
|
205
|
+
# push the test-failed component back on the stack
|
206
|
+
components.unshift value
|
207
|
+
else
|
208
|
+
return nil, requirements_for(item)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
else
|
212
|
+
return nil, "No value available for component #{item.inspect}" if components.empty?
|
213
|
+
component = components.shift
|
214
|
+
return nil, "Value for component #{item.inspect} doesn't match #{component}" if component != item
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
return nil, "Unused components were left: #{components.join '/'}" unless components.empty?
|
219
|
+
|
220
|
+
return nil, "template is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
|
221
|
+
return nil, "template is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
|
222
|
+
|
223
|
+
options.delete_if {|k, v| v.nil?} # Remove nil values.
|
224
|
+
return options, nil
|
225
|
+
end
|
226
|
+
|
227
|
+
def inspect
|
228
|
+
when_str = @requirements.empty? ? "" : " when #{@requirements.inspect}"
|
229
|
+
default_str = @defaults.empty? ? "" : " || #{@defaults.inspect}"
|
230
|
+
"<#{self.class.to_s} #{@items.collect{|c| c.kind_of?(String) ? c : c.inspect}.join(' ').inspect}#{default_str}#{when_str}>"
|
231
|
+
end
|
232
|
+
|
233
|
+
# Verify that the given value passes this template's requirements
|
234
|
+
def passes_requirements?(name, value)
|
235
|
+
return @defaults.key?(name) && @defaults[name].nil? if value.nil? # Make sure it's there if it should be
|
236
|
+
|
237
|
+
case @requirements[name]
|
238
|
+
when nil then true
|
239
|
+
when Regexp then
|
240
|
+
value = value.to_s
|
241
|
+
match = @requirements[name].match(value)
|
242
|
+
match && match[0].length == value.length
|
243
|
+
else
|
244
|
+
@requirements[name] == value.to_s
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def requirements_for(name)
|
249
|
+
name = name.to_s.sub(/^\*/,"").intern if (/^\*/ =~ name.inspect)
|
250
|
+
presence = (@defaults.key?(name) && @defaults[name].nil?)
|
251
|
+
requirement = case @requirements[name]
|
252
|
+
when nil then nil
|
253
|
+
when Regexp then "match #{@requirements[name].inspect}"
|
254
|
+
else "be equal to #{@requirements[name].inspect}"
|
255
|
+
end
|
256
|
+
if presence && requirement then "#{name} must be present and #{requirement}"
|
257
|
+
elsif presence || requirement then "#{name} must #{requirement || 'be present'}"
|
258
|
+
else "#{name} has no requirements"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|