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