]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
namespaces: move rbot-specific classes and modules from Irc::* to Irc::Bot::*
[user/henk/code/ruby/rbot.git] / lib / rbot / plugins.rb
1 #-- vim:sw=2:et
2 #++
3 #
4 # :title: rbot plugin management
5
6 require 'singleton'
7
8 module Irc
9 class Bot
10     Config.register Config::ArrayValue.new('plugins.blacklist',
11       :default => [], :wizard => false, :requires_rescan => true,
12       :desc => "Plugins that should not be loaded")
13 module Plugins
14   require 'rbot/messagemapper'
15
16 =begin rdoc
17   BotModule is the base class for the modules that enhance the rbot
18   functionality. Rather than subclassing BotModule, however, one should
19   subclass either CoreBotModule (reserved for system modules) or Plugin
20   (for user plugins).
21
22   A BotModule interacts with Irc events by defining one or more of the following
23   methods, which get called as appropriate when the corresponding Irc event
24   happens.
25
26   map(template, options)::
27   map!(template, options)::
28      map is the new, cleaner way to respond to specific message formats without
29      littering your plugin code with regexps, and should be used instead of
30      #register() and #privmsg() (see below) when possible.
31
32      The difference between map and map! is that map! will not register the new
33      command as an alternative name for the plugin.
34
35      Examples:
36
37        plugin.map 'karmastats', :action => 'karma_stats'
38
39        # while in the plugin...
40        def karma_stats(m, params)
41          m.reply "..."
42        end
43
44        # the default action is the first component
45        plugin.map 'karma'
46
47        # attributes can be pulled out of the match string
48        plugin.map 'karma for :key'
49        plugin.map 'karma :key'
50
51        # while in the plugin...
52        def karma(m, params)
53          item = params[:key]
54          m.reply 'karma for #{item}'
55        end
56
57        # you can setup defaults, to make parameters optional
58        plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
59
60        # the default auth check is also against the first component
61        # but that can be changed
62        plugin.map 'karmastats', :auth => 'karma'
63
64        # maps can be restricted to public or private message:
65        plugin.map 'karmastats', :private => false
66        plugin.map 'karmastats', :public => false
67
68      See MessageMapper#map for more information on the template format and the
69      allowed options.
70
71   listen(UserMessage)::
72                          Called for all messages of any type. To
73                          differentiate them, use message.kind_of? It'll be
74                          either a PrivMessage, NoticeMessage, KickMessage,
75                          QuitMessage, PartMessage, JoinMessage, NickMessage,
76                          etc.
77
78   ctcp_listen(UserMessage)::
79                          Called for all messages that contain a CTCP command.
80                          Use message.ctcp to get the CTCP command, and
81                          message.message to get the parameter string. To reply,
82                          use message.ctcp_reply, which sends a private NOTICE
83                          to the sender.
84
85   privmsg(PrivMessage)::
86                          Called for a PRIVMSG if the first word matches one
87                          the plugin #register()ed for. Use m.plugin to get
88                          that word and m.params for the rest of the message,
89                          if applicable.
90
91   unreplied(PrivMessage)::
92                          Called for a PRIVMSG which has not been replied to.
93
94   kick(KickMessage)::
95                          Called when a user (or the bot) is kicked from a
96                          channel the bot is in.
97
98   join(JoinMessage)::
99                          Called when a user (or the bot) joins a channel
100
101   part(PartMessage)::
102                          Called when a user (or the bot) parts a channel
103
104   quit(QuitMessage)::
105                          Called when a user (or the bot) quits IRC
106
107   nick(NickMessage)::
108                          Called when a user (or the bot) changes Nick
109   topic(TopicMessage)::
110                          Called when a user (or the bot) changes a channel
111                          topic
112
113   connect::              Called when a server is joined successfully, but
114                          before autojoin channels are joined (no params)
115
116   set_language(String)::
117                          Called when the user sets a new language
118                          whose name is the given String
119
120   save::                 Called when you are required to save your plugin's
121                          state, if you maintain data between sessions
122
123   cleanup::              called before your plugin is "unloaded", prior to a
124                          plugin reload or bot quit - close any open
125                          files/connections or flush caches here
126 =end
127
128   class BotModule
129     attr_reader :bot   # the associated bot
130
131     # Initialise your bot module. Always call super if you override this method,
132     # as important variables are set up for you:
133     #
134     # @bot::
135     #   the rbot instance
136     # @registry::
137     #   the botmodule's registry, which can be used to store permanent data
138     #   (see Registry::Accessor for additional documentation)
139     #
140     # Other instance variables which are defined and should not be overwritten
141     # byt the user, but aren't usually accessed directly, are:
142     #
143     # @manager::
144     #   the plugins manager instance
145     # @botmodule_triggers::
146     #   an Array of words this plugin #register()ed itself for
147     # @handler::
148     #   the MessageMapper that handles this plugin's maps
149     #
150     def initialize
151       @manager = Plugins::manager
152       @bot = @manager.bot
153
154       @botmodule_triggers = Array.new
155
156       @handler = MessageMapper.new(self)
157       @registry = Registry::Accessor.new(@bot, self.class.to_s.gsub(/^.*::/, ""))
158
159       @manager.add_botmodule(self)
160       if self.respond_to?('set_language')
161         self.set_language(@bot.lang.language)
162       end
163     end
164
165     # Returns the symbol :BotModule 
166     def botmodule_class
167       :BotModule
168     end
169
170     # Method called to flush the registry, thus ensuring that the botmodule's permanent
171     # data is committed to disk
172     #
173     def flush_registry
174       # debug "Flushing #{@registry}"
175       @registry.flush
176     end
177
178     # Method called to cleanup before the plugin is unloaded. If you overload
179     # this method to handle additional cleanup tasks, remember to call super()
180     # so that the default cleanup actions are taken care of as well.
181     #
182     def cleanup
183       # debug "Closing #{@registry}"
184       @registry.close
185     end
186
187     # Handle an Irc::PrivMessage for which this BotModule has a map. The method
188     # is called automatically and there is usually no need to call it
189     # explicitly.
190     #
191     def handle(m)
192       @handler.handle(m)
193     end
194
195     # Signal to other BotModules that an even happened.
196     #
197     def call_event(ev, *args)
198       @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *args)
199     end
200
201     # call-seq: map(template, options)
202     #
203     # This is the preferred way to register the BotModule so that it
204     # responds to appropriately-formed messages on Irc.
205     #
206     def map(*args)
207       @handler.map(self, *args)
208       # register this map
209       name = @handler.last.items[0]
210       self.register name, :auth => nil
211       unless self.respond_to?('privmsg')
212         def self.privmsg(m) #:nodoc:
213           handle(m)
214         end
215       end
216     end
217
218     # call-seq: map!(template, options)
219     #
220     # This is the same as map but doesn't register the new command
221     # as an alternative name for the plugin.
222     #
223     def map!(*args)
224       @handler.map(self, *args)
225       # register this map
226       name = @handler.last.items[0]
227       self.register name, :auth => nil, :hidden => true
228       unless self.respond_to?('privmsg')
229         def self.privmsg(m) #:nodoc:
230           handle(m)
231         end
232       end
233     end
234
235     # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
236     # usually _chan_ is either "*" for everywhere, public and private (in which
237     # case it can be omitted) or "?" for private communications
238     #
239     def default_auth(cmd, val, chan="*")
240       case cmd
241       when "*", ""
242         c = nil
243       else
244         c = cmd
245       end
246       Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
247     end
248
249     # Gets the default command path which would be given to command _cmd_
250     def propose_default_path(cmd)
251       [name, cmd].compact.join("::")
252     end
253
254     # Return an identifier for this plugin, defaults to a list of the message
255     # prefixes handled (used for error messages etc)
256     def name
257       self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
258     end
259
260     # Just calls name
261     def to_s
262       name
263     end
264
265     # Intern the name
266     def to_sym
267       self.name.to_sym
268     end
269
270     # Return a help string for your module. For complex modules, you may wish
271     # to break your help into topics, and return a list of available topics if
272     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
273     # this message - if your plugin handles multiple prefixes, make sure you
274     # return the correct help for the prefix requested
275     def help(plugin, topic)
276       "no help"
277     end
278
279     # Register the plugin as a handler for messages prefixed _cmd_.
280     #
281     # This can be called multiple times for a plugin to handle multiple message
282     # prefixes.
283     #
284     # This command is now superceded by the #map() command, which should be used
285     # instead whenever possible.
286     # 
287     def register(cmd, opts={})
288       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
289       who = @manager.who_handles?(cmd)
290       if who
291         raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
292         return
293       end
294       if opts.has_key?(:auth)
295         @manager.register(self, cmd, opts[:auth])
296       else
297         @manager.register(self, cmd, propose_default_path(cmd))
298       end
299       @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
300     end
301
302     # Default usage method provided as a utility for simple plugins. The
303     # MessageMapper uses 'usage' as its default fallback method.
304     #
305     def usage(m, params = {})
306       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
307     end
308
309   end
310
311   # A CoreBotModule is a BotModule that provides core functionality.
312   #
313   # This class should not be used by user plugins, as it's reserved for system
314   # plugins such as the ones that handle authentication, configuration and basic
315   # functionality.
316   #
317   class CoreBotModule < BotModule
318     def botmodule_class
319       :CoreBotModule
320     end
321   end
322
323   # A Plugin is a BotModule that provides additional functionality.
324   #
325   # A user-defined plugin should subclass this, and then define any of the
326   # methods described in the documentation for BotModule to handle interaction
327   # with Irc events.
328   #
329   class Plugin < BotModule
330     def botmodule_class
331       :Plugin
332     end
333   end
334
335   # Singleton to manage multiple plugins and delegate messages to them for
336   # handling
337   class PluginManagerClass
338     include Singleton
339     attr_reader :bot
340     attr_reader :botmodules
341
342     # This is the list of patterns commonly delegated to plugins.
343     # A fast delegation lookup is enabled for them.
344     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
345       connect|names|nick|
346       listen|ctcp_listen|privmsg|unreplied|
347       kick|join|part|quit|
348       save|cleanup|flush_registry|
349       set_.*|event_.*
350     )$}x
351
352     def initialize
353       @botmodules = {
354         :CoreBotModule => [],
355         :Plugin => []
356       }
357
358       @names_hash = Hash.new
359       @commandmappers = Hash.new
360       @delegate_list = Hash.new { |h, k|
361         h[k] = Array.new
362       }
363
364       @dirs = []
365
366       @failed = Array.new
367       @ignored = Array.new
368
369       bot_associate(nil)
370     end
371
372     # Reset lists of botmodules
373     def reset_botmodule_lists
374       @botmodules[:CoreBotModule].clear
375       @botmodules[:Plugin].clear
376       @names_hash.clear
377       @commandmappers.clear
378       @failures_shown = false
379     end
380
381     # Associate with bot _bot_
382     def bot_associate(bot)
383       reset_botmodule_lists
384       @bot = bot
385     end
386
387     # Returns the botmodule with the given _name_
388     def [](name)
389       @names_hash[name.to_sym]
390     end
391
392     # Returns +true+ if _cmd_ has already been registered as a command
393     def who_handles?(cmd)
394       return nil unless @commandmappers.has_key?(cmd.to_sym)
395       return @commandmappers[cmd.to_sym][:botmodule]
396     end
397
398     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
399     def register(botmodule, cmd, auth_path)
400       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
401       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
402     end
403
404     def add_botmodule(botmodule)
405       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
406       kl = botmodule.botmodule_class
407       if @names_hash.has_key?(botmodule.to_sym)
408         case self[botmodule].botmodule_class
409         when kl
410           raise "#{kl} #{botmodule} already registered!"
411         else
412           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
413         end
414       end
415       @botmodules[kl] << botmodule
416       @names_hash[botmodule.to_sym] = botmodule
417     end
418
419     # Returns an array of the loaded plugins
420     def core_modules
421       @botmodules[:CoreBotModule]
422     end
423
424     # Returns an array of the loaded plugins
425     def plugins
426       @botmodules[:Plugin]
427     end
428
429     # Returns a hash of the registered message prefixes and associated
430     # plugins
431     def commands
432       @commandmappers
433     end
434
435     # Makes a string of error _err_ by adding text _str_
436     def report_error(str, err)
437       ([str, err.inspect] + err.backtrace).join("\n")
438     end
439
440     # This method is the one that actually loads a module from the
441     # file _fname_
442     #
443     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
444     #
445     # It returns the Symbol :loaded on success, and an Exception
446     # on failure
447     #
448     def load_botmodule_file(fname, desc=nil)
449       # create a new, anonymous module to "house" the plugin
450       # the idea here is to prevent namespace pollution. perhaps there
451       # is another way?
452       plugin_module = Module.new
453
454       desc = desc.to_s + " " if desc
455
456       begin
457         plugin_string = IO.readlines(fname).join("")
458         debug "loading #{desc}#{fname}"
459         plugin_module.module_eval(plugin_string, fname)
460         return :loaded
461       rescue Exception => err
462         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
463         error report_error("#{desc}#{fname} load failed", err)
464         bt = err.backtrace.select { |line|
465           line.match(/^(\(eval\)|#{fname}):\d+/)
466         }
467         bt.map! { |el|
468           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
469             "#{fname}#{$1}#{$3}"
470           }
471         }
472         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
473           "#{fname}#{$1}#{$3}"
474         }
475         newerr = err.class.new(msg)
476         newerr.set_backtrace(bt)
477         return newerr
478       end
479     end
480     private :load_botmodule_file
481
482     # add one or more directories to the list of directories to
483     # load botmodules from
484     #
485     # TODO find a way to specify necessary plugins which _must_ be loaded
486     #
487     def add_botmodule_dir(*dirlist)
488       @dirs += dirlist
489       debug "Botmodule loading path: #{@dirs.join(', ')}"
490     end
491
492     def clear_botmodule_dirs
493       @dirs.clear
494       debug "Botmodule loading path cleared"
495     end
496
497     # load plugins from pre-assigned list of directories
498     def scan
499       @failed.clear
500       @ignored.clear
501       @delegate_list.clear
502
503       processed = Hash.new
504
505       @bot.config['plugins.blacklist'].each { |p|
506         pn = p + ".rb"
507         processed[pn.intern] = :blacklisted
508       }
509
510       dirs = @dirs
511       dirs.each {|dir|
512         if(FileTest.directory?(dir))
513           d = Dir.new(dir)
514           d.sort.each {|file|
515
516             next if(file =~ /^\./)
517
518             if processed.has_key?(file.intern)
519               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
520               next
521             end
522
523             if(file =~ /^(.+\.rb)\.disabled$/)
524               # GB: Do we want to do this? This means that a disabled plugin in a directory
525               #     will disable in all subsequent directories. This was probably meant
526               #     to be used before plugins.blacklist was implemented, so I think
527               #     we don't need this anymore
528               processed[$1.intern] = :disabled
529               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
530               next
531             end
532
533             next unless(file =~ /\.rb$/)
534
535             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
536             case did_it
537             when Symbol
538               processed[file.intern] = did_it
539             when Exception
540               @failed <<  { :name => file, :dir => dir, :reason => did_it }
541             end
542
543           }
544         end
545       }
546       debug "finished loading plugins: #{status(true)}"
547       (core_modules + plugins).each { |p|
548        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
549          @delegate_list[m.intern] << p
550        }
551       }
552     end
553
554     # call the save method for each active plugin
555     def save
556       delegate 'flush_registry'
557       delegate 'save'
558     end
559
560     # call the cleanup method for each active plugin
561     def cleanup
562       delegate 'cleanup'
563       reset_botmodule_lists
564     end
565
566     # drop all plugins and rescan plugins on disk
567     # calls save and cleanup for each plugin before dropping them
568     def rescan
569       save
570       cleanup
571       scan
572     end
573
574     def status(short=false)
575       output = []
576       if self.core_length > 0
577         if short
578           output << n_("%{count} core module loaded", "%{count} core modules loaded",
579                     self.core_length) % {:count => self.core_length}
580         else
581           output <<  n_("%{count} core module: %{list}",
582                      "%{count} core modules: %{list}", self.core_length) %
583                      { :count => self.core_length,
584                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
585         end
586       else
587         output << _("no core botmodules loaded")
588       end
589       # Active plugins first
590       if(self.length > 0)
591         if short
592           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
593                        self.length) % {:count => self.length}
594         else
595           output << n_("%{count} plugin: %{list}",
596                        "%{count} plugins: %{list}", self.length) %
597                    { :count => self.length,
598                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
599         end
600       else
601         output << "no plugins active"
602       end
603       # Ignored plugins next
604       unless @ignored.empty? or @failures_shown
605         if short
606           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
607                        "%{highlight}%{count} plugins ignored%{highlight}",
608                        @ignored.length) %
609                     { :count => @ignored.length, :highlight => Underline }
610         else
611           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
612                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
613                        @ignored.length) %
614                     { :count => @ignored.length, :highlight => Underline,
615                       :bold => Bold, :command => "help ignored plugins"}
616         end
617       end
618       # Failed plugins next
619       unless @failed.empty? or @failures_shown
620         if short
621           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
622                        "%{highlight}%{count} plugins failed to load%{highlight}",
623                        @failed.length) %
624                     { :count => @failed.length, :highlight => Reverse }
625         else
626           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
627                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
628                        @failed.length) %
629                     { :count => @failed.length, :highlight => Reverse,
630                       :bold => Bold, :command => "help failed plugins"}
631         end
632       end
633       output.join '; '
634     end
635
636     # return list of help topics (plugin names)
637     def helptopics
638       rv = status
639       @failures_shown = true
640       rv
641     end
642
643     def length
644       plugins.length
645     end
646
647     def core_length
648       core_modules.length
649     end
650
651     # return help for +topic+ (call associated plugin's help method)
652     def help(topic="")
653       case topic
654       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
655         # debug "Failures: #{@failed.inspect}"
656         return _("no plugins failed to load") if @failed.empty?
657         return @failed.collect { |p|
658           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
659               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
660               :exception => p[:reason].class, :reason => p[:reason],
661           } + if $1 && !p[:reason].backtrace.empty?
662                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
663               else
664                 ''
665               end
666         }.join("\n")
667       when /ignored?\s*plugins?/
668         return _('no plugins were ignored') if @ignored.empty?
669
670         tmp = Hash.new
671         @ignored.each do |p|
672           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
673           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
674         end
675
676         return tmp.map do |dir, reasons|
677           # FIXME get rid of these string concatenations to make gettext easier
678           s = reasons.map { |r, list|
679             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
680           }.join('; ')
681           "in #{dir}: #{s}"
682         end.join('; ')
683       when /^(\S+)\s*(.*)$/
684         key = $1
685         params = $2
686
687         # Let's see if we can match a plugin by the given name
688         (core_modules + plugins).each { |p|
689           next unless p.name == key
690           begin
691             return p.help(key, params)
692           rescue Exception => err
693             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
694             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
695           end
696         }
697
698         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
699         k = key.to_sym
700         if commands.has_key?(k)
701           p = commands[k][:botmodule]
702           begin
703             return p.help(key, params)
704           rescue Exception => err
705             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
706             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
707           end
708         end
709       end
710       return false
711     end
712
713     # see if each plugin handles +method+, and if so, call it, passing
714     # +message+ as a parameter
715     def delegate(method, *args)
716       # debug "Delegating #{method.inspect}"
717       ret = Array.new
718       if method.match(DEFAULT_DELEGATE_PATTERNS)
719         debug "fast-delegating #{method}"
720         m = method.to_sym
721         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
722         return [] unless @delegate_list.has_key?(m)
723         @delegate_list[m].each { |p|
724           begin
725             ret.push p.send(method, *args)
726           rescue Exception => err
727             raise if err.kind_of?(SystemExit)
728             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
729             raise if err.kind_of?(BDB::Fatal)
730           end
731         }
732       else
733         debug "slow-delegating #{method}"
734         (core_modules + plugins).each { |p|
735           if(p.respond_to? method)
736             begin
737               # debug "#{p.botmodule_class} #{p.name} responds"
738               ret.push p.send(method, *args)
739             rescue Exception => err
740               raise if err.kind_of?(SystemExit)
741               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
742               raise if err.kind_of?(BDB::Fatal)
743             end
744           end
745         }
746       end
747       return ret
748       # debug "Finished delegating #{method.inspect}"
749     end
750
751     # see if we have a plugin that wants to handle this message, if so, pass
752     # it to the plugin and return true, otherwise false
753     def privmsg(m)
754       # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
755       return unless m.plugin
756       k = m.plugin.to_sym
757       if commands.has_key?(k)
758         p = commands[k][:botmodule]
759         a = commands[k][:auth]
760         # We check here for things that don't check themselves
761         # (e.g. mapped things)
762         # debug "Checking auth ..."
763         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
764           # debug "Checking response ..."
765           if p.respond_to?("privmsg")
766             begin
767               # debug "#{p.botmodule_class} #{p.name} responds"
768               p.privmsg(m)
769             rescue Exception => err
770               raise if err.kind_of?(SystemExit)
771               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
772               raise if err.kind_of?(BDB::Fatal)
773             end
774             # debug "Successfully delegated #{m.message}"
775             return true
776           else
777             # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
778           end
779         else
780           # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
781         end
782       end
783       # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
784       return false
785       # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
786     end
787   end
788
789   # Returns the only PluginManagerClass instance
790   def Plugins.manager
791     return PluginManagerClass.instance
792   end
793
794 end
795 end
796 end