1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
|
-- Prosody IM
-- Copyright (C) 2008-2012 Matthew Wild
-- Copyright (C) 2008-2012 Waqas Hussain
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
--
local config = require "core.configmanager";
local modulemanager = require "modulemanager"; -- This is necessary to avoid require loops
local array = require "util.array";
local set = require "util.set";
local logger = require "util.logger";
local pluginloader = require "util.pluginloader";
local timer = require "util.timer";
local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat;
local error, setmetatable, type = error, setmetatable, type;
local ipairs, pairs, select, unpack = ipairs, pairs, select, unpack;
local tonumber, tostring = tonumber, tostring;
local prosody = prosody;
local hosts = prosody.hosts;
-- FIXME: This assert() is to try and catch an obscure bug (2013-04-05)
local core_post_stanza = assert(prosody.core_post_stanza,
"prosody.core_post_stanza is nil, please report this as a bug");
-- Registry of shared module data
local shared_data = setmetatable({}, { __mode = "v" });
local NULL = {};
local api = {};
-- Returns the name of the current module
function api:get_name()
return self.name;
end
-- Returns the host that the current module is serving
function api:get_host()
return self.host;
end
function api:get_host_type()
return self.host ~= "*" and hosts[self.host].type or nil;
end
function api:set_global()
self.host = "*";
-- Update the logger
local _log = logger.init("mod_"..self.name);
self.log = function (self, ...) return _log(...); end;
self._log = _log;
self.global = true;
end
function api:add_feature(xmlns)
self:add_item("feature", xmlns);
end
function api:add_identity(category, type, name)
self:add_item("identity", {category = category, type = type, name = name});
end
function api:add_extension(data)
self:add_item("extension", data);
end
function api:has_feature(xmlns)
for _, feature in ipairs(self:get_host_items("feature")) do
if feature == xmlns then return true; end
end
return false;
end
function api:has_identity(category, type, name)
for _, id in ipairs(self:get_host_items("identity")) do
if id.category == category and id.type == type and id.name == name then
return true;
end
end
return false;
end
function api:fire_event(...)
return (hosts[self.host] or prosody).events.fire_event(...);
end
function api:hook_object_event(object, event, handler, priority)
self.event_handlers:set(object, event, handler, true);
return object.add_handler(event, handler, priority);
end
function api:unhook_object_event(object, event, handler)
return object.remove_handler(event, handler);
end
function api:hook(event, handler, priority)
return self:hook_object_event((hosts[self.host] or prosody).events, event, handler, priority);
end
function api:hook_global(event, handler, priority)
return self:hook_object_event(prosody.events, event, handler, priority);
end
function api:hook_tag(xmlns, name, handler, priority)
if not handler and type(name) == "function" then
-- If only 2 options then they specified no xmlns
xmlns, name, handler, priority = nil, xmlns, name, handler;
elseif not (handler and name) then
self:log("warn", "Error: Insufficient parameters to module:hook_stanza()");
return;
end
return self:hook("stanza/"..(xmlns and (xmlns..":") or "")..name, function (data) return handler(data.origin, data.stanza, data); end, priority);
end
api.hook_stanza = api.hook_tag; -- COMPAT w/pre-0.9
function api:require(lib)
local f, n = pluginloader.load_code(self.name, lib..".lib.lua", self.environment);
if not f then
f, n = pluginloader.load_code(lib, lib..".lib.lua", self.environment);
end
if not f then error("Failed to load plugin library '"..lib.."', error: "..n); end -- FIXME better error message
return f();
end
function api:depends(name)
if not self.dependencies then
self.dependencies = {};
self:hook("module-reloaded", function (event)
if self.dependencies[event.module] and not self.reloading then
self:log("info", "Auto-reloading due to reload of %s:%s", event.host, event.module);
modulemanager.reload(self.host, self.name);
return;
end
end);
self:hook("module-unloaded", function (event)
if self.dependencies[event.module] then
self:log("info", "Auto-unloading due to unload of %s:%s", event.host, event.module);
modulemanager.unload(self.host, self.name);
end
end);
end
local mod = modulemanager.get_module(self.host, name) or modulemanager.get_module("*", name);
if mod and mod.module.host == "*" and self.host ~= "*"
and modulemanager.module_has_method(mod, "add_host") then
mod = nil; -- Target is a shared module, so we still want to load it on our host
end
if not mod then
local err;
mod, err = modulemanager.load(self.host, name);
if not mod then
return error(("Unable to load required module, mod_%s: %s"):format(name, ((err or "unknown error"):gsub("%-", " ")) ));
end
end
self.dependencies[name] = true;
return mod;
end
-- Returns one or more shared tables at the specified virtual paths
-- Intentionally does not allow the table at a path to be _set_, it
-- is auto-created if it does not exist.
function api:shared(...)
if not self.shared_data then self.shared_data = {}; end
local paths = { n = select("#", ...), ... };
local data_array = {};
local default_path_components = { self.host, self.name };
for i = 1, paths.n do
local path = paths[i];
if path:sub(1,1) ~= "/" then -- Prepend default components
local n_components = select(2, path:gsub("/", "%1"));
path = (n_components<#default_path_components and "/" or "")..t_concat(default_path_components, "/", 1, #default_path_components-n_components).."/"..path;
end
local shared = shared_data[path];
if not shared then
shared = {};
if path:match("%-cache$") then
setmetatable(shared, { __mode = "kv" });
end
shared_data[path] = shared;
end
t_insert(data_array, shared);
self.shared_data[path] = shared;
end
return unpack(data_array);
end
function api:get_option(name, default_value)
local value = config.get(self.host, name);
if value == nil then
value = default_value;
end
return value;
end
function api:get_option_string(name, default_value)
local value = self:get_option(name, default_value);
if type(value) == "table" then
if #value > 1 then
self:log("error", "Config option '%s' does not take a list, using just the first item", name);
end
value = value[1];
end
if value == nil then
return nil;
end
return tostring(value);
end
function api:get_option_number(name, ...)
local value = self:get_option(name, ...);
if type(value) == "table" then
if #value > 1 then
self:log("error", "Config option '%s' does not take a list, using just the first item", name);
end
value = value[1];
end
local ret = tonumber(value);
if value ~= nil and ret == nil then
self:log("error", "Config option '%s' not understood, expecting a number", name);
end
return ret;
end
function api:get_option_boolean(name, ...)
local value = self:get_option(name, ...);
if type(value) == "table" then
if #value > 1 then
self:log("error", "Config option '%s' does not take a list, using just the first item", name);
end
value = value[1];
end
if value == nil then
return nil;
end
local ret = value == true or value == "true" or value == 1 or nil;
if ret == nil then
ret = (value == false or value == "false" or value == 0);
if ret then
ret = false;
else
ret = nil;
end
end
if ret == nil then
self:log("error", "Config option '%s' not understood, expecting true/false", name);
end
return ret;
end
function api:get_option_array(name, ...)
local value = self:get_option(name, ...);
if value == nil then
return nil;
end
if type(value) ~= "table" then
return array{ value }; -- Assume any non-list is a single-item list
end
return array():append(value); -- Clone
end
function api:get_option_set(name, ...)
local value = self:get_option_array(name, ...);
if value == nil then
return nil;
end
return set.new(value);
end
function api:context(host)
return setmetatable({host=host or "*"}, {__index=self,__newindex=self});
end
function api:add_item(key, value)
self.items = self.items or {};
self.items[key] = self.items[key] or {};
t_insert(self.items[key], value);
self:fire_event("item-added/"..key, {source = self, item = value});
end
function api:remove_item(key, value)
local t = self.items and self.items[key] or NULL;
for i = #t,1,-1 do
if t[i] == value then
t_remove(self.items[key], i);
self:fire_event("item-removed/"..key, {source = self, item = value});
return value;
end
end
end
function api:get_host_items(key)
local result = modulemanager.get_items(key, self.host) or {};
return result;
end
function api:handle_items(type, added_cb, removed_cb, existing)
self:hook("item-added/"..type, added_cb);
self:hook("item-removed/"..type, removed_cb);
if existing ~= false then
for _, item in ipairs(self:get_host_items(type)) do
added_cb({ item = item });
end
end
end
function api:provides(name, item)
if not item then item = self.environment; end
if not item.name then
local item_name = self.name;
-- Strip a provider prefix to find the item name
-- (e.g. "auth_foo" -> "foo" for an auth provider)
if item_name:find(name.."_", 1, true) == 1 then
item_name = item_name:sub(#name+2);
end
item.name = item_name;
end
self:add_item(name.."-provider", item);
end
function api:send(stanza)
return core_post_stanza(hosts[self.host], stanza);
end
function api:add_timer(delay, callback)
return timer.add_task(delay, function (t)
if self.loaded == false then return; end
return callback(t);
end);
end
local path_sep = package.config:sub(1,1);
function api:get_directory()
return self.path and (self.path:gsub("%"..path_sep.."[^"..path_sep.."]*$", "")) or nil;
end
function api:load_resource(path, mode)
path = config.resolve_relative_path(self:get_directory(), path);
return io.open(path, mode);
end
function api:open_store(name, type)
return storagemanager.open(self.host, name or self.name, type);
end
return api;
|