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 '%{command}'") % {:command => "#{@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
477 output << n_("%{count} core module loaded", "%{count} core modules loaded",
478 self.core_length) % {:count => self.core_length}
480 output << n_("%{count} core module: %{list}",
481 "%{count} core modules: %{list}", self.core_length) %
482 { :count => self.core_length,
483 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
486 output << _("no core botmodules loaded")
488 # Active plugins first
491 output << n_("%{count} plugin loaded", "%{count} plugins loaded",
492 self.length) % {:count => self.length}
494 output << n_("%{count} plugin: %{list}",
495 "%{count} plugins: %{list}", self.length) %
496 { :count => self.length,
497 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
500 output << "no plugins active"
502 # Ignored plugins next
503 unless @ignored.empty? or @failures_shown
505 output << n_("%{highlight}%{count} plugin ignored%{highlight}",
506 "%{highlight}%{count} plugins ignored%{highlight}",
508 { :count => @ignored.length, :highlight => Underline }
510 output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
511 "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
513 { :count => @ignored.length, :highlight => Underline,
514 :bold => Bold, :command => "help ignored plugins"}
517 # Failed plugins next
518 unless @failed.empty? or @failures_shown
520 output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
521 "%{highlight}%{count} plugins failed to load%{highlight}",
523 { :count => @ignored.length, :highlight => Reverse }
525 output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
526 "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
528 { :count => @ignored.length, :highlight => Reverse,
529 :bold => Bold, :command => "#{Bold}help failed plugins#{Bold}"}
535 # return list of help topics (plugin names)
538 @failures_shown = true
550 # return help for +topic+ (call associated plugin's help method)
553 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
554 # debug "Failures: #{@failed.inspect}"
555 return _("no plugins failed to load") if @failed.empty?
556 return @failed.collect { |p|
557 _('%{highlight}%{plugin}%{highlight} in %{dir}failed with error %{exception}: %{reason}') % {
558 :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
559 :exception => p[:reason].class, :reason => p[:reason],
560 } + if $1 && !p[:reason].backtrace.empty?
561 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
566 when /ignored?\s*plugins?/
567 return _('no plugins were ignored') if @ignored.empty?
571 reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
572 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
575 return tmp.map do |dir, reasons|
576 # FIXME get rid of these string concatenations to make gettext easier
577 s = reasons.map { |r, list|
578 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
582 when /^(\S+)\s*(.*)$/
586 # Let's see if we can match a plugin by the given name
587 (core_modules + plugins).each { |p|
588 next unless p.name == key
590 return p.help(key, params)
591 rescue Exception => err
592 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
593 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
597 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
599 if commands.has_key?(k)
600 p = commands[k][:botmodule]
602 return p.help(key, params)
603 rescue Exception => err
604 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
605 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
612 # see if each plugin handles +method+, and if so, call it, passing
613 # +message+ as a parameter
614 def delegate(method, *args)
615 # debug "Delegating #{method.inspect}"
617 [core_modules, plugins].each { |pl|
619 if(p.respond_to? method)
621 # debug "#{p.botmodule_class} #{p.name} responds"
622 ret.push p.send(method, *args)
623 rescue Exception => err
624 raise if err.kind_of?(SystemExit)
625 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
626 raise if err.kind_of?(BDB::Fatal)
632 # debug "Finished delegating #{method.inspect}"
635 # see if we have a plugin that wants to handle this message, if so, pass
636 # it to the plugin and return true, otherwise false
638 # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
639 return unless m.plugin
641 if commands.has_key?(k)
642 p = commands[k][:botmodule]
643 a = commands[k][:auth]
644 # We check here for things that don't check themselves
645 # (e.g. mapped things)
646 # debug "Checking auth ..."
647 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
648 # debug "Checking response ..."
649 if p.respond_to?("privmsg")
651 # debug "#{p.botmodule_class} #{p.name} responds"
653 rescue Exception => err
654 raise if err.kind_of?(SystemExit)
655 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
656 raise if err.kind_of?(BDB::Fatal)
658 # debug "Successfully delegated #{m.message}"
661 # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
664 # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
667 # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
669 # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
673 # Returns the only PluginManagerClass instance
675 return PluginManagerClass.instance