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

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

Overview

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

Constant Summary collapse

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

Constructor Details

#initializePluginManagerClass

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 '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

#botObject (readonly)

Returns the value of attribute bot.



408
409
410
# File 'lib/rbot/plugins.rb', line 408

def bot
  @bot
end

#botmodulesObject (readonly)

Returns the value of attribute botmodules.



409
410
411
# File 'lib/rbot/plugins.rb', line 409

def botmodules
  @botmodules
end

#mapsObject (readonly)

Returns the value of attribute maps.



410
411
412
# File 'lib/rbot/plugins.rb', line 410

def maps
  @maps
end

Instance Method Details

#[](name) ⇒ Object

Returns the botmodule with the given name



479
480
481
# File 'lib/rbot/plugins.rb', line 479

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

#add_botmodule(botmodule) ⇒ Object

Raises:

  • (TypeError)


508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
# File '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

#add_core_module_dir(*dirlist) ⇒ Object

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



614
615
616
617
# File 'lib/rbot/plugins.rb', line 614

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

#add_plugin_dir(*dirlist) ⇒ Object

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



621
622
623
624
# File 'lib/rbot/plugins.rb', line 621

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

#bot_associate(bot) ⇒ Object

Associate with bot bot



473
474
475
476
# File 'lib/rbot/plugins.rb', line 473

def bot_associate(bot)
  reset_botmodule_lists
  @bot = bot
end

#cleanupObject

call the cleanup method for each active plugin



716
717
718
719
# File 'lib/rbot/plugins.rb', line 716

def cleanup
  delegate 'cleanup'
  reset_botmodule_lists
end

#clear_botmodule_dirsObject



626
627
628
629
630
# File 'lib/rbot/plugins.rb', line 626

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

#commandsObject

Returns a hash of the registered message prefixes and associated plugins



536
537
538
# File 'lib/rbot/plugins.rb', line 536

def commands
  @commandmappers
end

#core_lengthObject



802
803
804
# File 'lib/rbot/plugins.rb', line 802

def core_length
  core_modules.length
end

#core_modulesObject

Returns an array of the loaded plugins



525
526
527
# File 'lib/rbot/plugins.rb', line 525

def core_modules
  @botmodules[:CoreBotModule]
end

#delegate(method, *args) ⇒ Object

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



905
906
907
908
909
910
911
912
913
914
915
916
917
918
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
# File 'lib/rbot/plugins.rb', line 905

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)
        raise if err.kind_of?(BDB::Fatal)
      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)
          raise if err.kind_of?(BDB::Fatal)
        end
      end
    }
  end
  return ret
  # debug "Finished delegating #{method.inspect}"
end

#help(topic = "") ⇒ Object

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



807
808
809
810
811
812
813
814
815
816
817
818
819
820
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
# File 'lib/rbot/plugins.rb', line 807

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

#helptopicsObject

return list of help topics (plugin names)



792
793
794
795
796
# File 'lib/rbot/plugins.rb', line 792

def helptopics
  rv = status
  @failures_shown = true
  rv
end

#inspectObject



448
449
450
451
452
453
454
455
456
457
458
459
# File '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

#irc_delegate(method, m) ⇒ Object

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



1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
# File 'lib/rbot/plugins.rb', line 1009

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

#lengthObject



798
799
800
# File 'lib/rbot/plugins.rb', line 798

def length
  plugins.length
end

#mark_priorities_dirtyObject

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



542
543
544
# File 'lib/rbot/plugins.rb', line 542

def mark_priorities_dirty
  @sorted_modules = nil
end

#pluginsObject

Returns an array of the loaded plugins



530
531
532
# File 'lib/rbot/plugins.rb', line 530

def plugins
  @botmodules[:Plugin]
end

#privmsg(m) ⇒ Object

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



971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
# File 'lib/rbot/plugins.rb', line 971

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)
          raise if err.kind_of?(BDB::Fatal)
        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

#register(botmodule, cmd, auth_path) ⇒ Object

Registers botmodule botmodule with command cmd and command path auth_path

Raises:

  • (TypeError)


490
491
492
493
# File '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

#register_map(botmodule, map) ⇒ Object

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 '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

#report_error(str, err) ⇒ Object

Makes a string of error err by adding text str



547
548
549
# File 'lib/rbot/plugins.rb', line 547

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

#rescanObject

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



723
724
725
726
727
# File 'lib/rbot/plugins.rb', line 723

def rescan
  save
  cleanup
  scan
end

#reset_botmodule_listsObject

Reset lists of botmodules



462
463
464
465
466
467
468
469
470
# File '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

#saveObject

call the save method for each active plugin



710
711
712
713
# File 'lib/rbot/plugins.rb', line 710

def save
  delegate 'flush_registry'
  delegate 'save'
end

#scanObject

load plugins from pre-assigned list of directories



692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
# File 'lib/rbot/plugins.rb', line 692

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

#scan_botmodules(opts = {}) ⇒ Object



632
633
634
635
636
637
638
639
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
# File 'lib/rbot/plugins.rb', line 632

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

      did_it = load_botmodule_file("#{dir}/#{file}", "plugin")
      case did_it
      when Symbol
        processed[file.intern] = did_it
      when Exception
        @failed << { :name => file, :dir => dir, :reason => did_it }
      end
    end
  end
end

#sort_modulesObject



868
869
870
871
872
873
874
875
876
# File 'lib/rbot/plugins.rb', line 868

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

#status(short = false) ⇒ Object



729
730
731
732
733
734
735
736
737
738
739
740
741
742
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
# File 'lib/rbot/plugins.rb', line 729

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

#who_handles?(cmd) ⇒ Boolean

Returns true if cmd has already been registered as a command

Returns:

  • (Boolean)


484
485
486
487
# File '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