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 # return an identifier for this plugin, defaults to a list of the message
157 # prefixes handled (used for error messages etc)
159 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin)?$/,"")
167 # return a help string for your module. for complex modules, you may wish
168 # to break your help into topics, and return a list of available topics if
169 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
170 # this message - if your plugin handles multiple prefixes, make sure you
171 # return the correct help for the prefix requested
172 def help(plugin, topic)
176 # register the plugin as a handler for messages prefixed +name+
177 # this can be called multiple times for a plugin to handle multiple
179 def register(name, opts={})
180 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
181 return if @manager.knows?(name, @botmodule_class)
182 @manager.register(name, @botmodule_class, self)
183 @botmodule_triggers << name unless opts.fetch(:hidden, false)
186 # default usage method provided as a utility for simple plugins. The
187 # MessageMapper uses 'usage' as its default fallback method.
188 def usage(m, params = {})
189 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
194 class CoreBotModule < BotModule
200 class Plugin < BotModule
206 # Singleton to manage multiple plugins and delegate messages to them for
208 class PluginManagerClass
211 attr_reader :botmodules
219 # Reset lists of botmodules
220 def reset_botmodule_lists
233 # Associate with bot _bot_
234 def bot_associate(bot)
235 reset_botmodule_lists
239 # Returns +true+ if _name_ is a known botmodule of class kl
241 return @commandmappers[kl.to_sym].has_key?(name.to_sym)
244 # Returns +true+ if _name_ is a known botmodule of class kl
245 def register(name, kl, botmodule)
246 raise TypeError, "Third argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
247 @commandmappers[kl.to_sym][name.to_sym] = botmodule
250 def add_botmodule(kl, botmodule)
251 raise TypeError, "Second argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
252 raise "#{kl.to_s} #{botmodule.name} already registered!" if @botmodules[kl.to_sym].include?(botmodule)
253 @botmodules[kl.to_sym] << botmodule
256 # Returns an array of the loaded plugins
258 @botmodules[:coremodule]
261 # Returns an array of the loaded plugins
266 # Returns a hash of the registered message prefixes and associated
269 @commandmappers[:plugin]
272 # Returns a hash of the registered message prefixes and associated
275 @commandmappers[:coremodule]
278 # Makes a string of error _err_ by adding text _str_
279 def report_error(str, err)
280 ([str, err.inspect] + err.backtrace).join("\n")
283 # This method is the one that actually loads a module from the
286 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
288 # It returns the Symbol :loaded on success, and an Exception
291 def load_botmodule_file(fname, desc=nil)
292 # create a new, anonymous module to "house" the plugin
293 # the idea here is to prevent namespace pollution. perhaps there
295 plugin_module = Module.new
297 desc = desc.to_s + " " if desc
300 plugin_string = IO.readlines(fname).join("")
301 debug "loading #{desc}#{fname}"
302 plugin_module.module_eval(plugin_string, fname)
304 rescue Exception => err
305 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
306 warning report_error("#{desc}#{fname} load failed", err)
307 bt = err.backtrace.select { |line|
308 line.match(/^(\(eval\)|#{fname}):\d+/)
311 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
315 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
318 newerr = err.class.new(msg)
319 newerr.set_backtrace(bt)
323 private :load_botmodule_file
325 # add one or more directories to the list of directories to
326 # load botmodules from
328 def add_botmodule_dir(*dirlist)
330 debug "Botmodule loading path: #{@dirs.join(', ')}"
333 # load plugins from pre-assigned list of directories
339 @bot.config['plugins.blacklist'].each { |p|
341 processed[pn.intern] = :blacklisted
346 if(FileTest.directory?(dir))
350 next if(file =~ /^\./)
352 if processed.has_key?(file.intern)
353 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
357 if(file =~ /^(.+\.rb)\.disabled$/)
358 # GB: Do we want to do this? This means that a disabled plugin in a directory
359 # will disable in all subsequent directories. This was probably meant
360 # to be used before plugins.blacklist was implemented, so I think
361 # we don't need this anymore
362 processed[$1.intern] = :disabled
363 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
367 next unless(file =~ /\.rb$/)
369 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
372 processed[file.intern] = did_it
374 @failed << { :name => file, :dir => dir, :reason => did_it }
380 debug "finished loading plugins: #{status(true)}"
383 # call the save method for each active plugin
385 delegate 'flush_registry'
389 # call the cleanup method for each active plugin
392 reset_botmodule_lists
395 # drop all plugins and rescan plugins on disk
396 # calls save and cleanup for each plugin before dropping them
403 def status(short=false)
405 if self.core_length > 0
406 list << "#{self.core_length} core module#{'s' if core_length > 1}"
410 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
413 list << "no core botmodules loaded"
415 # Active plugins first
417 list << "; #{self.length} plugin#{'s' if length > 1}"
421 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
424 list << "no plugins active"
426 # Ignored plugins next
427 unless @ignored.empty?
428 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
429 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
431 # Failed plugins next
432 unless @failed.empty?
433 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
434 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
439 # return list of help topics (plugin names)
441 return " [#{status}]"
452 # return help for +topic+ (call associated plugin's help method)
455 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
456 # debug "Failures: #{@failed.inspect}"
457 return "no plugins failed to load" if @failed.empty?
458 return (@failed.inject(Array.new) { |list, p|
459 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
460 list << "with error #{p[:reason].class}: #{p[:reason]}"
461 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
464 when /ignored?\s*plugins?/
465 return "no plugins were ignored" if @ignored.empty?
466 return (@ignored.inject(Array.new) { |list, p|
469 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
471 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
475 when /^(\S+)\s*(.*)$/
478 [core_commands, plugin_commands].each { |pl|
481 return pl[key].help(key, params)
482 rescue Exception => err
483 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
484 error report_error("#{p.botmodule_class} #{plugins[key].name} help() failed:", err)
493 # see if each plugin handles +method+, and if so, call it, passing
494 # +message+ as a parameter
495 def delegate(method, *args)
496 debug "Delegating #{method.inspect}"
497 [core_modules, plugins].each { |pl|
499 if(p.respond_to? method)
501 debug "#{p.botmodule_class} #{p.name} responds"
503 rescue Exception => err
504 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
505 raise if err.class <= BDB::Fatal
510 debug "Finished delegating #{method.inspect}"
513 # see if we have a plugin that wants to handle this message, if so, pass
514 # it to the plugin and return true, otherwise false
516 debug "Delegating privmsg with key #{m.plugin}"
517 return unless m.plugin
519 [core_commands, plugin_commands].each { |pl|
520 # We do it this way to skip creating spurious keys
529 # TODO This should probably be checked elsewhere
530 debug "Checking auth ..."
531 if @bot.auth.allow?(m.plugin, m.source, m.replyto)
532 debug "Checking response ..."
533 if p.respond_to?("privmsg")
535 debug "#{p.botmodule_class} #{p.name} responds"
537 rescue Exception => err
538 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
539 raise if err.class <= BDB::Fatal
541 debug "Successfully delegated privmsg with key #{m.plugin}"
544 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsgs"
547 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to use #{m.plugin} on #{m.replyto}"
550 debug "No #{pl.values.first.botmodule_class} registered #{m.plugin}" unless pl.empty?
552 debug "Finished delegating privmsg with key #{m.plugin}" + ( pl.empty? ? "" : " to #{pl.values.first.botmodule_class}s" )
555 rescue Exception => e
556 error report_error("couldn't delegate #{m}", e)
558 debug "Finished delegating privmsg with key #{m.plugin}"
562 # Returns the only PluginManagerClass instance
563 def Plugins.pluginmanager
564 return PluginManagerClass.instance