2 BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
3 :default => [], :wizard => false, :requires_restart => true,
4 :desc => "Plugins that should not be loaded")
6 require 'rbot/messagemapper'
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:
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.
22 plugin.map 'karmastats', :action => 'karma_stats'
24 # while in the plugin...
25 def karma_stats(m, params)
29 # the default action is the first component
32 # attributes can be pulled out of the match string
33 plugin.map 'karma for :key'
34 plugin.map 'karma :key'
36 # while in the plugin...
39 m.reply 'karma for #{item}'
42 # you can setup defaults, to make parameters optional
43 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
45 # the default auth check is also against the first component
46 # but that can be changed
47 plugin.map 'karmastats', :auth => 'karma'
49 # maps can be restricted to public or private message:
50 plugin.map 'karmastats', :private false,
51 plugin.map 'karmastats', :public false,
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,
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,
68 Called when a user (or the bot) is kicked from a
69 channel the bot is in.
72 Called when a user (or the bot) joins a channel
75 Called when a user (or the bot) parts a channel
78 Called when a user (or the bot) quits IRC
81 Called when a user (or the bot) changes Nick
83 Called when a user (or the bot) changes a channel
86 connect():: Called when a server is joined successfully, but
87 before autojoin channels are joined (no params)
89 save:: Called when you are required to save your plugin's
90 state, if you maintain data between sessions
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
98 attr_reader :bot # the associated bot
99 # initialise your plugin. Always call super if you override this method,
100 # as important variables are set up for you
104 @handler = MessageMapper.new(self)
105 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
109 # debug "Flushing #{@registry}"
114 # debug "Closing #{@registry}"
125 name = @handler.last.items[0]
127 unless self.respond_to?('privmsg')
137 name = @handler.last.items[0]
138 self.register name, {:hidden => true}
139 unless self.respond_to?('privmsg')
146 # return an identifier for this plugin, defaults to a list of the message
147 # prefixes handled (used for error messages etc)
152 # return a help string for your module. for complex modules, you may wish
153 # to break your help into topics, and return a list of available topics if
154 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
155 # this message - if your plugin handles multiple prefixes, make sure you
156 # return the correct help for the prefix requested
157 def help(plugin, topic)
161 # register the plugin as a handler for messages prefixed +name+
162 # this can be called multiple times for a plugin to handle multiple
164 def register(name,opts={})
165 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
166 return if Plugins.plugins.has_key?(name)
167 Plugins.plugins[name] = self
168 @names << name unless opts.fetch(:hidden, false)
171 # default usage method provided as a utility for simple plugins. The
172 # MessageMapper uses 'usage' as its default fallback method.
173 def usage(m, params = {})
174 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
179 # class to manage multiple plugins and delegate messages to them for
182 # hash of registered message prefixes and associated plugins
184 # associated IrcBot class
187 # bot:: associated IrcBot class
188 # dirlist:: array of directories to scan (in order) for plugins
190 # create a new plugin handler, scanning for plugins in +dirlist+
191 def initialize(bot, dirlist)
197 # access to associated bot
202 # access to list of plugins
207 # load plugins from pre-assigned list of directories
213 @@bot.config['plugins.blacklist'].each { |p|
215 processed[pn.intern] = :blacklisted
219 dirs << Config::datadir + "/plugins"
221 dirs.reverse.each {|dir|
222 if(FileTest.directory?(dir))
226 next if(file =~ /^\./)
228 if processed.has_key?(file.intern)
229 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
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]}
243 next unless(file =~ /\.rb$/)
245 tmpfilename = "#{dir}/#{file}"
247 # create a new, anonymous module to "house" the plugin
248 # the idea here is to prevent namespace pollution. perhaps there
250 plugin_module = Module.new
253 plugin_string = IO.readlines(tmpfilename).join("")
254 debug "loading plugin #{tmpfilename}"
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+/)
265 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
266 "#{tmpfilename}#{$1}#{$3}"
269 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
270 "#{tmpfilename}#{$1}#{$3}"
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}"
284 # call the save method for each active plugin
286 delegate 'flush_registry'
290 # call the cleanup method for each active plugin
295 # drop all plugins and rescan plugins on disk
296 # calls save and cleanup for each plugin before dropping them
304 # return list of help topics (plugin names)
306 # Active plugins first
307 if(@@plugins.length > 0)
308 list = " [#{length} plugin#{'s' if length > 1}: " + @@plugins.values.uniq.collect{|p| p.name}.sort.join(", ")
310 list = " [no plugins active"
312 # Ignored plugins next
313 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}: use #{Bold}help ignored plugins#{Bold} to see why" unless @ignored.empty?
314 # Failed plugins next
315 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}: use #{Bold}help failed plugins#{Bold} to see why" unless @failed.empty?
321 @@plugins.values.uniq.length
324 # return help for +topic+ (call associated plugin's help method)
327 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
328 # debug "Failures: #{@failed.inspect}"
329 return "no plugins failed to load" if @failed.empty?
330 return (@failed.inject(Array.new) { |list, p|
331 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
332 list << "with error #{p[:reason].class}: #{p[:reason]}"
333 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
336 when /ignored?\s*plugins?/
337 return "no plugins were ignored" if @ignored.empty?
338 return (@ignored.inject(Array.new) { |list, p|
341 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
343 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
347 when /^(\S+)\s*(.*)$/
350 if(@@plugins.has_key?(key))
352 return @@plugins[key].help(key, params)
353 rescue Exception => err
354 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
355 error "plugin #{@@plugins[key].name} help() failed: #{err.class}: #{err}"
356 error err.backtrace.join("\n")
364 # see if each plugin handles +method+, and if so, call it, passing
365 # +message+ as a parameter
366 def delegate(method, *args)
367 @@plugins.values.uniq.each {|p|
368 if(p.respond_to? method)
371 rescue Exception => err
372 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
373 error "plugin #{p.name} #{method}() failed: #{err.class}: #{err}"
374 error err.backtrace.join("\n")
380 # see if we have a plugin that wants to handle this message, if so, pass
381 # it to the plugin and return true, otherwise false
383 return unless(m.plugin)
384 if (@@plugins.has_key?(m.plugin) &&
385 @@plugins[m.plugin].respond_to?("privmsg") &&
386 @@bot.auth.allow?(m.plugin, m.source, m.replyto))
388 @@plugins[m.plugin].privmsg(m)
389 rescue Exception => err
390 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
391 error "plugin #{@@plugins[m.plugin].name} privmsg() failed: #{err.class}: #{err}"
392 error err.backtrace.join("\n")