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