Class: Irc::Bot::Plugins::PluginManagerClass

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb

Overview

Singleton to manage multiple plugins and delegate messages to them for handling

Constant Summary

DEFAULT_DELEGATE_PATTERNS =

This is the list of patterns commonly delegated to plugins. A fast delegation lookup is enabled for them.

%r{^(?:
  connect|names|nick|
  listen|ctcp_listen|privmsg|unreplied|
  kick|join|part|quit|
  save|cleanup|flush_registry|
  set_.*|event_.*
)$}x

Instance Attribute Summary (collapse)

Instance Method Summary (collapse)

Methods included from Singleton

#_dump

Constructor Details

- (PluginManagerClass) initialize

Returns a new instance of PluginManagerClass



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 422

def initialize
  @botmodules = {
    :CoreBotModule => [],
    :Plugin => []
  }

  @names_hash = Hash.new
  @commandmappers = Hash.new
  @maps = Hash.new

  # modules will be sorted on first delegate call
  @sorted_modules = nil

  @delegate_list = Hash.new { |h, k|
    h[k] = Array.new
  }

  @core_module_dirs = []
  @plugin_dirs = []

  @failed = Array.new
  @ignored = Array.new

  bot_associate(nil)
end

Instance Attribute Details

- (Object) bot (readonly)

Returns the value of attribute bot



408
409
410
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 408

def bot
  @bot
end

- (Object) botmodules (readonly)

Returns the value of attribute botmodules



409
410
411
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 409

def botmodules
  @botmodules
end

- (Object) maps (readonly)

Returns the value of attribute maps



410
411
412
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 410

def maps
  @maps
end

Instance Method Details

- (Object) [](name)

Returns the botmodule with the given name



479
480
481
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 479

def [](name)
  @names_hash[name.to_sym]
end

- (Object) add_botmodule(botmodule)

Raises:

  • (TypeError)


508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 508

def add_botmodule(botmodule)
  raise TypeError, "Argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
  kl = botmodule.botmodule_class
  if @names_hash.has_key?(botmodule.to_sym)
    case self[botmodule].botmodule_class
    when kl
      raise "#{kl} #{botmodule} already registered!"
    else
      raise "#{self[botmodule].botmodule_class} #{botmodule} already registered, cannot re-register as #{kl}"
    end
  end
  @botmodules[kl] << botmodule
  @names_hash[botmodule.to_sym] = botmodule
  mark_priorities_dirty
end

- (Object) add_core_module_dir(*dirlist)

add one or more directories to the list of directories to load core modules from



622
623
624
625
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 622

def add_core_module_dir(*dirlist)
  @core_module_dirs += dirlist
  debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
end

- (Object) add_plugin_dir(*dirlist)

add one or more directories to the list of directories to load plugins from



629
630
631
632
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 629

def add_plugin_dir(*dirlist)
  @plugin_dirs += dirlist
  debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
end

- (Object) bot_associate(bot)

Associate with bot bot



473
474
475
476
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 473

def bot_associate(bot)
  reset_botmodule_lists
  @bot = bot
end

- (Object) cleanup

call the cleanup method for each active plugin



730
731
732
733
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 730

def cleanup
  delegate 'cleanup'
  reset_botmodule_lists
end

- (Object) clear_botmodule_dirs



634
635
636
637
638
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 634

def clear_botmodule_dirs
  @core_module_dirs.clear
  @plugin_dirs.clear
  debug "Core module and plugin loading paths cleared"
end

- (Object) commands

Returns a hash of the registered message prefixes and associated plugins



536
537
538
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 536

def commands
  @commandmappers
end

- (Object) core_length



816
817
818
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 816

def core_length
  core_modules.length
end

- (Object) core_modules

Returns an array of the loaded plugins



525
526
527
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 525

def core_modules
  @botmodules[:CoreBotModule]
end

- (Object) delegate(method, *args)

call-seq: delegate</span><span class=“method-args”>(method, m, opts={})</span> <span class=“method-name”>delegate</span><span class=“method-args”>(method, opts={})

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



919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 919

