4 # :title: rbot plugin management
7 require_relative './core/utils/where_is.rb'
11 Config.register Config::ArrayValue.new('plugins.blacklist',
12 :default => [], :wizard => false, :requires_rescan => true,
13 :desc => "Plugins that should not be loaded")
14 Config.register Config::ArrayValue.new('plugins.whitelist',
15 :default => [], :wizard => false, :requires_rescan => true,
16 :desc => "Only whitelisted plugins will be loaded unless the list is empty")
18 require 'rbot/messagemapper'
21 BotModule is the base class for the modules that enhance the rbot
22 functionality. Rather than subclassing BotModule, however, one should
23 subclass either CoreBotModule (reserved for system modules) or Plugin
26 A BotModule interacts with Irc events by defining one or more of the following
27 methods, which get called as appropriate when the corresponding Irc event
30 map(template, options)::
31 map!(template, options)::
32 map is the new, cleaner way to respond to specific message formats without
33 littering your plugin code with regexps, and should be used instead of
34 #register() and #privmsg() (see below) when possible.
36 The difference between map and map! is that map! will not register the new
37 command as an alternative name for the plugin.
41 plugin.map 'pointstats', :action => 'point_stats'
43 # while in the plugin...
44 def point_stats(m, params)
48 # the default action is the first component
51 # attributes can be pulled out of the match string
52 plugin.map 'points for :key'
53 plugin.map 'points :key'
55 # while in the plugin...
58 m.reply 'points for #{item}'
61 # you can setup defaults, to make parameters optional
62 plugin.map 'points :key', :defaults => {:key => 'defaultvalue'}
64 # the default auth check is also against the first component
65 # but that can be changed
66 plugin.map 'pointstats', :auth => 'points'
68 # maps can be restricted to public or private message:
69 plugin.map 'pointstats', :private => false
70 plugin.map 'pointstats', :public => false
72 See MessageMapper#map for more information on the template format and the
76 Called for all messages of any type. To
77 differentiate them, use message.kind_of? It'll be
78 either a PrivMessage, NoticeMessage, KickMessage,
79 QuitMessage, PartMessage, JoinMessage, NickMessage,
82 ctcp_listen(UserMessage)::
83 Called for all messages that contain a CTCP command.
84 Use message.ctcp to get the CTCP command, and
85 message.message to get the parameter string. To reply,
86 use message.ctcp_reply, which sends a private NOTICE
89 message(PrivMessage)::
90 Called for all PRIVMSG. Hook on this method if you
91 need to handle PRIVMSGs regardless of whether they are
92 addressed to the bot or not, and regardless of
94 privmsg(PrivMessage)::
95 Called for a PRIVMSG if the first word matches one
96 the plugin #register()ed for. Use m.plugin to get
97 that word and m.params for the rest of the message,
100 unreplied(PrivMessage)::
101 Called for a PRIVMSG which has not been replied to.
103 notice(NoticeMessage)::
104 Called for all Notices. Please notice that in general
105 should not be replied to.
108 Called when a user (or the bot) is kicked from a
109 channel the bot is in.
111 invite(InviteMessage)::
112 Called when the bot is invited to a channel.
115 Called when a user (or the bot) joins a channel
118 Called when a user (or the bot) parts a channel
121 Called when a user (or the bot) quits IRC
124 Called when a user (or the bot) changes Nick
125 modechange(ModeChangeMessage)::
126 Called when a User or Channel mode is changed
127 topic(TopicMessage)::
128 Called when a user (or the bot) changes a channel
131 welcome(WelcomeMessage)::
132 Called when the welcome message is received on
133 joining a server succesfully.
136 Called when the Message Of The Day is fully
137 recevied from the server.
139 connect:: Called when a server is joined successfully, but
140 before autojoin channels are joined (no params)
142 set_language(String)::
143 Called when the user sets a new language
144 whose name is the given String
146 save:: Called when you are required to save your plugin's
147 state, if you maintain data between sessions
149 cleanup:: called before your plugin is "unloaded", prior to a
150 plugin reload or bot quit - close any open
151 files/connections or flush caches here
158 # the plugin registry
159 attr_reader :registry
161 # the message map handler
164 # the directory in which the plugin is located
165 attr_reader :plugin_path
167 # Initialise your bot module. Always call super if you override this method,
168 # as important variables are set up for you:
173 # the botmodule's registry, which can be used to store permanent data
174 # (see Registry::Accessor for additional documentation)
176 # Other instance variables which are defined and should not be overwritten
177 # byt the user, but aren't usually accessed directly, are:
180 # the plugins manager instance
181 # @botmodule_triggers::
182 # an Array of words this plugin #register()ed itself for
184 # the MessageMapper that handles this plugin's maps
187 @manager = Plugins::manager
191 @botmodule_triggers = Array.new
193 @handler = MessageMapper.new(self)
194 @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
196 @manager.add_botmodule(self)
197 @plugin_path = @manager.next_plugin_path
198 if self.respond_to?('set_language')
199 self.set_language(@bot.lang.language)
203 # Changing the value of @priority directly will cause problems,
204 # Please use priority=.
209 # Returns the symbol :BotModule
214 # Method called to flush the registry, thus ensuring that the botmodule's permanent
215 # data is committed to disk
218 # debug "Flushing #{@registry}"
223 # Method called to cleanup before the plugin is unloaded. If you overload
224 # this method to handle additional cleanup tasks, remember to call super()
225 # so that the default cleanup actions are taken care of as well.
228 # debug "Closing #{@registry}"
232 # Handle an Irc::PrivMessage for which this BotModule has a map. The method
233 # is called automatically and there is usually no need to call it
240 # Signal to other BotModules that an even happened.
242 def call_event(ev, *args)
243 @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
246 # call-seq: map(template, options)
248 # This is the preferred way to register the BotModule so that it
249 # responds to appropriately-formed messages on Irc.
255 # call-seq: map!(template, options)
257 # This is the same as map but doesn't register the new command
258 # as an alternative name for the plugin.
264 # Auxiliary method called by #map and #map!
265 def do_map(silent, *args)
266 @handler.map(self, *args)
270 self.register name, :auth => nil, :hidden => silent
271 @manager.register_map(self, map)
272 unless self.respond_to?('privmsg')
273 def self.privmsg(m) #:nodoc:
279 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
280 # usually _chan_ is either "*" for everywhere, public and private (in which
281 # case it can be omitted) or "?" for private communications
283 def default_auth(cmd, val, chan="*")
290 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
293 # Gets the default command path which would be given to command _cmd_
294 def propose_default_path(cmd)
295 [name, cmd].compact.join("::")
298 # Return an identifier for this plugin, defaults to a list of the message
299 # prefixes handled (used for error messages etc)
301 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
314 # Return a help string for your module. For complex modules, you may wish
315 # to break your help into topics, and return a list of available topics if
316 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
317 # this message - if your plugin handles multiple prefixes, make sure you
318 # return the correct help for the prefix requested
319 def help(plugin, topic)
323 # Register the plugin as a handler for messages prefixed _cmd_.
325 # This can be called multiple times for a plugin to handle multiple message
328 # This command is now superceded by the #map() command, which should be used
329 # instead whenever possible.
331 def register(cmd, opts={})
332 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
333 who = @manager.who_handles?(cmd)
335 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
338 if opts.has_key?(:auth)
339 @manager.register(self, cmd, opts[:auth])
341 @manager.register(self, cmd, propose_default_path(cmd))
343 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
346 # Default usage method provided as a utility for simple plugins. The
347 # MessageMapper uses 'usage' as its default fallback method.
349 def usage(m, params = {})
350 if params[:failures].respond_to? :find
351 friendly = params[:failures].find do |f|
352 f.kind_of? MessageMapper::FriendlyFailure
355 m.reply friendly.friendly
359 m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
362 # Define the priority of the module. During event delegation, lower
363 # priority modules will be called first. Default priority is 1
367 @bot.plugins.mark_priorities_dirty
371 # Directory name to be joined to the botclass to access data files. By
372 # default this is the plugin name itself, but may be overridden, for
373 # example by plugins that share their datafiles or for backwards
379 # Filename for a datafile built joining the botclass, plugin dirname and
382 @bot.path dirname, *fname
386 # A CoreBotModule is a BotModule that provides core functionality.
388 # This class should not be used by user plugins, as it's reserved for system
389 # plugins such as the ones that handle authentication, configuration and basic
392 class CoreBotModule < BotModule
398 # A Plugin is a BotModule that provides additional functionality.
400 # A user-defined plugin should subclass this, and then define any of the
401 # methods described in the documentation for BotModule to handle interaction
404 class Plugin < BotModule
410 # Singleton to manage multiple plugins and delegate messages to them for
412 class PluginManagerClass
415 attr_reader :botmodules
418 attr_reader :core_module_dirs
419 attr_reader :plugin_dirs
420 attr_reader :next_plugin_path
422 # This is the list of patterns commonly delegated to plugins.
423 # A fast delegation lookup is enabled for them.
424 DEFAULT_DELEGATE_PATTERNS = %r{^(?:
426 listen|ctcp_listen|privmsg|unreplied|
428 save|cleanup|flush_registry|
434 :CoreBotModule => [],
438 @names_hash = Hash.new
439 @commandmappers = Hash.new
442 # modules will be sorted on first delegate call
443 @sorted_modules = nil
445 @delegate_list = Hash.new { |h, k|
449 @core_module_dirs = []
459 ret = self.to_s[0..-2]
460 ret << ' corebotmodules='
461 ret << @botmodules[:CoreBotModule].map { |m|
465 ret << @botmodules[:Plugin].map { |m|
471 # Reset lists of botmodules
474 # optional instance of a botmodule to remove from the lists
475 def reset_botmodule_lists(botmodule=nil)
477 # deletes only references of the botmodule
478 @botmodules[:CoreBotModule].delete botmodule
479 @botmodules[:Plugin].delete botmodule
480 @names_hash.delete_if {|key, value| value == botmodule}
481 @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
482 @delegate_list.each_pair { |cmd, list|
483 list.delete botmodule
485 @delegate_list.delete_if {|key, value| value.empty?}
486 @maps.delete_if {|key, value| value[:botmodule] == botmodule }
487 @failures_shown = false
489 @botmodules[:CoreBotModule].clear
490 @botmodules[:Plugin].clear
492 @commandmappers.clear
495 @failures_shown = false
497 mark_priorities_dirty
500 # Associate with bot _bot_
501 def bot_associate(bot)
502 reset_botmodule_lists
506 # Returns the botmodule with the given _name_
509 @names_hash[name.to_sym]
512 # Returns +true+ if a botmodule named _name_ exists.
515 @names_hash.has_key?(name.to_sym)
518 # Returns +true+ if _cmd_ has already been registered as a command
519 def who_handles?(cmd)
520 return nil unless @commandmappers.has_key?(cmd.to_sym)
521 return @commandmappers[cmd.to_sym][:botmodule]
524 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
525 def register(botmodule, cmd, auth_path)
526 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
527 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
530 # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
531 # which has three keys:
533 # botmodule:: the associated botmodule
534 # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
535 # map:: the actual MessageTemplate object
538 def register_map(botmodule, map)
539 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
540 @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
543 def add_botmodule(botmodule)
544 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
545 kl = botmodule.botmodule_class
546 if @names_hash.has_key?(botmodule.to_sym)
547 case self[botmodule].botmodule_class
549 raise "#{kl} #{botmodule} already registered!"
551 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
554 @botmodules[kl] << botmodule
555 @names_hash[botmodule.to_sym] = botmodule
556 # add itself to the delegate list for the fast-delegation
557 # of methods like cleanup or privmsg, etc..
558 botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
559 @delegate_list[m.intern] << botmodule
561 mark_priorities_dirty
564 # Returns an array of the loaded plugins
566 @botmodules[:CoreBotModule]
569 # Returns an array of the loaded plugins
574 # Returns a hash of the registered message prefixes and associated
580 # Tells the PluginManager that the next time it delegates an event, it
581 # should sort the modules by priority
582 def mark_priorities_dirty
583 @sorted_modules = nil
586 # Makes a string of error _err_ by adding text _str_
587 def report_error(str, err)
588 ([str, err.inspect] + err.backtrace).join("\n")
592 plugins.find { |plugin| plugin.name == name }
595 # This method is the one that actually loads a module from the
598 # _desc_ is a simple description of what we are loading
599 # (plugin/botmodule/whatever) for error reporting
601 # It returns the Symbol :loaded on success, and an Exception
604 def load_botmodule_file(fname, desc=nil)
605 # create a new, anonymous module to "house" the plugin
606 # the idea here is to prevent namespace pollution. perhaps there
608 plugin_module = Module.new
610 # each plugin uses its own textdomain, we bind it automatically here
611 bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
613 desc = desc.to_s + " " if desc
616 plugin_string = IO.read(fname)
617 debug "loading #{desc}#{fname}"
619 # set path of the plugin that will be loaded next (see BotModule#initialize)
620 @next_plugin_path = File.dirname fname
622 plugin_module.module_eval(plugin_string, fname)
624 @next_plugin_path = nil
627 rescue Exception => err
628 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
629 error report_error("#{desc}#{fname} load failed", err)
630 bt = err.backtrace.select { |line|
631 line.match(/^(\(eval\)|#{fname}):\d+/)
634 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
638 msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
641 msg.gsub!(fname, File.basename(fname))
643 newerr = err.class.new(msg)
644 rescue ArgumentError => aerr_in_err
645 # Somebody should hang the ActiveSupport developers by their balls
646 # with barbed wire. Their MissingSourceFile extension to LoadError
647 # _expects_ a second argument, breaking the usual Exception interface
648 # (instead, the smart thing to do would have been to make the second
649 # parameter optional and run the code in the from_message method if
651 # Anyway, we try to cope with this in the simplest possible way. On
652 # the upside, this new block can be extended to handle other similar
654 if err.class.respond_to? :from_message
655 newerr = err.class.from_message(msg)
656 elsif ([:file, :line, :column, :offset, :problem, :context] & err.methods).length == 6
657 # Another ‘brillian’ overload, this time from Psych::SyntaxError
658 # In this case we'll just leave the message as-is
663 rescue NoMethodError => nmerr_in_err
664 # Another braindead extension to StandardError, OAuth2::Error,
665 # doesn't get a string as message, but a response
666 if err.respond_to? :response
667 newerr = err.class.new(err.response)
672 newerr.set_backtrace(bt)
677 # add one or more directories to the list of directories to
678 # load core modules from
679 def add_core_module_dir(*dirlist)
680 @core_module_dirs += dirlist
681 debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
684 # add one or more directories to the list of directories to
686 def add_plugin_dir(*dirlist)
687 @plugin_dirs += dirlist
688 debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
691 def clear_botmodule_dirs
692 @core_module_dirs.clear
694 debug "Core module and plugin loading paths cleared"
697 def scan_botmodules(opts={})
703 dirs = @core_module_dirs
707 @bot.config['plugins.blacklist'].each { |p|
709 processed[pn.intern] = :blacklisted
712 whitelist = @bot.config['plugins.whitelist'].map { |p|
718 next unless FileTest.directory?(dir)
720 d.sort.each do |file|
721 next unless file =~ /\.rb$/
722 next if file =~ /^\./
726 if !whitelist.empty? && !whitelist.include?(file)
727 @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
729 elsif processed.has_key?(file.intern)
730 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
734 if(file =~ /^(.+\.rb)\.disabled$/)
735 # GB: Do we want to do this? This means that a disabled plugin in a directory
736 # will disable in all subsequent directories. This was probably meant
737 # to be used before plugins.blacklist was implemented, so I think
738 # we don't need this anymore
739 processed[$1.intern] = :disabled
740 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
746 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
747 rescue Exception => e
754 processed[file.intern] = did_it
756 @failed << { :name => file, :dir => dir, :reason => did_it }
762 # load plugins from pre-assigned list of directories
768 scan_botmodules(:type => :core)
769 scan_botmodules(:type => :plugins)
771 debug "finished loading plugins: #{status(true)}"
772 mark_priorities_dirty
775 # call the save method for each active plugin
778 # optional instance of a botmodule to save
779 def save(botmodule=nil)
781 botmodule.flush_registry
782 botmodule.save if botmodule.respond_to? 'save'
784 delegate 'flush_registry'
789 # call the cleanup method for each active plugin
792 # optional instance of a botmodule to cleanup
793 def cleanup(botmodule=nil)
799 reset_botmodule_lists(botmodule)
802 # drops botmodules and rescan botmodules on disk
803 # calls save and cleanup for each botmodule before dropping them
804 # a optional _botmodule_ argument might specify a botmodule
805 # instance that should be reloaded
808 # instance of the botmodule to rescan
809 def rescan(botmodule=nil)
815 filename = where_is(botmodule.class)
816 err = load_botmodule_file(filename, "plugin")
817 if err.is_a? Exception
818 @failed << { :name => botmodule.to_s,
819 :dir => File.dirname(filename), :reason => err }
826 def status(short=false)
828 if self.core_length > 0
830 output << n_("%{count} core module loaded", "%{count} core modules loaded",
831 self.core_length) % {:count => self.core_length}
833 output << n_("%{count} core module: %{list}",
834 "%{count} core modules: %{list}", self.core_length) %
835 { :count => self.core_length,
836 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
839 output << _("no core botmodules loaded")
841 # Active plugins first
844 output << n_("%{count} plugin loaded", "%{count} plugins loaded",
845 self.length) % {:count => self.length}
847 output << n_("%{count} plugin: %{list}",
848 "%{count} plugins: %{list}", self.length) %
849 { :count => self.length,
850 :list => plugins.collect{ |p| p.name}.sort.join(", ") }
853 output << "no plugins active"
855 # Ignored plugins next
856 unless @ignored.empty? or @failures_shown
858 output << n_("%{highlight}%{count} plugin ignored%{highlight}",
859 "%{highlight}%{count} plugins ignored%{highlight}",
861 { :count => @ignored.length, :highlight => Underline }
863 output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
864 "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
866 { :count => @ignored.length, :highlight => Underline,
867 :bold => Bold, :command => "help ignored plugins"}
870 # Failed plugins next
871 unless @failed.empty? or @failures_shown
873 output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
874 "%{highlight}%{count} plugins failed to load%{highlight}",
876 { :count => @failed.length, :highlight => Reverse }
878 output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
879 "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
881 { :count => @failed.length, :highlight => Reverse,
882 :bold => Bold, :command => "help failed plugins"}
888 # returns the last logged failure (if present) of a botmodule
891 # name of the botmodule
892 def botmodule_failure(name)
893 failure = @failed.find { |f| f[:name] == name }
895 "%{exception}: %{reason}" % {
896 :exception => failure[:reason].class,
897 :reason => failure[:reason]
902 # return list of help topics (plugin names)
905 @failures_shown = true
917 # return help for +topic+ (call associated plugin's help method)
920 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
921 # debug "Failures: #{@failed.inspect}"
922 return _("no plugins failed to load") if @failed.empty?
923 return @failed.collect { |p|
924 _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
925 :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
926 :exception => p[:reason].class, :reason => p[:reason],
927 } + if $1 && !p[:reason].backtrace.empty?
928 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
933 when /ignored?\s*plugins?/
934 return _('no plugins were ignored') if @ignored.empty?
938 reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
939 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
942 return tmp.map do |dir, reasons|
943 # FIXME get rid of these string concatenations to make gettext easier
944 s = reasons.map { |r, list|
945 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
949 when /^(\S+)\s*(.*)$/
953 # Let's see if we can match a plugin by the given name
954 (core_modules + plugins).each { |p|
955 next unless p.name == key
957 return p.help(key, params)
958 rescue Exception => err
959 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
960 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
964 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
966 if commands.has_key?(k)
967 p = commands[k][:botmodule]
969 return p.help(key, params)
970 rescue Exception => err
971 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
972 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
980 @sorted_modules = (core_modules + plugins).sort do |a, b|
981 a.priority <=> b.priority
984 @delegate_list.each_value do |list|
985 list.sort! {|a,b| a.priority <=> b.priority}
989 # delegate(method, [m,] opts={})
991 # see if each plugin handles _method_, and if so, call it, passing
992 # _m_ as a parameter (if present). BotModules are called in order of
993 # priority from lowest to highest.
995 # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
996 # will only be delegated to plugins with negative priority. Conversely, if
997 # it's a fake message (see BotModule#fake_message), it will only be
998 # delegated to plugins with positive priority.
1000 # Note that _m_ can also be an exploded Array, but in this case the last
1001 # element of it cannot be a Hash, or it will be interpreted as the options
1002 # Hash for delegate itself. The last element can be a subclass of a Hash, though.
1003 # To be on the safe side, you can add an empty Hash as last parameter for delegate
1004 # when calling it with an exploded Array:
1005 # @bot.plugins.delegate(method, *(args.push Hash.new))
1007 # Currently supported options are the following:
1009 # if specified, the delegation will only consider plugins with a priority
1010 # higher than the specified value
1012 # if specified, the delegation will only consider plugins with a priority
1013 # lower than the specified value
1015 def delegate(method, *args)
1016 # if the priorities order of the delegate list is dirty,
1017 # meaning some modules have been added or priorities have been
1018 # changed, then the delegate list will need to be sorted before
1019 # delegation. This should always be true for the first delegation.
1020 sort_modules unless @sorted_modules
1023 opts.merge!(args.pop) if args.last.class == Hash
1026 if BasicUserMessage === m
1027 # ignored messages should not be delegated
1028 # to plugins with positive priority
1029 opts[:below] ||= 0 if m.ignored?
1030 # fake messages should not be delegated
1031 # to plugins with negative priority
1032 opts[:above] ||= 0 if m.recurse_depth > 0
1035 above = opts[:above]
1036 below = opts[:below]
1038 # debug "Delegating #{method.inspect}"
1040 if method.match(DEFAULT_DELEGATE_PATTERNS)
1041 debug "fast-delegating #{method}"
1043 debug "no-one to delegate to" unless @delegate_list.has_key?(m)
1044 return [] unless @delegate_list.has_key?(m)
1045 @delegate_list[m].each { |p|
1048 unless (above and above >= prio) or (below and below <= prio)
1049 ret.push p.send(method, *args)
1051 rescue Exception => err
1052 raise if err.kind_of?(SystemExit)
1053 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1057 debug "slow-delegating #{method}"
1058 @sorted_modules.each { |p|
1059 if(p.respond_to? method)
1061 # debug "#{p.botmodule_class} #{p.name} responds"
1063 unless (above and above >= prio) or (below and below <= prio)
1064 ret.push p.send(method, *args)
1066 rescue Exception => err
1067 raise if err.kind_of?(SystemExit)
1068 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1074 # debug "Finished delegating #{method.inspect}"
1077 # see if we have a plugin that wants to handle this message, if so, pass
1078 # it to the plugin and return true, otherwise false
1080 debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
1081 return unless m.plugin
1083 if commands.has_key?(k)
1084 p = commands[k][:botmodule]
1085 a = commands[k][:auth]
1086 # We check here for things that don't check themselves
1087 # (e.g. mapped things)
1088 debug "Checking auth ..."
1089 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
1090 debug "Checking response ..."
1091 if p.respond_to?("privmsg")
1093 debug "#{p.botmodule_class} #{p.name} responds"
1095 rescue Exception => err
1096 raise if err.kind_of?(SystemExit)
1097 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1099 debug "Successfully delegated #{m.inspect}"
1102 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1105 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1108 debug "Command #{k} isn't handled"
1113 # delegate IRC messages, by delegating 'listen' first, and the actual method
1114 # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1116 def irc_delegate(method, m)
1117 delegate('listen', m)
1118 if method.to_sym == :privmsg
1119 delegate('ctcp_listen', m) if m.ctcp
1120 delegate('message', m)
1121 privmsg(m) if m.address? and not m.ignored?
1122 delegate('unreplied', m) unless m.replied
1129 # Returns the only PluginManagerClass instance
1131 return PluginManagerClass.instance