4 BotConfig.register BotConfigArrayValue.new('plugins.blacklist',
5 :default => [], :wizard => false, :requires_rescan => 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(self)
119 # debug "Flushing #{@registry}"
124 # debug "Closing #{@registry}"
133 @handler.map(self, *args)
135 name = @handler.last.items[0]
136 self.register name, :auth => nil
137 unless self.respond_to?('privmsg')
145 @handler.map(self, *args)
147 name = @handler.last.items[0]
148 self.register name, :auth => nil, :hidden => true
149 unless self.respond_to?('privmsg')
156 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
157 # usually _chan_ is either "*" for everywhere, public and private (in which
158 # case it can be omitted) or "?" for private communications
160 def default_auth(cmd, val, chan="*")
167 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
170 # Gets the default command path which would be given to command _cmd_
171 def propose_default_path(cmd)
172 [name, cmd].compact.join("::")
175 # return an identifier for this plugin, defaults to a list of the message
176 # prefixes handled (used for error messages etc)
178 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
186 # return a help string for your module. for complex modules, you may wish
187 # to break your help into topics, and return a list of available topics if
188 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
189 # this message - if your plugin handles multiple prefixes, make sure you
190 # return the correct help for the prefix requested
191 def help(plugin, topic)
195 # register the plugin as a handler for messages prefixed +name+
196 # this can be called multiple times for a plugin to handle multiple
198 def register(cmd, opts={})
199 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
200 return if @manager.knows?(cmd, @botmodule_class)
201 if opts.has_key?(:auth)
202 @manager.register(self, cmd, opts[:auth])
204 @manager.register(self, cmd, propose_default_path(cmd))
206 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
209 # default usage method provided as a utility for simple plugins. The
210 # MessageMapper uses 'usage' as its default fallback method.
211 def usage(m, params = {})
212 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
217 class CoreBotModule < BotModule
223 class Plugin < BotModule
229 # Singleton to manage multiple plugins and delegate messages to them for
231 class PluginManagerClass
234 attr_reader :botmodules
242 # Reset lists of botmodules
243 def reset_botmodule_lists
256 # Associate with bot _bot_
257 def bot_associate(bot)
258 reset_botmodule_lists
262 # Returns +true+ if _name_ is a known botmodule of class kl
264 return @commandmappers[kl.to_sym].has_key?(name.to_sym)
267 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
268 def register(botmodule, cmd, auth_path)
269 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
270 kl = botmodule.botmodule_class
271 @commandmappers[kl.to_sym][cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
272 h = @commandmappers[kl.to_sym][cmd.to_sym]
273 # debug "Registered command mapper for #{cmd.to_sym} (#{kl.to_sym}): #{h[:botmodule].name} with command path #{h[:auth]}"
276 def add_botmodule(botmodule)
277 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
278 kl = botmodule.botmodule_class
279 raise "#{kl.to_s} #{botmodule.name} already registered!" if @botmodules[kl.to_sym].include?(botmodule)
280 @botmodules[kl.to_sym] << botmodule
283 # Returns an array of the loaded plugins
285 @botmodules[:coremodule]
288 # Returns an array of the loaded plugins
293 # Returns a hash of the registered message prefixes and associated
296 @commandmappers[:plugin]
299 # Returns a hash of the registered message prefixes and associated
302 @commandmappers[:coremodule]
305 # Makes a string of error _err_ by adding text _str_
306 def report_error(str, err)
307 ([str, err.inspect] + err.backtrace).join("\n")
310 # This method is the one that actually loads a module from the
313 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
315 # It returns the Symbol :loaded on success, and an Exception
318 def load_botmodule_file(fname, desc=nil)
319 # create a new, anonymous module to "house" the plugin
320 # the idea here is to prevent namespace pollution. perhaps there
322 plugin_module = Module.new
324 desc = desc.to_s + " " if desc
327 plugin_string = IO.readlines(fname).join("")
328 debug "loading #{desc}#{fname}"
329 plugin_module.module_eval(plugin_string, fname)
331 rescue Exception => err
332 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
333 warning report_error("#{desc}#{fname} load failed", err)
334 bt = err.backtrace.select { |line|
335 line.match(/^(\(eval\)|#{fname}):\d+/)
338 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
342 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
345 newerr = err.class.new(msg)
346 newerr.set_backtrace(bt)
350 private :load_botmodule_file
352 # add one or more directories to the list of directories to
353 # load botmodules from
355 # TODO find a way to specify necessary plugins which _must_ be loaded
357 def add_botmodule_dir(*dirlist)
359 debug "Botmodule loading path: #{@dirs.join(', ')}"
362 # load plugins from pre-assigned list of directories
368 @bot.config['plugins.blacklist'].each { |p|
370 processed[pn.intern] = :blacklisted
375 if(FileTest.directory?(dir))
379 next if(file =~ /^\./)
381 if processed.has_key?(file.intern)
382 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
386 if(file =~ /^(.+\.rb)\.disabled$/)
387 # GB: Do we want to do this? This means that a disabled plugin in a directory
388 # will disable in all subsequent directories. This was probably meant
389 # to be used before plugins.blacklist was implemented, so I think
390 # we don't need this anymore
391 processed[$1.intern] = :disabled
392 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
396 next unless(file =~ /\.rb$/)
398 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
401 processed[file.intern] = did_it
403 @failed << { :name => file, :dir => dir, :reason => did_it }
409 debug "finished loading plugins: #{status(true)}"
412 # call the save method for each active plugin
414 delegate 'flush_registry'
418 # call the cleanup method for each active plugin
421 reset_botmodule_lists
424 # drop all plugins and rescan plugins on disk
425 # calls save and cleanup for each plugin before dropping them
432 def status(short=false)
434 if self.core_length > 0
435 list << "#{self.core_length} core module#{'s' if core_length > 1}"
439 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
442 list << "no core botmodules loaded"
444 # Active plugins first
446 list << "; #{self.length} plugin#{'s' if length > 1}"
450 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
453 list << "no plugins active"
455 # Ignored plugins next
456 unless @ignored.empty?
457 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
458 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
460 # Failed plugins next
461 unless @failed.empty?
462 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
463 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
468 # return list of help topics (plugin names)
481 # return help for +topic+ (call associated plugin's help method)
484 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
485 # debug "Failures: #{@failed.inspect}"
486 return "no plugins failed to load" if @failed.empty?
487 return @failed.inject(Array.new) { |list, p|
488 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
489 list << "with error #{p[:reason].class}: #{p[:reason]}"
490 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
493 when /ignored?\s*plugins?/
494 return "no plugins were ignored" if @ignored.empty?
495 return @ignored.inject(Array.new) { |list, p|
498 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
500 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
504 when /^(\S+)\s*(.*)$/
507 (core_modules + plugins).each { |p|
508 next unless p.name == key
510 return p.help(key, params)
511 rescue Exception => err
512 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
513 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
517 [core_commands, plugin_commands].each { |pl|
518 next unless pl.has_key?(k)
519 p = pl[k][:botmodule]
521 return p.help(p.name, topic)
522 rescue Exception => err
523 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
524 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
531 # see if each plugin handles +method+, and if so, call it, passing
532 # +message+ as a parameter
533 def delegate(method, *args)
534 # debug "Delegating #{method.inspect}"
535 [core_modules, plugins].each { |pl|
537 if(p.respond_to? method)
539 # debug "#{p.botmodule_class} #{p.name} responds"
541 rescue Exception => err
542 raise if err.kind_of?(SystemExit)
543 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
544 raise if err.kind_of?(BDB::Fatal)
549 # debug "Finished delegating #{method.inspect}"
552 # see if we have a plugin that wants to handle this message, if so, pass
553 # it to the plugin and return true, otherwise false
555 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
556 return unless m.plugin
557 [core_commands, plugin_commands].each { |pl|
558 # We do it this way to skip creating spurious keys
562 p = pl[k][:botmodule]
569 # We check here for things that don't check themselves
570 # (e.g. mapped things)
571 # debug "Checking auth ..."
572 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
573 # debug "Checking response ..."
574 if p.respond_to?("privmsg")
576 # debug "#{p.botmodule_class} #{p.name} responds"
578 rescue Exception => err
579 raise if err.kind_of?(SystemExit)
580 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
581 raise if err.kind_of?(BDB::Fatal)
583 # debug "Successfully delegated #{m.message}"
586 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
589 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
592 # debug "No #{pl.values.first[:botmodule].botmodule_class} registered #{m.plugin.inspect}" unless pl.empty?
594 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
597 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
601 # Returns the only PluginManagerClass instance
602 def Plugins.pluginmanager
603 return PluginManagerClass.instance