rbot 0.9.9 → 0.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/AUTHORS +8 -0
- data/ChangeLog +51 -0
- data/INSTALL +4 -0
- data/README +1 -0
- data/REQUIREMENTS +11 -0
- data/TODO +2 -0
- data/bin/rbot +21 -2
- data/data/rbot/languages/german.lang +4 -1
- data/data/rbot/languages/russian.lang +75 -0
- data/data/rbot/plugins/autoop.rb +42 -51
- data/data/rbot/plugins/bans.rb +205 -0
- data/data/rbot/plugins/bash.rb +56 -0
- data/data/rbot/plugins/chucknorris.rb +74 -0
- data/data/rbot/plugins/chucknorris.yml.gz +0 -0
- data/data/rbot/plugins/deepthoughts.rb +95 -0
- data/data/rbot/plugins/demauro.rb +95 -0
- data/data/rbot/plugins/digg.rb +51 -0
- data/data/rbot/plugins/figlet.rb +24 -0
- data/data/rbot/plugins/forecast.rb +133 -0
- data/data/rbot/plugins/freshmeat.rb +13 -7
- data/data/rbot/plugins/google.rb +2 -0
- data/data/rbot/plugins/grouphug.rb +36 -0
- data/data/rbot/plugins/imdb.rb +92 -0
- data/data/rbot/plugins/insult.rb +8 -1
- data/data/rbot/plugins/iplookup.rb +227 -0
- data/data/rbot/plugins/karma.rb +2 -2
- data/data/rbot/plugins/keywords.rb +470 -0
- data/data/rbot/plugins/lart.rb +132 -146
- data/data/rbot/plugins/lastfm.rb +25 -0
- data/data/rbot/plugins/markov.rb +204 -0
- data/data/rbot/plugins/math.rb +5 -1
- data/data/rbot/plugins/nickserv.rb +71 -11
- data/data/rbot/plugins/opme.rb +19 -19
- data/data/rbot/plugins/quakeauth.rb +2 -2
- data/data/rbot/plugins/quotes.rb +40 -25
- data/data/rbot/plugins/remind.rb +1 -1
- data/data/rbot/plugins/rot13.rb +2 -2
- data/data/rbot/plugins/roulette.rb +49 -15
- data/data/rbot/plugins/rss.rb +585 -0
- data/data/rbot/plugins/rubyurl.rb +39 -0
- data/data/rbot/plugins/seen.rb +2 -1
- data/data/rbot/plugins/slashdot.rb +5 -5
- data/data/rbot/plugins/spell.rb +5 -0
- data/data/rbot/plugins/theyfightcrime.rb +121 -0
- data/data/rbot/plugins/threat.rb +55 -0
- data/data/rbot/plugins/tinyurl.rb +39 -0
- data/data/rbot/plugins/topic.rb +204 -0
- data/data/rbot/plugins/urban.rb +71 -0
- data/data/rbot/plugins/url.rb +399 -4
- data/data/rbot/plugins/wow.rb +123 -0
- data/data/rbot/plugins/wserver.rb +1 -1
- data/data/rbot/templates/levels.rbot +2 -0
- data/lib/rbot/auth.rb +207 -96
- data/lib/rbot/channel.rb +5 -5
- data/lib/rbot/config.rb +125 -24
- data/lib/rbot/dbhash.rb +87 -21
- data/lib/rbot/httputil.rb +181 -13
- data/lib/rbot/ircbot.rb +525 -179
- data/lib/rbot/ircsocket.rb +330 -54
- data/lib/rbot/message.rb +66 -23
- data/lib/rbot/messagemapper.rb +25 -17
- data/lib/rbot/plugins.rb +244 -115
- data/lib/rbot/post-clean.rb +1 -0
- data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
- data/lib/rbot/rbotconfig.rb +29 -14
- data/lib/rbot/registry.rb +111 -72
- data/lib/rbot/rfc2812.rb +208 -197
- data/lib/rbot/timer.rb +4 -0
- data/lib/rbot/utils.rb +2 -2
- metadata +127 -104
- data/data/rbot/plugins/rss.rb.disabled +0 -414
- data/lib/rbot/keywords.rb +0 -433
data/lib/rbot/message.rb
CHANGED
@@ -1,28 +1,37 @@
|
|
1
1
|
module Irc
|
2
|
+
BotConfig.register BotConfigArrayValue.new('core.address_prefix',
|
3
|
+
:default => [], :wizard => true,
|
4
|
+
:desc => "what non nick-matching prefixes should the bot respond to as if addressed (e.g !, so that '!foo' is treated like 'rbot: foo')"
|
5
|
+
)
|
6
|
+
|
7
|
+
Color = "\003"
|
8
|
+
Bold = "\002"
|
9
|
+
Underline = "\037"
|
10
|
+
Reverse = "\026"
|
2
11
|
|
3
12
|
# base user message class, all user messages derive from this
|
4
13
|
# (a user message is defined as having a source hostmask, a target
|
5
14
|
# nick/channel and a message part)
|
6
15
|
class BasicUserMessage
|
7
|
-
|
16
|
+
|
8
17
|
# associated bot
|
9
18
|
attr_reader :bot
|
10
|
-
|
19
|
+
|
11
20
|
# when the message was received
|
12
21
|
attr_reader :time
|
13
22
|
|
14
23
|
# hostmask of message source
|
15
24
|
attr_reader :source
|
16
|
-
|
25
|
+
|
17
26
|
# nick of message source
|
18
27
|
attr_reader :sourcenick
|
19
|
-
|
28
|
+
|
20
29
|
# url part of message source
|
21
30
|
attr_reader :sourceaddress
|
22
|
-
|
31
|
+
|
23
32
|
# nick/channel message was sent to
|
24
33
|
attr_reader :target
|
25
|
-
|
34
|
+
|
26
35
|
# contents of the message
|
27
36
|
attr_accessor :message
|
28
37
|
|
@@ -35,6 +44,8 @@ module Irc
|
|
35
44
|
# target:: nick/channel message is destined for
|
36
45
|
# message:: message part
|
37
46
|
def initialize(bot, source, target, message)
|
47
|
+
@msg_wants_id = false unless defined? @msg_wants_id
|
48
|
+
|
38
49
|
@time = Time.now
|
39
50
|
@bot = bot
|
40
51
|
@source = source
|
@@ -43,18 +54,32 @@ module Irc
|
|
43
54
|
@message = BasicUserMessage.stripcolour message
|
44
55
|
@replied = false
|
45
56
|
|
57
|
+
@identified = false
|
58
|
+
if @msg_wants_id && @bot.capabilities["identify-msg".to_sym]
|
59
|
+
if @message =~ /([-+])(.*)/
|
60
|
+
@identified = ($1=="+")
|
61
|
+
@message = $2
|
62
|
+
else
|
63
|
+
warning "Message does not have identification"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
46
67
|
# split source into consituent parts
|
47
68
|
if source =~ /^((\S+)!(\S+))$/
|
48
69
|
@sourcenick = $2
|
49
70
|
@sourceaddress = $3
|
50
71
|
end
|
51
|
-
|
72
|
+
|
52
73
|
if target && target.downcase == @bot.nick.downcase
|
53
74
|
@address = true
|
54
75
|
end
|
55
|
-
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
def identified?
|
80
|
+
return @identified
|
56
81
|
end
|
57
|
-
|
82
|
+
|
58
83
|
# returns true if the message was addressed to the bot.
|
59
84
|
# This includes any private message to the bot, or any public message
|
60
85
|
# which looks like it's addressed to the bot, e.g. "bot: foo", "bot, foo",
|
@@ -83,10 +108,10 @@ module Irc
|
|
83
108
|
# The +message+ member will have any bot addressing "^bot: " removed
|
84
109
|
# (address? will return true in this case)
|
85
110
|
class UserMessage < BasicUserMessage
|
86
|
-
|
111
|
+
|
87
112
|
# for plugin messages, the name of the plugin invoked by the message
|
88
113
|
attr_reader :plugin
|
89
|
-
|
114
|
+
|
90
115
|
# for plugin messages, the rest of the message, with the plugin name
|
91
116
|
# removed
|
92
117
|
attr_reader :params
|
@@ -98,11 +123,11 @@ module Irc
|
|
98
123
|
|
99
124
|
# channel the message was in, nil for privately addressed messages
|
100
125
|
attr_reader :channel
|
101
|
-
|
126
|
+
|
102
127
|
# for PRIVMSGs, true if the message was a CTCP ACTION (CTCP stuff
|
103
128
|
# will be stripped from the message)
|
104
129
|
attr_reader :action
|
105
|
-
|
130
|
+
|
106
131
|
# instantiate a new UserMessage
|
107
132
|
# bot:: associated bot class
|
108
133
|
# source:: hostmask of the message source
|
@@ -114,7 +139,7 @@ module Irc
|
|
114
139
|
@private = false
|
115
140
|
@plugin = nil
|
116
141
|
@action = false
|
117
|
-
|
142
|
+
|
118
143
|
if target.downcase == @bot.nick.downcase
|
119
144
|
@private = true
|
120
145
|
@address = true
|
@@ -127,25 +152,25 @@ module Irc
|
|
127
152
|
|
128
153
|
# check for option extra addressing prefixes, e.g "|search foo", or
|
129
154
|
# "!version" - first match wins
|
130
|
-
bot.
|
155
|
+
bot.config['core.address_prefix'].each {|mprefix|
|
131
156
|
if @message.gsub!(/^#{Regexp.escape(mprefix)}\s*/, "")
|
132
157
|
@address = true
|
133
158
|
break
|
134
159
|
end
|
135
160
|
}
|
136
|
-
|
161
|
+
|
137
162
|
# even if they used above prefixes, we allow for silly people who
|
138
|
-
# combine all possible types, e.g. "|rbot: hello", or
|
163
|
+
# combine all possible types, e.g. "|rbot: hello", or
|
139
164
|
# "/msg rbot rbot: hello", etc
|
140
|
-
if @message.gsub!(/^\s*#{bot.nick}\s*([:;,>]|\s)\s
|
165
|
+
if @message.gsub!(/^\s*#{Regexp.escape(bot.nick)}\s*([:;,>]|\s)\s*/i, "")
|
141
166
|
@address = true
|
142
167
|
end
|
143
|
-
|
168
|
+
|
144
169
|
if(@message =~ /^\001ACTION\s(.+)\001/)
|
145
170
|
@message = $1
|
146
171
|
@action = true
|
147
172
|
end
|
148
|
-
|
173
|
+
|
149
174
|
# free splitting for plugins
|
150
175
|
@params = @message.dup
|
151
176
|
if @params.gsub!(/^\s*(\S+)[\s$]*/, "")
|
@@ -178,6 +203,16 @@ module Irc
|
|
178
203
|
@replied = true
|
179
204
|
end
|
180
205
|
|
206
|
+
# convenience method to reply to a message with an action. It's the
|
207
|
+
# same as doing:
|
208
|
+
# <tt>@bot.action m.replyto, string</tt>
|
209
|
+
# So if the message is private, it will reply to the user. If it was
|
210
|
+
# in a channel, it will reply in the channel.
|
211
|
+
def act(string)
|
212
|
+
@bot.action @replyto, string
|
213
|
+
@replied = true
|
214
|
+
end
|
215
|
+
|
181
216
|
# convenience method to reply "okay" in the current language to the
|
182
217
|
# message
|
183
218
|
def okay
|
@@ -188,10 +223,18 @@ module Irc
|
|
188
223
|
|
189
224
|
# class to manage IRC PRIVMSGs
|
190
225
|
class PrivMessage < UserMessage
|
226
|
+
def initialize(bot, source, target, message)
|
227
|
+
@msg_wants_id = true
|
228
|
+
super
|
229
|
+
end
|
191
230
|
end
|
192
|
-
|
231
|
+
|
193
232
|
# class to manage IRC NOTICEs
|
194
233
|
class NoticeMessage < UserMessage
|
234
|
+
def initialize(bot, source, target, message)
|
235
|
+
@msg_wants_id = true
|
236
|
+
super
|
237
|
+
end
|
195
238
|
end
|
196
239
|
|
197
240
|
# class to manage IRC KICKs
|
@@ -200,7 +243,7 @@ module Irc
|
|
200
243
|
class KickMessage < BasicUserMessage
|
201
244
|
# channel user was kicked from
|
202
245
|
attr_reader :channel
|
203
|
-
|
246
|
+
|
204
247
|
def initialize(bot, source, target, channel, message="")
|
205
248
|
super(bot, source, target, message)
|
206
249
|
@channel = channel
|
@@ -248,7 +291,7 @@ module Irc
|
|
248
291
|
@address = (sourcenick.downcase == @bot.nick.downcase)
|
249
292
|
end
|
250
293
|
end
|
251
|
-
|
294
|
+
|
252
295
|
# class to manage channel parts
|
253
296
|
# same as a join, but can have a message too
|
254
297
|
class PartMessage < JoinMessage
|
data/lib/rbot/messagemapper.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
module Irc
|
2
|
-
|
2
|
+
|
3
3
|
# +MessageMapper+ is a class designed to reduce the amount of regexps and
|
4
4
|
# string parsing plugins and bot modules need to do, in order to process
|
5
5
|
# and respond to messages.
|
6
|
-
#
|
6
|
+
#
|
7
7
|
# You add templates to the MessageMapper which are examined by the handle
|
8
8
|
# method when handling a message. The templates tell the mapper which
|
9
9
|
# method in its parent class (your class) to invoke for that message. The
|
@@ -13,7 +13,7 @@ module Irc
|
|
13
13
|
# A template such as "foo :option :otheroption" will match the string "foo
|
14
14
|
# bar baz" and, by default, result in method +foo+ being called, if
|
15
15
|
# present, in the parent class. It will receive two parameters, the
|
16
|
-
# Message (derived from BasicUserMessage) and a Hash containing
|
16
|
+
# Message (derived from BasicUserMessage) and a Hash containing
|
17
17
|
# {:option => "bar", :otheroption => "baz"}
|
18
18
|
# See the #map method for more details.
|
19
19
|
class MessageMapper
|
@@ -30,7 +30,7 @@ module Irc
|
|
30
30
|
@templates = Array.new
|
31
31
|
@fallback = 'usage'
|
32
32
|
end
|
33
|
-
|
33
|
+
|
34
34
|
# args:: hash format containing arguments for this template
|
35
35
|
#
|
36
36
|
# map a template string to an action. example:
|
@@ -38,7 +38,7 @@ module Irc
|
|
38
38
|
# (other examples follow). By default, maps a matched string to an
|
39
39
|
# action with the name of the first word in the template. The action is
|
40
40
|
# a method which takes a message and a parameter hash for arguments.
|
41
|
-
#
|
41
|
+
#
|
42
42
|
# The :action => 'method_name' option can be used to override this
|
43
43
|
# default behaviour. Example:
|
44
44
|
# map 'myplugin :parameter1 :parameter2', :action => 'mymethod'
|
@@ -50,14 +50,14 @@ module Irc
|
|
50
50
|
# Static parameters (not prefixed with ':' or '*') must match the
|
51
51
|
# respective component of the message exactly. Example:
|
52
52
|
# map 'myplugin :foo is :bar'
|
53
|
-
# will only match messages of the form "myplugin something is
|
53
|
+
# will only match messages of the form "myplugin something is
|
54
54
|
# somethingelse"
|
55
55
|
#
|
56
56
|
# Dynamic parameters can be specified by a colon ':' to match a single
|
57
|
-
# component (whitespace seperated), or a * to
|
57
|
+
# component (whitespace seperated), or a * to suck up all following
|
58
58
|
# parameters into an array. Example:
|
59
59
|
# map 'myplugin :parameter1 *rest'
|
60
|
-
#
|
60
|
+
#
|
61
61
|
# You can provide defaults for dynamic components using the :defaults
|
62
62
|
# parameter. If a component has a default, then it is optional. e.g:
|
63
63
|
# map 'myplugin :foo :bar', :defaults => {:bar => 'qux'}
|
@@ -81,7 +81,7 @@ module Irc
|
|
81
81
|
# map 'karma :key', :defaults => {:key => false}
|
82
82
|
# # match 'karma for something' and call my karma() method
|
83
83
|
# map 'karma for :key'
|
84
|
-
#
|
84
|
+
#
|
85
85
|
# # two matches, one for public messages in a channel, one for
|
86
86
|
# # private messages which therefore require a channel argument
|
87
87
|
# map 'urls search :channel :limit :string', :action => 'search',
|
@@ -91,19 +91,19 @@ module Irc
|
|
91
91
|
# plugin.map 'urls search :limit :string', :action => 'search',
|
92
92
|
# :defaults => {:limit => 4},
|
93
93
|
# :requirements => {:limit => /^\d+$/},
|
94
|
-
# :private => false
|
94
|
+
# :private => false
|
95
95
|
#
|
96
96
|
def map(*args)
|
97
97
|
@templates << Template.new(*args)
|
98
98
|
end
|
99
|
-
|
99
|
+
|
100
100
|
def each
|
101
101
|
@templates.each {|tmpl| yield tmpl}
|
102
102
|
end
|
103
103
|
def last
|
104
104
|
@templates.last
|
105
105
|
end
|
106
|
-
|
106
|
+
|
107
107
|
# m:: derived from BasicUserMessage
|
108
108
|
#
|
109
109
|
# examine the message +m+, comparing it with each map()'d template to
|
@@ -121,7 +121,10 @@ module Irc
|
|
121
121
|
failures << [tmpl, failure]
|
122
122
|
else
|
123
123
|
action = tmpl.options[:action] ? tmpl.options[:action] : tmpl.items[0]
|
124
|
-
|
124
|
+
unless @parent.respond_to?(action)
|
125
|
+
failures << [tmpl, "class does not respond to action #{action}"]
|
126
|
+
next
|
127
|
+
end
|
125
128
|
auth = tmpl.options[:auth] ? tmpl.options[:auth] : tmpl.items[0]
|
126
129
|
debug "checking auth for #{auth}"
|
127
130
|
if m.bot.auth.allow?(auth, m.source, m.replyto)
|
@@ -135,7 +138,9 @@ module Irc
|
|
135
138
|
return false
|
136
139
|
end
|
137
140
|
end
|
138
|
-
|
141
|
+
failures.each {|f, r|
|
142
|
+
debug "#{f.inspect} => #{r}"
|
143
|
+
}
|
139
144
|
debug "no handler found, trying fallback"
|
140
145
|
if @fallback != nil && @parent.respond_to?(@fallback)
|
141
146
|
if m.bot.auth.allow?(@fallback, m.source, m.replyto)
|
@@ -172,8 +177,11 @@ module Irc
|
|
172
177
|
# Verify uniqueness of each component.
|
173
178
|
@items.inject({}) do |seen, item|
|
174
179
|
if item.kind_of? Symbol
|
175
|
-
|
176
|
-
|
180
|
+
# We must remove the initial * when present,
|
181
|
+
# because the parameters hash will intern both :item and *item as :item
|
182
|
+
it = item.to_s.sub(/^\*/,"").intern
|
183
|
+
raise ArgumentError, "Illegal template -- duplicate item #{it} in #{str.inspect}" if seen.key? it
|
184
|
+
seen[it] = true
|
177
185
|
end
|
178
186
|
seen
|
179
187
|
end
|
@@ -219,7 +227,7 @@ module Irc
|
|
219
227
|
|
220
228
|
return nil, "template is not configured for private messages" if @options.has_key?(:private) && !@options[:private] && m.private?
|
221
229
|
return nil, "template is not configured for public messages" if @options.has_key?(:public) && !@options[:public] && !m.private?
|
222
|
-
|
230
|
+
|
223
231
|
options.delete_if {|k, v| v.nil?} # Remove nil values.
|
224
232
|
return options, nil
|
225
233
|
end
|
data/lib/rbot/plugins.rb
CHANGED
@@ -1,93 +1,99 @@
|
|
1
1
|
module Irc
|
2
|
+
BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
|
3
|
+
:default => [], :wizard => false, :requires_restart => true,
|
4
|
+
:desc => "Plugins that should not be loaded")
|
2
5
|
module Plugins
|
3
6
|
require 'rbot/messagemapper'
|
4
7
|
|
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
|
-
|
8
|
+
=begin
|
9
|
+
base class for all rbot plugins
|
10
|
+
certain methods will be called if they are provided, if you define one of
|
11
|
+
the following methods, it will be called as appropriate:
|
12
|
+
|
13
|
+
map(template, options)::
|
14
|
+
map!(template, options)::
|
15
|
+
map is the new, cleaner way to respond to specific message formats
|
16
|
+
without littering your plugin code with regexps. The difference
|
17
|
+
between map and map! is that map! will not register the new command
|
18
|
+
as an alternative name for the plugin.
|
19
|
+
|
20
|
+
Examples:
|
21
|
+
|
22
|
+
plugin.map 'karmastats', :action => 'karma_stats'
|
23
|
+
|
24
|
+
# while in the plugin...
|
25
|
+
def karma_stats(m, params)
|
26
|
+
m.reply "..."
|
27
|
+
end
|
28
|
+
|
29
|
+
# the default action is the first component
|
30
|
+
plugin.map 'karma'
|
31
|
+
|
32
|
+
# attributes can be pulled out of the match string
|
33
|
+
plugin.map 'karma for :key'
|
34
|
+
plugin.map 'karma :key'
|
35
|
+
|
36
|
+
# while in the plugin...
|
37
|
+
def karma(m, params)
|
38
|
+
item = params[:key]
|
39
|
+
m.reply 'karma for #{item}'
|
40
|
+
end
|
41
|
+
|
42
|
+
# you can setup defaults, to make parameters optional
|
43
|
+
plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
|
44
|
+
|
45
|
+
# the default auth check is also against the first component
|
46
|
+
# but that can be changed
|
47
|
+
plugin.map 'karmastats', :auth => 'karma'
|
48
|
+
|
49
|
+
# maps can be restricted to public or private message:
|
50
|
+
plugin.map 'karmastats', :private false,
|
51
|
+
plugin.map 'karmastats', :public false,
|
52
|
+
end
|
53
|
+
|
54
|
+
listen(UserMessage)::
|
55
|
+
Called for all messages of any type. To
|
56
|
+
differentiate them, use message.kind_of? It'll be
|
57
|
+
either a PrivMessage, NoticeMessage, KickMessage,
|
58
|
+
QuitMessage, PartMessage, JoinMessage, NickMessage,
|
59
|
+
etc.
|
60
|
+
|
61
|
+
privmsg(PrivMessage)::
|
62
|
+
called for a PRIVMSG if the first word matches one
|
63
|
+
the plugin register()d for. Use m.plugin to get
|
64
|
+
that word and m.params for the rest of the message,
|
65
|
+
if applicable.
|
66
|
+
|
67
|
+
kick(KickMessage)::
|
68
|
+
Called when a user (or the bot) is kicked from a
|
69
|
+
channel the bot is in.
|
70
|
+
|
71
|
+
join(JoinMessage)::
|
72
|
+
Called when a user (or the bot) joins a channel
|
73
|
+
|
74
|
+
part(PartMessage)::
|
75
|
+
Called when a user (or the bot) parts a channel
|
76
|
+
|
77
|
+
quit(QuitMessage)::
|
78
|
+
Called when a user (or the bot) quits IRC
|
79
|
+
|
80
|
+
nick(NickMessage)::
|
81
|
+
Called when a user (or the bot) changes Nick
|
82
|
+
topic(TopicMessage)::
|
83
|
+
Called when a user (or the bot) changes a channel
|
84
|
+
topic
|
85
|
+
|
86
|
+
connect():: Called when a server is joined successfully, but
|
87
|
+
before autojoin channels are joined (no params)
|
88
|
+
|
89
|
+
save:: Called when you are required to save your plugin's
|
90
|
+
state, if you maintain data between sessions
|
91
|
+
|
92
|
+
cleanup:: called before your plugin is "unloaded", prior to a
|
93
|
+
plugin reload or bot quit - close any open
|
94
|
+
files/connections or flush caches here
|
95
|
+
=end
|
96
|
+
|
91
97
|
class Plugin
|
92
98
|
attr_reader :bot # the associated bot
|
93
99
|
# initialise your plugin. Always call super if you override this method,
|
@@ -99,6 +105,20 @@ module Plugins
|
|
99
105
|
@registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
|
100
106
|
end
|
101
107
|
|
108
|
+
def flush_registry
|
109
|
+
# debug "Flushing #{@registry}"
|
110
|
+
@registry.flush
|
111
|
+
end
|
112
|
+
|
113
|
+
def cleanup
|
114
|
+
# debug "Closing #{@registry}"
|
115
|
+
@registry.close
|
116
|
+
end
|
117
|
+
|
118
|
+
def handle(m)
|
119
|
+
@handler.handle(m)
|
120
|
+
end
|
121
|
+
|
102
122
|
def map(*args)
|
103
123
|
@handler.map(*args)
|
104
124
|
# register this map
|
@@ -106,7 +126,19 @@ module Plugins
|
|
106
126
|
self.register name
|
107
127
|
unless self.respond_to?('privmsg')
|
108
128
|
def self.privmsg(m)
|
109
|
-
|
129
|
+
handle(m)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def map!(*args)
|
135
|
+
@handler.map(*args)
|
136
|
+
# register this map
|
137
|
+
name = @handler.last.items[0]
|
138
|
+
self.register name, {:hidden => true}
|
139
|
+
unless self.respond_to?('privmsg')
|
140
|
+
def self.privmsg(m)
|
141
|
+
handle(m)
|
110
142
|
end
|
111
143
|
end
|
112
144
|
end
|
@@ -116,23 +148,24 @@ module Plugins
|
|
116
148
|
def name
|
117
149
|
@names.join("|")
|
118
150
|
end
|
119
|
-
|
151
|
+
|
120
152
|
# return a help string for your module. for complex modules, you may wish
|
121
153
|
# to break your help into topics, and return a list of available topics if
|
122
154
|
# +topic+ is nil. +plugin+ is passed containing the matching prefix for
|
123
|
-
# this message - if your plugin handles multiple prefixes, make sure
|
155
|
+
# this message - if your plugin handles multiple prefixes, make sure you
|
124
156
|
# return the correct help for the prefix requested
|
125
157
|
def help(plugin, topic)
|
126
158
|
"no help"
|
127
159
|
end
|
128
|
-
|
160
|
+
|
129
161
|
# register the plugin as a handler for messages prefixed +name+
|
130
162
|
# this can be called multiple times for a plugin to handle multiple
|
131
163
|
# message prefixes
|
132
|
-
def register(name)
|
164
|
+
def register(name,opts={})
|
165
|
+
raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
|
133
166
|
return if Plugins.plugins.has_key?(name)
|
134
167
|
Plugins.plugins[name] = self
|
135
|
-
@names << name
|
168
|
+
@names << name unless opts.fetch(:hidden, false)
|
136
169
|
end
|
137
170
|
|
138
171
|
# default usage method provided as a utility for simple plugins. The
|
@@ -160,7 +193,7 @@ module Plugins
|
|
160
193
|
@dirs = dirlist
|
161
194
|
scan
|
162
195
|
end
|
163
|
-
|
196
|
+
|
164
197
|
# access to associated bot
|
165
198
|
def Plugins.bot
|
166
199
|
@@bot
|
@@ -173,29 +206,75 @@ module Plugins
|
|
173
206
|
|
174
207
|
# load plugins from pre-assigned list of directories
|
175
208
|
def scan
|
209
|
+
@failed = Array.new
|
210
|
+
@ignored = Array.new
|
211
|
+
processed = Hash.new
|
212
|
+
|
213
|
+
@@bot.config['plugins.blacklist'].each { |p|
|
214
|
+
pn = p + ".rb"
|
215
|
+
processed[pn.intern] = :blacklisted
|
216
|
+
}
|
217
|
+
|
176
218
|
dirs = Array.new
|
177
219
|
dirs << Config::datadir + "/plugins"
|
178
220
|
dirs += @dirs
|
179
|
-
dirs.each {|dir|
|
221
|
+
dirs.reverse.each {|dir|
|
180
222
|
if(FileTest.directory?(dir))
|
181
223
|
d = Dir.new(dir)
|
182
224
|
d.sort.each {|file|
|
225
|
+
|
183
226
|
next if(file =~ /^\./)
|
227
|
+
|
228
|
+
if processed.has_key?(file.intern)
|
229
|
+
@ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
|
230
|
+
next
|
231
|
+
end
|
232
|
+
|
233
|
+
if(file =~ /^(.+\.rb)\.disabled$/)
|
234
|
+
# GB: Do we want to do this? This means that a disabled plugin in a directory
|
235
|
+
# will disable in all subsequent directories. This was probably meant
|
236
|
+
# to be used before plugins.blacklist was implemented, so I think
|
237
|
+
# we don't need this anymore
|
238
|
+
processed[$1.intern] = :disabled
|
239
|
+
@ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
|
240
|
+
next
|
241
|
+
end
|
242
|
+
|
184
243
|
next unless(file =~ /\.rb$/)
|
244
|
+
|
185
245
|
tmpfilename = "#{dir}/#{file}"
|
186
246
|
|
187
247
|
# create a new, anonymous module to "house" the plugin
|
188
248
|
# the idea here is to prevent namespace pollution. perhaps there
|
189
249
|
# is another way?
|
190
250
|
plugin_module = Module.new
|
191
|
-
|
251
|
+
|
192
252
|
begin
|
193
253
|
plugin_string = IO.readlines(tmpfilename).join("")
|
194
254
|
debug "loading plugin #{tmpfilename}"
|
195
|
-
plugin_module.module_eval(plugin_string)
|
196
|
-
|
197
|
-
|
198
|
-
|
255
|
+
plugin_module.module_eval(plugin_string, tmpfilename)
|
256
|
+
processed[file.intern] = :loaded
|
257
|
+
rescue Exception => err
|
258
|
+
# rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
|
259
|
+
warning "plugin #{tmpfilename} load failed\n" + err.inspect
|
260
|
+
debug err.backtrace.join("\n")
|
261
|
+
bt = err.backtrace.select { |line|
|
262
|
+
line.match(/^(\(eval\)|#{tmpfilename}):\d+/)
|
263
|
+
}
|
264
|
+
bt.map! { |el|
|
265
|
+
el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
|
266
|
+
"#{tmpfilename}#{$1}#{$3}"
|
267
|
+
}
|
268
|
+
}
|
269
|
+
msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
|
270
|
+
"#{tmpfilename}#{$1}#{$3}"
|
271
|
+
}
|
272
|
+
newerr = err.class.new(msg)
|
273
|
+
newerr.set_backtrace(bt)
|
274
|
+
# debug "Simplified error: " << newerr.inspect
|
275
|
+
# debug newerr.backtrace.join("\n")
|
276
|
+
@failed << { :name => file, :dir => dir, :reason => newerr }
|
277
|
+
# debug "Failures: #{@failed.inspect}"
|
199
278
|
end
|
200
279
|
}
|
201
280
|
end
|
@@ -204,6 +283,7 @@ module Plugins
|
|
204
283
|
|
205
284
|
# call the save method for each active plugin
|
206
285
|
def save
|
286
|
+
delegate 'flush_registry'
|
207
287
|
delegate 'save'
|
208
288
|
end
|
209
289
|
|
@@ -219,16 +299,37 @@ module Plugins
|
|
219
299
|
cleanup
|
220
300
|
@@plugins = Hash.new
|
221
301
|
scan
|
302
|
+
|
222
303
|
end
|
223
304
|
|
224
|
-
|
225
|
-
|
305
|
+
def status(short=false)
|
306
|
+
# Active plugins first
|
226
307
|
if(@@plugins.length > 0)
|
227
|
-
|
228
|
-
|
308
|
+
list = "#{length} plugin#{'s' if length > 1}"
|
309
|
+
if short
|
310
|
+
list << " loaded"
|
311
|
+
else
|
312
|
+
list << ": " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ")
|
313
|
+
end
|
229
314
|
else
|
230
|
-
|
315
|
+
list = "no plugins active"
|
231
316
|
end
|
317
|
+
# Ignored plugins next
|
318
|
+
unless @ignored.empty?
|
319
|
+
list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
|
320
|
+
list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
|
321
|
+
end
|
322
|
+
# Failed plugins next
|
323
|
+
unless @failed.empty?
|
324
|
+
list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
|
325
|
+
list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
|
326
|
+
end
|
327
|
+
list
|
328
|
+
end
|
329
|
+
|
330
|
+
# return list of help topics (plugin names)
|
331
|
+
def helptopics
|
332
|
+
return " [#{status}]"
|
232
333
|
end
|
233
334
|
|
234
335
|
def length
|
@@ -237,22 +338,44 @@ module Plugins
|
|
237
338
|
|
238
339
|
# return help for +topic+ (call associated plugin's help method)
|
239
340
|
def help(topic="")
|
240
|
-
|
341
|
+
case topic
|
342
|
+
when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
|
343
|
+
# debug "Failures: #{@failed.inspect}"
|
344
|
+
return "no plugins failed to load" if @failed.empty?
|
345
|
+
return (@failed.inject(Array.new) { |list, p|
|
346
|
+
list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
|
347
|
+
list << "with error #{p[:reason].class}: #{p[:reason]}"
|
348
|
+
list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
|
349
|
+
list
|
350
|
+
}).join("\n")
|
351
|
+
when /ignored?\s*plugins?/
|
352
|
+
return "no plugins were ignored" if @ignored.empty?
|
353
|
+
return (@ignored.inject(Array.new) { |list, p|
|
354
|
+
case p[:reason]
|
355
|
+
when :loaded
|
356
|
+
list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
|
357
|
+
else
|
358
|
+
list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
|
359
|
+
end
|
360
|
+
list
|
361
|
+
}).join(", ")
|
362
|
+
when /^(\S+)\s*(.*)$/
|
241
363
|
key = $1
|
242
364
|
params = $2
|
243
365
|
if(@@plugins.has_key?(key))
|
244
366
|
begin
|
245
367
|
return @@plugins[key].help(key, params)
|
246
|
-
rescue
|
247
|
-
|
248
|
-
|
368
|
+
rescue Exception => err
|
369
|
+
#rescue TimeoutError, StandardError, NameError, SyntaxError => err
|
370
|
+
error "plugin #{@@plugins[key].name} help() failed: #{err.class}: #{err}"
|
371
|
+
error err.backtrace.join("\n")
|
249
372
|
end
|
250
373
|
else
|
251
374
|
return false
|
252
375
|
end
|
253
376
|
end
|
254
377
|
end
|
255
|
-
|
378
|
+
|
256
379
|
# see if each plugin handles +method+, and if so, call it, passing
|
257
380
|
# +message+ as a parameter
|
258
381
|
def delegate(method, *args)
|
@@ -260,9 +383,10 @@ module Plugins
|
|
260
383
|
if(p.respond_to? method)
|
261
384
|
begin
|
262
385
|
p.send method, *args
|
263
|
-
rescue
|
264
|
-
|
265
|
-
|
386
|
+
rescue Exception => err
|
387
|
+
#rescue TimeoutError, StandardError, NameError, SyntaxError => err
|
388
|
+
error "plugin #{p.name} #{method}() failed: #{err.class}: #{err}"
|
389
|
+
error err.backtrace.join("\n")
|
266
390
|
end
|
267
391
|
end
|
268
392
|
}
|
@@ -277,9 +401,14 @@ module Plugins
|
|
277
401
|
@@bot.auth.allow?(m.plugin, m.source, m.replyto))
|
278
402
|
begin
|
279
403
|
@@plugins[m.plugin].privmsg(m)
|
280
|
-
rescue
|
281
|
-
|
282
|
-
|
404
|
+
rescue BDB::Fatal => err
|
405
|
+
error "plugin #{@@plugins[m.plugin].name} privmsg() failed: #{err.class}: #{err}"
|
406
|
+
error err.backtrace.join("\n")
|
407
|
+
raise
|
408
|
+
rescue Exception => err
|
409
|
+
#rescue TimeoutError, StandardError, NameError, SyntaxError => err
|
410
|
+
error "plugin #{@@plugins[m.plugin].name} privmsg() failed: #{err.class}: #{err}"
|
411
|
+
error err.backtrace.join("\n")
|
283
412
|
end
|
284
413
|
return true
|
285
414
|
end
|