2 -- Copyright (C) 2008-2012 Matthew Wild
3 -- Copyright (C) 2008-2012 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
9 local array
= require
"util.array";
10 local set
= require
"util.set";
11 local it
= require
"util.iterators";
12 local logger
= require
"util.logger";
13 local pluginloader
= require
"util.pluginloader";
14 local timer
= require
"util.timer";
15 local resolve_relative_path
= require
"util.paths".resolve_relative_path
;
16 local st
= require
"util.stanza";
17 local cache
= require
"util.cache";
18 local errutil
= require
"util.error";
19 local promise
= require
"util.promise";
20 local time_now
= require
"util.time".now
;
21 local format = require
"util.format".format;
23 local t_insert
, t_remove
, t_concat
= table.insert
, table.remove, table.concat
;
24 local error, setmetatable
, type = error, setmetatable
, type;
25 local ipairs
, pairs
, select
= ipairs
, pairs
, select
;
26 local tonumber, tostring = tonumber, tostring;
27 local require
= require
;
28 local pack
= table.pack
or require
"util.table".pack
; -- table.pack is only in 5.2
29 local unpack
= table.unpack
or unpack
; --luacheck: ignore 113 -- renamed in 5.2
31 local prosody
= prosody
;
32 local hosts
= prosody
.hosts
;
34 -- FIXME: This assert() is to try and catch an obscure bug (2013-04-05)
35 local core_post_stanza
= assert(prosody
.core_post_stanza
,
36 "prosody.core_post_stanza is nil, please report this as a bug");
38 -- Registry of shared module data
39 local shared_data
= setmetatable({}, { __mode
= "v" });
45 -- Returns the name of the current module
46 function api
:get_name()
50 -- Returns the host that the current module is serving
51 function api
:get_host()
55 function api
:get_host_type()
56 return (self
.host
== "*" and "global") or hosts
[self
.host
].type or "local";
59 function api
:set_global()
62 local _log
= logger
.init("mod_"..self
.name
);
63 self
.log = function (self
, ...) return _log(...); end; --luacheck: ignore self
68 function api
:add_feature(xmlns
)
69 self
:add_item("feature", xmlns
);
71 function api
:add_identity(category
, identity_type
, name
)
72 self
:add_item("identity", {category
= category
, type = identity_type
, name
= name
});
74 function api
:add_extension(data
)
75 self
:add_item("extension", data
);
78 function api
:fire_event(...)
79 return (hosts
[self
.host
] or prosody
).events
.fire_event(...);
82 function api
:hook_object_event(object
, event
, handler
, priority
)
83 self
.event_handlers
:set(object
, event
, handler
, true);
84 return object
.add_handler(event
, handler
, priority
);
87 function api
:unhook_object_event(object
, event
, handler
)
88 self
.event_handlers
:set(object
, event
, handler
, nil);
89 return object
.remove_handler(event
, handler
);
92 function api
:hook(event
, handler
, priority
)
93 return self
:hook_object_event((hosts
[self
.host
] or prosody
).events
, event
, handler
, priority
);
96 function api
:hook_global(event
, handler
, priority
)
97 return self
:hook_object_event(prosody
.events
, event
, handler
, priority
);
100 function api
:hook_tag(xmlns
, name
, handler
, priority
)
101 if not handler
and type(name
) == "function" then
102 -- If only 2 options then they specified no xmlns
103 xmlns
, name
, handler
, priority
= nil, xmlns
, name
, handler
;
104 elseif not (handler
and name
) then
105 self
:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
108 return self
:hook("stanza/"..(xmlns
and (xmlns
..":") or "")..name
,
109 function (data
) return handler(data
.origin
, data
.stanza
, data
); end, priority
);
111 api
.hook_stanza
= api
.hook_tag
; -- COMPAT w/pre-0.9
113 function api
:unhook(event
, handler
)
114 return self
:unhook_object_event((hosts
[self
.host
] or prosody
).events
, event
, handler
);
117 function api
:wrap_object_event(events_object
, event
, handler
)
118 return self
:hook_object_event(assert(events_object
.wrappers
, "no wrappers"), event
, handler
);
121 function api
:wrap_event(event
, handler
)
122 return self
:wrap_object_event((hosts
[self
.host
] or prosody
).events
, event
, handler
);
125 function api
:wrap_global(event
, handler
)
126 return self
:hook_object_event(prosody
.events
, event
, handler
);
129 function api
:require(lib
)
130 local f
, n
= pluginloader
.load_code_ext(self
.name
, lib
, "lib.lua", self
.environment
);
131 if not f
then error("Failed to load plugin library '"..lib
.."', error: "..n
); end -- FIXME better error message
135 function api
:depends(name
)
136 local modulemanager
= require
"core.modulemanager";
137 if self
:get_option_inherited_set("modules_disabled", {}):contains(name
) then
138 error("Dependency on disabled module mod_"..name
);
140 if not self
.dependencies
then
141 self
.dependencies
= {};
142 self
:hook("module-reloaded", function (event
)
143 if self
.dependencies
[event
.module
] and not self
.reloading
then
144 self
:log("info", "Auto-reloading due to reload of %s:%s", event
.host
, event
.module
);
145 modulemanager
.reload(self
.host
, self
.name
);
149 self
:hook("module-unloaded", function (event
)
150 if self
.dependencies
[event
.module
] then
151 self
:log("info", "Auto-unloading due to unload of %s:%s", event
.host
, event
.module
);
152 modulemanager
.unload(self
.host
, self
.name
);
156 local mod = modulemanager
.get_module(self
.host
, name
) or modulemanager
.get_module("*", name
);
157 if mod and mod.module
.host
== "*" and self
.host
~= "*"
158 and modulemanager
.module_has_method(mod, "add_host") then
159 mod = nil; -- Target is a shared module, so we still want to load it on our host
163 mod, err
= modulemanager
.load(self
.host
, name
);
165 return error(("Unable to load required module, mod_%s: %s"):format(name
, ((err
or "unknown error"):gsub("%-", " ")) ));
168 self
.dependencies
[name
] = true;
172 local function get_shared_table_from_path(module
, tables
, path
)
173 if path
:sub(1,1) ~= "/" then -- Prepend default components
174 local default_path_components
= { module
.host
, module
.name
};
175 local n_components
= select(2, path
:gsub("/", "%1"));
176 path
= (n_components
<#default_path_components
and "/" or "")
177 ..t_concat(default_path_components
, "/", 1, #default_path_components
-n_components
).."/"..path
;
179 local shared
= tables
[path
];
182 if path
:match("%-cache$") then
183 setmetatable(shared
, { __mode
= "kv" });
185 tables
[path
] = shared
;
190 -- Returns a shared table at the specified virtual path
191 -- Intentionally does not allow the table to be _set_, it
192 -- is auto-created if it does not exist.
193 function api
:shared(path
)
194 if not self
.shared_data
then self
.shared_data
= {}; end
195 local shared
= get_shared_table_from_path(self
, shared_data
, path
);
196 self
.shared_data
[path
] = shared
;
200 function api
:get_option(name
, default_value
)
201 local config
= require
"core.configmanager";
202 local value
= config
.get(self
.host
, name
);
204 value
= default_value
;
209 function api
:get_option_scalar(name
, default_value
)
210 local value
= self
:get_option(name
, default_value
);
211 if type(value
) == "table" then
213 self
:log("error", "Config option '%s' does not take a list, using just the first item", name
);
220 function api
:get_option_string(name
, default_value
)
221 local value
= self
:get_option_scalar(name
, default_value
);
225 return tostring(value
);
228 function api
:get_option_number(name
, ...)
229 local value
= self
:get_option_scalar(name
, ...);
230 local ret
= tonumber(value
);
231 if value
~= nil and ret
== nil then
232 self
:log("error", "Config option '%s' not understood, expecting a number", name
);
237 function api
:get_option_boolean(name
, ...)
238 local value
= self
:get_option_scalar(name
, ...);
242 local ret
= value
== true or value
== "true" or value
== 1 or nil;
244 ret
= (value
== false or value
== "false" or value
== 0);
252 self
:log("error", "Config option '%s' not understood, expecting true/false", name
);
257 function api
:get_option_array(name
, ...)
258 local value
= self
:get_option(name
, ...);
264 if type(value
) ~= "table" then
265 return array
{ value
}; -- Assume any non-list is a single-item list
268 return array():append(value
); -- Clone
271 function api
:get_option_set(name
, ...)
272 local value
= self
:get_option_array(name
, ...);
278 return set
.new(value
);
281 function api
:get_option_inherited_set(name
, ...)
282 local value
= self
:get_option_set(name
, ...);
283 local global_value
= self
:context("*"):get_option_set(name
, ...);
286 elseif not global_value
then
289 value
:include(global_value
);
293 function api
:get_option_path(name
, default
, parent
)
294 if parent
== nil then
295 parent
= self
:get_directory();
296 elseif prosody
.paths
[parent
] then
297 parent
= prosody
.paths
[parent
];
299 local value
= self
:get_option_string(name
, default
);
303 return resolve_relative_path(parent
, value
);
307 function api
:context(host
)
308 return setmetatable({host
=host
or "*"}, {__index
=self
,__newindex
=self
});
311 function api
:add_item(key
, value
)
312 self
.items
= self
.items
or {};
313 self
.items
[key
] = self
.items
[key
] or {};
314 t_insert(self
.items
[key
], value
);
315 self
:fire_event("item-added/"..key
, {source
= self
, item
= value
});
317 function api
:remove_item(key
, value
)
318 local t
= self
.items
and self
.items
[key
] or NULL
;
320 if t
[i
] == value
then
321 t_remove(self
.items
[key
], i
);
322 self
:fire_event("item-removed/"..key
, {source
= self
, item
= value
});
328 function api
:get_host_items(key
)
329 local modulemanager
= require
"core.modulemanager";
330 local result
= modulemanager
.get_items(key
, self
.host
) or {};
334 function api
:handle_items(item_type
, added_cb
, removed_cb
, existing
)
335 self
:hook("item-added/"..item_type
, added_cb
);
336 self
:hook("item-removed/"..item_type
, removed_cb
);
337 if existing
~= false then
338 for _
, item
in ipairs(self
:get_host_items(item_type
)) do
339 added_cb({ item
= item
});
344 function api
:provides(name
, item
)
345 -- if not item then item = setmetatable({}, { __index = function(t,k) return rawget(self.environment, k); end }); end
348 for k
,v
in pairs(self
.environment
) do
349 if k
~= "module" then item
[k
] = v
; end
352 if not item
.name
then
353 local item_name
= self
.name
;
354 -- Strip a provider prefix to find the item name
355 -- (e.g. "auth_foo" -> "foo" for an auth provider)
356 if item_name
:find(name
.."_", 1, true) == 1 then
357 item_name
= item_name
:sub(#name
+2);
359 item
.name
= item_name
;
361 item
._provided_by
= self
.name
;
362 self
:add_item(name
.."-provider", item
);
365 function api
:send(stanza
, origin
)
366 return core_post_stanza(origin
or hosts
[self
.host
], stanza
);
369 function api
:send_iq(stanza
, origin
, timeout
)
370 local iq_cache
= self
._iq_cache
;
372 iq_cache
= cache
.new(256, function (_
, iq
)
373 iq
.reject(errutil
.new({
374 type = "wait", condition
= "resource-constraint",
375 text
= "evicted from iq tracking cache"
378 self
._iq_cache
= iq_cache
;
382 if stanza
.attr
.from
== self
.host
then
384 else -- assume bare since we can't hook full jids
387 local result_event
= "iq-result/"..event_type
.."/"..stanza
.attr
.id
;
388 local error_event
= "iq-error/"..event_type
.."/"..stanza
.attr
.id
;
389 local cache_key
= event_type
.."/"..stanza
.attr
.id
;
391 local p
= promise
.new(function (resolve
, reject
)
392 local function result_handler(event
)
393 if event
.stanza
.attr
.from
== stanza
.attr
.to
then
399 local function error_handler(event
)
400 if event
.stanza
.attr
.from
== stanza
.attr
.to
then
401 reject(errutil
.from_stanza(event
.stanza
), event
);
406 if iq_cache
:get(cache_key
) then
408 type = "modify", condition
= "conflict",
409 text
= "IQ stanza id attribute already used",
414 self
:hook(result_event
, result_handler
);
415 self
:hook(error_event
, error_handler
);
417 local timeout_handle
= self
:add_timer(timeout
or 120, function ()
419 type = "wait", condition
= "remote-server-timeout",
420 text
= "IQ stanza timed out",
424 local ok
= iq_cache
:set(cache_key
, {
425 reject
= reject
, resolve
= resolve
,
426 timeout_handle
= timeout_handle
,
427 result_handler
= result_handler
, error_handler
= error_handler
;
432 type = "wait", condition
= "internal-server-error",
433 text
= "Could not store IQ tracking data"
438 self
:send(stanza
, origin
);
441 p
:finally(function ()
442 local iq
= iq_cache
:get(cache_key
);
444 self
:unhook(result_event
, iq
.result_handler
);
445 self
:unhook(error_event
, iq
.error_handler
);
446 iq
.timeout_handle
:stop();
447 iq_cache
:set(cache_key
, nil);
454 function api
:broadcast(jids
, stanza
, iter
)
455 for jid
in (iter
or it
.values
)(jids
) do
456 local new_stanza
= st
.clone(stanza
);
457 new_stanza
.attr
.to
= jid
;
458 self
:send(new_stanza
);
462 local timer_methods
= { }
464 __index
= timer_methods
;
466 function timer_methods
:stop( )
469 timer_methods
.disarm
= timer_methods
.stop
470 function timer_methods
:reschedule(delay
)
471 timer
.reschedule(self
.id
, delay
)
474 local function timer_callback(now
, id
, t
) --luacheck: ignore 212/id
475 if t
.module_env
.loaded
== false then return; end
476 return t
.callback(now
, unpack(t
, 1, t
.n
));
479 function api
:add_timer(delay
, callback
, ...)
482 t
.callback
= callback
;
483 t
.id
= timer
.add_task(delay
, timer_callback
, t
);
484 return setmetatable(t
, timer_mt
);
487 local path_sep
= package
.config
:sub(1,1);
488 function api
:get_directory()
489 return self
.path
and (self
.path
:gsub("%"..path_sep
.."[^"..path_sep
.."]*$", "")) or nil;
492 function api
:load_resource(path
, mode
)
493 path
= resolve_relative_path(self
:get_directory(), path
);
494 return io
.open(path
, mode
);
497 function api
:open_store(name
, store_type
)
498 return require
"core.storagemanager".open(self
.host
, name
or self
.name
, store_type
);
501 function api
:measure(name
, stat_type
)
502 local measure
= require
"core.statsmanager".measure
;
503 return measure(stat_type
, "/"..self
.host
.."/mod_"..self
.name
.."/"..name
);
506 function api
:measure_object_event(events_object
, event_name
, stat_name
)
507 local m
= self
:measure(stat_name
or event_name
, "times");
508 local function handler(handlers
, _event_name
, _event_data
)
509 local finished
= m();
510 local ret
= handlers(_event_name
, _event_data
);
514 return self
:hook_object_event(events_object
, event_name
, handler
);
517 function api
:measure_event(event_name
, stat_name
)
518 return self
:measure_object_event((hosts
[self
.host
] or prosody
).events
.wrappers
, event_name
, stat_name
);
521 function api
:measure_global_event(event_name
, stat_name
)
522 return self
:measure_object_event(prosody
.events
.wrappers
, event_name
, stat_name
);
525 local status_priorities
= { error = 3, warn
= 2, info
= 1, core
= 0 };
527 function api
:set_status(status_type
, status_message
, override
)
528 local priority
= status_priorities
[status_type
];
530 self
:log("error", "set_status: Invalid status type '%s', assuming 'info'");
531 status_type
, priority
= "info", status_priorities
.info
;
533 local current_priority
= status_priorities
[self
.status_type
] or 0;
534 -- By default an 'error' status can only be overwritten by another 'error' status
535 if (current_priority
>= status_priorities
.error and priority
< current_priority
and override
~= true)
536 or (override
== false and current_priority
> priority
) then
537 self
:log("debug", "moduleapi: ignoring status [prio %d override %s]: %s", priority
, override
, status_message
);
540 self
.status_type
, self
.status_message
, self
.status_time
= status_type
, status_message
, time_now();
541 self
:fire_event("module-status/updated", { name
= self
.name
});
544 function api
:log_status(level
, msg
, ...)
545 self
:set_status(level
, format(msg
, ...));
546 return self
:log(level
, msg
, ...);
549 function api
:get_status()
550 return self
.status_type
, self
.status_message
, self
.status_time
;