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 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
102 # initialise your bot module. Always call super if you override this method,
103 # as important variables are set up for you
105 @manager = Plugins::pluginmanager
108 @botmodule_triggers = Array.new
110 @handler = MessageMapper.new(self)
111 @registry = BotRegistryAccessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
113 @manager.add_botmodule(self)
121 # debug "Flushing #{@registry}"
126 # debug "Closing #{@registry}"
135 @handler.map(self, *args)
137 name = @handler.last.items[0]
138 self.register name, :auth => nil
139 unless self.respond_to?('privmsg')
147 @handler.map(self, *args)
149 name = @handler.last.items[0]
150 self.register name, :auth => nil, :hidden => true
151 unless self.respond_to?('privmsg')
158 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
159 # usually _chan_ is either "*" for everywhere, public and private (in which
160 # case it can be omitted) or "?" for private communications
162 def default_auth(cmd, val, chan="*")
169 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
172 # Gets the default command path which would be given to command _cmd_
173 def propose_default_path(cmd)
174 [name, cmd].compact.join("::")
177 # return an identifier for this plugin, defaults to a list of the message
178 # prefixes handled (used for error messages etc)
180 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
193 # return a help string for your module. for complex modules, you may wish
194 # to break your help into topics, and return a list of available topics if
195 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
196 # this message - if your plugin handles multiple prefixes, make sure you
197 # return the correct help for the prefix requested
198 def help(plugin, topic)
202 # register the plugin as a handler for messages prefixed +name+
203 # this can be called multiple times for a plugin to handle multiple
205 def register(cmd, opts={})
206 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
207 who = @manager.who_handles?(cmd)
209 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
212 if opts.has_key?(:auth)
213 @manager.register(self, cmd, opts[:auth])
215 @manager.register(self, cmd, propose_default_path(cmd))
217 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
220 # default usage method provided as a utility for simple plugins. The
221 # MessageMapper uses 'usage' as its default fallback method.
222 def usage(m, params = {})
223 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
228 class CoreBotModule < BotModule
234 class Plugin < BotModule
240 # Singleton to manage multiple plugins and delegate messages to them for
242 class PluginManagerClass
245 attr_reader :botmodules
253 # Reset lists of botmodules
254 def reset_botmodule_lists
256 :CoreBotModule => [],
259 @names_hash = Hash.new
260 @commandmappers = Hash.new
263 # Associate with bot _bot_
264 def bot_associate(bot)
265 reset_botmodule_lists
269 # Returns the botmodule with the given _name_
271 @names_hash[name.to_sym]
274 # Returns +true+ if _cmd_ has already been registered as a command
275 def who_handles?(cmd)
276 return nil unless @commandmappers.has_key?(cmd.to_sym)
277 return @commandmappers[cmd.to_sym][:botmodule]
280 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
281 def register(botmodule, cmd, auth_path)
282 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
283 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
286 def add_botmodule(botmodule)
287 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
288 kl = botmodule.botmodule_class
289 if @names_hash.has_key?(botmodule.to_sym)
290 case self[botmodule].botmodule_class
292 raise "#{kl} #{botmodule} already registered!"
294 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
297 @botmodules[kl] << botmodule
298 @names_hash[botmodule.to_sym] = botmodule
301 # Returns an array of the loaded plugins
303 @botmodules[:CoreBotModule]
306 # Returns an array of the loaded plugins
311 # Returns a hash of the registered message prefixes and associated
317 # Makes a string of error _err_ by adding text _str_
318 def report_error(str, err)
319 ([str, err.inspect] + err.backtrace).join("\n")
322 # This method is the one that actually loads a module from the
325 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
327 # It returns the Symbol :loaded on success, and an Exception
330 def load_botmodule_file(fname, desc=nil)
331 # create a new, anonymous module to "house" the plugin
332 # the idea here is to prevent namespace pollution. perhaps there
334 plugin_module = Module.new
336 desc = desc.to_s + " " if desc
339 plugin_string = IO.readlines(fname).join("")
340 debug "loading #{desc}#{fname}"
341 plugin_module.module_eval(plugin_string, fname)
343 rescue Exception => err
344 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
345 warning report_error("#{desc}#{fname} load failed", err)
346 bt = err.backtrace.select { |line|
347 line.match(/^(\(eval\)|#{fname}):\d+/)
350 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
354 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
357 newerr = err.class.new(msg)
358 newerr.set_backtrace(bt)
362 private :load_botmodule_file
364 # add one or more directories to the list of directories to
365 # load botmodules from
367 # TODO find a way to specify necessary plugins which _must_ be loaded
369 def add_botmodule_dir(*dirlist)
371 debug "Botmodule loading path: #{@dirs.join(', ')}"
374 # load plugins from pre-assigned list of directories
380 @bot.config['plugins.blacklist'].each { |p|
382 processed[pn.intern] = :blacklisted
387 if(FileTest.directory?(dir))
391 next if(file =~ /^\./)
393 if processed.has_key?(file.intern)
394 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
398 if(file =~ /^(.+\.rb)\.disabled$/)
399 # GB: Do we want to do this? This means that a disabled plugin in a directory
400 # will disable in all subsequent directories. This was probably meant
401 # to be used before plugins.blacklist was implemented, so I think
402 # we don't need this anymore
403 processed[$1.intern] = :disabled
404 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
408 next unless(file =~ /\.rb$/)
410 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
413 processed[file.intern] = did_it
415 @failed << { :name => file, :dir => dir, :reason => did_it }
421 debug "finished loading plugins: #{status(true)}"
424 # call the save method for each active plugin
426 delegate 'flush_registry'
430 # call the cleanup method for each active plugin
433 reset_botmodule_lists
436 # drop all plugins and rescan plugins on disk
437 # calls save and cleanup for each plugin before dropping them
444 def status(short=false)
446 if self.core_length > 0
447 list << "#{self.core_length} core module#{'s' if core_length > 1}"
451 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
454 list << "no core botmodules loaded"
456 # Active plugins first
458 list << "; #{self.length} plugin#{'s' if length > 1}"
462 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
465 list << "no plugins active"
467 # Ignored plugins next
468 unless @ignored.empty?
469 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
470 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
472 # Failed plugins next
473 unless @failed.empty?
474 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
475 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
480 # return list of help topics (plugin names)
493 # return help for +topic+ (call associated plugin's help method)
496 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
497 # debug "Failures: #{@failed.inspect}"
498 return "no plugins failed to load" if @failed.empty?
499 return @failed.inject(Array.new) { |list, p|
500 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
501 list << "with error #{p[:reason].class}: #{p[:reason]}"
502 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
505 when /ignored?\s*plugins?/
506 return "no plugins were ignored" if @ignored.empty?
507 return @ignored.inject(Array.new) { |list, p|
510 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
512 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
516 when /^(\S+)\s*(.*)$/
520 # Let's see if we can match a plugin by the given name
521 (core_modules + plugins).each { |p|
522 next unless p.name == key
524 return p.help(key, params)
525 rescue Exception => err
526 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
527 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
531 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
533 if commands.has_key?(k)
534 p = commands[k][:botmodule]
536 return p.help(key, params)
537 rescue Exception => err
538 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
539 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
546 # see if each plugin handles +method+, and if so, call it, passing
547 # +message+ as a parameter
548 def delegate(method, *args)
549 # debug "Delegating #{method.inspect}"
550 [core_modules, plugins].each { |pl|
552 if(p.respond_to? method)
554 # debug "#{p.botmodule_class} #{p.name} responds"
556 rescue Exception => err
557 raise if err.kind_of?(SystemExit)
558 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
559 raise if err.kind_of?(BDB::Fatal)
564 # debug "Finished delegating #{method.inspect}"
567 # see if we have a plugin that wants to handle this message, if so, pass
568 # it to the plugin and return true, otherwise false
570 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
571 return unless m.plugin
573 if commands.has_key?(k)
574 p = commands[k][:botmodule]
575 a = commands[k][:auth]
576 # We check here for things that don't check themselves
577 # (e.g. mapped things)
578 # debug "Checking auth ..."
579 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
580 # debug "Checking response ..."
581 if p.respond_to?("privmsg")
583 # debug "#{p.botmodule_class} #{p.name} responds"
585 rescue Exception => err
586 raise if err.kind_of?(SystemExit)
587 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
588 raise if err.kind_of?(BDB::Fatal)
590 # debug "Successfully delegated #{m.message}"
593 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
596 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
599 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
601 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
605 # Returns the only PluginManagerClass instance
606 def Plugins.pluginmanager
607 return PluginManagerClass.instance