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 'karmastats', :action => 'karma_stats'
43 # while in the plugin...
44 def karma_stats(m, params)
48 # the default action is the first component
51 # attributes can be pulled out of the match string
52 plugin.map 'karma for :key'
53 plugin.map 'karma :key'
55 # while in the plugin...
58 m.reply 'karma for #{item}'
61 # you can setup defaults, to make parameters optional
62 plugin.map 'karma :key', :defaults => {:key => 'defaultvalue'}
64 # the default auth check is also against the first component
65 # but that can be changed
66 plugin.map 'karmastats', :auth => 'karma'
68 # maps can be restricted to public or private message:
69 plugin.map 'karmastats', :private => false
70 plugin.map 'karmastats', :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 # Initialise your bot module. Always call super if you override this method,
165 # as important variables are set up for you:
170 # the botmodule's registry, which can be used to store permanent data
171 # (see Registry::Accessor for additional documentation)
173 # Other instance variables which are defined and should not be overwritten
174 # byt the user, but aren't usually accessed directly, are:
177 # the plugins manager instance
178 # @botmodule_triggers::
179 # an Array of words this plugin #register()ed itself for
181 # the MessageMapper that handles this plugin's maps
184 @manager = Plugins::manager
188 @botmodule_triggers = Array.new
190 @handler = MessageMapper.new(self)
191 @registry = @bot.registry_factory.create(@bot.path, self.class.to_s.gsub(/^.*::/, ''))
193 @manager.add_botmodule(self)
194 if self.respond_to?('set_language')
195 self.set_language(@bot.lang.language)
199 # Changing the value of @priority directly will cause problems,
200 # Please use priority=.
205 # Returns the symbol :BotModule
210 # Method called to flush the registry, thus ensuring that the botmodule's permanent
211 # data is committed to disk
214 # debug "Flushing #{@registry}"
218 # Method called to cleanup before the plugin is unloaded. If you overload
219 # this method to handle additional cleanup tasks, remember to call super()
220 # so that the default cleanup actions are taken care of as well.
223 # debug "Closing #{@registry}"
227 # Handle an Irc::PrivMessage for which this BotModule has a map. The method
228 # is called automatically and there is usually no need to call it
235 # Signal to other BotModules that an even happened.
237 def call_event(ev, *args)
238 @bot.plugins.delegate('event_' + ev.to_s.gsub(/[^\w\?!]+/, '_'), *(args.push Hash.new))
241 # call-seq: map(template, options)
243 # This is the preferred way to register the BotModule so that it
244 # responds to appropriately-formed messages on Irc.
250 # call-seq: map!(template, options)
252 # This is the same as map but doesn't register the new command
253 # as an alternative name for the plugin.
259 # Auxiliary method called by #map and #map!
260 def do_map(silent, *args)
261 @handler.map(self, *args)
265 self.register name, :auth => nil, :hidden => silent
266 @manager.register_map(self, map)
267 unless self.respond_to?('privmsg')
268 def self.privmsg(m) #:nodoc:
274 # Sets the default auth for command path _cmd_ to _val_ on channel _chan_:
275 # usually _chan_ is either "*" for everywhere, public and private (in which
276 # case it can be omitted) or "?" for private communications
278 def default_auth(cmd, val, chan="*")
285 Auth::defaultbotuser.set_default_permission(propose_default_path(c), val)
288 # Gets the default command path which would be given to command _cmd_
289 def propose_default_path(cmd)
290 [name, cmd].compact.join("::")
293 # Return an identifier for this plugin, defaults to a list of the message
294 # prefixes handled (used for error messages etc)
296 self.class.to_s.downcase.sub(/^#<module:.*?>::/,"").sub(/(plugin|module)?$/,"")
309 # Return a help string for your module. For complex modules, you may wish
310 # to break your help into topics, and return a list of available topics if
311 # +topic+ is nil. +plugin+ is passed containing the matching prefix for
312 # this message - if your plugin handles multiple prefixes, make sure you
313 # return the correct help for the prefix requested
314 def help(plugin, topic)
318 # Register the plugin as a handler for messages prefixed _cmd_.
320 # This can be called multiple times for a plugin to handle multiple message
323 # This command is now superceded by the #map() command, which should be used
324 # instead whenever possible.
326 def register(cmd, opts={})
327 raise ArgumentError, "Second argument must be a hash!" unless opts.kind_of?(Hash)
328 who = @manager.who_handles?(cmd)
330 raise "Command #{cmd} is already handled by #{who.botmodule_class} #{who}" if who != self
333 if opts.has_key?(:auth)
334 @manager.register(self, cmd, opts[:auth])
336 @manager.register(self, cmd, propose_default_path(cmd))
338 @botmodule_triggers << cmd unless opts.fetch(:hidden, false)
341 # Default usage method provided as a utility for simple plugins. The
342 # MessageMapper uses 'usage' as its default fallback method.
344 def usage(m, params = {})
345 if params[:failures].respond_to? :find
346 friendly = params[:failures].find do |f|
347 f.kind_of? MessageMapper::FriendlyFailure
350 m.reply friendly.friendly
354 m.reply(_("incorrect usage, ask for help using '%{command}'") % {:command => "#{@bot.nick}: help #{m.plugin}"})
357 # Define the priority of the module. During event delegation, lower
358 # priority modules will be called first. Default priority is 1
362 @bot.plugins.mark_priorities_dirty
366 # Directory name to be joined to the botclass to access data files. By
367 # default this is the plugin name itself, but may be overridden, for
368 # example by plugins that share their datafiles or for backwards
374 # Filename for a datafile built joining the botclass, plugin dirname and
377 @bot.path dirname, *fname
381 # A CoreBotModule is a BotModule that provides core functionality.
383 # This class should not be used by user plugins, as it's reserved for system
384 # plugins such as the ones that handle authentication, configuration and basic
387 class CoreBotModule < BotModule
393 # A Plugin is a BotModule that provides additional functionality.
395 # A user-defined plugin should subclass this, and then define any of the
396 # methods described in the documentation for BotModule to handle interaction
399 class Plugin < BotModule
405 # Singleton to manage multiple plugins and delegate messages to them for
407 class PluginManagerClass
410 attr_reader :botmodules
413 # This is the list of patterns commonly delegated to plugins.
414 # A fast delegation lookup is enabled for them.
415 DEFAULT_DELEGATE_PATTERNS = %r{^(?:
417 listen|ctcp_listen|privmsg|unreplied|
419 save|cleanup|flush_registry|
425 :CoreBotModule => [],
429 @names_hash = Hash.new
430 @commandmappers = Hash.new
433 # modules will be sorted on first delegate call
434 @sorted_modules = nil
436 @delegate_list = Hash.new { |h, k|
440 @core_module_dirs = []
450 ret = self.to_s[0..-2]
451 ret << ' corebotmodules='
452 ret << @botmodules[:CoreBotModule].map { |m|
456 ret << @botmodules[:Plugin].map { |m|
462 # Reset lists of botmodules
465 # optional instance of a botmodule to remove from the lists
466 def reset_botmodule_lists(botmodule=nil)
468 # deletes only references of the botmodule
469 @botmodules[:CoreBotModule].delete botmodule
470 @botmodules[:Plugin].delete botmodule
471 @names_hash.delete_if {|key, value| value == botmodule}
472 @commandmappers.delete_if {|key, value| value[:botmodule] == botmodule }
473 @delegate_list.each_pair { |cmd, list|
474 list.delete botmodule
476 @delegate_list.delete_if {|key, value| value.empty?}
477 @maps.delete_if {|key, value| value[:botmodule] == botmodule }
478 @failures_shown = false
480 @botmodules[:CoreBotModule].clear
481 @botmodules[:Plugin].clear
483 @commandmappers.clear
486 @failures_shown = false
488 mark_priorities_dirty
491 # Associate with bot _bot_
492 def bot_associate(bot)
493 reset_botmodule_lists
497 # Returns the botmodule with the given _name_
500 @names_hash[name.to_sym]
503 # Returns +true+ if a botmodule named _name_ exists.
506 @names_hash.has_key?(name.to_sym)
509 # Returns +true+ if _cmd_ has already been registered as a command
510 def who_handles?(cmd)
511 return nil unless @commandmappers.has_key?(cmd.to_sym)
512 return @commandmappers[cmd.to_sym][:botmodule]
515 # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
516 def register(botmodule, cmd, auth_path)
517 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
518 @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
521 # Registers botmodule _botmodule_ with map _map_. This adds the map to the #maps hash
522 # which has three keys:
524 # botmodule:: the associated botmodule
525 # auth:: an array of auth keys checked by the map; the first is the full_auth_path of the map
526 # map:: the actual MessageTemplate object
529 def register_map(botmodule, map)
530 raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
531 @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
534 def add_botmodule(botmodule)
535 raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
536 kl = botmodule.botmodule_class
537 if @names_hash.has_key?(botmodule.to_sym)
538 case self[botmodule].botmodule_class
540 raise "#{kl} #{botmodule} already registered!"
542 raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
545 @botmodules[kl] << botmodule
546 @names_hash[botmodule.to_sym] = botmodule
547 # add itself to the delegate list for the fast-delegation
548 # of methods like cleanup or privmsg, etc..
549 botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
550 @delegate_list[m.intern] << botmodule
552 mark_priorities_dirty
555 # Returns an array of the loaded plugins
557 @botmodules[:CoreBotModule]
560 # Returns an array of the loaded plugins
565 # Returns a hash of the registered message prefixes and associated
571 # Tells the PluginManager that the next time it delegates an event, it
572 # should sort the modules by priority
573 def mark_priorities_dirty
574 @sorted_modules = nil
577 # Makes a string of error _err_ by adding text _str_
578 def report_error(str, err)
579 ([str, err.inspect] + err.backtrace).join("\n")
582 # This method is the one that actually loads a module from the
585 # _desc_ is a simple description of what we are loading
586 # (plugin/botmodule/whatever) for error reporting
588 # It returns the Symbol :loaded on success, and an Exception
591 def load_botmodule_file(fname, desc=nil)
592 # create a new, anonymous module to "house" the plugin
593 # the idea here is to prevent namespace pollution. perhaps there
595 plugin_module = Module.new
597 # each plugin uses its own textdomain, we bind it automatically here
598 bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
600 desc = desc.to_s + " " if desc
603 plugin_string = IO.read(fname)
604 debug "loading #{desc}#{fname}"
605 plugin_module.module_eval(plugin_string, fname)
608 rescue Exception => err
609 # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
610 error report_error("#{desc}#{fname} load failed", err)
611 bt = err.backtrace.select { |line|
612 line.match(/^(\(eval\)|#{fname}):\d+/)
615 el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
619 msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
622 msg.gsub!(fname, File.basename(fname))
624 newerr = err.class.new(msg)
625 rescue ArgumentError => aerr_in_err
626 # Somebody should hang the ActiveSupport developers by their balls
627 # with barbed wire. Their MissingSourceFile extension to LoadError
628 # _expects_ a second argument, breaking the usual Exception interface
629 # (instead, the smart thing to do would have been to make the second
630 # parameter optional and run the code in the from_message method if
632 # Anyway, we try to cope with this in the simplest possible way. On
633 # the upside, this new block can be extended to handle other similar
635 if err.class.respond_to? :from_message
636 newerr = err.class.from_message(msg)
640 rescue NoMethodError => nmerr_in_err
641 # Another braindead extension to StandardError, OAuth2::Error,
642 # doesn't get a string as message, but a response
643 if err.respond_to? :response
644 newerr = err.class.new(err.response)
649 newerr.set_backtrace(bt)
653 private :load_botmodule_file
655 # add one or more directories to the list of directories to
656 # load core modules from
657 def add_core_module_dir(*dirlist)
658 @core_module_dirs += dirlist
659 debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
662 # add one or more directories to the list of directories to
664 def add_plugin_dir(*dirlist)
665 @plugin_dirs += dirlist
666 debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
669 def clear_botmodule_dirs
670 @core_module_dirs.clear
672 debug "Core module and plugin loading paths cleared"
675 def scan_botmodules(opts={})
681 dirs = @core_module_dirs
685 @bot.config['plugins.blacklist'].each { |p|
687 processed[pn.intern] = :blacklisted
690 whitelist = @bot.config['plugins.whitelist'].map { |p|
696 next unless FileTest.directory?(dir)
698 d.sort.each do |file|
699 next unless file =~ /\.rb$/
700 next if file =~ /^\./
704 if !whitelist.empty? && !whitelist.include?(file)
705 @ignored << {:name => file, :dir => dir, :reason => :"not whitelisted" }
707 elsif processed.has_key?(file.intern)
708 @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
712 if(file =~ /^(.+\.rb)\.disabled$/)
713 # GB: Do we want to do this? This means that a disabled plugin in a directory
714 # will disable in all subsequent directories. This was probably meant
715 # to be used before plugins.blacklist was implemented, so I think
716 # we don't need this anymore
717 processed[$1.intern] = :disabled
718 @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
724 did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
725 rescue Exception => e
732 processed[file.intern] = did_it
734 @failed << { :name => file, :dir => dir, :reason => did_it }
740 # load plugins from pre-assigned list of directories
746 scan_botmodules(:type => :core)
747 scan_botmodules(:type => :plugins)
749 debug "finished loading plugins: #{status(true)}"
750 mark_priorities_dirty
753 # call the save method for each active plugin
756 # optional instance of a botmodule to save
757 def save(botmodule=nil)
759 botmodule.flush_registry
760 botmodule.save if botmodule.respond_to? 'save'
762 delegate 'flush_registry'
767 # call the cleanup method for each active plugin
770 # optional instance of a botmodule to cleanup
771 def cleanup(botmodule=nil)
777 reset_botmodule_lists(botmodule)
780 # drops botmodules and rescan botmodules on disk
781 # calls save and cleanup for each botmodule before dropping them
782 # a optional _botmodule_ argument might specify a botmodule
783 # instance that should be reloaded
786 # instance of the botmodule to rescan
787 def rescan(botmodule=nil)
793 filename = where_is(botmodule.class)
794 err = load_botmodule_file(filename, "plugin")
795 if err.is_a? Exception
796 @failed << { :name => botmodule.to_s,
797 :dir => File.dirname(filename), :reason => err }
804 def status(short=false)
806 if self.core_length > 0
808 output << n_("%{count} core module loaded", "%{count} core modules loaded",
809 self.core_length) % {:count => self.core_length}
811 output << n_("%{count} core module: %{list}",
812 "%{count} core modules: %{list}", self.core_length) %
813 { :count => self.core_length,
814 :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
817 output << _("no core botmodules loaded")
819 # Active plugins first
822 output << n_("%{count} plugin loaded", "%{count} plugins loaded",
823 self.length) % {:count => self.length}
825 output << n_("%{count} plugin: %{list}",
826 "%{count} plugins: %{list}", self.length) %
827 { :count => self.length,
828 :list => plugins.collect{ |p| p.name}.sort.join(", ") }
831 output << "no plugins active"
833 # Ignored plugins next
834 unless @ignored.empty? or @failures_shown
836 output << n_("%{highlight}%{count} plugin ignored%{highlight}",
837 "%{highlight}%{count} plugins ignored%{highlight}",
839 { :count => @ignored.length, :highlight => Underline }
841 output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
842 "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
844 { :count => @ignored.length, :highlight => Underline,
845 :bold => Bold, :command => "help ignored plugins"}
848 # Failed plugins next
849 unless @failed.empty? or @failures_shown
851 output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
852 "%{highlight}%{count} plugins failed to load%{highlight}",
854 { :count => @failed.length, :highlight => Reverse }
856 output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
857 "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
859 { :count => @failed.length, :highlight => Reverse,
860 :bold => Bold, :command => "help failed plugins"}
866 # returns the last logged failure (if present) of a botmodule
869 # name of the botmodule
870 def botmodule_failure(name)
871 failure = @failed.find { |f| f[:name] == name }
873 "%{exception}: %{reason}" % {
874 :exception => failure[:reason].class,
875 :reason => failure[:reason]
880 # return list of help topics (plugin names)
883 @failures_shown = true
895 # return help for +topic+ (call associated plugin's help method)
898 when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
899 # debug "Failures: #{@failed.inspect}"
900 return _("no plugins failed to load") if @failed.empty?
901 return @failed.collect { |p|
902 _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
903 :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
904 :exception => p[:reason].class, :reason => p[:reason],
905 } + if $1 && !p[:reason].backtrace.empty?
906 _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
911 when /ignored?\s*plugins?/
912 return _('no plugins were ignored') if @ignored.empty?
916 reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
917 ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
920 return tmp.map do |dir, reasons|
921 # FIXME get rid of these string concatenations to make gettext easier
922 s = reasons.map { |r, list|
923 list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
927 when /^(\S+)\s*(.*)$/
931 # Let's see if we can match a plugin by the given name
932 (core_modules + plugins).each { |p|
933 next unless p.name == key
935 return p.help(key, params)
936 rescue Exception => err
937 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
938 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
942 # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
944 if commands.has_key?(k)
945 p = commands[k][:botmodule]
947 return p.help(key, params)
948 rescue Exception => err
949 #rescue TimeoutError, StandardError, NameError, SyntaxError => err
950 error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
958 @sorted_modules = (core_modules + plugins).sort do |a, b|
959 a.priority <=> b.priority
962 @delegate_list.each_value do |list|
963 list.sort! {|a,b| a.priority <=> b.priority}
967 # delegate(method, [m,] opts={})
969 # see if each plugin handles _method_, and if so, call it, passing
970 # _m_ as a parameter (if present). BotModules are called in order of
971 # priority from lowest to highest.
973 # If the passed _m_ is a BasicUserMessage and is marked as #ignored?, it
974 # will only be delegated to plugins with negative priority. Conversely, if
975 # it's a fake message (see BotModule#fake_message), it will only be
976 # delegated to plugins with positive priority.
978 # Note that _m_ can also be an exploded Array, but in this case the last
979 # element of it cannot be a Hash, or it will be interpreted as the options
980 # Hash for delegate itself. The last element can be a subclass of a Hash, though.
981 # To be on the safe side, you can add an empty Hash as last parameter for delegate
982 # when calling it with an exploded Array:
983 # @bot.plugins.delegate(method, *(args.push Hash.new))
985 # Currently supported options are the following:
987 # if specified, the delegation will only consider plugins with a priority
988 # higher than the specified value
990 # if specified, the delegation will only consider plugins with a priority
991 # lower than the specified value
993 def delegate(method, *args)
994 # if the priorities order of the delegate list is dirty,
995 # meaning some modules have been added or priorities have been
996 # changed, then the delegate list will need to be sorted before
997 # delegation. This should always be true for the first delegation.
998 sort_modules unless @sorted_modules
1001 opts.merge(args.pop) if args.last.class == Hash
1004 if BasicUserMessage === m
1005 # ignored messages should not be delegated
1006 # to plugins with positive priority
1007 opts[:below] ||= 0 if m.ignored?
1008 # fake messages should not be delegated
1009 # to plugins with negative priority
1010 opts[:above] ||= 0 if m.recurse_depth > 0
1013 above = opts[:above]
1014 below = opts[:below]
1016 # debug "Delegating #{method.inspect}"
1018 if method.match(DEFAULT_DELEGATE_PATTERNS)
1019 debug "fast-delegating #{method}"
1021 debug "no-one to delegate to" unless @delegate_list.has_key?(m)
1022 return [] unless @delegate_list.has_key?(m)
1023 @delegate_list[m].each { |p|
1026 unless (above and above >= prio) or (below and below <= prio)
1027 ret.push p.send(method, *args)
1029 rescue Exception => err
1030 raise if err.kind_of?(SystemExit)
1031 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1035 debug "slow-delegating #{method}"
1036 @sorted_modules.each { |p|
1037 if(p.respond_to? method)
1039 # debug "#{p.botmodule_class} #{p.name} responds"
1041 unless (above and above >= prio) or (below and below <= prio)
1042 ret.push p.send(method, *args)
1044 rescue Exception => err
1045 raise if err.kind_of?(SystemExit)
1046 error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
1052 # debug "Finished delegating #{method.inspect}"
1055 # see if we have a plugin that wants to handle this message, if so, pass
1056 # it to the plugin and return true, otherwise false
1058 debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
1059 return unless m.plugin
1061 if commands.has_key?(k)
1062 p = commands[k][:botmodule]
1063 a = commands[k][:auth]
1064 # We check here for things that don't check themselves
1065 # (e.g. mapped things)
1066 debug "Checking auth ..."
1067 if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
1068 debug "Checking response ..."
1069 if p.respond_to?("privmsg")
1071 debug "#{p.botmodule_class} #{p.name} responds"
1073 rescue Exception => err
1074 raise if err.kind_of?(SystemExit)
1075 error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
1077 debug "Successfully delegated #{m.inspect}"
1080 debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
1083 debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
1086 debug "Command #{k} isn't handled"
1091 # delegate IRC messages, by delegating 'listen' first, and the actual method
1092 # afterwards. Delegating 'privmsg' also delegates ctcp_listen and message
1094 def irc_delegate(method, m)
1095 delegate('listen', m)
1096 if method.to_sym == :privmsg
1097 delegate('ctcp_listen', m) if m.ctcp
1098 delegate('message', m)
1099 privmsg(m) if m.address? and not m.ignored?
1100 delegate('unreplied', m) unless m.replied
1107 # Returns the only PluginManagerClass instance
1109 return PluginManagerClass.instance