4 BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
5 :default => [], :wizard => false, :requires_restart => true,
6 :desc => "Plugins that should not be loaded")
8 require 'rbot/messagemapper'
11 base class for all rbot plugins
12 certain methods will be called if they are provided, if you define one of
13 the following methods, it will be called as appropriate:
15 map(template, options)::
16 map!(template, options)::
17 map is the new, cleaner way to respond to specific message formats
18 without littering your plugin code with regexps. The difference
19 between map and map! is that map! will not register the new command
20 as an alternative name for the plugin.
24 plugin.map 'karmastats', :action => 'karma_stats'
26 # while in the plugin...
27 def karma_stats(m, params)
31 # the default action is the first component
34 # attributes can be pulled out of the match string
35 plugin.map 'karma for :key'
36 plugin.map 'karma :key'
38 # while in the plugin...
41 m.reply 'karma for #{item}'
44 # you can setup defaults, to make parameters optional
45 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
47 # the default auth check is also against the first component
48 # but that can be changed
49 plugin.map 'karmastats', :auth => 'karma'
51 # maps can be restricted to public or private message:
52 plugin.map 'karmastats', :private false,
53 plugin.map 'karmastats', :public false,
57 Called for all messages of any type. To
58 differentiate them, use message.kind_of? It'll be
59 either a PrivMessage, NoticeMessage, KickMessage,
60 QuitMessage, PartMessage, JoinMessage, NickMessage,
63 privmsg(PrivMessage)::
64 called for a PRIVMSG if the first word matches one
65 the plugin register()d for. Use m.plugin to get
66 that word and m.params for the rest of the message,
70 Called when a user (or the bot) is kicked from a
71 channel the bot is in.
74 Called when a user (or the bot) joins a channel
77 Called when a user (or the bot) parts a channel
80 Called when a user (or the bot) quits IRC
83 Called when a user (or the bot) changes Nick
85 Called when a user (or the bot) changes a channel
88 connect():: Called when a server is joined successfully, but
89 before autojoin channels are joined (no params)
91 save:: Called when you are required to save your plugin's
92 state, if you maintain data between sessions
94 cleanup:: called before your plugin is "unloaded", prior to a
95 plugin reload or bot quit - close any open
96 files/connections or flush caches here
100 attr_reader :bot # the associated bot
101 attr_reader :botmodule_class # the botmodule class (:coremodule or :plugin)
103 # initialise your bot module. Always call super if you override this method,
104 # as important variables are set up for you
106 @manager = Plugins::pluginmanager
109 @botmodule_class = kl.to_sym
110 @botmodule_triggers = Array.new
112 @handler = MessageMapper.new(self)
113 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
115 @manager.add_botmodule(kl, self)
119 # debug "Flushing #{@registry}"
124 # debug "Closing #{@registry}"
135 name = @handler.last.items[0]
137 unless self.respond_to?('privmsg')
147 name = @handler.last.items[0]
148 self.register name, {:hidden => true}
149 unless self.respond_to?('privmsg')
156 # Sets the default auth for command _cmd_ to _val_ on channel _chan_:
157 # usually _chan_ is either "*" for everywhere, public and private (in
158 # which case it can be omitted) or "?" for private communications
160 def default_auth(cmd, val, chan="*")
161 Auth::anonbotuser.set_permission(cmd, val)
164 # return an identifier for this plugin, defaults to a list of the message
165 # prefixes handled (used for error messages etc)
167 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin)?$/,"")
175 # return a help string for your module. for complex modules, you may wish
176 # to break your help into topics, and return a list of available topics if
177 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
178 # this message - if your plugin handles multiple prefixes, make sure you
179 # return the correct help for the prefix requested
180 def help(plugin, topic)
184 # register the plugin as a handler for messages prefixed +name+
185 # this can be called multiple times for a plugin to handle multiple
187 def register(name, opts={})
188 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
189 return if @manager.knows?(name, @botmodule_class)
190 @manager.register(name, @botmodule_class, self)
191 @botmodule_triggers << name unless opts.fetch(:hidden, false)
194 # default usage method provided as a utility for simple plugins. The
195 # MessageMapper uses 'usage' as its default fallback method.
196 def usage(m, params = {})
197 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
202 class CoreBotModule < BotModule
208 class Plugin < BotModule
214 # Singleton to manage multiple plugins and delegate messages to them for
216 class PluginManagerClass
219 attr_reader :botmodules
227 # Reset lists of botmodules
228 def reset_botmodule_lists
241 # Associate with bot _bot_
242 def bot_associate(bot)
243 reset_botmodule_lists
247 # Returns +true+ if _name_ is a known botmodule of class kl
249 return @commandmappers[kl.to_sym].has_key?(name.to_sym)
252 # Returns +true+ if _name_ is a known botmodule of class kl
253 def register(name, kl, botmodule)
254 raise TypeError, "Third argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
255 @commandmappers[kl.to_sym][name.to_sym] = botmodule
258 def add_botmodule(kl, botmodule)
259 raise TypeError, "Second argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
260 raise "#{kl.to_s} #{botmodule.name} already registered!" if @botmodules[kl.to_sym].include?(botmodule)
261 @botmodules[kl.to_sym] << botmodule
264 # Returns an array of the loaded plugins
266 @botmodules[:coremodule]
269 # Returns an array of the loaded plugins
274 # Returns a hash of the registered message prefixes and associated
277 @commandmappers[:plugin]
280 # Returns a hash of the registered message prefixes and associated
283 @commandmappers[:coremodule]
286 # Makes a string of error _err_ by adding text _str_
287 def report_error(str, err)
288 ([str, err.inspect] + err.backtrace).join("\n")
291 # This method is the one that actually loads a module from the
294 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
296 # It returns the Symbol :loaded on success, and an Exception
299 def load_botmodule_file(fname, desc=nil)
300 # create a new, anonymous module to "house" the plugin
301 # the idea here is to prevent namespace pollution. perhaps there
303 plugin_module = Module.new
305 desc = desc.to_s + " " if desc
308 plugin_string = IO.readlines(fname).join("")
309 debug "loading #{desc}#{fname}"
310 plugin_module.module_eval(plugin_string, fname)
312 rescue Exception => err
313 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
314 warning report_error("#{desc}#{fname} load failed", err)
315 bt = err.backtrace.select { |line|
316 line.match(/^(\(eval\)|#{fname}):\d+/)
319 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
323 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
326 newerr = err.class.new(msg)
327 newerr.set_backtrace(bt)
331 private :load_botmodule_file
333 # add one or more directories to the list of directories to
334 # load botmodules from
336 # TODO find a way to specify necessary plugins which _must_ be loaded
338 def add_botmodule_dir(*dirlist)
340 debug "Botmodule loading path: #{@dirs.join(', ')}"
343 # load plugins from pre-assigned list of directories
349 @bot.config['plugins.blacklist'].each { |p|
351 processed[pn.intern] = :blacklisted
356 if(FileTest.directory?(dir))
360 next if(file =~ /^\./)
362 if processed.has_key?(file.intern)
363 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
367 if(file =~ /^(.+\.rb)\.disabled$/)
368 # GB: Do we want to do this? This means that a disabled plugin in a directory
369 # will disable in all subsequent directories. This was probably meant
370 # to be used before plugins.blacklist was implemented, so I think
371 # we don't need this anymore
372 processed[$1.intern] = :disabled
373 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
377 next unless(file =~ /\.rb$/)
379 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
382 processed[file.intern] = did_it
384 @failed << { :name => file, :dir => dir, :reason => did_it }
390 debug "finished loading plugins: #{status(true)}"
393 # call the save method for each active plugin
395 delegate 'flush_registry'
399 # call the cleanup method for each active plugin
402 reset_botmodule_lists
405 # drop all plugins and rescan plugins on disk
406 # calls save and cleanup for each plugin before dropping them
413 def status(short=false)
415 if self.core_length > 0
416 list << "#{self.core_length} core module#{'s' if core_length > 1}"
420 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
423 list << "no core botmodules loaded"
425 # Active plugins first
427 list << "; #{self.length} plugin#{'s' if length > 1}"
431 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
434 list << "no plugins active"
436 # Ignored plugins next
437 unless @ignored.empty?
438 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
439 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
441 # Failed plugins next
442 unless @failed.empty?
443 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
444 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
449 # return list of help topics (plugin names)
462 # return help for +topic+ (call associated plugin's help method)
465 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
466 # debug "Failures: #{@failed.inspect}"
467 return "no plugins failed to load" if @failed.empty?
468 return (@failed.inject(Array.new) { |list, p|
469 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
470 list << "with error #{p[:reason].class}: #{p[:reason]}"
471 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
474 when /ignored?\s*plugins?/
475 return "no plugins were ignored" if @ignored.empty?
476 return (@ignored.inject(Array.new) { |list, p|
479 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
481 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
485 when /^(\S+)\s*(.*)$/
488 # TODO should also check core_module and plugins
489 [core_commands, plugin_commands].each { |pl|
492 return pl[key].help(key, params)
493 rescue Exception => err
494 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
495 error report_error("#{p.botmodule_class} #{plugins[key].name} help() failed:", err)
504 # see if each plugin handles +method+, and if so, call it, passing
505 # +message+ as a parameter
506 def delegate(method, *args)
507 debug "Delegating #{method.inspect}"
508 [core_modules, plugins].each { |pl|
510 if(p.respond_to? method)
512 debug "#{p.botmodule_class} #{p.name} responds"
514 rescue Exception => err
515 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
516 raise if err.class <= BDB::Fatal
521 debug "Finished delegating #{method.inspect}"
524 # see if we have a plugin that wants to handle this message, if so, pass
525 # it to the plugin and return true, otherwise false
527 debug "Delegating privmsg with key #{m.plugin}"
528 return unless m.plugin
530 [core_commands, plugin_commands].each { |pl|
531 # We do it this way to skip creating spurious keys
540 # TODO This should probably be checked elsewhere
541 debug "Checking auth ..."
542 if @bot.auth.allow?(m.plugin, m.source, m.replyto)
543 debug "Checking response ..."
544 if p.respond_to?("privmsg")
546 debug "#{p.botmodule_class} #{p.name} responds"
548 rescue Exception => err
549 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
550 raise if err.class <= BDB::Fatal
552 debug "Successfully delegated privmsg with key #{m.plugin}"
555 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsgs"
558 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to use #{m.plugin} on #{m.replyto}"
561 debug "No #{pl.values.first.botmodule_class} registered #{m.plugin}" unless pl.empty?
563 debug "Finished delegating privmsg with key #{m.plugin}" + ( pl.empty? ? "" : " to #{pl.values.first.botmodule_class}s" )
566 rescue Exception => e
567 error report_error("couldn't delegate #{m}", e)
569 debug "Finished delegating privmsg with key #{m.plugin}"
573 # Returns the only PluginManagerClass instance
574 def Plugins.pluginmanager
575 return PluginManagerClass.instance