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,
69 unreplied(PrivMessage)::
70 Called for a PRIVMSG which has not been replied to.
73 Called when a user (or the bot) is kicked from a
74 channel the bot is in.
77 Called when a user (or the bot) joins a channel
80 Called when a user (or the bot) parts a channel
83 Called when a user (or the bot) quits IRC
86 Called when a user (or the bot) changes Nick
88 Called when a user (or the bot) changes a channel
91 connect():: Called when a server is joined successfully, but
92 before autojoin channels are joined (no params)
94 set_language(String)::
95 Called when the user sets a new language
96 whose name is the given String
98 save:: Called when you are required to save your plugin's
99 state, if you maintain data between sessions
101 cleanup:: called before your plugin is "unloaded", prior to a
102 plugin reload or bot quit - close any open
103 files/connections or flush caches here
107 attr_reader :bot # the associated bot
109 # initialise your bot module. Always call super if you override this method,
110 # as important variables are set up for you
112 @manager = Plugins::manager
115 @botmodule_triggers = Array.new
117 @handler = MessageMapper.new(self)
118 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
120 @manager.add_botmodule(self)
121 if self.respond_to?('set_language')
122 self.set_language(@bot.lang.language)
131 # debug "Flushing #{@registry}"
136 # debug "Closing #{@registry}"
145 @handler.map(self, *args)
147 name = @handler.last.items[0]
148 self.register name, :auth => nil
149 unless self.respond_to?('privmsg')
157 @handler.map(self, *args)
159 name = @handler.last.items[0]
160 self.register name, :auth => nil, :hidden => true
161 unless self.respond_to?('privmsg')
168 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
169 # usually _chan_ is either "*" for everywhere, public and private (in which
170 # case it can be omitted) or "?" for private communications
172 def default_auth(cmd, val, chan="*")
179 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
182 # Gets the default command path which would be given to command _cmd_
183 def propose_default_path(cmd)
184 [name, cmd].compact.join("::")
187 # return an identifier for this plugin, defaults to a list of the message
188 # prefixes handled (used for error messages etc)
190 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
203 # return a help string for your module. for complex modules, you may wish
204 # to break your help into topics, and return a list of available topics if
205 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
206 # this message - if your plugin handles multiple prefixes, make sure you
207 # return the correct help for the prefix requested
208 def help(plugin, topic)
212 # register the plugin as a handler for messages prefixed +name+
213 # this can be called multiple times for a plugin to handle multiple
215 def register(cmd, opts={})
216 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
217 who = @manager.who_handles?(cmd)
219 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
222 if opts.has_key?(:auth)
223 @manager.register(self, cmd, opts[:auth])
225 @manager.register(self, cmd, propose_default_path(cmd))
227 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
230 # default usage method provided as a utility for simple plugins. The
231 # MessageMapper uses 'usage' as its default fallback method.
232 def usage(m, params = {})
233 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
238 class CoreBotModule < BotModule
244 class Plugin < BotModule
250 # Singleton to manage multiple plugins and delegate messages to them for
252 class PluginManagerClass
255 attr_reader :botmodules
259 :CoreBotModule => [],
263 @names_hash = Hash.new
264 @commandmappers = Hash.new
274 # Reset lists of botmodules
275 def reset_botmodule_lists
276 @botmodules[:CoreBotModule].clear
277 @botmodules[:Plugin].clear
279 @commandmappers.clear
282 # Associate with bot _bot_
283 def bot_associate(bot)
284 reset_botmodule_lists
288 # Returns the botmodule with the given _name_
290 @names_hash[name.to_sym]
293 # Returns +true+ if _cmd_ has already been registered as a command
294 def who_handles?(cmd)
295 return nil unless @commandmappers.has_key?(cmd.to_sym)
296 return @commandmappers[cmd.to_sym][:botmodule]
299 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
300 def register(botmodule, cmd, auth_path)
301 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
302 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
305 def add_botmodule(botmodule)
306 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
307 kl = botmodule.botmodule_class
308 if @names_hash.has_key?(botmodule.to_sym)
309 case self[botmodule].botmodule_class
311 raise "#{kl} #{botmodule} already registered!"
313 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
316 @botmodules[kl] << botmodule
317 @names_hash[botmodule.to_sym] = botmodule
320 # Returns an array of the loaded plugins
322 @botmodules[:CoreBotModule]
325 # Returns an array of the loaded plugins
330 # Returns a hash of the registered message prefixes and associated
336 # Makes a string of error _err_ by adding text _str_
337 def report_error(str, err)
338 ([str, err.inspect] + err.backtrace).join("\n")
341 # This method is the one that actually loads a module from the
344 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
346 # It returns the Symbol :loaded on success, and an Exception
349 def load_botmodule_file(fname, desc=nil)
350 # create a new, anonymous module to "house" the plugin
351 # the idea here is to prevent namespace pollution. perhaps there
353 plugin_module = Module.new
355 desc = desc.to_s + " " if desc
358 plugin_string = IO.readlines(fname).join("")
359 debug "loading #{desc}#{fname}"
360 plugin_module.module_eval(plugin_string, fname)
362 rescue Exception => err
363 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
364 warning report_error("#{desc}#{fname} load failed", err)
365 bt = err.backtrace.select { |line|
366 line.match(/^(\(eval\)|#{fname}):\d+/)
369 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
373 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
376 newerr = err.class.new(msg)
377 newerr.set_backtrace(bt)
381 private :load_botmodule_file
383 # add one or more directories to the list of directories to
384 # load botmodules from
386 # TODO find a way to specify necessary plugins which _must_ be loaded
388 def add_botmodule_dir(*dirlist)
390 debug "Botmodule loading path: #{@dirs.join(', ')}"
393 def clear_botmodule_dirs
395 debug "Botmodule loading path cleared"
398 # load plugins from pre-assigned list of directories
404 @bot.config['plugins.blacklist'].each { |p|
406 processed[pn.intern] = :blacklisted
411 if(FileTest.directory?(dir))
415 next if(file =~ /^\./)
417 if processed.has_key?(file.intern)
418 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
422 if(file =~ /^(.+\.rb)\.disabled$/)
423 # GB: Do we want to do this? This means that a disabled plugin in a directory
424 # will disable in all subsequent directories. This was probably meant
425 # to be used before plugins.blacklist was implemented, so I think
426 # we don't need this anymore
427 processed[$1.intern] = :disabled
428 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
432 next unless(file =~ /\.rb$/)
434 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
437 processed[file.intern] = did_it
439 @failed << { :name => file, :dir => dir, :reason => did_it }
445 debug "finished loading plugins: #{status(true)}"
448 # call the save method for each active plugin
450 delegate 'flush_registry'
454 # call the cleanup method for each active plugin
457 reset_botmodule_lists
460 # drop all plugins and rescan plugins on disk
461 # calls save and cleanup for each plugin before dropping them
468 def status(short=false)
470 if self.core_length > 0
471 list << "#{self.core_length} core module#{'s' if core_length > 1}"
475 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
478 list << "no core botmodules loaded"
480 # Active plugins first
482 list << "; #{self.length} plugin#{'s' if length > 1}"
486 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
489 list << "no plugins active"
491 # Ignored plugins next
492 unless @ignored.empty?
493 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
494 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
496 # Failed plugins next
497 unless @failed.empty?
498 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
499 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
504 # return list of help topics (plugin names)
517 # return help for +topic+ (call associated plugin's help method)
520 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
521 # debug "Failures: #{@failed.inspect}"
522 return "no plugins failed to load" if @failed.empty?
523 return @failed.inject(Array.new) { |list, p|
524 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
525 list << "with error #{p[:reason].class}: #{p[:reason]}"
526 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
529 when /ignored?\s*plugins?/
530 return "no plugins were ignored" if @ignored.empty?
531 return @ignored.inject(Array.new) { |list, p|
534 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
536 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
540 when /^(\S+)\s*(.*)$/
544 # Let's see if we can match a plugin by the given name
545 (core_modules + plugins).each { |p|
546 next unless p.name == key
548 return p.help(key, params)
549 rescue Exception => err
550 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
551 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
555 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
557 if commands.has_key?(k)
558 p = commands[k][:botmodule]
560 return p.help(key, params)
561 rescue Exception => err
562 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
563 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
570 # see if each plugin handles +method+, and if so, call it, passing
571 # +message+ as a parameter
572 def delegate(method, *args)
573 # debug "Delegating #{method.inspect}"
574 [core_modules, plugins].each { |pl|
576 if(p.respond_to? method)
578 # debug "#{p.botmodule_class} #{p.name} responds"
580 rescue Exception => err
581 raise if err.kind_of?(SystemExit)
582 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
583 raise if err.kind_of?(BDB::Fatal)
588 # debug "Finished delegating #{method.inspect}"
591 # see if we have a plugin that wants to handle this message, if so, pass
592 # it to the plugin and return true, otherwise false
594 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
595 return unless m.plugin
597 if commands.has_key?(k)
598 p = commands[k][:botmodule]
599 a = commands[k][:auth]
600 # We check here for things that don't check themselves
601 # (e.g. mapped things)
602 # debug "Checking auth ..."
603 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
604 # debug "Checking response ..."
605 if p.respond_to?("privmsg")
607 # debug "#{p.botmodule_class} #{p.name} responds"
609 rescue Exception => err
610 raise if err.kind_of?(SystemExit)
611 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
612 raise if err.kind_of?(BDB::Fatal)
614 # debug "Successfully delegated #{m.message}"
617 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
620 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
623 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
625 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
629 # Returns the only PluginManagerClass instance
631 return PluginManagerClass.instance