Automated merge with http://waqas.ath.cx:8000/
[prosody.git] / core / modulemanager.lua
index b997c6e02e2bd7226a7d3443a4c19355243963c6..8292eaa5d0d4147ce92cab7cad93fb1aecf3b981 100644 (file)
@@ -1,4 +1,4 @@
--- Prosody IM v0.1
+-- Prosody IM v0.2
 -- Copyright (C) 2008 Matthew Wild
 -- Copyright (C) 2008 Waqas Hussain
 -- 
@@ -26,13 +26,18 @@ local log = logger.init("modulemanager");
 local addDiscoInfoHandler = require "core.discomanager".addDiscoInfoHandler;
 local eventmanager = require "core.eventmanager";
 local config = require "core.configmanager";
+local multitable_new = require "util.multitable".new;
+local register_actions = require "core.actions".register;
 
+local hosts = hosts;
 
 local loadfile, pcall = loadfile, pcall;
 local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv;
 local pairs, ipairs = pairs, ipairs;
-local t_insert = table.insert;
+local t_insert, t_concat = table.insert, table.concat;
 local type = type;
+local next = next;
+local rawget = rawget;
 
 local tostring, print = tostring, print;
 
@@ -43,19 +48,46 @@ module "modulemanager"
 
 local api = {}; -- Module API container
 
-local modulemap = {};
+local modulemap = { ["*"] = {} };
 
+local stanza_handlers = multitable_new();
 local handler_info = {};
-local stanza_handlers = {};
 
 local modulehelpers = setmetatable({}, { __index = _G });
 
+local features_table = multitable_new();
+local handler_table = multitable_new();
+local hooked = multitable_new();
+local event_hooks = multitable_new();
+
+local NULL = {};
+
 -- Load modules when a host is activated
 function load_modules_for_host(host)
+       -- Load modules from global section
+       local modules_enabled = config.get("*", "core", "modules_enabled");
+       local modules_disabled = config.get(host, "core", "modules_disabled");
+       local disabled_set = {};
+       if modules_enabled then
+               if modules_disabled then
+                       for _, module in ipairs(modules_disabled) do
+                               disabled_set[module] = true;
+                       end
+               end
+               for _, module in ipairs(modules_enabled) do
+                       if not disabled_set[module] then
+                               load(host, module);
+                       end
+               end
+       end
+
+       -- Load modules from just this host
        local modules_enabled = config.get(host, "core", "modules_enabled");
        if modules_enabled then
                for _, module in pairs(modules_enabled) do
-                       load(host, module);
+                       if not is_loaded(host, module) then
+                               load(host, module);
+                       end
                end
        end
 end
@@ -66,20 +98,23 @@ function load(host, module_name, config)
        if not (host and module_name) then
                return nil, "insufficient-parameters";
        end
-       local mod, err = loadfile(plugin_dir.."mod_"..module_name..".lua");
-       if not mod then
-               log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
-               return nil, err;
-       end
        
        if not modulemap[host] then
                modulemap[host] = {};
-               stanza_handlers[host] = {};
        elseif modulemap[host][module_name] then
                log("warn", "%s is already loaded for %s, so not loading again", module_name, host);
                return nil, "module-already-loaded";
+       elseif modulemap["*"][module_name] then
+               return nil, "global-module-already-loaded";
        end
        
+
+       local mod, err = loadfile(get_module_filename(module_name));
+       if not mod then
+               log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
+               return nil, err;
+       end
+
        local _log = logger.init(host..":"..module_name);
        local api_instance = setmetatable({ name = module_name, host = host, config = config,  _log = _log, log = function (self, ...) return _log(...); end }, { __index = api });
 
@@ -93,60 +128,124 @@ function load(host, module_name, config)
                return nil, ret;
        end
        
-       modulemap[host][module_name] = mod;
+       -- Use modified host, if the module set one
+       modulemap[api_instance.host][module_name] = pluginenv;
        
        return true;
 end
 
+function get_module(host, name)
+       return modulemap[host] and modulemap[host][name];
+end
+
 function is_loaded(host, name)
        return modulemap[host] and modulemap[host][name] and true;
 end
 
 function unload(host, name, ...)
-       local mod = modulemap[host] and modulemap[host][name];
+       local mod = get_module(host, name); 
        if not mod then return nil, "module-not-loaded"; end
        
-       if type(mod.unload) == "function" then
-               local ok, err = pcall(mod.unload, ...)
+       if module_has_method(mod, "unload") then
+               local ok, err = call_module_method(mod, "unload");
                if (not ok) and err then
-                       log("warn", "Non-fatal error unloading module '%s' from '%s': %s", name, host, err);
+                       log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err);
                end
        end
