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
280 @failures_shown = false
283 # Associate with bot _bot_
284 def bot_associate(bot)
285 reset_botmodule_lists
289 # Returns the botmodule with the given _name_
291 @names_hash[name.to_sym]
294 # Returns +true+ if _cmd_ has already been registered as a command
295 def who_handles?(cmd)
296 return nil unless @commandmappers.has_key?(cmd.to_sym)
297 return @commandmappers[cmd.to_sym][:botmodule]
300 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
301 def register(botmodule, cmd, auth_path)
302 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
303 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
306 def add_botmodule(botmodule)
307 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
308 kl = botmodule.botmodule_class
309 if @names_hash.has_key?(botmodule.to_sym)
310 case self[botmodule].botmodule_class
312 raise "#{kl} #{botmodule} already registered!"
314 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
317 @botmodules[kl] << botmodule
318 @names_hash[botmodule.to_sym] = botmodule
321 # Returns an array of the loaded plugins
323 @botmodules[:CoreBotModule]
326 # Returns an array of the loaded plugins
331 # Returns a hash of the registered message prefixes and associated
337 # Makes a string of error _err_ by adding text _str_
338 def report_error(str, err)
339 ([str, err.inspect] + err.backtrace).join("\n")
342 # This method is the one that actually loads a module from the
345 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
347 # It returns the Symbol :loaded on success, and an Exception
350 def load_botmodule_file(fname, desc=nil)
351 # create a new, anonymous module to "house" the plugin
352 # the idea here is to prevent namespace pollution. perhaps there
354 plugin_module = Module.new
356 desc = desc.to_s + " " if desc
359 plugin_string = IO.readlines(fname).join("")
360 debug "loading #{desc}#{fname}"
361 plugin_module.module_eval(plugin_string, fname)
363 rescue Exception => err
364 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
365 warning report_error("#{desc}#{fname} load failed", err)
366 bt = err.backtrace.select { |line|
367 line.match(/^(\(eval\)|#{fname}):\d+/)
370 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
374 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
377 newerr = err.class.new(msg)
378 newerr.set_backtrace(bt)
382 private :load_botmodule_file
384 # add one or more directories to the list of directories to
385 # load botmodules from
387 # TODO find a way to specify necessary plugins which _must_ be loaded
389 def add_botmodule_dir(*dirlist)
391 debug "Botmodule loading path: #{@dirs.join(', ')}"
394 def clear_botmodule_dirs
396 debug "Botmodule loading path cleared"
399 # load plugins from pre-assigned list of directories
405 @bot.config['plugins.blacklist'].each { |p|
407 processed[pn.intern] = :blacklisted
412 if(FileTest.directory?(dir))
416 next if(file =~ /^\./)
418 if processed.has_key?(file.intern)
419 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
423 if(file =~ /^(.+\.rb)\.disabled$/)
424 # GB: Do we want to do this? This means that a disabled plugin in a directory
425 # will disable in all subsequent directories. This was probably meant
426 # to be used before plugins.blacklist was implemented, so I think
427 # we don't need this anymore
428 processed[$1.intern] = :disabled
429 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
433 next unless(file =~ /\.rb$/)
435 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
438 processed[file.intern] = did_it
440 @failed << { :name => file, :dir => dir, :reason => did_it }
446 debug "finished loading plugins: #{status(true)}"
449 # call the save method for each active plugin
451 delegate 'flush_registry'
455 # call the cleanup method for each active plugin
458 reset_botmodule_lists
461 # drop all plugins and rescan plugins on disk
462 # calls save and cleanup for each plugin before dropping them
469 def status(short=false)
471 if self.core_length > 0
472 list << "#{self.core_length} core module#{'s' if core_length > 1}"
476 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
479 list << "no core botmodules loaded"
481 # Active plugins first
483 list << "; #{self.length} plugin#{'s' if length > 1}"
487 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
490 list << "no plugins active"
492 # Ignored plugins next
493 unless @ignored.empty? or @failures_shown
494 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
495 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
497 # Failed plugins next
498 unless @failed.empty? or @failures_shown
499 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
500 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
505 # return list of help topics (plugin names)
508 @failures_shown = true
520 # return help for +topic+ (call associated plugin's help method)
523 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
524 # debug "Failures: #{@failed.inspect}"
525 return "no plugins failed to load" if @failed.empty?
526 return @failed.inject(Array.new) { |list, p|
527 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
528 list << "with error #{p[:reason].class}: #{p[:reason]}"
529 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
532 when /ignored?\s*plugins?/
533 return "no plugins were ignored" if @ignored.empty?
537 reason = p[:loaded] ? 'overruled by previous' : p[:reason].to_s
538 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
541 return tmp.map do |dir, reasons|
542 s = reasons.map { |r, list|
543 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
547 when /^(\S+)\s*(.*)$/
551 # Let's see if we can match a plugin by the given name
552 (core_modules + plugins).each { |p|
553 next unless p.name == key
555 return p.help(key, params)
556 rescue Exception => err
557 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
558 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
562 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
564 if commands.has_key?(k)
565 p = commands[k][:botmodule]
567 return p.help(key, params)
568 rescue Exception => err
569 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
570 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
577 # see if each plugin handles +method+, and if so, call it, passing
578 # +message+ as a parameter
579 def delegate(method, *args)
580 # debug "Delegating #{method.inspect}"
581 [core_modules, plugins].each { |pl|
583 if(p.respond_to? method)
585 # debug "#{p.botmodule_class} #{p.name} responds"
587 rescue Exception => err
588 raise if err.kind_of?(SystemExit)
589 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
590 raise if err.kind_of?(BDB::Fatal)
595 # debug "Finished delegating #{method.inspect}"
598 # see if we have a plugin that wants to handle this message, if so, pass
599 # it to the plugin and return true, otherwise false
601 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
602 return unless m.plugin
604 if commands.has_key?(k)
605 p = commands[k][:botmodule]
606 a = commands[k][:auth]
607 # We check here for things that don't check themselves
608 # (e.g. mapped things)
609 # debug "Checking auth ..."
610 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
611 # debug "Checking response ..."
612 if p.respond_to?("privmsg")
614 # debug "#{p.botmodule_class} #{p.name} responds"
616 rescue Exception => err
617 raise if err.kind_of?(SystemExit)
618 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
619 raise if err.kind_of?(BDB::Fatal)
621 # debug "Successfully delegated #{m.message}"
624 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
627 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
630 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
632 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
636 # Returns the only PluginManagerClass instance
638 return PluginManagerClass.instance