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