-       
+       modulemap[host][name] = nil;
+       features_table:remove(host, name);
+       local params = handler_table:get(host, name); -- , {module.host, origin_type, tag, xmlns}
+       for _, param in pairs(params or NULL) do
+               local handlers = stanza_handlers:get(param[1], param[2], param[3], param[4]);
+               if handlers then
+                       handler_info[handlers[1]] = nil;
+                       stanza_handlers:remove(param[1], param[2], param[3], param[4]);
+               end
+       end
+       event_hooks:remove(host, name);
+       return true;
 end
 
-function handle_stanza(host, origin, stanza)
-       local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type;
-       
-       local handlers = stanza_handlers[host];
-       if not handlers then
-               log("warn", "No handlers for %s", host);
-               return false;
+function reload(host, name, ...)
+       local mod = get_module(host, name);
+       if not mod then return nil, "module-not-loaded"; end
+
+       local _mod, err = loadfile(get_module_filename(name)); -- checking for syntax errors
+       if not _mod then
+               log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil");
+               return nil, err;
        end
-       
-       if name == "iq" and xmlns == "jabber:client" and handlers[origin_type] then
-               local child = stanza.tags[1];
-               if child then
-                       local xmlns = child.attr.xmlns or xmlns;
-                       log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
-                       local handler = handlers[origin_type][name] and handlers[origin_type][name][xmlns];
-                       if handler then
-                               log("debug", "Passing stanza to mod_%s", handler_info[handler].name);
-                               return handler(origin, stanza) or true;
+
+       local saved;
+
+       if module_has_method(mod, "save") then
+               local ok, ret, err = call_module_method(mod, "save");
+               if ok then
+                       saved = ret;
+               else
+                       log("warn", "Error saving module '%s:%s' state: %s", host, module, ret);
+                       if not config.get(host, "core", "force_module_reload") then
+                               log("warn", "Aborting reload due to error, set force_module_reload to ignore this");
+                               return nil, "save-state-failed";
+                       else
+                               log("warn", "Continuing with reload (using the force)");
                        end
                end
-       elseif handlers[origin_type] then
-               local handler = handlers[origin_type][name];
-               if  handler then
-                       handler = handler[xmlns];
-                       if handler then
-                               log("debug", "Passing stanza to mod_%s", handler_info[handler].name);
-                               return handler(origin, stanza) or true;
+       end
+
+       unload(host, name, ...);
+       local ok, err = load(host, name, ...);
+       if ok then
+               mod = get_module(host, name);
+               if module_has_method(mod, "restore") then
+                       local ok, err = call_module_method(mod, "restore", saved or {})
+                       if (not ok) and err then
+                               log("warn", "Error restoring module '%s' from '%s': %s", name, host, err);
                        end
                end
+               return true;
        end
-       log("debug", "Stanza unhandled by any modules, xmlns: %s", stanza.attr.xmlns);
-       return false; -- we didn't handle it
+       return ok, err;
+end
+
+function handle_stanza(host, origin, stanza)
+       local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type;
+       if name == "iq" and xmlns == "jabber:client" then
+               if stanza.attr.type == "get" or stanza.attr.type == "set" then
+                       xmlns = stanza.tags[1].attr.xmlns;
+                       log("debug", "Stanza of type %s from %s has xmlns: %s", name, origin_type, xmlns);
+               else
+                       log("debug", "Discarding %s from %s of type: %s", name, origin_type, stanza.attr.type);
+                       return true;
+               end
+       end
+       local handlers = stanza_handlers:get(host, origin_type, name, xmlns);
+       if handlers then
+               log("debug", "Passing stanza to mod_%s", handler_info[handlers[1]].name);
+               (handlers[1])(origin, stanza);
+               return true;
+       else
+               log("debug", "Stanza unhandled by any modules, xmlns: %s", stanza.attr.xmlns); -- we didn't handle it
+       end
+end
+
+function module_has_method(module, method)
+       return type(module.module[method]) == "function";
+end
+
+function call_module_method(module, method, ...)
+       if module_has_method(module, method) then       
+               local f = module.module[method];
+               return pcall(f, ...);
+       else
+               return false, "no-such-method";
+       end
+end
+
+local _modulepath = { plugin_dir, "mod_", nil, ".lua"};
+function get_module_filename(name)
+       _modulepath[3] = name;
+       return t_concat(_modulepath);
 end
 
 ----- API functions exposed to modules -----------
@@ -162,66 +261,87 @@ function api:get_host()
        return self.host;
 end
 
+function api:get_host_type()
+       return hosts[self.host].type;
+end
 
-local function _add_iq_handler(module, origin_type, xmlns, handler)
-       local handlers = stanza_handlers[module.host];
-       handlers[origin_type] = handlers[origin_type] or {};
-       handlers[origin_type].iq = handlers[origin_type].iq or {};
-       if not handlers[origin_type].iq[xmlns] then
-               handlers[origin_type].iq[xmlns]= handler;
+function api:set_global()
+       self.host = "*";
+end
+
+local function _add_handler(module, origin_type, tag, xmlns, handler)
+       local handlers = stanza_handlers:get(module.host, origin_type, tag, xmlns);
+       local msg = (tag == "iq") and "namespace" or "payload namespace";
+       if not handlers then
+               stanza_handlers:add(module.host, origin_type, tag, xmlns, handler);
                handler_info[handler] = module;