def delegate(method, *args)
  # if the priorities order of the delegate list is dirty,
  # meaning some modules have been added or priorities have been
  # changed, then the delegate list will need to be sorted before
  # delegation.  This should always be true for the first delegation.
  sort_modules unless @sorted_modules

  opts = {}
  opts.merge(args.pop) if args.last.class == Hash

  m = args.first
  if BasicUserMessage === m
    # ignored messages should not be delegated
    # to plugins with positive priority
    opts[:below] ||= 0 if m.ignored?
    # fake messages should not be delegated
    # to plugins with negative priority
    opts[:above] ||= 0 if m.recurse_depth > 0
  end

  above = opts[:above]
  below = opts[:below]

  # debug "Delegating #{method.inspect}"
  ret = Array.new
  if method.match(DEFAULT_DELEGATE_PATTERNS)
    debug "fast-delegating #{method}"
    m = method.to_sym
    debug "no-one to delegate to" unless @delegate_list.has_key?(m)
    return [] unless @delegate_list.has_key?(m)
    @delegate_list[m].each { |p|
      begin
        prio = p.priority
        unless (above and above >= prio) or (below and below <= prio)
          ret.push p.send(method, *args)
        end
      rescue Exception => err
        raise if err.kind_of?(SystemExit)
        error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
      end
    }
  else
    debug "slow-delegating #{method}"
    @sorted_modules.each { |p|
      if(p.respond_to? method)
        begin
          # debug "#{p.botmodule_class} #{p.name} responds"
          prio = p.priority
          unless (above and above >= prio) or (below and below <= prio)
            ret.push p.send(method, *args)
          end
        rescue Exception => err
          raise if err.kind_of?(SystemExit)
          error report_error("#{p.botmodule_class} #{p.name} #{method}() failed:", err)
        end
      end
    }
  end
  return ret
  # debug "Finished delegating #{method.inspect}"
end

- (Object) help(topic = "")

