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 set_language(String)::
92 Called when the user sets a new language
93 whose name is the given String
95 save:: Called when you are required to save your plugin's
96 state, if you maintain data between sessions
98 cleanup:: called before your plugin is "unloaded", prior to a
99 plugin reload or bot quit - close any open
100 files/connections or flush caches here
104 attr_reader :bot # the associated bot
106 # initialise your bot module. Always call super if you override this method,
107 # as important variables are set up for you
109 @manager = Plugins::pluginmanager
112 @botmodule_triggers = Array.new
114 @handler = MessageMapper.new(self)
115 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
117 @manager.add_botmodule(self)
118 if self.respond_to?('set_language')
119 self.set_language(@bot.lang.language)
128 # debug "Flushing #{@registry}"
133 # debug "Closing #{@registry}"
142 @handler.map(self, *args)
144 name = @handler.last.items[0]
145 self.register name, :auth => nil
146 unless self.respond_to?('privmsg')
154 @handler.map(self, *args)
156 name = @handler.last.items[0]
157 self.register name, :auth => nil, :hidden => true
158 unless self.respond_to?('privmsg')
165 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
166 # usually _chan_ is either "*" for everywhere, public and private (in which
167 # case it can be omitted) or "?" for private communications
169 def default_auth(cmd, val, chan="*")
176 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
179 # Gets the default command path which would be given to command _cmd_
180 def propose_default_path(cmd)
181 [name, cmd].compact.join("::")
184 # return an identifier for this plugin, defaults to a list of the message
185 # prefixes handled (used for error messages etc)
187 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
200 # return a help string for your module. for complex modules, you may wish
201 # to break your help into topics, and return a list of available topics if
202 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
203 # this message - if your plugin handles multiple prefixes, make sure you
204 # return the correct help for the prefix requested
205 def help(plugin, topic)
209 # register the plugin as a handler for messages prefixed +name+
210 # this can be called multiple times for a plugin to handle multiple
212 def register(cmd, opts={})
213 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
214 who = @manager.who_handles?(cmd)
216 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
219 if opts.has_key?(:auth)
220 @manager.register(self, cmd, opts[:auth])
222 @manager.register(self, cmd, propose_default_path(cmd))
224 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
227 # default usage method provided as a utility for simple plugins. The
228 # MessageMapper uses 'usage' as its default fallback method.
229 def usage(m, params = {})
230 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
235 class CoreBotModule < BotModule
241 class Plugin < BotModule
247 # Singleton to manage multiple plugins and delegate messages to them for
249 class PluginManagerClass
252 attr_reader :botmodules
256 :CoreBotModule => [],
260 @names_hash = Hash.new
261 @commandmappers = Hash.new
271 # Reset lists of botmodules
272 def reset_botmodule_lists
273 @botmodules[:CoreBotModule].clear
274 @botmodules[:Plugin].clear
276 @commandmappers.clear
279 # Associate with bot _bot_
280 def bot_associate(bot)
281 reset_botmodule_lists
285 # Returns the botmodule with the given _name_
287 @names_hash[name.to_sym]
290 # Returns +true+ if _cmd_ has already been registered as a command
291 def who_handles?(cmd)
292 return nil unless @commandmappers.has_key?(cmd.to_sym)
293 return @commandmappers[cmd.to_sym][:botmodule]
296 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
297 def register(botmodule, cmd, auth_path)
298 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
299 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
302 def add_botmodule(botmodule)
303 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
304 kl = botmodule.botmodule_class
305 if @names_hash.has_key?(botmodule.to_sym)
306 case self[botmodule].botmodule_class
308 raise "#{kl} #{botmodule} already registered!"
310 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
313 @botmodules[kl] << botmodule
314 @names_hash[botmodule.to_sym] = botmodule
317 # Returns an array of the loaded plugins
319 @botmodules[:CoreBotModule]
322 # Returns an array of the loaded plugins
327 # Returns a hash of the registered message prefixes and associated
333 # Makes a string of error _err_ by adding text _str_
334 def report_error(str, err)
335 ([str, err.inspect] + err.backtrace).join("\n")
338 # This method is the one that actually loads a module from the
341 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
343 # It returns the Symbol :loaded on success, and an Exception
346 def load_botmodule_file(fname, desc=nil)
347 # create a new, anonymous module to "house" the plugin
348 # the idea here is to prevent namespace pollution. perhaps there
350 plugin_module = Module.new
352 desc = desc.to_s + " " if desc
355 plugin_string = IO.readlines(fname).join("")
356 debug "loading #{desc}#{fname}"
357 plugin_module.module_eval(plugin_string, fname)
359 rescue Exception => err
360 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
361 warning report_error("#{desc}#{fname} load failed", err)
362 bt = err.backtrace.select { |line|
363 line.match(/^(\(eval\)|#{fname}):\d+/)
366 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
370 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
373 newerr = err.class.new(msg)
374 newerr.set_backtrace(bt)
378 private :load_botmodule_file
380 # add one or more directories to the list of directories to
381 # load botmodules from
383 # TODO find a way to specify necessary plugins which _must_ be loaded
385 def add_botmodule_dir(*dirlist)
387 debug "Botmodule loading path: #{@dirs.join(', ')}"
390 # load plugins from pre-assigned list of directories
396 @bot.config['plugins.blacklist'].each { |p|
398 processed[pn.intern] = :blacklisted
403 if(FileTest.directory?(dir))
407 next if(file =~ /^\./)
409 if processed.has_key?(file.intern)
410 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
414 if(file =~ /^(.+\.rb)\.disabled$/)
415 # GB: Do we want to do this? This means that a disabled plugin in a directory
416 # will disable in all subsequent directories. This was probably meant
417 # to be used before plugins.blacklist was implemented, so I think
418 # we don't need this anymore
419 processed[$1.intern] = :disabled
420 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
424 next unless(file =~ /\.rb$/)
426 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
429 processed[file.intern] = did_it
431 @failed << { :name => file, :dir => dir, :reason => did_it }
437 debug "finished loading plugins: #{status(true)}"
440 # call the save method for each active plugin
442 delegate 'flush_registry'
446 # call the cleanup method for each active plugin
449 reset_botmodule_lists
452 # drop all plugins and rescan plugins on disk
453 # calls save and cleanup for each plugin before dropping them
460 def status(short=false)
462 if self.core_length > 0
463 list << "#{self.core_length} core module#{'s' if core_length > 1}"
467 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
470 list << "no core botmodules loaded"
472 # Active plugins first
474 list << "; #{self.length} plugin#{'s' if length > 1}"
478 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
481 list << "no plugins active"
483 # Ignored plugins next
484 unless @ignored.empty?
485 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
486 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
488 # Failed plugins next
489 unless @failed.empty?
490 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
491 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
496 # return list of help topics (plugin names)
509 # return help for +topic+ (call associated plugin's help method)
512 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
513 # debug "Failures: #{@failed.inspect}"
514 return "no plugins failed to load" if @failed.empty?
515 return @failed.inject(Array.new) { |list, p|
516 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
517 list << "with error #{p[:reason].class}: #{p[:reason]}"
518 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
521 when /ignored?\s*plugins?/
522 return "no plugins were ignored" if @ignored.empty?
523 return @ignored.inject(Array.new) { |list, p|
526 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
528 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
532 when /^(\S+)\s*(.*)$/
536 # Let's see if we can match a plugin by the given name
537 (core_modules + plugins).each { |p|
538 next unless p.name == key
540 return p.help(key, params)
541 rescue Exception => err
542 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
543 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
547 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
549 if commands.has_key?(k)
550 p = commands[k][:botmodule]
552 return p.help(key, params)
553 rescue Exception => err
554 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
555 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
562 # see if each plugin handles +method+, and if so, call it, passing
563 # +message+ as a parameter
564 def delegate(method, *args)
565 # debug "Delegating #{method.inspect}"
566 [core_modules, plugins].each { |pl|
568 if(p.respond_to? method)
570 # debug "#{p.botmodule_class} #{p.name} responds"
572 rescue Exception => err
573 raise if err.kind_of?(SystemExit)
574 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
575 raise if err.kind_of?(BDB::Fatal)
580 # debug "Finished delegating #{method.inspect}"
583 # see if we have a plugin that wants to handle this message, if so, pass
584 # it to the plugin and return true, otherwise false
586 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
587 return unless m.plugin
589 if commands.has_key?(k)
590 p = commands[k][:botmodule]
591 a = commands[k][:auth]
592 # We check here for things that don't check themselves
593 # (e.g. mapped things)
594 # debug "Checking auth ..."
595 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
596 # debug "Checking response ..."
597 if p.respond_to?("privmsg")
599 # debug "#{p.botmodule_class} #{p.name} responds"
601 rescue Exception => err
602 raise if err.kind_of?(SystemExit)
603 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
604 raise if err.kind_of?(BDB::Fatal)
606 # debug "Successfully delegated #{m.message}"
609 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
612 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
615 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
617 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
621 # Returns the only PluginManagerClass instance
622 def Plugins.pluginmanager
623 return PluginManagerClass.instance