-               module:log("debug", "I now handle tag 'iq' [%s] with payload namespace '%s'", origin_type, xmlns);
+               handler_table:add(module.host, module.name, {module.host, origin_type, tag, xmlns});
+               --module:log("debug", "I now handle tag '%s' [%s] with %s '%s'", tag, origin_type, msg, xmlns);
        else
-               module:log("warn", "I wanted to handle tag 'iq' [%s] with payload namespace '%s' but mod_%s already handles that", origin_type, xmlns, handler_info[handlers[origin_type].iq[xmlns]].name);
+               module:log("warn", "I wanted to handle tag '%s' [%s] with %s '%s' but mod_%s already handles that", tag, origin_type, msg, xmlns, handler_info[handlers[1]].module.name);
        end
 end
 
-function api:add_iq_handler(origin_type, xmlns, handler)
-       if not (origin_type and handler and xmlns) then return false; end
+function api:add_handler(origin_type, tag, xmlns, handler)
+       if not (origin_type and tag and xmlns and handler) then return false; end
        if type(origin_type) == "table" then
                for _, origin_type in ipairs(origin_type) do
-                       _add_iq_handler(self, origin_type, xmlns, handler);
+                       _add_handler(self, origin_type, tag, xmlns, handler);
                end
-               return;
+       else
+               _add_handler(self, origin_type, tag, xmlns, handler);
        end
-       _add_iq_handler(self, origin_type, xmlns, handler);
 end
-
-function api:add_feature(xmlns)
-       addDiscoInfoHandler(self.host, function(reply, to, from, node)
-               if #node == 0 then
-                       reply:tag("feature", {var = xmlns}):up();
-                       return true;
-               end
-       end);
+function api:add_iq_handler(origin_type, xmlns, handler)
+       self:add_handler(origin_type, "iq", xmlns, handler);
 end
 
-api.add_event_hook = eventmanager.add_event_hook;
-
-local function _add_handler(module, origin_type, tag, xmlns, handler)
-       local handlers = stanza_handlers[module.host];
-       handlers[origin_type] = handlers[origin_type] or {};
-       if not handlers[origin_type][tag] then
-               handlers[origin_type][tag] = handlers[origin_type][tag] or {};
-               handlers[origin_type][tag][xmlns]= handler;
-               handler_info[handler] = module;
-               module:log("debug", "I now handle tag '%s' [%s] with xmlns '%s'", tag, origin_type, xmlns);
-       elseif handler_info[handlers[origin_type][tag]] then
-               log("warning", "I wanted to handle tag '%s' [%s] but mod_%s already handles that", tag, origin_type, handler_info[handlers[origin_type][tag]].module.name);
+addDiscoInfoHandler("*host", function(reply, to, from, node)
+       if #node == 0 then
+               local done = {};
+               for module, features in pairs(features_table:get(to) or NULL) do -- for each module
+                       for feature in pairs(features) do
+                               if not done[feature] then
+                                       reply:tag("feature", {var = feature}):up(); -- TODO cache
+                                       done[feature] = true;
+                               end
+                       end
+               end
+               return next(done) ~= nil;
        end
+end);
+
+function api:add_feature(xmlns)
+       features_table:set(self.host, self.name, xmlns, true);
 end
 
-function api:add_handler(origin_type, tag, xmlns, handler)
-       if not (origin_type and tag and xmlns and handler) then return false; end
-       if type(origin_type) == "table" then
-               for _, origin_type in ipairs(origin_type) do
-                       _add_handler(self, origin_type, tag, xmlns, handler);
-               end
-               return;
+local event_hook = function(host, mod_name, event_name, ...)
+       if type((...)) == "table" and (...).host and (...).host ~= host then return; end
+       for handler in pairs(event_hooks:get(host, mod_name, event_name) or NULL) do
+               handler(...);
+       end
+end;
+function api:add_event_hook(name, handler)
+       if not hooked:get(self.host, self.name, name) then
+               eventmanager.add_event_hook(name, function(...) event_hook(self.host, self.name, name, ...); end);
+               hooked:set(self.host, self.name, name, true);
        end
-       _add_handler(self, origin_type, tag, xmlns, handler);
+       event_hooks:set(self.host, self.name, name, handler, true);
 end
 
 --------------------------------------------------------------------
 
+local actions = {};
+
+function actions.load(params)
+       --return true, "Module loaded ("..params.module.." on "..params.host..")";
+       return load(params.host, params.module);
+end
+
+function actions.unload(params)
+       return unload(params.host, params.module);
+end
+
+register_actions("/modules", actions);
+
 return _M;