]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
plugins: refactor common code in #map() and #map!() to auxiliary #do_map() method
[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       do_map(false, *args)
208     end
209
210     # call-seq: map!(template, options)
211     #
212     # This is the same as map but doesn't register the new command
213     # as an alternative name for the plugin.
214     #
215     def map!(*args)
216       do_map(true, *args)
217     end
218
219     # Auxiliary method called by #map and #map!
220     def do_map(silent, *args)
221       @handler.map(self, *args)
222       # register this map
223       name = @handler.last.items[0]
224       self.register name, :auth => nil, :hidden => silent
225       unless self.respond_to?('privmsg')
226         def self.privmsg(m) #:nodoc:
227           handle(m)
228         end
229       end
230     end
231
232     # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
233     # usually _chan_ is either "*" for everywhere, public and private (in which
234     # case it can be omitted) or "?" for private communications
235     #
236     def default_auth(cmd, val, chan="*")
237       case cmd
238       when "*", ""
239         c = nil
240       else
241         c = cmd
242       end
243       Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
244     end
245
246     # Gets the default command path which would be given to command _cmd_
247     def propose_default_path(cmd)
248       [name, cmd].compact.join("::")
249     end
250
251     # Return an identifier for this plugin, defaults to a list of the message
252     # prefixes handled (used for error messages etc)
253     def name
254       self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
255     end
256
257     # Just calls name
258     def to_s
259       name
260     end
261
262     # Intern the name
263     def to_sym
264       self.name.to_sym
265     end
266
267     # Return a help string for your module. For complex modules, you may wish
268     # to break your help into topics, and return a list of available topics if
269     # +topic+ is nil. +plugin+ is passed containing the matching prefix for
270     # this message - if your plugin handles multiple prefixes, make sure you
271     # return the correct help for the prefix requested
272     def help(plugin, topic)
273       "no help"
274     end
275
276     # Register the plugin as a handler for messages prefixed _cmd_.
277     #
278     # This can be called multiple times for a plugin to handle multiple message
279     # prefixes.
280     #
281     # This command is now superceded by the #map() command, which should be used
282     # instead whenever possible.
283     # 
284     def register(cmd, opts={})
285       raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
286       who = @manager.who_handles?(cmd)
287       if who
288         raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
289         return
290       end
291       if opts.has_key?(:auth)
292         @manager.register(self, cmd, opts[:auth])
293       else
294         @manager.register(self, cmd, propose_default_path(cmd))
295       end
296       @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
297     end
298
299     # Default usage method provided as a utility for simple plugins. The
300     # MessageMapper uses 'usage' as its default fallback method.
301     #
302     def usage(m, params = {})
303       m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
304     end
305
306   end
307
308   # A CoreBotModule is a BotModule that provides core functionality.
309   #
310   # This class should not be used by user plugins, as it's reserved for system
311   # plugins such as the ones that handle authentication, configuration and basic
312   # functionality.
313   #
314   class CoreBotModule < BotModule
315     def botmodule_class
316       :CoreBotModule
317     end
318   end
319
320   # A Plugin is a BotModule that provides additional functionality.
321   #
322   # A user-defined plugin should subclass this, and then define any of the
323   # methods described in the documentation for BotModule to handle interaction
324   # with Irc events.
325   #
326   class Plugin < BotModule
327     def botmodule_class
328       :Plugin
329     end
330   end
331
332   # Singleton to manage multiple plugins and delegate messages to them for
333   # handling
334   class PluginManagerClass
335     include Singleton
336     attr_reader :bot
337     attr_reader :botmodules
338
339     # This is the list of patterns commonly delegated to plugins.
340     # A fast delegation lookup is enabled for them.
341     DEFAULT_DELEGATE_PATTERNS = %r{^(?:
342       connect|names|nick|
343       listen|ctcp_listen|privmsg|unreplied|
344       kick|join|part|quit|
345       save|cleanup|flush_registry|
346       set_.*|event_.*
347     )$}x
348
349     def initialize
350       @botmodules = {
351         :CoreBotModule => [],
352         :Plugin => []
353       }
354
355       @names_hash = Hash.new
356       @commandmappers = Hash.new
357       @delegate_list = Hash.new { |h, k|
358         h[k] = Array.new
359       }
360
361       @dirs = []
362
363       @failed = Array.new
364       @ignored = Array.new
365
366       bot_associate(nil)
367     end
368
369     def inspect
370       ret = self.to_s[0..-2]
371       ret << ' corebotmodules='
372       ret << @botmodules[:CoreBotModule].map { |m|
373         m.name
374       }.inspect
375       ret << ' plugins='
376       ret << @botmodules[:Plugin].map { |m|
377         m.name
378       }.inspect
379       ret << ">"
380     end
381
382     # Reset lists of botmodules
383     def reset_botmodule_lists
384       @botmodules[:CoreBotModule].clear
385       @botmodules[:Plugin].clear
386       @names_hash.clear
387       @commandmappers.clear
388       @failures_shown = false
389     end
390
391     # Associate with bot _bot_
392     def bot_associate(bot)
393       reset_botmodule_lists
394       @bot = bot
395     end
396
397     # Returns the botmodule with the given _name_
398     def [](name)
399       @names_hash[name.to_sym]
400     end
401
402     # Returns +true+ if _cmd_ has already been registered as a command
403     def who_handles?(cmd)
404       return nil unless @commandmappers.has_key?(cmd.to_sym)
405       return @commandmappers[cmd.to_sym][:botmodule]
406     end
407
408     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
409     def register(botmodule, cmd, auth_path)
410       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
411       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
412     end
413
414     def add_botmodule(botmodule)
415       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
416       kl = botmodule.botmodule_class
417       if @names_hash.has_key?(botmodule.to_sym)
418         case self[botmodule].botmodule_class
419         when kl
420           raise "#{kl} #{botmodule} already registered!"
421         else
422           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
423         end
424       end
425       @botmodules[kl] << botmodule
426       @names_hash[botmodule.to_sym] = botmodule
427     end
428
429     # Returns an array of the loaded plugins
430     def core_modules
431       @botmodules[:CoreBotModule]
432     end
433
434     # Returns an array of the loaded plugins
435     def plugins
436       @botmodules[:Plugin]
437     end
438
439     # Returns a hash of the registered message prefixes and associated
440     # plugins
441     def commands
442       @commandmappers
443     end
444
445     # Makes a string of error _err_ by adding text _str_
446     def report_error(str, err)
447       ([str, err.inspect] + err.backtrace).join("\n")
448     end
449
450     # This method is the one that actually loads a module from the
451     # file _fname_
452     #
453     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
454     #
455     # It returns the Symbol :loaded on success, and an Exception
456     # on failure
457     #
458     def load_botmodule_file(fname, desc=nil)
459       # create a new, anonymous module to "house" the plugin
460       # the idea here is to prevent namespace pollution. perhaps there
461       # is another way?
462       plugin_module = Module.new
463
464       desc = desc.to_s + " " if desc
465
466       begin
467         plugin_string = IO.readlines(fname).join("")
468         debug "loading #{desc}#{fname}"
469         plugin_module.module_eval(plugin_string, fname)
470         return :loaded
471       rescue Exception => err
472         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
473         error report_error("#{desc}#{fname} load failed", err)
474         bt = err.backtrace.select { |line|
475           line.match(/^(\(eval\)|#{fname}):\d+/)
476         }
477         bt.map! { |el|
478           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
479             "#{fname}#{$1}#{$3}"
480           }
481         }
482         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
483           "#{fname}#{$1}#{$3}"
484         }
485         newerr = err.class.new(msg)
486         newerr.set_backtrace(bt)
487         return newerr
488       end
489     end
490     private :load_botmodule_file
491
492     # add one or more directories to the list of directories to
493     # load botmodules from
494     #
495     # TODO find a way to specify necessary plugins which _must_ be loaded
496     #
497     def add_botmodule_dir(*dirlist)
498       @dirs += dirlist
499       debug "Botmodule loading path: #{@dirs.join(', ')}"
500     end
501
502     def clear_botmodule_dirs
503       @dirs.clear
504       debug "Botmodule loading path cleared"
505     end
506
507     # load plugins from pre-assigned list of directories
508     def scan
509       @failed.clear
510       @ignored.clear
511       @delegate_list.clear
512
513       processed = Hash.new
514
515       @bot.config['plugins.blacklist'].each { |p|
516         pn = p + ".rb"
517         processed[pn.intern] = :blacklisted
518       }
519
520       dirs = @dirs
521       dirs.each {|dir|
522         if(FileTest.directory?(dir))
523           d = Dir.new(dir)
524           d.sort.each {|file|
525
526             next if(file =~ /^\./)
527
528             if processed.has_key?(file.intern)
529               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
530               next
531             end
532
533             if(file =~ /^(.+\.rb)\.disabled$/)
534               # GB: Do we want to do this? This means that a disabled plugin in a directory
535               #     will disable in all subsequent directories. This was probably meant
536               #     to be used before plugins.blacklist was implemented, so I think
537               #     we don't need this anymore
538               processed[$1.intern] = :disabled
539               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
540               next
541             end
542
543             next unless(file =~ /\.rb$/)
544
545             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
546             case did_it
547             when Symbol
548               processed[file.intern] = did_it
549             when Exception
550               @failed <<  { :name => file, :dir => dir, :reason => did_it }
551             end
552
553           }
554         end
555       }
556       debug "finished loading plugins: #{status(true)}"
557       (core_modules + plugins).each { |p|
558        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
559          @delegate_list[m.intern] << p
560        }
561       }
562     end
563
564     # call the save method for each active plugin
565     def save
566       delegate 'flush_registry'
567       delegate 'save'
568     end
569
570     # call the cleanup method for each active plugin
571     def cleanup
572       delegate 'cleanup'
573       reset_botmodule_lists
574     end
575
576     # drop all plugins and rescan plugins on disk
577     # calls save and cleanup for each plugin before dropping them
578     def rescan
579       save
580       cleanup
581       scan
582     end
583
584     def status(short=false)
585       output = []
586       if self.core_length > 0
587         if short
588           output << n_("%{count} core module loaded", "%{count} core modules loaded",
589                     self.core_length) % {:count => self.core_length}
590         else
591           output <<  n_("%{count} core module: %{list}",
592                      "%{count} core modules: %{list}", self.core_length) %
593                      { :count => self.core_length,
594                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
595         end
596       else
597         output << _("no core botmodules loaded")
598       end
599       # Active plugins first
600       if(self.length > 0)
601         if short
602           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
603                        self.length) % {:count => self.length}
604         else
605           output << n_("%{count} plugin: %{list}",
606                        "%{count} plugins: %{list}", self.length) %
607                    { :count => self.length,
608                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
609         end
610       else
611         output << "no plugins active"
612       end
613       # Ignored plugins next
614       unless @ignored.empty? or @failures_shown
615         if short
616           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
617                        "%{highlight}%{count} plugins ignored%{highlight}",
618                        @ignored.length) %
619                     { :count => @ignored.length, :highlight => Underline }
620         else
621           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
622                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
623                        @ignored.length) %
624                     { :count => @ignored.length, :highlight => Underline,
625                       :bold => Bold, :command => "help ignored plugins"}
626         end
627       end
628       # Failed plugins next
629       unless @failed.empty? or @failures_shown
630         if short
631           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
632                        "%{highlight}%{count} plugins failed to load%{highlight}",
633                        @failed.length) %
634                     { :count => @failed.length, :highlight => Reverse }
635         else
636           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
637                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
638                        @failed.length) %
639                     { :count => @failed.length, :highlight => Reverse,
640                       :bold => Bold, :command => "help failed plugins"}
641         end
642       end
643       output.join '; '
644     end
645
646     # return list of help topics (plugin names)
647     def helptopics
648       rv = status
649       @failures_shown = true
650       rv
651     end
652
653     def length
654       plugins.length
655     end
656
657     def core_length
658       core_modules.length
659     end
660
661     # return help for +topic+ (call associated plugin's help method)
662     def help(topic="")
663       case topic
664       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
665         # debug "Failures: #{@failed.inspect}"
666         return _("no plugins failed to load") if @failed.empty?
667         return @failed.collect { |p|
668           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
669               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
670               :exception => p[:reason].class, :reason => p[:reason],
671           } + if $1 && !p[:reason].backtrace.empty?
672                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
673               else
674                 ''
675               end
676         }.join("\n")
677       when /ignored?\s*plugins?/
678         return _('no plugins were ignored') if @ignored.empty?
679
680         tmp = Hash.new
681         @ignored.each do |p|
682           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
683           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
684         end
685
686         return tmp.map do |dir, reasons|
687           # FIXME get rid of these string concatenations to make gettext easier
688           s = reasons.map { |r, list|
689             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
690           }.join('; ')
691           "in #{dir}: #{s}"
692         end.join('; ')
693       when /^(\S+)\s*(.*)$/
694         key = $1
695         params = $2
696
697         # Let's see if we can match a plugin by the given name
698         (core_modules + plugins).each { |p|
699           next unless p.name == key
700           begin
701             return p.help(key, params)
702           rescue Exception => err
703             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
704             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
705           end
706         }
707
708         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
709         k = key.to_sym
710         if commands.has_key?(k)
711           p = commands[k][:botmodule]
712           begin
713             return p.help(key, params)
714           rescue Exception => err
715             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
716             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
717           end
718         end
719       end
720       return false
721     end
722
723     # see if each plugin handles +method+, and if so, call it, passing
724     # +message+ as a parameter
725     def delegate(method, *args)
726       # debug "Delegating #{method.inspect}"
727       ret = Array.new
728       if method.match(DEFAULT_DELEGATE_PATTERNS)
729         debug "fast-delegating #{method}"
730         m = method.to_sym
731         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
732         return [] unless @delegate_list.has_key?(m)
733         @delegate_list[m].each { |p|
734           begin
735             ret.push p.send(method, *args)
736           rescue Exception => err
737             raise if err.kind_of?(SystemExit)
738             error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
739             raise if err.kind_of?(BDB::Fatal)
740           end
741         }
742       else
743         debug "slow-delegating #{method}"
744         (core_modules + plugins).each { |p|
745           if(p.respond_to? method)
746             begin
747               # debug "#{p.botmodule_class} #{p.name} responds"
748               ret.push p.send(method, *args)
749             rescue Exception => err
750               raise if err.kind_of?(SystemExit)
751               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
752               raise if err.kind_of?(BDB::Fatal)
753             end
754           end
755         }
756       end
757       return ret
758       # debug "Finished delegating #{method.inspect}"
759     end
760
761     # see if we have a plugin that wants to handle this message, if so, pass
762     # it to the plugin and return true, otherwise false
763     def privmsg(m)
764       # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
765       return unless m.plugin
766       k = m.plugin.to_sym
767       if commands.has_key?(k)
768         p = commands[k][:botmodule]
769         a = commands[k][:auth]
770         # We check here for things that don't check themselves
771         # (e.g. mapped things)
772         # debug "Checking auth ..."
773         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
774           # debug "Checking response ..."
775           if p.respond_to?("privmsg")
776             begin
777               # debug "#{p.botmodule_class} #{p.name} responds"
778               p.privmsg(m)
779             rescue Exception => err
780               raise if err.kind_of?(SystemExit)
781               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
782               raise if err.kind_of?(BDB::Fatal)
783             end
784             # debug "Successfully delegated #{m.message}"
785             return true
786           else
787             # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
788           end
789         else
790           # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
791         end
792       end
793       # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
794       return false
795       # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
796     end
797   end
798
799   # Returns the only PluginManagerClass instance
800   def Plugins.manager
801     return PluginManagerClass.instance
802   end
803
804 end
805 end
806 end