+ # Associate with bot _bot_
+ def bot_associate(bot)
+ reset_botmodule_lists
+ @bot = bot
+ end
+
+ # Returns the botmodule with the given _name_
+ def [](name)
+ return if not name
+ @names_hash[name.to_sym]
+ end
+
+ # Returns +true+ if a botmodule named _name_ exists.
+ def has_key?(name)
+ return if not name
+ @names_hash.has_key?(name.to_sym)
+ end
+
+ # Returns +true+ if _cmd_ has already been registered as a command
+ def who_handles?(cmd)
+ return nil unless @commandmappers.has_key?(cmd.to_sym)
+ return @commandmappers[cmd.to_sym][:botmodule]
+ end
+
+ # Registers botmodule _botmodule_ with command _cmd_ and command path _auth_path_
+ 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
+
+ # 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
+ #
+ #
+ 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
+
+ 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
+ # add itself to the delegate list for the fast-delegation
+ # of methods like cleanup or privmsg, etc..
+ botmodule.methods.grep(DEFAULT_DELEGATE_PATTERNS).each { |m|
+ @delegate_list[m.intern] << botmodule
+ }
+ mark_priorities_dirty
+ end
+
+ # Returns an array of the loaded plugins
+ def core_modules
+ @botmodules[:CoreBotModule]
+ end
+
+ # Returns an array of the loaded plugins
+ def plugins
+ @botmodules[:Plugin]
+ end
+
+ # Returns a hash of the registered message prefixes and associated
+ # plugins
+ def commands
+ @commandmappers
+ end
+
+ # Tells the PluginManager that the next time it delegates an event, it
+ # should sort the modules by priority
+ def mark_priorities_dirty
+ @sorted_modules = nil
+ end
+
+ # Makes a string of error _err_ by adding text _str_
+ def report_error(str, err)
+ ([str, err.inspect] + err.backtrace).join("\n")
+ 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) for error reporting
+ #
+ # It returns the Symbol :loaded on success, and an Exception
+ # on failure
+ #
+ def load_botmodule_file(fname, desc=nil)
+ # create a new, anonymous module to "house" the plugin
+ # the idea here is to prevent namespace pollution. perhaps there
+ # is another way?
+ plugin_module = Module.new
+
+ # each plugin uses its own textdomain, we bind it automatically here
+ bindtextdomain_to(plugin_module, "rbot-#{File.basename(fname, '.rb')}")
+
+ desc = desc.to_s + " " if desc
+
+ begin
+ plugin_string = IO.read(fname)
+ debug "loading #{desc}#{fname}"
+ plugin_module.module_eval(plugin_string, fname)
+
+ # this sets a BOTMODULE_FNAME constant in all BotModule
+ # classes defined in the module. This allows us to know
+ # the filename the plugin was declared in from outside
+ # the plugin itself (from within, a __FILE__ would work.)
+ plugin_module.constants.each do |const|
+ cls = plugin_module.const_get(const)
+ if cls.is_a? Class and cls < BotModule
+ cls.const_set("BOTMODULE_FNAME", fname)
+ end
+ end
+
+ return :loaded
+ rescue Exception => err
+ # rescue TimeoutError, StandardError, NameError, LoadError, SyntaxError => err
+ error report_error("#{desc}#{fname} load failed", err)
+ bt = err.backtrace.select { |line|
+ line.match(/^(\(eval\)|#{fname}):\d+/)
+ }
+ bt.map! { |el|
+ el.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+ "#{fname}#{$1}#{$3}"
+ }
+ }
+ msg = err.to_s.gsub(/^\(eval\)(:\d+)(:in `.*')?(:.*)?/) { |m|
+ "#{fname}#{$1}#{$3}"
+ }
+ msg.gsub!(fname, File.basename(fname))
+ begin
+ newerr = err.class.new(msg)
+ rescue ArgumentError => aerr_in_err
+ # Somebody should hang the ActiveSupport developers by their balls
+ # with barbed wire. Their MissingSourceFile extension to LoadError
+ # _expects_ a second argument, breaking the usual Exception interface
+ # (instead, the smart thing to do would have been to make the second
+ # parameter optional and run the code in the from_message method if
+ # it was missing).
+ # Anyway, we try to cope with this in the simplest possible way. On
+ # the upside, this new block can be extended to handle other similar
+ # idiotic approaches
+ if err.class.respond_to? :from_message
+ newerr = err.class.from_message(msg)
+ else
+ raise aerr_in_err
+ end
+ rescue NoMethodError => nmerr_in_err
+ # Another braindead extension to StandardError, OAuth2::Error,
+ # doesn't get a string as message, but a response
+ if err.respond_to? :response
+ newerr = err.class.new(err.response)
+ else
+ raise nmerr_in_err
+ end
+ end
+ newerr.set_backtrace(bt)
+ return newerr
+ end
+ end
+ private :load_botmodule_file
+
+ # add one or more directories to the list of directories to
+ # load core modules from
+ def add_core_module_dir(*dirlist)
+ @core_module_dirs += dirlist
+ debug "Core module loading paths: #{@core_module_dirs.join(', ')}"
+ end
+
+ # add one or more directories to the list of directories to
+ # load plugins from
+ def add_plugin_dir(*dirlist)
+ @plugin_dirs += dirlist
+ debug "Plugin loading paths: #{@plugin_dirs.join(', ')}"
+ end
+
+ def clear_botmodule_dirs
+ @core_module_dirs.clear
+ @plugin_dirs.clear
+ debug "Core module and plugin loading paths cleared"
+ end
+
+ 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
+