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
260 # Reset lists of botmodules
261 def reset_botmodule_lists
263 :CoreBotModule => [],
266 @names_hash = Hash.new
267 @commandmappers = Hash.new
270 # Associate with bot _bot_
271 def bot_associate(bot)
272 reset_botmodule_lists
276 # Returns the botmodule with the given _name_
278 @names_hash[name.to_sym]
281 # Returns +true+ if _cmd_ has already been registered as a command
282 def who_handles?(cmd)
283 return nil unless @commandmappers.has_key?(cmd.to_sym)
284 return @commandmappers[cmd.to_sym][:botmodule]
287 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
288 def register(botmodule, cmd, auth_path)
289 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
290 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
293 def add_botmodule(botmodule)
294 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
295 kl = botmodule.botmodule_class
296 if @names_hash.has_key?(botmodule.to_sym)
297 case self[botmodule].botmodule_class
299 raise "#{kl} #{botmodule} already registered!"
301 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
304 @botmodules[kl] << botmodule
305 @names_hash[botmodule.to_sym] = botmodule
308 # Returns an array of the loaded plugins
310 @botmodules[:CoreBotModule]
313 # Returns an array of the loaded plugins
318 # Returns a hash of the registered message prefixes and associated
324 # Makes a string of error _err_ by adding text _str_
325 def report_error(str, err)
326 ([str, err.inspect] + err.backtrace).join("\n")
329 # This method is the one that actually loads a module from the
332 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
334 # It returns the Symbol :loaded on success, and an Exception
337 def load_botmodule_file(fname, desc=nil)
338 # create a new, anonymous module to "house" the plugin
339 # the idea here is to prevent namespace pollution. perhaps there
341 plugin_module = Module.new
343 desc = desc.to_s + " " if desc
346 plugin_string = IO.readlines(fname).join("")
347 debug "loading #{desc}#{fname}"
348 plugin_module.module_eval(plugin_string, fname)
350 rescue Exception => err
351 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
352 warning report_error("#{desc}#{fname} load failed", err)
353 bt = err.backtrace.select { |line|
354 line.match(/^(\(eval\)|#{fname}):\d+/)
357 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
361 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
364 newerr = err.class.new(msg)
365 newerr.set_backtrace(bt)
369 private :load_botmodule_file
371 # add one or more directories to the list of directories to
372 # load botmodules from
374 # TODO find a way to specify necessary plugins which _must_ be loaded
376 def add_botmodule_dir(*dirlist)
378 debug "Botmodule loading path: #{@dirs.join(', ')}"
381 # load plugins from pre-assigned list of directories
387 @bot.config['plugins.blacklist'].each { |p|
389 processed[pn.intern] = :blacklisted
394 if(FileTest.directory?(dir))
398 next if(file =~ /^\./)
400 if processed.has_key?(file.intern)
401 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
405 if(file =~ /^(.+\.rb)\.disabled$/)
406 # GB: Do we want to do this? This means that a disabled plugin in a directory
407 # will disable in all subsequent directories. This was probably meant
408 # to be used before plugins.blacklist was implemented, so I think
409 # we don't need this anymore
410 processed[$1.intern] = :disabled
411 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
415 next unless(file =~ /\.rb$/)
417 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
420 processed[file.intern] = did_it
422 @failed << { :name => file, :dir => dir, :reason => did_it }
428 debug "finished loading plugins: #{status(true)}"
431 # call the save method for each active plugin
433 delegate 'flush_registry'
437 # call the cleanup method for each active plugin
440 reset_botmodule_lists
443 # drop all plugins and rescan plugins on disk
444 # calls save and cleanup for each plugin before dropping them
451 def status(short=false)
453 if self.core_length > 0
454 list << "#{self.core_length} core module#{'s' if core_length > 1}"
458 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
461 list << "no core botmodules loaded"
463 # Active plugins first
465 list << "; #{self.length} plugin#{'s' if length > 1}"
469 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
472 list << "no plugins active"
474 # Ignored plugins next
475 unless @ignored.empty?
476 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
477 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
479 # Failed plugins next
480 unless @failed.empty?
481 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
482 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
487 # return list of help topics (plugin names)
500 # return help for +topic+ (call associated plugin's help method)
503 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
504 # debug "Failures: #{@failed.inspect}"
505 return "no plugins failed to load" if @failed.empty?
506 return @failed.inject(Array.new) { |list, p|
507 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
508 list << "with error #{p[:reason].class}: #{p[:reason]}"
509 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
512 when /ignored?\s*plugins?/
513 return "no plugins were ignored" if @ignored.empty?
514 return @ignored.inject(Array.new) { |list, p|
517 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
519 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
523 when /^(\S+)\s*(.*)$/
527 # Let's see if we can match a plugin by the given name
528 (core_modules + plugins).each { |p|
529 next unless p.name == key
531 return p.help(key, params)
532 rescue Exception => err
533 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
534 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
538 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
540 if commands.has_key?(k)
541 p = commands[k][:botmodule]
543 return p.help(key, params)
544 rescue Exception => err
545 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
546 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
553 # see if each plugin handles +method+, and if so, call it, passing
554 # +message+ as a parameter
555 def delegate(method, *args)
556 # debug "Delegating #{method.inspect}"
557 [core_modules, plugins].each { |pl|
559 if(p.respond_to? method)
561 # debug "#{p.botmodule_class} #{p.name} responds"
563 rescue Exception => err
564 raise if err.kind_of?(SystemExit)
565 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
566 raise if err.kind_of?(BDB::Fatal)
571 # debug "Finished delegating #{method.inspect}"
574 # see if we have a plugin that wants to handle this message, if so, pass
575 # it to the plugin and return true, otherwise false
577 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
578 return unless m.plugin
580 if commands.has_key?(k)
581 p = commands[k][:botmodule]
582 a = commands[k][:auth]
583 # We check here for things that don't check themselves
584 # (e.g. mapped things)
585 # debug "Checking auth ..."
586 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
587 # debug "Checking response ..."
588 if p.respond_to?("privmsg")
590 # debug "#{p.botmodule_class} #{p.name} responds"
592 rescue Exception => err
593 raise if err.kind_of?(SystemExit)
594 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
595 raise if err.kind_of?(BDB::Fatal)
597 # debug "Successfully delegated #{m.message}"
600 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
603 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
606 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
608 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
612 # Returns the only PluginManagerClass instance
613 def Plugins.pluginmanager
614 return PluginManagerClass.instance