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
420 @bot.save_mutex.synchronize do
423 reset_botmodule_lists
426 # drop all plugins and rescan plugins on disk
427 # calls save and cleanup for each plugin before dropping them
434 def status(short=false)
436 if self.core_length > 0
437 list << "#{self.core_length} core module#{'s' if core_length > 1}"
441 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
444 list << "no core botmodules loaded"
446 # Active plugins first
448 list << "; #{self.length} plugin#{'s' if length > 1}"
452 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
455 list << "no plugins active"
457 # Ignored plugins next
458 unless @ignored.empty?
459 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
460 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
462 # Failed plugins next
463 unless @failed.empty?
464 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
465 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
470 # return list of help topics (plugin names)
483 # return help for +topic+ (call associated plugin's help method)
486 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
487 # debug "Failures: #{@failed.inspect}"
488 return "no plugins failed to load" if @failed.empty?
489 return @failed.inject(Array.new) { |list, p|
490 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
491 list << "with error #{p[:reason].class}: #{p[:reason]}"
492 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
495 when /ignored?\s*plugins?/
496 return "no plugins were ignored" if @ignored.empty?
497 return @ignored.inject(Array.new) { |list, p|
500 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
502 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
506 when /^(\S+)\s*(.*)$/
509 (core_modules + plugins).each { |p|
510 # debug "checking #{p.name.inspect} against #{key.inspect}"
512 return p.help(params)
513 rescue Exception => err
514 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
515 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
518 [core_commands, plugin_commands].each { |pl|
519 # debug "looking for #{key.inspect} in #{pl.keys.sort.inspect}"
521 p = pl[key][:botmodule]
523 return p.help(key, params)
524 rescue Exception => err
525 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
526 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
534 # see if each plugin handles +method+, and if so, call it, passing
535 # +message+ as a parameter
536 def delegate(method, *args)
537 # debug "Delegating #{method.inspect}"
538 [core_modules, plugins].each { |pl|
540 if(p.respond_to? method)
542 # debug "#{p.botmodule_class} #{p.name} responds"
544 rescue Exception => err
545 raise if err.kind_of?(SystemExit)
546 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
547 raise if err.kind_of?(BDB::Fatal)
552 # debug "Finished delegating #{method.inspect}"
555 # see if we have a plugin that wants to handle this message, if so, pass
556 # it to the plugin and return true, otherwise false
558 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
559 return unless m.plugin
560 [core_commands, plugin_commands].each { |pl|
561 # We do it this way to skip creating spurious keys
565 p = pl[k][:botmodule]
572 # We check here for things that don't check themselves
573 # (e.g. mapped things)
574 # debug "Checking auth ..."
575 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
576 # debug "Checking response ..."
577 if p.respond_to?("privmsg")
579 # debug "#{p.botmodule_class} #{p.name} responds"
581 rescue Exception => err
582 raise if err.kind_of?(SystemExit)
583 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
584 raise if err.kind_of?(BDB::Fatal)
586 # debug "Successfully delegated #{m.message}"
589 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
592 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
595 # debug "No #{pl.values.first[:botmodule].botmodule_class} registered #{m.plugin.inspect}" unless pl.empty?
597 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
600 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
604 # Returns the only PluginManagerClass instance
605 def Plugins.pluginmanager
606 return PluginManagerClass.instance