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.
Files changed (72) hide show
  1. data/AUTHORS +8 -0
  2. data/ChangeLog +51 -0
  3. data/INSTALL +4 -0
  4. data/README +1 -0
  5. data/REQUIREMENTS +11 -0
  6. data/TODO +2 -0
  7. data/bin/rbot +21 -2
  8. data/data/rbot/languages/german.lang +4 -1
  9. data/data/rbot/languages/russian.lang +75 -0
  10. data/data/rbot/plugins/autoop.rb +42 -51
  11. data/data/rbot/plugins/bans.rb +205 -0
  12. data/data/rbot/plugins/bash.rb +56 -0
  13. data/data/rbot/plugins/chucknorris.rb +74 -0
  14. data/data/rbot/plugins/chucknorris.yml.gz +0 -0
  15. data/data/rbot/plugins/deepthoughts.rb +95 -0
  16. data/data/rbot/plugins/demauro.rb +95 -0
  17. data/data/rbot/plugins/digg.rb +51 -0
  18. data/data/rbot/plugins/figlet.rb +24 -0
  19. data/data/rbot/plugins/forecast.rb +133 -0
  20. data/data/rbot/plugins/freshmeat.rb +13 -7
  21. data/data/rbot/plugins/google.rb +2 -0
  22. data/data/rbot/plugins/grouphug.rb +36 -0
  23. data/data/rbot/plugins/imdb.rb +92 -0
  24. data/data/rbot/plugins/insult.rb +8 -1
  25. data/data/rbot/plugins/iplookup.rb +227 -0
  26. data/data/rbot/plugins/karma.rb +2 -2
  27. data/data/rbot/plugins/keywords.rb +470 -0
  28. data/data/rbot/plugins/lart.rb +132 -146
  29. data/data/rbot/plugins/lastfm.rb +25 -0
  30. data/data/rbot/plugins/markov.rb +204 -0
  31. data/data/rbot/plugins/math.rb +5 -1
  32. data/data/rbot/plugins/nickserv.rb +71 -11
  33. data/data/rbot/plugins/opme.rb +19 -19
  34. data/data/rbot/plugins/quakeauth.rb +2 -2
  35. data/data/rbot/plugins/quotes.rb +40 -25
  36. data/data/rbot/plugins/remind.rb +1 -1
  37. data/data/rbot/plugins/rot13.rb +2 -2
  38. data/data/rbot/plugins/roulette.rb +49 -15
  39. data/data/rbot/plugins/rss.rb +585 -0
  40. data/data/rbot/plugins/rubyurl.rb +39 -0
  41. data/data/rbot/plugins/seen.rb +2 -1
  42. data/data/rbot/plugins/slashdot.rb +5 -5
  43. data/data/rbot/plugins/spell.rb +5 -0
  44. data/data/rbot/plugins/theyfightcrime.rb +121 -0
  45. data/data/rbot/plugins/threat.rb +55 -0
  46. data/data/rbot/plugins/tinyurl.rb +39 -0
  47. data/data/rbot/plugins/topic.rb +204 -0
  48. data/data/rbot/plugins/urban.rb +71 -0
  49. data/data/rbot/plugins/url.rb +399 -4
  50. data/data/rbot/plugins/wow.rb +123 -0
  51. data/data/rbot/plugins/wserver.rb +1 -1
  52. data/data/rbot/templates/levels.rbot +2 -0
  53. data/lib/rbot/auth.rb +207 -96
  54. data/lib/rbot/channel.rb +5 -5
  55. data/lib/rbot/config.rb +125 -24
  56. data/lib/rbot/dbhash.rb +87 -21
  57. data/lib/rbot/httputil.rb +181 -13
  58. data/lib/rbot/ircbot.rb +525 -179
  59. data/lib/rbot/ircsocket.rb +330 -54
  60. data/lib/rbot/message.rb +66 -23
  61. data/lib/rbot/messagemapper.rb +25 -17
  62. data/lib/rbot/plugins.rb +244 -115
  63. data/lib/rbot/post-clean.rb +1 -0
  64. data/lib/rbot/{post-install.rb → post-config.rb} +1 -1
  65. data/lib/rbot/rbotconfig.rb +29 -14
  66. data/lib/rbot/registry.rb +111 -72
  67. data/lib/rbot/rfc2812.rb +208 -197
  68. data/lib/rbot/timer.rb +4 -0
  69. data/lib/rbot/utils.rb +2 -2
  70. metadata +127 -104
  71. data/data/rbot/plugins/rss.rb.disabled +0 -414
  72. 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.addressing_prefixes.each {|mprefix|
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
@@ -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 such up all following
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
- next unless @parent.respond_to?(action)
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
- debug failures.inspect
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
- raise ArgumentError, "Illegal template -- duplicate item #{item}\n #{str.inspect}" if seen.key? item
176
- seen[item] = true
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
- # base class for all rbot plugins
6
- # certain methods will be called if they are provided, if you define one of
7
- # the following methods, it will be called as appropriate:
8
- #
9
- # map(template, options)::
10
- # map is the new, cleaner way to respond to specific message formats
11
- # without littering your plugin code with regexps. examples:
12
- #
13
- # plugin.map 'karmastats', :action => 'karma_stats'
14
- #
15
- # # while in the plugin...
16
- # def karma_stats(m, params)
17
- # m.reply "..."
18
- # end
19
- #
20
- # # the default action is the first component
21
- # plugin.map 'karma'
22
- #
23
- # # attributes can be pulled out of the match string
24
- # plugin.map 'karma for :key'
25
- # plugin.map 'karma :key'
26
- #
27
- # # while in the plugin...
28
- # def karma(m, params)
29
- # item = params[:key]
30
- # m.reply 'karma for #{item}'
31
- # end
32
- #
33
- # # you can setup defaults, to make parameters optional
34
- # plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
35
- #
36
- # # the default auth check is also against the first component
37
- # # but that can be changed
38
- # plugin.map 'karmastats', :auth => 'karma'
39
- #
40
- # # maps can be restricted to public or private message:
41
- # plugin.map 'karmastats', :private false,
42
- # plugin.map 'karmastats', :public false,
43
- # end
44
- #
45
- # To activate your maps, you simply register them
46
- # plugin.register_maps
47
- # This also sets the privmsg handler to use the map lookups for
48
- # handling messages. You can still use listen(), kick() etc methods
49
- #
50
- # listen(UserMessage)::
51
- # Called for all messages of any type. To
52
- # differentiate them, use message.kind_of? It'll be
53
- # either a PrivMessage, NoticeMessage, KickMessage,
54
- # QuitMessage, PartMessage, JoinMessage, NickMessage,
55
- # etc.
56
- #
57
- # privmsg(PrivMessage)::
58
- # called for a PRIVMSG if the first word matches one
59
- # the plugin register()d for. Use m.plugin to get
60
- # that word and m.params for the rest of the message,
61
- # if applicable.
62
- #
63
- # kick(KickMessage)::
64
- # Called when a user (or the bot) is kicked from a
65
- # channel the bot is in.
66
- #
67
- # join(JoinMessage)::
68
- # Called when a user (or the bot) joins a channel
69
- #
70
- # part(PartMessage)::
71
- # Called when a user (or the bot) parts a channel
72
- #
73
- # quit(QuitMessage)::
74
- # Called when a user (or the bot) quits IRC
75
- #
76
- # nick(NickMessage)::
77
- # Called when a user (or the bot) changes Nick
78
- # topic(TopicMessage)::
79
- # Called when a user (or the bot) changes a channel
80
- # topic
81
- #
82
- # connect():: Called when a server is joined successfully, but
83
- # before autojoin channels are joined (no params)
84
- #
85
- # save:: Called when you are required to save your plugin's
86
- # state, if you maintain data between sessions
87
- #
88
- # cleanup:: called before your plugin is "unloaded", prior to a
89
- # plugin reload or bot quit - close any open
90
- # files/connections or flush caches here
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
- @handler.handle(m)
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 your
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
- rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
197
- puts "warning: plugin #{tmpfilename} load failed: " + err
198
- puts err.backtrace.join("\n")
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
- # return list of help topics (plugin names)
225
- def helptopics
305
+ def status(short=false)
306
+ # Active plugins first
226
307
  if(@@plugins.length > 0)
227
- # return " [plugins: " + @@plugins.keys.sort.join(", ") + "]"
228
- return " [#{length} plugins: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ") + "]"
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
- return " [no plugins active]"
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
- if(topic =~ /^(\S+)\s*(.*)$/)
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 TimeoutError, StandardError, NameError, SyntaxError => err
247
- puts "plugin #{@@plugins[key].name} help() failed: " + err
248
- puts err.backtrace.join("\n")
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 TimeoutError, StandardError, NameError, SyntaxError => err
264
- puts "plugin #{p.name} #{method}() failed: " + err
265
- puts err.backtrace.join("\n")
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 TimeoutError, StandardError, NameError, SyntaxError => err
281
- puts "plugin #{@@plugins[m.plugin].name} privmsg() failed: " + err
282
- puts err.backtrace.join("\n")
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