]> git.netwichtig.de Git - user/henk/code/ruby/rbot.git/blob - lib/rbot/plugins.rb
Improved inspect methods all around
[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     def inspect
373       ret = self.to_s[0..-2]
374       ret << ' corebotmodules='
375       ret << @botmodules[:CoreBotModule].map { |m|
376         m.name
377       }.inspect
378       ret << ' plugins='
379       ret << @botmodules[:Plugin].map { |m|
380         m.name
381       }.inspect
382       ret << ">"
383     end
384
385     # Reset lists of botmodules
386     def reset_botmodule_lists
387       @botmodules[:CoreBotModule].clear
388       @botmodules[:Plugin].clear
389       @names_hash.clear
390       @commandmappers.clear
391       @failures_shown = false
392     end
393
394     # Associate with bot _bot_
395     def bot_associate(bot)
396       reset_botmodule_lists
397       @bot = bot
398     end
399
400     # Returns the botmodule with the given _name_
401     def [](name)
402       @names_hash[name.to_sym]
403     end
404
405     # Returns +true+ if _cmd_ has already been registered as a command
406     def who_handles?(cmd)
407       return nil unless @commandmappers.has_key?(cmd.to_sym)
408       return @commandmappers[cmd.to_sym][:botmodule]
409     end
410
411     # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
412     def register(botmodule, cmd, auth_path)
413       raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
414       @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
415     end
416
417     def add_botmodule(botmodule)
418       raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
419       kl = botmodule.botmodule_class
420       if @names_hash.has_key?(botmodule.to_sym)
421         case self[botmodule].botmodule_class
422         when kl
423           raise "#{kl} #{botmodule} already registered!"
424         else
425           raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
426         end
427       end
428       @botmodules[kl] << botmodule
429       @names_hash[botmodule.to_sym] = botmodule
430     end
431
432     # Returns an array of the loaded plugins
433     def core_modules
434       @botmodules[:CoreBotModule]
435     end
436
437     # Returns an array of the loaded plugins
438     def plugins
439       @botmodules[:Plugin]
440     end
441
442     # Returns a hash of the registered message prefixes and associated
443     # plugins
444     def commands
445       @commandmappers
446     end
447
448     # Makes a string of error _err_ by adding text _str_
449     def report_error(str, err)
450       ([str, err.inspect] + err.backtrace).join("\n")
451     end
452
453     # This method is the one that actually loads a module from the
454     # file _fname_
455     #
456     # _desc_ is a simple description of what we are loading (plugin/botmodule/whatever)
457     #
458     # It returns the Symbol :loaded on success, and an Exception
459     # on failure
460     #
461     def load_botmodule_file(fname, desc=nil)
462       # create a new, anonymous module to "house" the plugin
463       # the idea here is to prevent namespace pollution. perhaps there
464       # is another way?
465       plugin_module = Module.new
466
467       desc = desc.to_s + " " if desc
468
469       begin
470         plugin_string = IO.readlines(fname).join("")
471         debug "loading #{desc}#{fname}"
472         plugin_module.module_eval(plugin_string, fname)
473         return :loaded
474       rescue Exception => err
475         # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
476         error report_error("#{desc}#{fname} load failed", err)
477         bt = err.backtrace.select { |line|
478           line.match(/^(\(eval\)|#{fname}):\d+/)
479         }
480         bt.map! { |el|
481           el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
482             "#{fname}#{$1}#{$3}"
483           }
484         }
485         msg = err.to_str.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
486           "#{fname}#{$1}#{$3}"
487         }
488         newerr = err.class.new(msg)
489         newerr.set_backtrace(bt)
490         return newerr
491       end
492     end
493     private :load_botmodule_file
494
495     # add one or more directories to the list of directories to
496     # load botmodules from
497     #
498     # TODO find a way to specify necessary plugins which _must_ be loaded
499     #
500     def add_botmodule_dir(*dirlist)
501       @dirs += dirlist
502       debug "Botmodule loading path: #{@dirs.join(', ')}"
503     end
504
505     def clear_botmodule_dirs
506       @dirs.clear
507       debug "Botmodule loading path cleared"
508     end
509
510     # load plugins from pre-assigned list of directories
511     def scan
512       @failed.clear
513       @ignored.clear
514       @delegate_list.clear
515
516       processed = Hash.new
517
518       @bot.config['plugins.blacklist'].each { |p|
519         pn = p + ".rb"
520         processed[pn.intern] = :blacklisted
521       }
522
523       dirs = @dirs
524       dirs.each {|dir|
525         if(FileTest.directory?(dir))
526           d = Dir.new(dir)
527           d.sort.each {|file|
528
529             next if(file =~ /^\./)
530
531             if processed.has_key?(file.intern)
532               @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
533               next
534             end
535
536             if(file =~ /^(.+\.rb)\.disabled$/)
537               # GB: Do we want to do this? This means that a disabled plugin in a directory
538               #     will disable in all subsequent directories. This was probably meant
539               #     to be used before plugins.blacklist was implemented, so I think
540               #     we don't need this anymore
541               processed[$1.intern] = :disabled
542               @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
543               next
544             end
545
546             next unless(file =~ /\.rb$/)
547
548             did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
549             case did_it
550             when Symbol
551               processed[file.intern] = did_it
552             when Exception
553               @failed <<  { :name => file, :dir => dir, :reason => did_it }
554             end
555
556           }
557         end
558       }
559       debug "finished loading plugins: #{status(true)}"
560       (core_modules + plugins).each { |p|
561        p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
562          @delegate_list[m.intern] << p
563        }
564       }
565     end
566
567     # call the save method for each active plugin
568     def save
569       delegate 'flush_registry'
570       delegate 'save'
571     end
572
573     # call the cleanup method for each active plugin
574     def cleanup
575       delegate 'cleanup'
576       reset_botmodule_lists
577     end
578
579     # drop all plugins and rescan plugins on disk
580     # calls save and cleanup for each plugin before dropping them
581     def rescan
582       save
583       cleanup
584       scan
585     end
586
587     def status(short=false)
588       output = []
589       if self.core_length > 0
590         if short
591           output << n_("%{count} core module loaded", "%{count} core modules loaded",
592                     self.core_length) % {:count => self.core_length}
593         else
594           output <<  n_("%{count} core module: %{list}",
595                      "%{count} core modules: %{list}", self.core_length) %
596                      { :count => self.core_length,
597                        :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
598         end
599       else
600         output << _("no core botmodules loaded")
601       end
602       # Active plugins first
603       if(self.length > 0)
604         if short
605           output << n_("%{count} plugin loaded", "%{count} plugins loaded",
606                        self.length) % {:count => self.length}
607         else
608           output << n_("%{count} plugin: %{list}",
609                        "%{count} plugins: %{list}", self.length) %
610                    { :count => self.length,
611                      :list => plugins.collect{ |p| p.name}.sort.join(", ") }
612         end
613       else
614         output << "no plugins active"
615       end
616       # Ignored plugins next
617       unless @ignored.empty? or @failures_shown
618         if short
619           output << n_("%{highlight}%{count} plugin ignored%{highlight}",
620                        "%{highlight}%{count} plugins ignored%{highlight}",
621                        @ignored.length) %
622                     { :count => @ignored.length, :highlight => Underline }
623         else
624           output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
625                        "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
626                        @ignored.length) %
627                     { :count => @ignored.length, :highlight => Underline,
628                       :bold => Bold, :command => "help ignored plugins"}
629         end
630       end
631       # Failed plugins next
632       unless @failed.empty? or @failures_shown
633         if short
634           output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
635                        "%{highlight}%{count} plugins failed to load%{highlight}",
636                        @failed.length) %
637                     { :count => @failed.length, :highlight => Reverse }
638         else
639           output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
640                        "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
641                        @failed.length) %
642                     { :count => @failed.length, :highlight => Reverse,
643                       :bold => Bold, :command => "help failed plugins"}
644         end
645       end
646       output.join '; '
647     end
648
649     # return list of help topics (plugin names)
650     def helptopics
651       rv = status
652       @failures_shown = true
653       rv
654     end
655
656     def length
657       plugins.length
658     end
659
660     def core_length
661       core_modules.length
662     end
663
664     # return help for +topic+ (call associated plugin's help method)
665     def help(topic="")
666       case topic
667       when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
668         # debug "Failures: #{@failed.inspect}"
669         return _("no plugins failed to load") if @failed.empty?
670         return @failed.collect { |p|
671           _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
672               :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
673               :exception => p[:reason].class, :reason => p[:reason],
674           } + if $1 && !p[:reason].backtrace.empty?
675                 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
676               else
677                 ''
678               end
679         }.join("\n")
680       when /ignored?\s*plugins?/
681         return _('no plugins were ignored') if @ignored.empty?
682
683         tmp = Hash.new
684         @ignored.each do |p|
685           reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
686           ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
687         end
688
689         return tmp.map do |dir, reasons|
690           # FIXME get rid of these string concatenations to make gettext easier
691           s = reasons.map { |r, list|
692             list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
693           }.join('; ')
694           "in #{dir}: #{s}"
695         end.join('; ')
696       when /^(\S+)\s*(.*)$/
697         key = $1
698         params = $2
699
700         # Let's see if we can match a plugin by the given name
701         (core_modules + plugins).each { |p|
702           next unless p.name == key
703           begin
704             return p.help(key, params)
705           rescue Exception => err
706             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
707             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
708           end
709         }
710
711         # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
712         k = key.to_sym
713         if commands.has_key?(k)
714           p = commands[k][:botmodule]
715           begin
716             return p.help(key, params)
717           rescue Exception => err
718             #rescue TimeoutError, StandardError, NameError, SyntaxError => err
719             error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
720           end
721         end
722       end
723       return false
724     end
725
726     # see if each plugin handles +method+, and if so, call it, passing
727     # +message+ as a parameter
728     def delegate(method, *args)
729       # debug "Delegating #{method.inspect}"
730       ret = Array.new
731       if method.match(DEFAULT_DELEGATE_PATTERNS)
732         debug "fast-delegating #{method}"
733         m = method.to_sym
734         debug "no-one to delegate to" unless @delegate_list.has_key?(m)
735         return [] unless @delegate_list.has_key?(m)
736         @delegate_list[m].each { |p|
737           begin
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         }
745       else
746         debug "slow-delegating #{method}"
747         (core_modules + plugins).each { |p|
748           if(p.respond_to? method)
749             begin
750               # debug "#{p.botmodule_class} #{p.name} responds"
751               ret.push p.send(method, *args)
752             rescue Exception => err
753               raise if err.kind_of?(SystemExit)
754               error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
755               raise if err.kind_of?(BDB::Fatal)
756             end
757           end
758         }
759       end
760       return ret
761       # debug "Finished delegating #{method.inspect}"
762     end
763
764     # see if we have a plugin that wants to handle this message, if so, pass
765     # it to the plugin and return true, otherwise false
766     def privmsg(m)
767       # debug "Delegating privmsg #{m.message.inspect} from #{m.source} to #{m.replyto} with pluginkey #{m.plugin.inspect}"
768       return unless m.plugin
769       k = m.plugin.to_sym
770       if commands.has_key?(k)
771         p = commands[k][:botmodule]
772         a = commands[k][:auth]
773         # We check here for things that don't check themselves
774         # (e.g. mapped things)
775         # debug "Checking auth ..."
776         if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
777           # debug "Checking response ..."
778           if p.respond_to?("privmsg")
779             begin
780               # debug "#{p.botmodule_class} #{p.name} responds"
781               p.privmsg(m)
782             rescue Exception => err
783               raise if err.kind_of?(SystemExit)
784               error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
785               raise if err.kind_of?(BDB::Fatal)
786             end
787             # debug "Successfully delegated #{m.message}"
788             return true
789           else
790             # debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
791           end
792         else
793           # debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
794         end
795       end
796       # debug "Finished delegating privmsg with key #{m.plugin.inspect}" + ( pl.empty? ? "" : " to #{pl.values.first[:botmodule].botmodule_class}s" )
797       return false
798       # debug "Finished delegating privmsg with key #{m.plugin.inspect}"
799     end
800   end
801
802   # Returns the only PluginManagerClass instance
803   def Plugins.manager
804     return PluginManagerClass.instance
805   end
806
807 end
808 end
809 end