4 # :title: rbot plugin management
10 Config.register Config::ArrayValue.new('plugins.blacklist',
11 :default => [], :wizard => false, :requires_rescan => true,
12 :desc => "Plugins that should not be loaded")
13 Config.register Config::ArrayValue.new('plugins.whitelist',
14 :default => [], :wizard => false, :requires_rescan => true,
15 :desc => "Only whitelisted plugins will be loaded unless the list is empty")
17 require 'rbot/messagemapper'
20 BotModule is the base class for the modules that enhance the rbot
21 functionality. Rather than subclassing BotModule, however, one should
22 subclass either CoreBotModule (reserved for system modules) or Plugin
25 A BotModule interacts with Irc events by defining one or more of the following
26 methods, which get called as appropriate when the corresponding Irc event
29 map(template, options)::
30 map!(template, options)::
31 map is the new, cleaner way to respond to specific message formats without
32 littering your plugin code with regexps, and should be used instead of
33 #register() and #privmsg() (see below) when possible.
35 The difference between map and map! is that map! will not register the new
36 command as an alternative name for the plugin.
40 plugin.map 'karmastats', :action => 'karma_stats'
42 # while in the plugin...
43 def karma_stats(m, params)
47 # the default action is the first component
50 # attributes can be pulled out of the match string
51 plugin.map 'karma for :key'
52 plugin.map 'karma :key'
54 # while in the plugin...
57 m.reply 'karma for #{item}'
60 # you can setup defaults, to make parameters optional
61 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
63 # the default auth check is also against the first component
64 # but that can be changed
65 plugin.map 'karmastats', :auth => 'karma'
67 # maps can be restricted to public or private message:
68 plugin.map 'karmastats', :private => false
69 plugin.map 'karmastats', :public => false
71 See MessageMapper#map for more information on the template format and the
75 Called for all messages of any type. To
76 differentiate them, use message.kind_of? It'll be
77 either a PrivMessage, NoticeMessage, KickMessage,
78 QuitMessage, PartMessage, JoinMessage, NickMessage,
81 ctcp_listen(UserMessage)::
82 Called for all messages that contain a CTCP command.
83 Use message.ctcp to get the CTCP command, and
84 message.message to get the parameter string. To reply,
85 use message.ctcp_reply, which sends a private NOTICE
88 message(PrivMessage)::
89 Called for all PRIVMSG. Hook on this method if you
90 need to handle PRIVMSGs regardless of whether they are
91 addressed to the bot or not, and regardless of
93 privmsg(PrivMessage)::
94 Called for a PRIVMSG if the first word matches one
95 the plugin #register()ed for. Use m.plugin to get
96 that word and m.params for the rest of the message,
99 unreplied(PrivMessage)::
100 Called for a PRIVMSG which has not been replied to.
102 notice(NoticeMessage)::
103 Called for all Notices. Please notice that in general
104 should not be replied to.
107 Called when a user (or the bot) is kicked from a
108 channel the bot is in.
110 invite(InviteMessage)::
111 Called when the bot is invited to a channel.
114 Called when a user (or the bot) joins a channel
117 Called when a user (or the bot) parts a channel
120 Called when a user (or the bot) quits IRC
123 Called when a user (or the bot) changes Nick
124 modechange(ModeChangeMessage)::
125 Called when a User or Channel mode is changed
126 topic(TopicMessage)::
127 Called when a user (or the bot) changes a channel
130 welcome(WelcomeMessage)::
131 Called when the welcome message is received on
132 joining a server succesfully.
135 Called when the Message Of The Day is fully
136 recevied from the server.
138 connect:: Called when a server is joined successfully, but
139 before autojoin channels are joined (no params)
141 set_language(String)::
142 Called when the user sets a new language
143 whose name is the given String
145 save:: Called when you are required to save your plugin's
146 state, if you maintain data between sessions
148 cleanup:: called before your plugin is "unloaded", prior to a
149 plugin reload or bot quit - close any open
150 files/connections or flush caches here
157 # the plugin registry
158 attr_reader :registry
160 # the message map handler
163 # Initialise your bot module. Always call super if you override this method,
164 # as important variables are set up for you:
169 # the botmodule's registry, which can be used to store permanent data
170 # (see Registry::Accessor for additional documentation)
172 # Other instance variables which are defined and should not be overwritten
173 # byt the user, but aren't usually accessed directly, are:
176 # the plugins manager instance
177 # @botmodule_triggers::
178 # an Array of words this plugin #register()ed itself for
180 # the MessageMapper that handles this plugin's maps
183 @manager = Plugins::manager
187 @botmodule_triggers = Array.new
189 @handler = MessageMapper.new(self)
190 @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
192 @manager.add_botmodule(self)
193 if self.respond_to?('set_language')
194 self.set_language(@bot.lang.language)
198 # Changing the value of @priority directly will cause problems,
199 # Please use priority=.
204 # Returns the symbol :BotModule
209 # Method called to flush the registry, thus ensuring that the botmodule's permanent
210 # data is committed to disk
213 # debug "Flushing #{@registry}"
217 # Method called to cleanup before the plugin is unloaded. If you overload
218 # this method to handle additional cleanup tasks, remember to call super()
219 # so that the default cleanup actions are taken care of as well.
222 # debug "Closing #{@registry}"
226 # Handle an Irc::PrivMessage for which this BotModule has a map. The method
227 # is called automatically and there is usually no need to call it
234 # Signal to other BotModules that an even happened.
236 def call_event(ev, *args)
237 @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
240 # call-seq: map(template, options)
242 # This is the preferred way to register the BotModule so that it
243 # responds to appropriately-formed messages on Irc.
249 # call-seq: map!(template, options)
251 # This is the same as map but doesn't register the new command
252 # as an alternative name for the plugin.
258 # Auxiliary method called by #map and #map!
259 def do_map(silent, *args)
260 @handler.map(self, *args)
264 self.register name, :auth => nil, :hidden => silent
265 @manager.register_map(self, map)
266 unless self.respond_to?('privmsg')
267 def self.privmsg(m) #:nodoc:
273 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
274 # usually _chan_ is either "*" for everywhere, public and private (in which
275 # case it can be omitted) or "?" for private communications
277 def default_auth(cmd, val, chan="*")
284 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
287 # Gets the default command path which would be given to command _cmd_
288 def propose_default_path(cmd)
289 [name, cmd].compact.join("::")
292 # Return an identifier for this plugin, defaults to a list of the message
293 # prefixes handled (used for error messages etc)
295 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
308 # Return a help string for your module. For complex modules, you may wish
309 # to break your help into topics, and return a list of available topics if
310 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
311 # this message - if your plugin handles multiple prefixes, make sure you
312 # return the correct help for the prefix requested
313 def help(plugin, topic)
317 # Register the plugin as a handler for messages prefixed _cmd_.
319 # This can be called multiple times for a plugin to handle multiple message
322 # This command is now superceded by the #map() command, which should be used
323 # instead whenever possible.
325 def register(cmd, opts={})
326 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
327 who = @manager.who_handles?(cmd)
329 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
332 if opts.has_key?(:auth)
333 @manager.register(self, cmd, opts[:auth])
335 @manager.register(self, cmd, propose_default_path(cmd))
337 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
340 # Default usage method provided as a utility for simple plugins. The
341 # MessageMapper uses 'usage' as its default fallback method.
343 def usage(m, params = {})
344 if params[:failures].respond_to? :find
345 friendly = params[:failures].find do |f|
346 f.kind_of? MessageMapper::FriendlyFailure
349 m.reply friendly.friendly
353 m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
356 # Define the priority of the module. During event delegation, lower
357 # priority modules will be called first. Default priority is 1
361 @bot.plugins.mark_priorities_dirty
365 # Directory name to be joined to the botclass to access data files. By
366 # default this is the plugin name itself, but may be overridden, for
367 # example by plugins that share their datafiles or for backwards
373 # Filename for a datafile built joining the botclass, plugin dirname and
376 @bot.path dirname, *fname
380 # A CoreBotModule is a BotModule that provides core functionality.
382 # This class should not be used by user plugins, as it's reserved for system
383 # plugins such as the ones that handle authentication, configuration and basic
386 class CoreBotModule < BotModule
392 # A Plugin is a BotModule that provides additional functionality.
394 # A user-defined plugin should subclass this, and then define any of the
395 # methods described in the documentation for BotModule to handle interaction
398 class Plugin < BotModule
404 # Singleton to manage multiple plugins and delegate messages to them for
406 class PluginManagerClass
409 attr_reader :botmodules
412 # This is the list of patterns commonly delegated to plugins.
413 # A fast delegation lookup is enabled for them.
414 DEFAULT_DELEGATE_PATTERNS = %r{^(?:
416 listen|ctcp_listen|privmsg|unreplied|
418 save|cleanup|flush_registry|
424 :CoreBotModule => [],
428 @names_hash = Hash.new
429 @commandmappers = Hash.new
432 # modules will be sorted on first delegate call
433 @sorted_modules = nil
435 @delegate_list = Hash.new { |h, k|
439 @core_module_dirs = []
449 ret = self.to_s[0..-2]
450 ret << ' corebotmodules='
451 ret << @botmodules[:CoreBotModule].map { |m|
455 ret << @botmodules[:Plugin].map { |m|
461 # Reset lists of botmodules
464 # optional instance of a botmodule to remove from the lists
465 def reset_botmodule_lists(botmodule=nil)
467 # deletes only references of the botmodule
468 @botmodules[:CoreBotModule].delete botmodule
469 @botmodules[:Plugin].delete botmodule
470 @names_hash.delete_if {|key, value| value == botmodule}
471 @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
472 @delegate_list.each_pair { |cmd, list|
473 list.delete botmodule
475 @delegate_list.delete_if {|key, value| value.empty?}
476 @maps.delete_if {|key, value| value[:botmodule] == botmodule }
477 @failures_shown = false
479 @botmodules[:CoreBotModule].clear
480 @botmodules[:Plugin].clear
482 @commandmappers.clear
485 @failures_shown = false
487 mark_priorities_dirty
490 # Associate with bot _bot_
491 def bot_associate(bot)
492 reset_botmodule_lists
496 # Returns the botmodule with the given _name_
499 @names_hash[name.to_sym]
502 # Returns +true+ if a botmodule named _name_ exists.
505 @names_hash.has_key?(name.to_sym)
508 # Returns +true+ if _cmd_ has already been registered as a command
509 def who_handles?(cmd)
510 return nil unless @commandmappers.has_key?(cmd.to_sym)
511 return @commandmappers[cmd.to_sym][:botmodule]
514 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
515 def register(botmodule, cmd, auth_path)
516 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
517 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
520 # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
521 # which has three keys:
523 # botmodule:: the associated botmodule
524 # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
525 # map:: the actual MessageTemplate object
528 def register_map(botmodule, map)
529 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
530 @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
533 def add_botmodule(botmodule)
534 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
535 kl = botmodule.botmodule_class
536 if @names_hash.has_key?(botmodule.to_sym)
537 case self[botmodule].botmodule_class
539 raise "#{kl} #{botmodule} already registered!"
541 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
544 @botmodules[kl] << botmodule
545 @names_hash[botmodule.to_sym] = botmodule
546 # add itself to the delegate list for the fast-delegation
547 # of methods like cleanup or privmsg, etc..
548 botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
549 @delegate_list[m.intern] << botmodule
551 mark_priorities_dirty
554 # Returns an array of the loaded plugins
556 @botmodules[:CoreBotModule]
559 # Returns an array of the loaded plugins
564 # Returns a hash of the registered message prefixes and associated
570 # Tells the PluginManager that the next time it delegates an event, it
571 # should sort the modules by priority
572 def mark_priorities_dirty
573 @sorted_modules = nil
576 # Makes a string of error _err_ by adding text _str_
577 def report_error(str, err)
578 ([str, err.inspect] + err.backtrace).join("\n")
581 # This method is the one that actually loads a module from the
584 # _desc_ is a simple description of what we are loading
585 # (plugin/botmodule/whatever) for error reporting
587 # It returns the Symbol :loaded on success, and an Exception
590 def load_botmodule_file(fname, desc=nil)
591 # create a new, anonymous module to "house" the plugin
592 # the idea here is to prevent namespace pollution. perhaps there
594 plugin_module = Module.new
596 # each plugin uses its own textdomain, we bind it automatically here
597 bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
599 desc = desc.to_s + " " if desc
602 plugin_string = IO.read(fname)
603 debug "loading #{desc}#{fname}"
604 plugin_module.module_eval(plugin_string, fname)
606 # this sets a BOTMODULE_FNAME constant in all BotModule
607 # classes defined in the module. This allows us to know
608 # the filename the plugin was declared in from outside
609 # the plugin itself (from within, a __FILE__ would work.)
610 plugin_module.constants.each do |const|
611 cls = plugin_module.const_get(const)
612 if cls.is_a? Class and cls < BotModule
613 cls.const_set("BOTMODULE_FNAME", fname)
618 rescue Exception => err
619 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
620 error report_error("#{desc}#{fname} load failed", err)
621 bt = err.backtrace.select { |line|
622 line.match(/^(\(eval\)|#{fname}):\d+/)
625 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
629 msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
632 msg.gsub!(fname, File.basename(fname))
634 newerr = err.class.new(msg)
635 rescue ArgumentError => aerr_in_err
636 # Somebody should hang the ActiveSupport developers by their balls
637 # with barbed wire. Their MissingSourceFile extension to LoadError
638 # _expects_ a second argument, breaking the usual Exception interface
639 # (instead, the smart thing to do would have been to make the second
640 # parameter optional and run the code in the from_message method if
642 # Anyway, we try to cope with this in the simplest possible way. On
643 # the upside, this new block can be extended to handle other similar
645 if err.class.respond_to? :from_message
646 newerr = err.class.from_message(msg)
650 rescue NoMethodError => nmerr_in_err
651 # Another braindead extension to StandardError, OAuth2::Error,
652 # doesn't get a string as message, but a response
653 if err.respond_to? :response
654 newerr = err.class.new(err.response)
659 newerr.set_backtrace(bt)
663 private :load_botmodule_file
665 # add one or more directories to the list of directories to
666 # load core modules from
667 def add_core_module_dir(*dirlist)
668 @core_module_dirs += dirlist
669 debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
672 # add one or more directories to the list of directories to
674 def add_plugin_dir(*dirlist)
675 @plugin_dirs += dirlist
676 debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
679 def clear_botmodule_dirs
680 @core_module_dirs.clear
682 debug "Core module and plugin loading paths cleared"
685 def scan_botmodules(opts={})
691 dirs = @core_module_dirs
695 @bot.config['plugins.blacklist'].each { |p|
697 processed[pn.intern] = :blacklisted
700 whitelist = @bot.config['plugins.whitelist'].map { |p|
706 next unless FileTest.directory?(dir)
708 d.sort.each do |file|
709 next unless file =~ /\.rb$/
710 next if file =~ /^\./
714 if !whitelist.empty? && !whitelist.include?(file)
715 @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
717 elsif processed.has_key?(file.intern)
718 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
722 if(file =~ /^(.+\.rb)\.disabled$/)
723 # GB: Do we want to do this? This means that a disabled plugin in a directory
724 # will disable in all subsequent directories. This was probably meant
725 # to be used before plugins.blacklist was implemented, so I think
726 # we don't need this anymore
727 processed[$1.intern] = :disabled
728 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
734 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
735 rescue Exception => e
742 processed[file.intern] = did_it
744 @failed << { :name => file, :dir => dir, :reason => did_it }
750 # load plugins from pre-assigned list of directories
756 scan_botmodules(:type => :core)
757 scan_botmodules(:type => :plugins)
759 debug "finished loading plugins: #{status(true)}"
760 mark_priorities_dirty
763 # call the save method for each active plugin
766 # optional instance of a botmodule to save
767 def save(botmodule=nil)
769 botmodule.flush_registry
770 botmodule.save if botmodule.respond_to? 'save'
772 delegate 'flush_registry'
777 # call the cleanup method for each active plugin
780 # optional instance of a botmodule to cleanup
781 def cleanup(botmodule=nil)
787 reset_botmodule_lists(botmodule)
790 # drops botmodules and rescan botmodules on disk
791 # calls save and cleanup for each botmodule before dropping them
792 # a optional _botmodule_ argument might specify a botmodule
793 # instance that should be reloaded
796 # instance of the botmodule to rescan
797 def rescan(botmodule=nil)
803 filename = botmodule.class::BOTMODULE_FNAME
804 err = load_botmodule_file(filename, "plugin")
805 if err.is_a? Exception
806 @failed << { :name => botmodule.to_s,
807 :dir => File.dirname(filename), :reason => err }
814 def status(short=false)
816 if self.core_length > 0
818 output << n_("%{count} core module loaded", "%{count} core modules loaded",
819 self.core_length) % {:count => self.core_length}
821 output << n_("%{count} core module: %{list}",
822 "%{count} core modules: %{list}", self.core_length) %
823 { :count => self.core_length,
824 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
827 output << _("no core botmodules loaded")
829 # Active plugins first
832 output << n_("%{count} plugin loaded", "%{count} plugins loaded",
833 self.length) % {:count => self.length}
835 output << n_("%{count} plugin: %{list}",
836 "%{count} plugins: %{list}", self.length) %
837 { :count => self.length,
838 :list => plugins.collect{ |p| p.name}.sort.join(", ") }
841 output << "no plugins active"
843 # Ignored plugins next
844 unless @ignored.empty? or @failures_shown
846 output << n_("%{highlight}%{count} plugin ignored%{highlight}",
847 "%{highlight}%{count} plugins ignored%{highlight}",
849 { :count => @ignored.length, :highlight => Underline }
851 output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
852 "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
854 { :count => @ignored.length, :highlight => Underline,
855 :bold => Bold, :command => "help ignored plugins"}
858 # Failed plugins next
859 unless @failed.empty? or @failures_shown
861 output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
862 "%{highlight}%{count} plugins failed to load%{highlight}",
864 { :count => @failed.length, :highlight => Reverse }
866 output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
867 "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
869 { :count => @failed.length, :highlight => Reverse,
870 :bold => Bold, :command => "help failed plugins"}
876 # returns the last logged failure (if present) of a botmodule
879 # name of the botmodule
880 def botmodule_failure(name)
881 failure = @failed.find { |f| f[:name] == name }
883 "%{exception}: %{reason}" % {
884 :exception => failure[:reason].class,
885 :reason => failure[:reason]
890 # return list of help topics (plugin names)
893 @failures_shown = true
905 # return help for +topic+ (call associated plugin's help method)
908 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
909 # debug "Failures: #{@failed.inspect}"
910 return _("no plugins failed to load") if @failed.empty?
911 return @failed.collect { |p|
912 _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
913 :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
914 :exception => p[:reason].class, :reason => p[:reason],
915 } + if $1 && !p[:reason].backtrace.empty?
916 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
921 when /ignored?\s*plugins?/
922 return _('no plugins were ignored') if @ignored.empty?
926 reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
927 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
930 return tmp.map do |dir, reasons|
931 # FIXME get rid of these string concatenations to make gettext easier
932 s = reasons.map { |r, list|
933 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
937 when /^(\S+)\s*(.*)$/
941 # Let's see if we can match a plugin by the given name
942 (core_modules + plugins).each { |p|
943 next unless p.name == key
945 return p.help(key, params)
946 rescue Exception => err
947 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
948 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
952 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
954 if commands.has_key?(k)
955 p = commands[k][:botmodule]
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)
968 @sorted_modules = (core_modules + plugins).sort do |a, b|
969 a.priority <=> b.priority
972 @delegate_list.each_value do |list|
973 list.sort! {|a,b| a.priority <=> b.priority}
977 # delegate(method, [m,] opts={})
979 # see if each plugin handles _method_, and if so, call it, passing
980 # _m_ as a parameter (if present). BotModules are called in order of
981 # priority from lowest to highest.
983 # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
984 # will only be delegated to plugins with negative priority. Conversely, if
985 # it's a fake message (see BotModule#fake_message), it will only be
986 # delegated to plugins with positive priority.
988 # Note that _m_ can also be an exploded Array, but in this case the last
989 # element of it cannot be a Hash, or it will be interpreted as the options
990 # Hash for delegate itself. The last element can be a subclass of a Hash, though.
991 # To be on the safe side, you can add an empty Hash as last parameter for delegate
992 # when calling it with an exploded Array:
993 # @bot.plugins.delegate(method, *(args.push Hash.new))
995 # Currently supported options are the following:
997 # if specified, the delegation will only consider plugins with a priority
998 # higher than the specified value
1000 # if specified, the delegation will only consider plugins with a priority
1001 # lower than the specified value
1003 def delegate(method, *args)
1004 # if the priorities order of the delegate list is dirty,
1005 # meaning some modules have been added or priorities have been
1006 # changed, then the delegate list will need to be sorted before
1007 # delegation. This should always be true for the first delegation.
1008 sort_modules unless @sorted_modules
1011 opts.merge(args.pop) if args.last.class == Hash
1014 if BasicUserMessage === m
1015 # ignored messages should not be delegated
1016 # to plugins with positive priority
1017 opts[:below] ||= 0 if m.ignored?
1018 # fake messages should not be delegated
1019 # to plugins with negative priority
1020 opts[:above] ||= 0 if m.recurse_depth > 0
1023 above = opts[:above]
1024 below = opts[:below]
1026 # debug "Delegating #{method.inspect}"
1028 if method.match(DEFAULT_DELEGATE_PATTERNS)
1029 debug "fast-delegating #{method}"
1031 debug "no-one to delegate to" unless @delegate_list.has_key?(m)
1032 return [] unless @delegate_list.has_key?(m)
1033 @delegate_list[m].each { |p|
1036 unless (above and above >= prio) or (below and below <= prio)
1037 ret.push p.send(method, *args)
1039 rescue Exception => err
1040 raise if err.kind_of?(SystemExit)
1041 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1045 debug "slow-delegating #{method}"
1046 @sorted_modules.each { |p|
1047 if(p.respond_to? method)
1049 # debug "#{p.botmodule_class} #{p.name} responds"
1051 unless (above and above >= prio) or (below and below <= prio)
1052 ret.push p.send(method, *args)
1054 rescue Exception => err
1055 raise if err.kind_of?(SystemExit)
1056 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1062 # debug "Finished delegating #{method.inspect}"
1065 # see if we have a plugin that wants to handle this message, if so, pass
1066 # it to the plugin and return true, otherwise false
1068 debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
1069 return unless m.plugin
1071 if commands.has_key?(k)
1072 p = commands[k][:botmodule]
1073 a = commands[k][:auth]
1074 # We check here for things that don't check themselves
1075 # (e.g. mapped things)
1076 debug "Checking auth ..."
1077 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
1078 debug "Checking response ..."
1079 if p.respond_to?("privmsg")
1081 debug "#{p.botmodule_class} #{p.name} responds"
1083 rescue Exception => err
1084 raise if err.kind_of?(SystemExit)
1085 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1087 debug "Successfully delegated #{m.inspect}"
1090 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1093 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1096 debug "Command #{k} isn't handled"
1101 # delegate IRC messages, by delegating 'listen' first, and the actual method
1102 # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1104 def irc_delegate(method, m)
1105 delegate('listen', m)
1106 if method.to_sym == :privmsg
1107 delegate('ctcp_listen', m) if m.ctcp
1108 delegate('message', m)
1109 privmsg(m) if m.address? and not m.ignored?
1110 delegate('unreplied', m) unless m.replied
1117 # Returns the only PluginManagerClass instance
1119 return PluginManagerClass.instance