return help for topic (call associated plugin's help method)



821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 821

def help(topic="")
  case topic
  when /fail(?:ed)?\s*plugins?.*(trace(?:back)?s?)?/
    # debug "Failures: #{@failed.inspect}"
    return _("no plugins failed to load") if @failed.empty?
    return @failed.collect { |p|
      _('%{highlight}%{plugin}%{highlight} in %{dir} failed with error %{exception}: %{reason}') % {
          :highlight => Bold, :plugin => p[:name], :dir => p[:dir],
          :exception => p[:reason].class, :reason => p[:reason],
      } + if $1 && !p[:reason].backtrace.empty?
            _('at %{backtrace}') % {:backtrace => p[:reason].backtrace.join(', ')}
          else
            ''
          end
    }.join("\n")
  when /ignored?\s*plugins?/
    return _('no plugins were ignored') if @ignored.empty?

    tmp = Hash.new
    @ignored.each do |p|
      reason = p[:loaded] ? _('overruled by previous') : _(p[:reason].to_s)
      ((tmp[p[:dir]] ||= Hash.new)[reason] ||= Array.new).push(p[:name])
    end

    return tmp.map do |dir, reasons|
      # FIXME get rid of these string concatenations to make gettext easier
      s = reasons.map { |r, list|
        list.map { |_| _.sub(/\.rb$/, '') }.join(', ') + " (#{r})"
      }.join('; ')
      "in #{dir}: #{s}"
    end.join('; ')
  when /^(\S+)\s*(.*)$/
    key = $1
    params = $2

    # Let's see if we can match a plugin by the given name
    (core_modules + plugins).each { |p|
      next unless p.name == key
      begin
        return p.help(key, params)
      rescue Exception => err
        #rescue TimeoutError, StandardError, NameError, SyntaxError => err
        error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
      end
    }

    # Nope, let's see if it's a command, and ask for help at the corresponding botmodule
    k = key.to_sym
    if commands.has_key?(k)
      p = commands[k][:botmodule]
      begin
        return p.help(key, params)
      rescue Exception => err
        #rescue TimeoutError, StandardError, NameError, SyntaxError => err
        error report_error("#{p.botmodule_class} #{p.name} help() failed:", err)
      end
    end
  end
  return false
end

- (Object) helptopics

return list of help topics (plugin names)



806
807
808
809
810
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 806

def helptopics
  rv = status
  @failures_shown = true
  rv
end

- (Object) inspect



448
449
450
451
452
453
454
455
456
457
458
459
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 448

def inspect
  ret = self.to_s[0..-2]
  ret << ' corebotmodules='
  ret << @botmodules[:CoreBotModule].map { |m|
    m.name
  }.inspect
  ret << ' plugins='
  ret << @botmodules[:Plugin].map { |m|
    m.name
  }.inspect
  ret << ">"
end

- (Object) irc_delegate(method, m)

delegate IRC messages, by delegating 'listen' first, and the actual method afterwards. Delegating 'privmsg' also delegates ctcp_listen and message as appropriate.



1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 1020

def irc_delegate(method, m)
  delegate('listen', m)
  if method.to_sym == :privmsg
    delegate('ctcp_listen', m) if m.ctcp
    delegate('message', m)
    privmsg(m) if m.address? and not m.ignored?
    delegate('unreplied', m) unless m.replied
  else
    delegate(method, m)
  end
end

- (Object) length



812
813
814
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 812

def length
  plugins.length
end

- (Object) mark_priorities_dirty

Tells the PluginManager that the next time it delegates an event, it should sort the modules by priority



542
543
544
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 542

def mark_priorities_dirty
  @sorted_modules = nil
end

- (Object) plugins

Returns an array of the loaded plugins



530
531
532
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 530

def plugins
  @botmodules[:Plugin]
end

- (Object) privmsg(m)

see if we have a plugin that wants to handle this message, if so, pass it to the plugin and return true, otherwise false



983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 983

def privmsg(m)
  debug "Delegating privmsg #{m.inspect} with pluginkey #{m.plugin.inspect}"
  return unless m.plugin
  k = m.plugin.to_sym
  if commands.has_key?(k)
    p = commands[k][:botmodule]
    a = commands[k][:auth]
    # We check here for things that don't check themselves
    # (e.g. mapped things)
    debug "Checking auth ..."
    if a.nil? || @bot.auth.allow?(a, m.source, m.replyto)
      debug "Checking response ..."
      if p.respond_to?("privmsg")
        begin
          debug "#{p.botmodule_class} #{p.name} responds"
          p.privmsg(m)
        rescue Exception => err
          raise if err.kind_of?(SystemExit)
          error report_error("#{p.botmodule_class} #{p.name} privmsg() failed:", err)
        end
        debug "Successfully delegated #{m.inspect}"
        return true
      else
        debug "#{p.botmodule_class} #{p.name} is registered, but it doesn't respond to privmsg()"
      end
    else
      debug "#{p.botmodule_class} #{p.name} is registered, but #{m.source} isn't allowed to call #{m.plugin.inspect} on #{m.replyto}"
    end
  else
    debug "Command #{k} isn't handled"
  end
  return false
end

- (Object) register(botmodule, cmd, auth_path)

Registers botmodule botmodule with command cmd and command path auth_path

Raises:

  • (TypeError)


490
491
492
493
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 490

def register(botmodule, cmd, auth_path)
  raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
  @commandmappers[cmd.to_sym] = {:botmodule => botmodule, :auth => auth_path}
end

- (Object) register_map(botmodule, map)

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

Raises:

  • (TypeError)


503
504
505
506
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 503

def register_map(botmodule, map)
  raise TypeError, "First argument #{botmodule.inspect} is not of class BotModule" unless botmodule.kind_of?(BotModule)
  @maps[map.template] = { :botmodule => botmodule, :auth => [map.options[:full_auth_path]], :map => map }
end

- (Object) report_error(str, err)

Makes a string of error err by adding text str



547
548
549
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 547

def report_error(str, err)
  ([str, err.inspect] + err.backtrace).join("\n")
end

- (Object) rescan

drop all plugins and rescan plugins on disk calls save and cleanup for each plugin before dropping them



737
738
739
740
741
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 737

def rescan
  save
  cleanup
  scan
end

- (Object) reset_botmodule_lists

Reset lists of botmodules



462
463
464
465
466
467
468
469
470
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 462

def reset_botmodule_lists
  @botmodules[:CoreBotModule].clear
  @botmodules[:Plugin].clear
  @names_hash.clear
  @commandmappers.clear
  @maps.clear
  @failures_shown = false
  mark_priorities_dirty
end

- (Object) save

call the save method for each active plugin



724
725
726
727
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 724

def save
  delegate 'flush_registry'
  delegate 'save'
end

- (Object) scan

load plugins from pre-assigned list of directories



706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 706

def scan
  @failed.clear
  @ignored.clear
  @delegate_list.clear

  scan_botmodules(:type => :core)
  scan_botmodules(:type => :plugins)

  debug "finished loading plugins: #{status(true)}"
  (core_modules + plugins).each { |p|
   p.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
     @delegate_list[m.intern] << p
   }
  }
  mark_priorities_dirty
end

- (Object) scan_botmodules(opts = {})



640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 640

