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