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(self)
119 # debug "Flushing #{@registry}"
124 # debug "Closing #{@registry}"
133 @handler.map(self, *args)
135 name = @handler.last.items[0]
136 auth = @handler.last.options[:full_auth_path]
137 self.register name, :auth => auth
138 unless self.respond_to?('privmsg')
146 @handler.map(self, *args)
148 name = @handler.last.items[0]
149 self.register name, :auth => auth, :hidden => true
150 unless self.respond_to?('privmsg')
157 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
158 # usually _chan_ is either "*" for everywhere, public and private (in which
159 # case it can be omitted) or "?" for private communications
161 def default_auth(cmd, val, chan="*")
168 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
171 # Gets the default command path which would be given to command _cmd_
172 def propose_default_path(cmd)
173 [name, cmd].compact.join("::")
176 # return an identifier for this plugin, defaults to a list of the message
177 # prefixes handled (used for error messages etc)
179 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin)?$/,"")
187 # return a help string for your module. for complex modules, you may wish
188 # to break your help into topics, and return a list of available topics if
189 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
190 # this message - if your plugin handles multiple prefixes, make sure you
191 # return the correct help for the prefix requested
192 def help(plugin, topic)
196 # register the plugin as a handler for messages prefixed +name+
197 # this can be called multiple times for a plugin to handle multiple
199 def register(cmd, opts={})
200 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
201 return if @manager.knows?(cmd, @botmodule_class)
202 if opts.has_key?(:auth)
203 @manager.register(self, cmd, opts[:auth])
205 @manager.register(self, cmd, propose_default_path(cmd))
207 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
210 # default usage method provided as a utility for simple plugins. The
211 # MessageMapper uses 'usage' as its default fallback method.
212 def usage(m, params = {})
213 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
218 class CoreBotModule < BotModule
224 class Plugin < BotModule
230 # Singleton to manage multiple plugins and delegate messages to them for
232 class PluginManagerClass
235 attr_reader :botmodules
243 # Reset lists of botmodules
244 def reset_botmodule_lists
257 # Associate with bot _bot_
258 def bot_associate(bot)
259 reset_botmodule_lists
263 # Returns +true+ if _name_ is a known botmodule of class kl
265 return @commandmappers[kl.to_sym].has_key?(name.to_sym)
268 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
269 def register(botmodule, cmd, auth_path)
270 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
271 kl = botmodule.botmodule_class
272 @commandmappers[kl.to_sym][cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
275 def add_botmodule(botmodule)
276 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.class <= BotModule
277 kl = botmodule.botmodule_class
278 raise "#{kl.to_s} #{botmodule.name} already registered!" if @botmodules[kl.to_sym].include?(botmodule)
279 @botmodules[kl.to_sym] << botmodule
282 # Returns an array of the loaded plugins
284 @botmodules[:coremodule]
287 # Returns an array of the loaded plugins
292 # Returns a hash of the registered message prefixes and associated
295 @commandmappers[:plugin]
298 # Returns a hash of the registered message prefixes and associated
301 @commandmappers[:coremodule]
304 # Makes a string of error _err_ by adding text _str_
305 def report_error(str, err)
306 ([str, err.inspect] + err.backtrace).join("\n")
309 # This method is the one that actually loads a module from the
312 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
314 # It returns the Symbol :loaded on success, and an Exception
317 def load_botmodule_file(fname, desc=nil)
318 # create a new, anonymous module to "house" the plugin
319 # the idea here is to prevent namespace pollution. perhaps there
321 plugin_module = Module.new
323 desc = desc.to_s + " " if desc
326 plugin_string = IO.readlines(fname).join("")
327 debug "loading #{desc}#{fname}"
328 plugin_module.module_eval(plugin_string, fname)
330 rescue Exception => err
331 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
332 warning report_error("#{desc}#{fname} load failed", err)
333 bt = err.backtrace.select { |line|
334 line.match(/^(\(eval\)|#{fname}):\d+/)
337 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
341 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
344 newerr = err.class.new(msg)
345 newerr.set_backtrace(bt)
349 private :load_botmodule_file
351 # add one or more directories to the list of directories to
352 # load botmodules from
354 # TODO find a way to specify necessary plugins which _must_ be loaded
356 def add_botmodule_dir(*dirlist)
358 debug "Botmodule loading path: #{@dirs.join(', ')}"
361 # load plugins from pre-assigned list of directories
367 @bot.config['plugins.blacklist'].each { |p|
369 processed[pn.intern] = :blacklisted
374 if(FileTest.directory?(dir))
378 next if(file =~ /^\./)
380 if processed.has_key?(file.intern)
381 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
385 if(file =~ /^(.+\.rb)\.disabled$/)
386 # GB: Do we want to do this? This means that a disabled plugin in a directory
387 # will disable in all subsequent directories. This was probably meant
388 # to be used before plugins.blacklist was implemented, so I think
389 # we don't need this anymore
390 processed[$1.intern] = :disabled
391 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
395 next unless(file =~ /\.rb$/)
397 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
400 processed[file.intern] = did_it
402 @failed << { :name => file, :dir => dir, :reason => did_it }
408 debug "finished loading plugins: #{status(true)}"
411 # call the save method for each active plugin
413 delegate 'flush_registry'
417 # call the cleanup method for each active plugin
420 reset_botmodule_lists
423 # drop all plugins and rescan plugins on disk
424 # calls save and cleanup for each plugin before dropping them
431 def status(short=false)
433 if self.core_length > 0
434 list << "#{self.core_length} core module#{'s' if core_length > 1}"
438 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
441 list << "no core botmodules loaded"
443 # Active plugins first
445 list << "; #{self.length} plugin#{'s' if length > 1}"
449 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
452 list << "no plugins active"
454 # Ignored plugins next
455 unless @ignored.empty?
456 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
457 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
459 # Failed plugins next
460 unless @failed.empty?
461 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
462 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
467 # return list of help topics (plugin names)
480 # return help for +topic+ (call associated plugin's help method)
483 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
484 # debug "Failures: #{@failed.inspect}"
485 return "no plugins failed to load" if @failed.empty?
486 return (@failed.inject(Array.new) { |list, p|
487 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
488 list << "with error #{p[:reason].class}: #{p[:reason]}"
489 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
492 when /ignored?\s*plugins?/
493 return "no plugins were ignored" if @ignored.empty?
494 return (@ignored.inject(Array.new) { |list, p|
497 list << "#{p[:name]} in #{p[:dir]} (overruled by previous)"
499 list << "#{p[:name]} in #{p[:dir]} (#{p[:reason].to_s})"
503 when /^(\S+)\s*(.*)$/
506 # TODO should also check core_module and plugins
507 [core_commands, plugin_commands].each { |pl|
509 p = pl[key][:botmodule]
511 return p.help(key, params)
512 rescue Exception => err
513 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
514 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
523 # see if each plugin handles +method+, and if so, call it, passing
524 # +message+ as a parameter
525 def delegate(method, *args)
526 debug "Delegating #{method.inspect}"
527 [core_modules, plugins].each { |pl|
529 if(p.respond_to? method)
531 debug "#{p.botmodule_class} #{p.name} responds"
533 rescue Exception => err
534 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
535 raise if err.class <= BDB::Fatal
540 debug "Finished delegating #{method.inspect}"
543 # see if we have a plugin that wants to handle this message, if so, pass
544 # it to the plugin and return true, otherwise false
546 debug "Delegating privmsg with key #{m.plugin}"
547 return unless m.plugin
549 [core_commands, plugin_commands].each { |pl|
550 # We do it this way to skip creating spurious keys
554 p = pl[k][:botmodule]
561 # TODO This should probably be checked elsewhere
562 debug "Checking auth ..."
563 if @bot.auth.allow?(a, m.source, m.replyto)
564 debug "Checking response ..."
565 if p.respond_to?("privmsg")
567 debug "#{p.botmodule_class} #{p.name} responds"
569 rescue Exception => err
570 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
571 raise if err.class <= BDB::Fatal
573 debug "Successfully delegated privmsg with key #{m.plugin}"
576 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsgs"
579 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to use #{m.plugin} on #{m.replyto}"
582 debug "No #{pl.values.first[:botmodule].botmodule_class} registered #{m.plugin}" unless pl.empty?
584 debug "Finished delegating privmsg with key #{m.plugin}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
587 rescue Exception => e
588 error report_error("couldn't delegate #{m}", e)
590 debug "Finished delegating privmsg with key #{m.plugin}"
594 # Returns the only PluginManagerClass instance
595 def Plugins.pluginmanager
596 return PluginManagerClass.instance