def scan_botmodules(opts={})
  type = opts[:type]
  processed = Hash.new

  case type
  when :core
    dirs = @core_module_dirs
  when :plugins
    dirs = @plugin_dirs

    @bot.config['plugins.blacklist'].each { |p|
      pn = p + ".rb"
      processed[pn.intern] = :blacklisted
    }

    whitelist = @bot.config['plugins.whitelist'].map { |p|
      p + ".rb"
    }
  end

  dirs.each do |dir|
    next unless FileTest.directory?(dir)
    d = Dir.new(dir)
    d.sort.each do |file|
      next unless file =~ /\.rb$/
      next if file =~ /^\./

      case type
      when :plugins
        if !whitelist.empty? && !whitelist.include?(file)
          @ignored << {:name => file, :dir => dir, :reason => :not whitelisted" }
          next
        elsif processed.has_key?(file.intern)
          @ignored << {:name => file, :dir => dir, :reason => processed[file.intern]}
          next
        end

        if(file =~ /^(.+\.rb)\.disabled$/)
          # GB: Do we want to do this? This means that a disabled plugin in a directory
          #     will disable in all subsequent directories. This was probably meant
          #     to be used before plugins.blacklist was implemented, so I think
          #     we don't need this anymore
          processed[$1.intern] = :disabled
          @ignored << {:name => $1, :dir => dir, :reason => processed[$1.intern]}
          next
        end
      end

      begin
        did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
      rescue Exception => e
        error e
        did_it = e
      end

      case did_it
      when Symbol
        processed[file.intern] = did_it
      when Exception
        @failed << { :name => file, :dir => dir, :reason => did_it }
      end
    end
  end
end

- (Object) sort_modules



882
883
884
885
886
887
888
889
890
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 882

def sort_modules
  @sorted_modules = (core_modules + plugins).sort do |a, b|
    a.priority <=> b.priority
  end || []

  @delegate_list.each_value do |list|
    list.sort! {|a,b| a.priority <=> b.priority}
  end
end

- (Object) status(short = false)



743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 743

def status(short=false)
  output = []
  if self.core_length > 0
    if short
      output << n_("%{count} core module loaded", "%{count} core modules loaded",
                self.core_length) % {:count => self.core_length}
    else
      output <<  n_("%{count} core module: %{list}",
                 "%{count} core modules: %{list}", self.core_length) %
                 { :count => self.core_length,
                   :list => core_modules.collect{ |p| p.name}.sort.join(", ") }
    end
  else
    output << _("no core botmodules loaded")
  end
  # Active plugins first
  if(self.length > 0)
    if short
      output << n_("%{count} plugin loaded", "%{count} plugins loaded",
                   self.length) % {:count => self.length}
    else
      output << n_("%{count} plugin: %{list}",
                   "%{count} plugins: %{list}", self.length) %
               { :count => self.length,
                 :list => plugins.collect{ |p| p.name}.sort.join(", ") }
    end
  else
    output << "no plugins active"
  end
  # Ignored plugins next
  unless @ignored.empty? or @failures_shown
    if short
      output << n_("%{highlight}%{count} plugin ignored%{highlight}",
                   "%{highlight}%{count} plugins ignored%{highlight}",
                   @ignored.length) %
                { :count => @ignored.length, :highlight => Underline }
    else
      output << n_("%{highlight}%{count} plugin ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
                   "%{highlight}%{count} plugins ignored%{highlight}: use %{bold}%{command}%{bold} to see why",
                   @ignored.length) %
                { :count => @ignored.length, :highlight => Underline,
                  :bold => Bold, :command => "help ignored plugins"}
    end
  end
  # Failed plugins next
  unless @failed.empty? or @failures_shown
    if short
      output << n_("%{highlight}%{count} plugin failed to load%{highlight}",
                   "%{highlight}%{count} plugins failed to load%{highlight}",
                   @failed.length) %
                { :count => @failed.length, :highlight => Reverse }
    else
      output << n_("%{highlight}%{count} plugin failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
                   "%{highlight}%{count} plugins failed to load%{highlight}: use %{bold}%{command}%{bold} to see why",
                   @failed.length) %
                { :count => @failed.length, :highlight => Reverse,
                  :bold => Bold, :command => "help failed plugins"}
    end
  end
  output.join '; '
end

- (Boolean) who_handles?(cmd)

Returns true if cmd has already been registered as a command

Returns:

  • (Boolean)


484
485
486
487
# File '/home/apoc/projects/ruby/rbot/lib/rbot/plugins.rb', line 484

def who_handles?(cmd)
  return nil unless @commandmappers.has_key?(cmd.to_sym)
  return @commandmappers[cmd.to_sym][:botmodule]
end