| Class | Irc::Bot::Plugins::PluginManagerClass |
| In: |
lib/rbot/plugins.rb
|
| Parent: | Object |
| DEFAULT_DELEGATE_PATTERNS | = | %r{^(?: connect|names|nick| listen|ctcp_listen|privmsg|unreplied| kick|join|part|quit| save|cleanup|flush_registry| set_.*|event_.* )$}x | This is the list of patterns commonly delegated to plugins. A fast delegation lookup is enabled for them. |
| bot | [R] | |
| botmodules | [R] | |
| maps | [R] |
# File lib/rbot/plugins.rb, line 422
422: def initialize
423: @botmodules = {
424: :CoreBotModule => [],
425: :Plugin => []
426: }
427:
428: @names_hash = Hash.new
429: @commandmappers = Hash.new
430: @maps = Hash.new
431:
432: # modules will be sorted on first delegate call
433: @sorted_modules = nil
434:
435: @delegate_list = Hash.new { |h, k|
436: h[k] = Array.new
437: }
438:
439: @core_module_dirs = []
440: @plugin_dirs = []
441:
442: @failed = Array.new
443: @ignored = Array.new
444:
445: bot_associate(nil)
446: end
Returns the botmodule with the given name
# File lib/rbot/plugins.rb, line 479
479: def [](name)
480: @names_hash[name.to_sym]
481: end
# File lib/rbot/plugins.rb, line 508
508: def add_botmodule(botmodule)
509: raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
510: kl = botmodule.botmodule_class
511: if @names_hash.has_key?(botmodule.to_sym)
512: case self[botmodule].botmodule_class
513: when kl
514: raise "#{kl} #{botmodule} already registered!"
515: else
516: raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
517: end
518: end
519: @botmodules[kl] << botmodule
520: @names_hash[botmodule.to_sym] = botmodule
521: mark_priorities_dirty
522: end
add one or more directories to the list of directories to load core modules from
# File lib/rbot/plugins.rb, line 614
614: def add_core_module_dir(*dirlist)
615: @core_module_dirs += dirlist
616: debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
617: end
Associate with bot bot
# File lib/rbot/plugins.rb, line 473
473: def bot_associate(bot)
474: reset_botmodule_lists
475: @bot = bot
476: end
# File lib/rbot/plugins.rb, line 626
626: def clear_botmodule_dirs
627: @core_module_dirs.clear
628: @plugin_dirs.clear
629: debug "Core module and plugin loading paths cleared"
630: end
see if each plugin handles method, and if so, call it, passing m as a parameter (if present). BotModules are called in order of priority from lowest to highest.
If the passed m is a BasicUserMessage and is marked as ignored?, it will only be delegated to plugins with negative priority. Conversely, if it‘s a fake message (see BotModule#fake_message), it will only be delegated to plugins with positive priority.
Note that m can also be an exploded Array, but in this case the last element of it cannot be a Hash, or it will be interpreted as the options Hash for delegate itself. The last element can be a subclass of a Hash, though. To be on the safe side, you can add an empty Hash as last parameter for delegate when calling it with an exploded Array:
@bot.plugins.delegate(method, *(args.push Hash.new))
Currently supported options are the following:
| :above : | if specified, the delegation will only consider plugins with a priority higher than the specified value |
| :below : | if specified, the delegation will only consider plugins with a priority lower than the specified value |
# File lib/rbot/plugins.rb, line 905
905: def delegate(method, *args)
906: # if the priorities order of the delegate list is dirty,
907: # meaning some modules have been added or priorities have been
908: # changed, then the delegate list will need to be sorted before
909: # delegation. This should always be true for the first delegation.
910: sort_modules unless @sorted_modules
911:
912: opts = {}
913: opts.merge(args.pop) if args.last.class == Hash
914:
915: m = args.first
916: if BasicUserMessage === m
917: # ignored messages should not be delegated
918: # to plugins with positive priority
919: opts[:below] ||= 0 if m.ignored?
920: # fake messages should not be delegated
921: # to plugins with negative priority
922: opts[:above] ||= 0 if m.recurse_depth > 0
923: end
924:
925: above = opts[:above]
926: below = opts[:below]
927:
928: # debug "Delegating #{method.inspect}"
929: ret = Array.new
930: if method.match(DEFAULT_DELEGATE_PATTERNS)
931: debug "fast-delegating #{method}"
932: m = method.to_sym
933: debug "no-one to delegate to" unless @delegate_list.has_key?(m)
934: return [] unless @delegate_list.has_key?(m)
935: @delegate_list[m].each { |p|
936: begin
937: prio = p.priority
938: unless (above and above >= prio) or (below and below <= prio)
939: ret.push p.send(method, *args)
940: end
941: rescue Exception => err
942: raise if err.kind_of?(SystemExit)
943: error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
944: raise if err.kind_of?(BDB::Fatal)
945: end
946: }
947: else
948: debug "slow-delegating #{method}"
949: @sorted_modules.each { |p|
950: if(p.respond_to? method)
951: begin
952: # debug "#{p.botmodule_class} #{p.name} responds"
953: prio = p.priority
954: unless (above and above >= prio) or (below and below <= prio)
955: ret.push p.send(method, *args)
956: end
957: rescue Exception => err
958: raise if err.kind_of?(SystemExit)
959: error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
960: raise if err.kind_of?(BDB::Fatal)
961: end
962: end
963: }
964: end
965: return ret
966: # debug "Finished delegating #{method.inspect}"
967: end
return help for topic (call associated plugin‘s help method)
# File lib/rbot/plugins.rb, line 807
807: def help(topic="")
808: case topic
809: when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
810: # debug "Failures: #{@failed.inspect}"
811: return _("no plugins failed to load") if @failed.empty?
812: return @failed.collect { |p|
813: _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
814: :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
815: :exception => p[:reason].class, :reason => p[:reason],
816: } + if $1 && !p[:reason].backtrace.empty?
817: _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
818: else
819: ''
820: end
821: }.join("\n")
822: when /ignored?\s*plugins?/
823: return _('no plugins were ignored') if @ignored.empty?
824:
825: tmp = Hash.new
826: @ignored.each do |p|
827: reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
828: ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
829: end
830:
831: return tmp.map do |dir, reasons|
832: # FIXME get rid of these string concatenations to make gettext easier
833: s = reasons.map { |r, list|
834: list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
835: }.join('; ')
836: "in #{dir}: #{s}"
837: end.join('; ')
838: when /^(\S+)\s*(.*)$/
839: key = $1
840: params = $2
841:
842: # Let's see if we can match a plugin by the given name
843: (core_modules + plugins).each { |p|
844: next unless p.name == key
845: begin
846: return p.help(key, params)
847: rescue Exception => err
848: #rescue TimeoutError, StandardError, NameError, SyntaxError => err
849: error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
850: end
851: }
852:
853: # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
854: k = key.to_sym
855: if commands.has_key?(k)
856: p = commands[k][:botmodule]
857: begin
858: return p.help(key, params)
859: rescue Exception => err
860: #rescue TimeoutError, StandardError, NameError, SyntaxError => err
861: error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
862: end
863: end
864: end
865: return false
866: end
# File lib/rbot/plugins.rb, line 448
448: def inspect
449: ret = self.to_s[0..-2]
450: ret << ' corebotmodules='
451: ret << @botmodules[:CoreBotModule].map { |m|
452: m.name
453: }.inspect
454: ret << ' plugins='
455: ret << @botmodules[:Plugin].map { |m|
456: m.name
457: }.inspect
458: ret << ">"
459: end
delegate IRC messages, by delegating ‘listen’ first, and the actual method afterwards. Delegating ‘privmsg’ also delegates ctcp_listen and message as appropriate.
# File lib/rbot/plugins.rb, line 1009
1009: def irc_delegate(method, m)
1010: delegate('listen', m)
1011: if method.to_sym == :privmsg
1012: delegate('ctcp_listen', m) if m.ctcp
1013: delegate('message', m)
1014: privmsg(m) if m.address? and not m.ignored?
1015: delegate('unreplied', m) unless m.replied
1016: else
1017: delegate(method, m)
1018: end
1019: end
Tells the PluginManager that the next time it delegates an event, it should sort the modules by priority
# File lib/rbot/plugins.rb, line 542
542: def mark_priorities_dirty
543: @sorted_modules = nil
544: end
see if we have a plugin that wants to handle this message, if so, pass it to the plugin and return true, otherwise false
# File lib/rbot/plugins.rb, line 971
971: def privmsg(m)
972: debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
973: return unless m.plugin
974: k = m.plugin.to_sym
975: if commands.has_key?(k)
976: p = commands[k][:botmodule]
977: a = commands[k][:auth]
978: # We check here for things that don't check themselves
979: # (e.g. mapped things)
980: debug "Checking auth ..."
981: if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
982: debug "Checking response ..."
983: if p.respond_to?("privmsg")
984: begin
985: debug "#{p.botmodule_class} #{p.name} responds"
986: p.privmsg(m)
987: rescue Exception => err
988: raise if err.kind_of?(SystemExit)
989: error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
990: raise if err.kind_of?(BDB::Fatal)
991: end
992: debug "Successfully delegated #{m.inspect}"
993: return true
994: else
995: debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
996: end
997: else
998: debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
999: end
1000: else
1001: debug "Command #{k} isn't handled"
1002: end
1003: return false
1004: end
Registers botmodule botmodule with command cmd and command path auth_path
# File lib/rbot/plugins.rb, line 490
490: def register(botmodule, cmd, auth_path)
491: raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
492: @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
493: end
Registers botmodule botmodule with map map. This adds the map to the maps hash which has three keys:
| botmodule: | the associated botmodule |
| auth: | an array of auth keys checked by the map; the first is the full_auth_path of the map |
| map: | the actual MessageTemplate object |
# File lib/rbot/plugins.rb, line 503
503: def register_map(botmodule, map)
504: raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
505: @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
506: end
Makes a string of error err by adding text str
# File lib/rbot/plugins.rb, line 547
547: def report_error(str, err)
548: ([str, err.inspect] + err.backtrace).join("\n")
549: end
Reset lists of botmodules
# File lib/rbot/plugins.rb, line 462
462: def reset_botmodule_lists
463: @botmodules[:CoreBotModule].clear
464: @botmodules[:Plugin].clear
465: @names_hash.clear
466: @commandmappers.clear
467: @maps.clear
468: @failures_shown = false
469: mark_priorities_dirty
470: end
load plugins from pre-assigned list of directories
# File lib/rbot/plugins.rb, line 692
692: def scan
693: @failed.clear
694: @ignored.clear
695: @delegate_list.clear
696:
697: scan_botmodules(:type => :core)
698: scan_botmodules(:type => :plugins)
699:
700: debug "finished loading plugins: #{status(true)}"
701: (core_modules + plugins).each { |p|
702: p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
703: @delegate_list[m.intern] << p
704: }
705: }
706: mark_priorities_dirty
707: end
# File lib/rbot/plugins.rb, line 632
632: def scan_botmodules(opts={})
633: type = opts[:type]
634: processed = Hash.new
635:
636: case type
637: when :core
638: dirs = @core_module_dirs
639: when :plugins
640: dirs = @plugin_dirs
641:
642: @bot.config['plugins.blacklist'].each { |p|
643: pn = p + ".rb"
644: processed[pn.intern] = :blacklisted
645: }
646:
647: whitelist = @bot.config['plugins.whitelist'].map { |p|
648: p + ".rb"
649: }
650: end
651:
652: dirs.each do |dir|
653: next unless FileTest.directory?(dir)
654: d = Dir.new(dir)
655: d.sort.each do |file|
656: next unless file =~ /\.rb$/
657: next if file =~ /^\./
658:
659: case type
660: when :plugins
661: if !whitelist.empty? && !whitelist.include?(file)
662: @ignored << {:name => file, :dir => dir, :reason => "not whitelisted""not whitelisted" }
663: next
664: elsif processed.has_key?(file.intern)
665: @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
666: next
667: end
668:
669: if(file =~ /^(.+\.rb)\.disabled$/)
670: # GB: Do we want to do this? This means that a disabled plugin in a directory
671: # will disable in all subsequent directories. This was probably meant
672: # to be used before plugins.blacklist was implemented, so I think
673: # we don't need this anymore
674: processed[$1.intern] = :disabled
675: @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
676: next
677: end
678: end
679:
680: did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
681: case did_it
682: when Symbol
683: processed[file.intern] = did_it
684: when Exception
685: @failed << { :name => file, :dir => dir, :reason => did_it }
686: end
687: end
688: end
689: end
# File lib/rbot/plugins.rb, line 868
868: def sort_modules
869: @sorted_modules = (core_modules + plugins).sort do |a, b|
870: a.priority <=> b.priority
871: end || []
872:
873: @delegate_list.each_value do |list|
874: list.sort! {|a,b| a.priority <=> b.priority}
875: end
876: end
# File lib/rbot/plugins.rb, line 729
729: def status(short=false)
730: output = []
731: if self.core_length > 0
732: if short
733: output << n_("%{count} core module loaded", "%{count} core modules loaded",
734: self.core_length) % {:count => self.core_length}
735: else
736: output << n_("%{count} core module: %{list}",
737: "%{count} core modules: %{list}", self.core_length) %
738: { :count => self.core_length,
739: :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
740: end
741: else
742: output << _("no core botmodules loaded")
743: end
744: # Active plugins first
745: if(self.length > 0)
746: if short
747: output << n_("%{count} plugin loaded", "%{count} plugins loaded",
748: self.length) % {:count => self.length}
749: else
750: output << n_("%{count} plugin: %{list}",
751: "%{count} plugins: %{list}", self.length) %
752: { :count => self.length,
753: :list => plugins.collect{ |p| p.name}.sort.join(", ") }
754: end
755: else
756: output << "no plugins active"
757: end
758: # Ignored plugins next
759: unless @ignored.empty? or @failures_shown
760: if short
761: output << n_("%{highlight}%{count} plugin ignored%{highlight}",
762: "%{highlight}%{count} plugins ignored%{highlight}",
763: @ignored.length) %
764: { :count => @ignored.length, :highlight => Underline }
765: else
766: output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
767: "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
768: @ignored.length) %
769: { :count => @ignored.length, :highlight => Underline,
770: :bold => Bold, :command => "help ignored plugins"}
771: end
772: end
773: # Failed plugins next
774: unless @failed.empty? or @failures_shown
775: if short
776: output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
777: "%{highlight}%{count} plugins failed to load%{highlight}",
778: @failed.length) %
779: { :count => @failed.length, :highlight => Reverse }
780: else
781: output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
782: "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
783: @failed.length) %
784: { :count => @failed.length, :highlight => Reverse,
785: :bold => Bold, :command => "help failed plugins"}
786: end
787: end
788: output.join '; '
789: end
Returns true if cmd has already been registered as a command
# File lib/rbot/plugins.rb, line 484
484: def who_handles?(cmd)
485: return nil unless @commandmappers.has_key?(cmd.to_sym)
486: return @commandmappers[cmd.to_sym][:botmodule]
487: end
This method is the one that actually loads a module from the file fname
desc is a simple description of what we are loading (plugin/botmodule/whatever)
It returns the Symbol :loaded on success, and an Exception on failure
# File lib/rbot/plugins.rb, line 559
559: def load_botmodule_file(fname, desc=nil)
560: # create a new, anonymous module to "house" the plugin
561: # the idea here is to prevent namespace pollution. perhaps there
562: # is another way?
563: plugin_module = Module.new
564: # each plugin uses its own textdomain, we bind it automatically here
565: bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
566:
567: desc = desc.to_s + " " if desc
568:
569: begin
570: plugin_string = IO.read(fname)
571: debug "loading #{desc}#{fname}"
572: plugin_module.module_eval(plugin_string, fname)
573: return :loaded
574: rescue Exception => err
575: # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
576: error report_error("#{desc}#{fname} load failed", err)
577: bt = err.backtrace.select { |line|
578: line.match(/^(\(eval\)|#{fname}):\d+/)
579: }
580: bt.map! { |el|
581: el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
582: "#{fname}#{$1}#{$3}"
583: }
584: }
585: msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
586: "#{fname}#{$1}#{$3}"
587: }
588: begin
589: newerr = err.class.new(msg)
590: rescue ArgumentError => err_in_err
591: # Somebody should hang the ActiveSupport developers by their balls
592: # with barbed wire. Their MissingSourceFile extension to LoadError
593: # _expects_ a second argument, breaking the usual Exception interface
594: # (instead, the smart thing to do would have been to make the second
595: # parameter optional and run the code in the from_message method if
596: # it was missing).
597: # Anyway, we try to cope with this in the simplest possible way. On
598: # the upside, this new block can be extended to handle other similar
599: # idiotic approaches
600: if err.class.respond_to? :from_message
601: newerr = err.class.from_message(msg)
602: else
603: raise err_in_err
604: end
605: end
606: newerr.set_backtrace(bt)
607: return newerr
608: end
609: end