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}"
144 def call_event(ev, *args)
145 @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
149 @handler.map(self, *args)
151 name = @handler.last.items[0]
152 self.register name, :auth => nil
153 unless self.respond_to?('privmsg')
161 @handler.map(self, *args)
163 name = @handler.last.items[0]
164 self.register name, :auth => nil, :hidden => true
165 unless self.respond_to?('privmsg')
172 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
173 # usually _chan_ is either "*" for everywhere, public and private (in which
174 # case it can be omitted) or "?" for private communications
176 def default_auth(cmd, val, chan="*")
183 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
186 # Gets the default command path which would be given to command _cmd_
187 def propose_default_path(cmd)
188 [name, cmd].compact.join("::")
191 # return an identifier for this plugin, defaults to a list of the message
192 # prefixes handled (used for error messages etc)
194 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
207 # return a help string for your module. for complex modules, you may wish
208 # to break your help into topics, and return a list of available topics if
209 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
210 # this message - if your plugin handles multiple prefixes, make sure you
211 # return the correct help for the prefix requested
212 def help(plugin, topic)
216 # register the plugin as a handler for messages prefixed +name+
217 # this can be called multiple times for a plugin to handle multiple
219 def register(cmd, opts={})
220 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
221 who = @manager.who_handles?(cmd)
223 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
226 if opts.has_key?(:auth)
227 @manager.register(self, cmd, opts[:auth])
229 @manager.register(self, cmd, propose_default_path(cmd))
231 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
234 # default usage method provided as a utility for simple plugins. The
235 # MessageMapper uses 'usage' as its default fallback method.
236 def usage(m, params = {})
237 m.reply "incorrect usage, ask for help using '#{@bot.nick}: help #{m.plugin}'"
242 class CoreBotModule < BotModule
248 class Plugin < BotModule
254 # Singleton to manage multiple plugins and delegate messages to them for
256 class PluginManagerClass
259 attr_reader :botmodules
263 :CoreBotModule => [],
267 @names_hash = Hash.new
268 @commandmappers = Hash.new
278 # Reset lists of botmodules
279 def reset_botmodule_lists
280 @botmodules[:CoreBotModule].clear
281 @botmodules[:Plugin].clear
283 @commandmappers.clear
284 @failures_shown = false
287 # Associate with bot _bot_
288 def bot_associate(bot)
289 reset_botmodule_lists
293 # Returns the botmodule with the given _name_
295 @names_hash[name.to_sym]
298 # Returns +true+ if _cmd_ has already been registered as a command
299 def who_handles?(cmd)
300 return nil unless @commandmappers.has_key?(cmd.to_sym)
301 return @commandmappers[cmd.to_sym][:botmodule]
304 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
305 def register(botmodule, cmd, auth_path)
306 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
307 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
310 def add_botmodule(botmodule)
311 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
312 kl = botmodule.botmodule_class
313 if @names_hash.has_key?(botmodule.to_sym)
314 case self[botmodule].botmodule_class
316 raise "#{kl} #{botmodule} already registered!"
318 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
321 @botmodules[kl] << botmodule
322 @names_hash[botmodule.to_sym] = botmodule
325 # Returns an array of the loaded plugins
327 @botmodules[:CoreBotModule]
330 # Returns an array of the loaded plugins
335 # Returns a hash of the registered message prefixes and associated
341 # Makes a string of error _err_ by adding text _str_
342 def report_error(str, err)
343 ([str, err.inspect] + err.backtrace).join("\n")
346 # This method is the one that actually loads a module from the
349 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
351 # It returns the Symbol :loaded on success, and an Exception
354 def load_botmodule_file(fname, desc=nil)
355 # create a new, anonymous module to "house" the plugin
356 # the idea here is to prevent namespace pollution. perhaps there
358 plugin_module = Module.new
360 desc = desc.to_s + " " if desc
363 plugin_string = IO.readlines(fname).join("")
364 debug "loading #{desc}#{fname}"
365 plugin_module.module_eval(plugin_string, fname)
367 rescue Exception => err
368 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
369 warning report_error("#{desc}#{fname} load failed", err)
370 bt = err.backtrace.select { |line|
371 line.match(/^(\(eval\)|#{fname}):\d+/)
374 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
378 msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
381 newerr = err.class.new(msg)
382 newerr.set_backtrace(bt)
386 private :load_botmodule_file
388 # add one or more directories to the list of directories to
389 # load botmodules from
391 # TODO find a way to specify necessary plugins which _must_ be loaded
393 def add_botmodule_dir(*dirlist)
395 debug "Botmodule loading path: #{@dirs.join(', ')}"
398 def clear_botmodule_dirs
400 debug "Botmodule loading path cleared"
403 # load plugins from pre-assigned list of directories
409 @bot.config['plugins.blacklist'].each { |p|
411 processed[pn.intern] = :blacklisted
416 if(FileTest.directory?(dir))
420 next if(file =~ /^\./)
422 if processed.has_key?(file.intern)
423 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
427 if(file =~ /^(.+\.rb)\.disabled$/)
428 # GB: Do we want to do this? This means that a disabled plugin in a directory
429 # will disable in all subsequent directories. This was probably meant
430 # to be used before plugins.blacklist was implemented, so I think
431 # we don't need this anymore
432 processed[$1.intern] = :disabled
433 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
437 next unless(file =~ /\.rb$/)
439 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
442 processed[file.intern] = did_it
444 @failed << { :name => file, :dir => dir, :reason => did_it }
450 debug "finished loading plugins: #{status(true)}"
453 # call the save method for each active plugin
455 delegate 'flush_registry'
459 # call the cleanup method for each active plugin
462 reset_botmodule_lists
465 # drop all plugins and rescan plugins on disk
466 # calls save and cleanup for each plugin before dropping them
473 def status(short=false)
475 if self.core_length > 0
476 list << "#{self.core_length} core module#{'s' if core_length > 1}"
480 list << ": " + core_modules.collect{ |p| p.name}.sort.join(", ")
483 list << "no core botmodules loaded"
485 # Active plugins first
487 list << "; #{self.length} plugin#{'s' if length > 1}"
491 list << ": " + plugins.collect{ |p| p.name}.sort.join(", ")
494 list << "no plugins active"
496 # Ignored plugins next
497 unless @ignored.empty? or @failures_shown
498 list << "; #{Underline}#{@ignored.length} plugin#{'s' if @ignored.length > 1} ignored#{Underline}"
499 list << ": use #{Bold}help ignored plugins#{Bold} to see why" unless short
501 # Failed plugins next
502 unless @failed.empty? or @failures_shown
503 list << "; #{Reverse}#{@failed.length} plugin#{'s' if @failed.length > 1} failed to load#{Reverse}"
504 list << ": use #{Bold}help failed plugins#{Bold} to see why" unless short
509 # return list of help topics (plugin names)
512 @failures_shown = true
524 # return help for +topic+ (call associated plugin's help method)
527 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
528 # debug "Failures: #{@failed.inspect}"
529 return "no plugins failed to load" if @failed.empty?
530 return @failed.inject(Array.new) { |list, p|
531 list << "#{Bold}#{p[:name]}#{Bold} in #{p[:dir]} failed"
532 list << "with error #{p[:reason].class}: #{p[:reason]}"
533 list << "at #{p[:reason].backtrace.join(', ')}" if $1 and not p[:reason].backtrace.empty?
536 when /ignored?\s*plugins?/
537 return "no plugins were ignored" if @ignored.empty?
541 reason = p[:loaded] ? 'overruled by previous' : p[:reason].to_s
542 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
545 return tmp.map do |dir, reasons|
546 s = reasons.map { |r, list|
547 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
551 when /^(\S+)\s*(.*)$/
555 # Let's see if we can match a plugin by the given name
556 (core_modules + plugins).each { |p|
557 next unless p.name == key
559 return p.help(key, params)
560 rescue Exception => err
561 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
562 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
566 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
568 if commands.has_key?(k)
569 p = commands[k][:botmodule]
571 return p.help(key, params)
572 rescue Exception => err
573 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
574 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
581 # see if each plugin handles +method+, and if so, call it, passing
582 # +message+ as a parameter
583 def delegate(method, *args)
584 # debug "Delegating #{method.inspect}"
586 [core_modules, plugins].each { |pl|
588 if(p.respond_to? method)
590 # debug "#{p.botmodule_class} #{p.name} responds"
591 ret.push p.send(method, *args)
592 rescue Exception => err
593 raise if err.kind_of?(SystemExit)
594 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
595 raise if err.kind_of?(BDB::Fatal)
601 # debug "Finished delegating #{method.inspect}"
604 # see if we have a plugin that wants to handle this message, if so, pass
605 # it to the plugin and return true, otherwise false
607 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
608 return unless m.plugin
610 if commands.has_key?(k)
611 p = commands[k][:botmodule]
612 a = commands[k][:auth]
613 # We check here for things that don't check themselves
614 # (e.g. mapped things)
615 # debug "Checking auth ..."
616 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
617 # debug "Checking response ..."
618 if p.respond_to?("privmsg")
620 # debug "#{p.botmodule_class} #{p.name} responds"
622 rescue Exception => err
623 raise if err.kind_of?(SystemExit)
624 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
625 raise if err.kind_of?(BDB::Fatal)
627 # debug "Successfully delegated #{m.message}"
630 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
633 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
636 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
638 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
642 # Returns the only PluginManagerClass instance
644 return PluginManagerClass.instance