4 # :title: rbot plugin management
10 Config.register Config::ArrayValue.new('plugins.blacklist',
11 :default => [], :wizard => false, :requires_rescan => true,
12 :desc => "Plugins that should not be loaded")
13 Config.register Config::ArrayValue.new('plugins.whitelist',
14 :default => [], :wizard => false, :requires_rescan => true,
15 :desc => "Only whitelisted plugins will be loaded unless the list is empty")
17 require 'rbot/messagemapper'
20 BotModule is the base class for the modules that enhance the rbot
21 functionality. Rather than subclassing BotModule, however, one should
22 subclass either CoreBotModule (reserved for system modules) or Plugin
25 A BotModule interacts with Irc events by defining one or more of the following
26 methods, which get called as appropriate when the corresponding Irc event
29 map(template, options)::
30 map!(template, options)::
31 map is the new, cleaner way to respond to specific message formats without
32 littering your plugin code with regexps, and should be used instead of
33 #register() and #privmsg() (see below) when possible.
35 The difference between map and map! is that map! will not register the new
36 command as an alternative name for the plugin.
40 plugin.map 'karmastats', :action => 'karma_stats'
42 # while in the plugin...
43 def karma_stats(m, params)
47 # the default action is the first component
50 # attributes can be pulled out of the match string
51 plugin.map 'karma for :key'
52 plugin.map 'karma :key'
54 # while in the plugin...
57 m.reply 'karma for #{item}'
60 # you can setup defaults, to make parameters optional
61 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
63 # the default auth check is also against the first component
64 # but that can be changed
65 plugin.map 'karmastats', :auth => 'karma'
67 # maps can be restricted to public or private message:
68 plugin.map 'karmastats', :private => false
69 plugin.map 'karmastats', :public => false
71 See MessageMapper#map for more information on the template format and the
75 Called for all messages of any type. To
76 differentiate them, use message.kind_of? It'll be
77 either a PrivMessage, NoticeMessage, KickMessage,
78 QuitMessage, PartMessage, JoinMessage, NickMessage,
81 ctcp_listen(UserMessage)::
82 Called for all messages that contain a CTCP command.
83 Use message.ctcp to get the CTCP command, and
84 message.message to get the parameter string. To reply,
85 use message.ctcp_reply, which sends a private NOTICE
88 message(PrivMessage)::
89 Called for all PRIVMSG. Hook on this method if you
90 need to handle PRIVMSGs regardless of whether they are
91 addressed to the bot or not, and regardless of
93 privmsg(PrivMessage)::
94 Called for a PRIVMSG if the first word matches one
95 the plugin #register()ed for. Use m.plugin to get
96 that word and m.params for the rest of the message,
99 unreplied(PrivMessage)::
100 Called for a PRIVMSG which has not been replied to.
102 notice(NoticeMessage)::
103 Called for all Notices. Please notice that in general
104 should not be replied to.
107 Called when a user (or the bot) is kicked from a
108 channel the bot is in.
110 invite(InviteMessage)::
111 Called when the bot is invited to a channel.
114 Called when a user (or the bot) joins a channel
117 Called when a user (or the bot) parts a channel
120 Called when a user (or the bot) quits IRC
123 Called when a user (or the bot) changes Nick
124 modechange(ModeChangeMessage)::
125 Called when a User or Channel mode is changed
126 topic(TopicMessage)::
127 Called when a user (or the bot) changes a channel
130 welcome(WelcomeMessage)::
131 Called when the welcome message is received on
132 joining a server succesfully.
135 Called when the Message Of The Day is fully
136 recevied from the server.
138 connect:: Called when a server is joined successfully, but
139 before autojoin channels are joined (no params)
141 set_language(String)::
142 Called when the user sets a new language
143 whose name is the given String
145 save:: Called when you are required to save your plugin's
146 state, if you maintain data between sessions
148 cleanup:: called before your plugin is "unloaded", prior to a
149 plugin reload or bot quit - close any open
150 files/connections or flush caches here
157 # the plugin registry
158 attr_reader :registry
160 # the message map handler
163 # Initialise your bot module. Always call super if you override this method,
164 # as important variables are set up for you:
169 # the botmodule's registry, which can be used to store permanent data
170 # (see Registry::Accessor for additional documentation)
172 # Other instance variables which are defined and should not be overwritten
173 # byt the user, but aren't usually accessed directly, are:
176 # the plugins manager instance
177 # @botmodule_triggers::
178 # an Array of words this plugin #register()ed itself for
180 # the MessageMapper that handles this plugin's maps
183 @manager = Plugins::manager
187 @botmodule_triggers = Array.new
189 @handler = MessageMapper.new(self)
190 @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
192 @manager.add_botmodule(self)
193 if self.respond_to?('set_language')
194 self.set_language(@bot.lang.language)
198 # Changing the value of @priority directly will cause problems,
199 # Please use priority=.
204 # Returns the symbol :BotModule
209 # Method called to flush the registry, thus ensuring that the botmodule's permanent
210 # data is committed to disk
213 # debug "Flushing #{@registry}"
217 # Method called to cleanup before the plugin is unloaded. If you overload
218 # this method to handle additional cleanup tasks, remember to call super()
219 # so that the default cleanup actions are taken care of as well.
222 # debug "Closing #{@registry}"
226 # Handle an Irc::PrivMessage for which this BotModule has a map. The method
227 # is called automatically and there is usually no need to call it
234 # Signal to other BotModules that an even happened.
236 def call_event(ev, *args)
237 @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
240 # call-seq: map(template, options)
242 # This is the preferred way to register the BotModule so that it
243 # responds to appropriately-formed messages on Irc.
249 # call-seq: map!(template, options)
251 # This is the same as map but doesn't register the new command
252 # as an alternative name for the plugin.
258 # Auxiliary method called by #map and #map!
259 def do_map(silent, *args)
260 @handler.map(self, *args)
264 self.register name, :auth => nil, :hidden => silent
265 @manager.register_map(self, map)
266 unless self.respond_to?('privmsg')
267 def self.privmsg(m) #:nodoc:
273 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
274 # usually _chan_ is either "*" for everywhere, public and private (in which
275 # case it can be omitted) or "?" for private communications
277 def default_auth(cmd, val, chan="*")
284 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
287 # Gets the default command path which would be given to command _cmd_
288 def propose_default_path(cmd)
289 [name, cmd].compact.join("::")
292 # Return an identifier for this plugin, defaults to a list of the message
293 # prefixes handled (used for error messages etc)
295 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
308 # Return a help string for your module. For complex modules, you may wish
309 # to break your help into topics, and return a list of available topics if
310 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
311 # this message - if your plugin handles multiple prefixes, make sure you
312 # return the correct help for the prefix requested
313 def help(plugin, topic)
317 # Register the plugin as a handler for messages prefixed _cmd_.
319 # This can be called multiple times for a plugin to handle multiple message
322 # This command is now superceded by the #map() command, which should be used
323 # instead whenever possible.
325 def register(cmd, opts={})
326 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
327 who = @manager.who_handles?(cmd)
329 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
332 if opts.has_key?(:auth)
333 @manager.register(self, cmd, opts[:auth])
335 @manager.register(self, cmd, propose_default_path(cmd))
337 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
340 # Default usage method provided as a utility for simple plugins. The
341 # MessageMapper uses 'usage' as its default fallback method.
343 def usage(m, params = {})
344 if params[:failures].respond_to? :find
345 friendly = params[:failures].find do |f|
346 f.kind_of? MessageMapper::FriendlyFailure
349 m.reply friendly.friendly
353 m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
356 # Define the priority of the module. During event delegation, lower
357 # priority modules will be called first. Default priority is 1
361 @bot.plugins.mark_priorities_dirty
365 # Directory name to be joined to the botclass to access data files. By
366 # default this is the plugin name itself, but may be overridden, for
367 # example by plugins that share their datafiles or for backwards
373 # Filename for a datafile built joining the botclass, plugin dirname and
376 @bot.path dirname, *fname
380 # A CoreBotModule is a BotModule that provides core functionality.
382 # This class should not be used by user plugins, as it's reserved for system
383 # plugins such as the ones that handle authentication, configuration and basic
386 class CoreBotModule < BotModule
392 # A Plugin is a BotModule that provides additional functionality.
394 # A user-defined plugin should subclass this, and then define any of the
395 # methods described in the documentation for BotModule to handle interaction
398 class Plugin < BotModule
404 # Singleton to manage multiple plugins and delegate messages to them for
406 class PluginManagerClass
409 attr_reader :botmodules
412 # This is the list of patterns commonly delegated to plugins.
413 # A fast delegation lookup is enabled for them.
414 DEFAULT_DELEGATE_PATTERNS = %r{^(?:
416 listen|ctcp_listen|privmsg|unreplied|
418 save|cleanup|flush_registry|
424 :CoreBotModule => [],
428 @names_hash = Hash.new
429 @commandmappers = Hash.new
432 # modules will be sorted on first delegate call
433 @sorted_modules = nil
435 @delegate_list = Hash.new { |h, k|
439 @core_module_dirs = []
449 ret = self.to_s[0..-2]
450 ret << ' corebotmodules='
451 ret << @botmodules[:CoreBotModule].map { |m|
455 ret << @botmodules[:Plugin].map { |m|
461 # Reset lists of botmodules
462 def reset_botmodule_lists
463 @botmodules[:CoreBotModule].clear
464 @botmodules[:Plugin].clear
466 @commandmappers.clear
468 @failures_shown = false
469 mark_priorities_dirty
472 # Associate with bot _bot_
473 def bot_associate(bot)
474 reset_botmodule_lists
478 # Returns the botmodule with the given _name_
480 @names_hash[name.to_sym]
483 # Returns +true+ if _cmd_ has already been registered as a command
484 def who_handles?(cmd)
485 return nil unless @commandmappers.has_key?(cmd.to_sym)
486 return @commandmappers[cmd.to_sym][:botmodule]
489 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
490 def register(botmodule, cmd, auth_path)
491 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
492 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
495 # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
496 # which has three keys:
498 # botmodule:: the associated botmodule
499 # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
500 # map:: the actual MessageTemplate object
503 def register_map(botmodule, map)
504 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
505 @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
508 def add_botmodule(botmodule)
509 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
510 kl = botmodule.botmodule_class
511 if @names_hash.has_key?(botmodule.to_sym)
512 case self[botmodule].botmodule_class
514 raise "#{kl} #{botmodule} already registered!"
516 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
519 @botmodules[kl] << botmodule
520 @names_hash[botmodule.to_sym] = botmodule
521 mark_priorities_dirty
524 # Returns an array of the loaded plugins
526 @botmodules[:CoreBotModule]
529 # Returns an array of the loaded plugins
534 # Returns a hash of the registered message prefixes and associated
540 # Tells the PluginManager that the next time it delegates an event, it
541 # should sort the modules by priority
542 def mark_priorities_dirty
543 @sorted_modules = nil
546 # Makes a string of error _err_ by adding text _str_
547 def report_error(str, err)
548 ([str, err.inspect] + err.backtrace).join("\n")
551 # This method is the one that actually loads a module from the
554 # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
556 # It returns the Symbol :loaded on success, and an Exception
559 def load_botmodule_file(fname, desc=nil)
560 # create a new, anonymous module to "house" the plugin
561 # the idea here is to prevent namespace pollution. perhaps there
563 plugin_module = Module.new
564 # each plugin uses its own textdomain, we bind it automatically here
565 bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
567 desc = desc.to_s + " " if desc
570 plugin_string = IO.read(fname)
571 debug "loading #{desc}#{fname}"
572 plugin_module.module_eval(plugin_string, fname)
574 rescue Exception => err
575 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
576 error report_error("#{desc}#{fname} load failed", err)
577 bt = err.backtrace.select { |line|
578 line.match(/^(\(eval\)|#{fname}):\d+/)
581 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
585 msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
589 newerr = err.class.new(msg)
590 rescue ArgumentError => aerr_in_err
591 # Somebody should hang the ActiveSupport developers by their balls
592 # with barbed wire. Their MissingSourceFile extension to LoadError
593 # _expects_ a second argument, breaking the usual Exception interface
594 # (instead, the smart thing to do would have been to make the second
595 # parameter optional and run the code in the from_message method if
597 # Anyway, we try to cope with this in the simplest possible way. On
598 # the upside, this new block can be extended to handle other similar
600 if err.class.respond_to? :from_message
601 newerr = err.class.from_message(msg)
605 rescue NoMethodError => nmerr_in_err
606 # Another braindead extension to StandardError, OAuth2::Error,
607 # doesn't get a string as message, but a response
608 if err.respond_to? :response
609 newerr = err.class.new(err.response)
614 newerr.set_backtrace(bt)
618 private :load_botmodule_file
620 # add one or more directories to the list of directories to
621 # load core modules from
622 def add_core_module_dir(*dirlist)
623 @core_module_dirs += dirlist
624 debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
627 # add one or more directories to the list of directories to
629 def add_plugin_dir(*dirlist)
630 @plugin_dirs += dirlist
631 debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
634 def clear_botmodule_dirs
635 @core_module_dirs.clear
637 debug "Core module and plugin loading paths cleared"
640 def scan_botmodules(opts={})
646 dirs = @core_module_dirs
650 @bot.config['plugins.blacklist'].each { |p|
652 processed[pn.intern] = :blacklisted
655 whitelist = @bot.config['plugins.whitelist'].map { |p|
661 next unless FileTest.directory?(dir)
663 d.sort.each do |file|
664 next unless file =~ /\.rb$/
665 next if file =~ /^\./
669 if !whitelist.empty? && !whitelist.include?(file)
670 @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
672 elsif processed.has_key?(file.intern)
673 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
677 if(file =~ /^(.+\.rb)\.disabled$/)
678 # GB: Do we want to do this? This means that a disabled plugin in a directory
679 # will disable in all subsequent directories. This was probably meant
680 # to be used before plugins.blacklist was implemented, so I think
681 # we don't need this anymore
682 processed[$1.intern] = :disabled
683 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
689 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
690 rescue Exception => e
697 processed[file.intern] = did_it
699 @failed << { :name => file, :dir => dir, :reason => did_it }
705 # load plugins from pre-assigned list of directories
711 scan_botmodules(:type => :core)
712 scan_botmodules(:type => :plugins)
714 debug "finished loading plugins: #{status(true)}"
715 (core_modules + plugins).each { |p|
716 p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
717 @delegate_list[m.intern] << p
720 mark_priorities_dirty
723 # call the save method for each active plugin
725 delegate 'flush_registry'
729 # call the cleanup method for each active plugin
732 reset_botmodule_lists
735 # drop all plugins and rescan plugins on disk
736 # calls save and cleanup for each plugin before dropping them
743 def status(short=false)
745 if self.core_length > 0
747 output << n_("%{count} core module loaded", "%{count} core modules loaded",
748 self.core_length) % {:count => self.core_length}
750 output << n_("%{count} core module: %{list}",
751 "%{count} core modules: %{list}", self.core_length) %
752 { :count => self.core_length,
753 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
756 output << _("no core botmodules loaded")
758 # Active plugins first
761 output << n_("%{count} plugin loaded", "%{count} plugins loaded",
762 self.length) % {:count => self.length}
764 output << n_("%{count} plugin: %{list}",
765 "%{count} plugins: %{list}", self.length) %
766 { :count => self.length,
767 :list => plugins.collect{ |p| p.name}.sort.join(", ") }
770 output << "no plugins active"
772 # Ignored plugins next
773 unless @ignored.empty? or @failures_shown
775 output << n_("%{highlight}%{count} plugin ignored%{highlight}",
776 "%{highlight}%{count} plugins ignored%{highlight}",
778 { :count => @ignored.length, :highlight => Underline }
780 output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
781 "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
783 { :count => @ignored.length, :highlight => Underline,
784 :bold => Bold, :command => "help ignored plugins"}
787 # Failed plugins next
788 unless @failed.empty? or @failures_shown
790 output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
791 "%{highlight}%{count} plugins failed to load%{highlight}",
793 { :count => @failed.length, :highlight => Reverse }
795 output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
796 "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
798 { :count => @failed.length, :highlight => Reverse,
799 :bold => Bold, :command => "help failed plugins"}
805 # return list of help topics (plugin names)
808 @failures_shown = true
820 # return help for +topic+ (call associated plugin's help method)
823 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
824 # debug "Failures: #{@failed.inspect}"
825 return _("no plugins failed to load") if @failed.empty?
826 return @failed.collect { |p|
827 _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
828 :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
829 :exception => p[:reason].class, :reason => p[:reason],
830 } + if $1 && !p[:reason].backtrace.empty?
831 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
836 when /ignored?\s*plugins?/
837 return _('no plugins were ignored') if @ignored.empty?
841 reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
842 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
845 return tmp.map do |dir, reasons|
846 # FIXME get rid of these string concatenations to make gettext easier
847 s = reasons.map { |r, list|
848 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
852 when /^(\S+)\s*(.*)$/
856 # Let's see if we can match a plugin by the given name
857 (core_modules + plugins).each { |p|
858 next unless p.name == key
860 return p.help(key, params)
861 rescue Exception => err
862 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
863 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
867 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
869 if commands.has_key?(k)
870 p = commands[k][:botmodule]
872 return p.help(key, params)
873 rescue Exception => err
874 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
875 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
883 @sorted_modules = (core_modules + plugins).sort do |a, b|
884 a.priority <=> b.priority
887 @delegate_list.each_value do |list|
888 list.sort! {|a,b| a.priority <=> b.priority}
892 # call-seq: delegate</span><span class="method-args">(method, m, opts={})</span>
893 # <span class="method-name">delegate</span><span class="method-args">(method, opts={})
895 # see if each plugin handles _method_, and if so, call it, passing
896 # _m_ as a parameter (if present). BotModules are called in order of
897 # priority from lowest to highest.
899 # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
900 # will only be delegated to plugins with negative priority. Conversely, if
901 # it's a fake message (see BotModule#fake_message), it will only be
902 # delegated to plugins with positive priority.
904 # Note that _m_ can also be an exploded Array, but in this case the last
905 # element of it cannot be a Hash, or it will be interpreted as the options
906 # Hash for delegate itself. The last element can be a subclass of a Hash, though.
907 # To be on the safe side, you can add an empty Hash as last parameter for delegate
908 # when calling it with an exploded Array:
909 # @bot.plugins.delegate(method, *(args.push Hash.new))
911 # Currently supported options are the following:
913 # if specified, the delegation will only consider plugins with a priority
914 # higher than the specified value
916 # if specified, the delegation will only consider plugins with a priority
917 # lower than the specified value
919 def delegate(method, *args)
920 # if the priorities order of the delegate list is dirty,
921 # meaning some modules have been added or priorities have been
922 # changed, then the delegate list will need to be sorted before
923 # delegation. This should always be true for the first delegation.
924 sort_modules unless @sorted_modules
927 opts.merge(args.pop) if args.last.class == Hash
930 if BasicUserMessage === m
931 # ignored messages should not be delegated
932 # to plugins with positive priority
933 opts[:below] ||= 0 if m.ignored?
934 # fake messages should not be delegated
935 # to plugins with negative priority
936 opts[:above] ||= 0 if m.recurse_depth > 0
942 # debug "Delegating #{method.inspect}"
944 if method.match(DEFAULT_DELEGATE_PATTERNS)
945 debug "fast-delegating #{method}"
947 debug "no-one to delegate to" unless @delegate_list.has_key?(m)
948 return [] unless @delegate_list.has_key?(m)
949 @delegate_list[m].each { |p|
952 unless (above and above >= prio) or (below and below <= prio)
953 ret.push p.send(method, *args)
955 rescue Exception => err
956 raise if err.kind_of?(SystemExit)
957 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
961 debug "slow-delegating #{method}"
962 @sorted_modules.each { |p|
963 if(p.respond_to? method)
965 # debug "#{p.botmodule_class} #{p.name} responds"
967 unless (above and above >= prio) or (below and below <= prio)
968 ret.push p.send(method, *args)
970 rescue Exception => err
971 raise if err.kind_of?(SystemExit)
972 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
978 # debug "Finished delegating #{method.inspect}"
981 # see if we have a plugin that wants to handle this message, if so, pass
982 # it to the plugin and return true, otherwise false
984 debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
985 return unless m.plugin
987 if commands.has_key?(k)
988 p = commands[k][:botmodule]
989 a = commands[k][:auth]
990 # We check here for things that don't check themselves
991 # (e.g. mapped things)
992 debug "Checking auth ..."
993 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
994 debug "Checking response ..."
995 if p.respond_to?("privmsg")
997 debug "#{p.botmodule_class} #{p.name} responds"
999 rescue Exception => err
1000 raise if err.kind_of?(SystemExit)
1001 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1003 debug "Successfully delegated #{m.inspect}"
1006 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1009 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1012 debug "Command #{k} isn't handled"
1017 # delegate IRC messages, by delegating 'listen' first, and the actual method
1018 # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1020 def irc_delegate(method, m)
1021 delegate('listen', m)
1022 if method.to_sym == :privmsg
1023 delegate('ctcp_listen', m) if m.ctcp
1024 delegate('message', m)
1025 privmsg(m) if m.address? and not m.ignored?
1026 delegate('unreplied', m) unless m.replied
1033 # Returns the only PluginManagerClass instance
1035 return PluginManagerClass.instance