Software /
code /
prosody
Comparison
plugins/mod_privacy.lua @ 2496:f18b882af1d1
mod_privacy: Imported from prosody-modules.
author | Waqas Hussain <waqas20@gmail.com> |
---|---|
date | Tue, 26 Jan 2010 01:32:39 +0500 |
parent | 1522:569d58d21612 |
child | 2498:3d08a6cf57ea |
comparison
equal
deleted
inserted
replaced
2495:9c32f469b82c | 2496:f18b882af1d1 |
---|---|
1 -- Prosody IM | 1 -- Prosody IM |
2 -- Copyright (C) 2008-2009 Matthew Wild | 2 -- Copyright (C) 2008-2009 Matthew Wild |
3 -- Copyright (C) 2008-2009 Waqas Hussain | 3 -- Copyright (C) 2008-2009 Waqas Hussain |
4 -- Copyright (C) 2009 Thilo Cestonaro | |
4 -- | 5 -- |
5 -- This project is MIT/X11 licensed. Please see the | 6 -- This project is MIT/X11 licensed. Please see the |
6 -- COPYING file in the source package for more information. | 7 -- COPYING file in the source package for more information. |
7 -- | 8 -- |
8 | 9 |
9 | 10 local prosody = prosody; |
10 local st = require "util.stanza"; | 11 local st = require "util.stanza"; |
11 local datamanager = require "util.datamanager"; | 12 local datamanager = require "util.datamanager"; |
13 local bare_sessions = bare_sessions; | |
14 local util_Jid = require "util.jid"; | |
15 local jid_bare = util_Jid.bare; | |
16 local jid_split = util_Jid.split; | |
17 local load_roster = require "core.rostermanager".load_roster; | |
18 local to_number = _G.tonumber; | |
19 | |
20 function findNamedList (privacy_lists, name) | |
21 local ret = nil | |
22 if privacy_lists.lists == nil then | |
23 return nil; | |
24 end | |
25 | |
26 for i=1, #privacy_lists.lists do | |
27 if privacy_lists.lists[i].name == name then | |
28 ret = i; | |
29 break; | |
30 end | |
31 end | |
32 return ret; | |
33 end | |
34 | |
35 function isListUsed(origin, name, privacy_lists) | |
36 if bare_sessions[origin.username.."@"..origin.host].sessions ~= nil then | |
37 for resource, session in pairs(bare_sessions[origin.username.."@"..origin.host].sessions) do | |
38 if resource ~= origin.resource then | |
39 if session.activePrivacyList == name then | |
40 return true; | |
41 elseif session.activePrivacyList == nil and privacy_lists.default == name then | |
42 return true; | |
43 end | |
44 end | |
45 end | |
46 end | |
47 return false; | |
48 end | |
49 | |
50 function isAnotherSessionUsingDefaultList(origin) | |
51 local ret = false | |
52 if bare_sessions[origin.username.."@"..origin.host].sessions ~= nil then | |
53 for resource, session in pairs(bare_sessions[origin.username.."@"..origin.host].sessions) do | |
54 if resource ~= origin.resource and session.activePrivacyList == nil then | |
55 ret = true; | |
56 break; | |
57 end | |
58 end | |
59 end | |
60 return ret; | |
61 end | |
62 | |
63 function sendUnavailable(to, from) | |
64 --[[ example unavailable presence stanza | |
65 <presence from="node@host/resource" type="unavailable" to="node@host" > | |
66 <status>Logged out</status> | |
67 </presence> | |
68 ]]-- | |
69 local presence = st.presence({from=from, type="unavailable"}) | |
70 presence:tag("status"):text("Logged out"); | |
71 | |
72 local node, host = jid_bare(to); | |
73 local bare = node .. "@" .. host; | |
74 | |
75 if bare_sessions[bare].sessions ~= nil then | |
76 for resource, session in pairs(bare_sessions[bare].sessions) do | |
77 presence.attr.to = session.full_jid; | |
78 module:log("debug", "send unavailable to: %s; from: %s", tostring(presence.attr.to), tostring(presence.attr.from)); | |
79 origin.send(presence); | |
80 end | |
81 end | |
82 end | |
83 | |
84 function sendNeededUnavailablePersences(origin, listnameOrItem) -- TODO implement it correctly! | |
85 if type(listnameOrItem) == "string" then | |
86 local listname = listnameOrItem; | |
87 for _,list in ipairs(privacy_lists.lists) do | |
88 if list.name == listname then | |
89 for _,item in ipairs(list.items) do | |
90 sendNeededUnavailablePersences(origin, item); | |
91 end | |
92 end | |
93 end | |
94 elseif type(listnameOrItem) == "table" then | |
95 module:log("debug", "got an item, check wether to send unavailable presence stanza or not"); | |
96 local item = listnameOrItem; | |
97 local serialize = require "util.serialization".serialize; | |
98 | |
99 | |
100 if item["presence-out"] == true then | |
101 if item.type == "jid" then | |
102 sendUnavailable(item.value, origin.full_jid); | |
103 elseif item.type == "group" then | |
104 elseif item.type == "subscription" then | |
105 elseif item.type == nil then | |
106 end | |
107 elseif item["presence-in"] == true then | |
108 if item.type == "jid" then | |
109 sendUnavailable(origin.full_jid, item.value); | |
110 elseif item.type == "group" then | |
111 elseif item.type == "subscription" then | |
112 elseif item.type == nil then | |
113 end | |
114 end | |
115 else | |
116 module:log("debug", "got unknown type: %s", type(listnameOrItem)); | |
117 end | |
118 end | |
119 | |
120 function declineList (privacy_lists, origin, stanza, which) | |
121 if which == "default" then | |
122 if isAnotherSessionUsingDefaultList(origin) then | |
123 return { "cancel", "conflict", "Another session is online and using the default list."}; | |
124 end | |
125 privacy_lists.default = nil; | |
126 origin.send(st.reply(stanza)); | |
127 elseif which == "active" then | |
128 origin.activePrivacyList = nil; | |
129 origin.send(st.reply(stanza)); | |
130 else | |
131 return {"modify", "bad-request", "Neither default nor active list specifed to decline."}; | |
132 end | |
133 return true; | |
134 end | |
135 | |
136 function activateList (privacy_lists, origin, stanza, which, name) | |
137 local idx = findNamedList(privacy_lists, name); | |
138 | |
139 if privacy_lists.default == nil then | |
140 privacy_lists.default = ""; | |
141 end | |
142 if origin.activePrivacyList == nil then | |
143 origin.activePrivacyList = ""; | |
144 end | |
145 | |
146 if which == "default" and idx ~= nil then | |
147 if isAnotherSessionUsingDefaultList(origin) then | |
148 return {"cancel", "conflict", "Another session is online and using the default list."}; | |
149 end | |
150 privacy_lists.default = name; | |
151 origin.send(st.reply(stanza)); | |
152 --[[ | |
153 if origin.activePrivacyList == nil then | |
154 sendNeededUnavailablePersences(origin, name); | |
155 end | |
156 ]]-- | |
157 elseif which == "active" and idx ~= nil then | |
158 origin.activePrivacyList = name; | |
159 origin.send(st.reply(stanza)); | |
160 -- sendNeededUnavailablePersences(origin, name); | |
161 else | |
162 return {"modify", "bad-request", "Either not active or default given or unknown list name specified."}; | |
163 end | |
164 return true; | |
165 end | |
166 | |
167 function deleteList (privacy_lists, origin, stanza, name) | |
168 local idx = findNamedList(privacy_lists, name); | |
169 | |
170 if idx ~= nil then | |
171 if isListUsed(origin, name, privacy_lists) then | |
172 return {"cancel", "conflict", "Another session is online and using the list which should be deleted."}; | |
173 end | |
174 if privacy_lists.default == name then | |
175 privacy_lists.default = ""; | |
176 end | |
177 if origin.activePrivacyList == name then | |
178 origin.activePrivacyList = ""; | |
179 end | |
180 table.remove(privacy_lists.lists, idx); | |
181 origin.send(st.reply(stanza)); | |
182 return true; | |
183 end | |
184 return {"modify", "bad-request", "Not existing list specifed to be deleted."}; | |
185 end | |
186 | |
187 local function sortByOrder(a, b) | |
188 if a.order < b.order then | |
189 return true; | |
190 end | |
191 return false; | |
192 end | |
193 | |
194 function createOrReplaceList (privacy_lists, origin, stanza, name, entries, roster) | |
195 local idx = findNamedList(privacy_lists, name); | |
196 local bare_jid = origin.username.."@"..origin.host; | |
197 | |
198 if privacy_lists.lists == nil then | |
199 privacy_lists.lists = {}; | |
200 end | |
201 | |
202 if idx == nil then | |
203 idx = #privacy_lists.lists + 1; | |
204 end | |
205 | |
206 local orderCheck = {}; | |
207 local list = {}; | |
208 list.name = name; | |
209 list.items = {}; | |
210 | |
211 for _,item in ipairs(entries) do | |
212 if to_number(item.attr.order) == nil or to_number(item.attr.order) < 0 or orderCheck[item.attr.order] ~= nil then | |
213 return {"modify", "bad-request", "Order attribute not valid."}; | |
214 end | |
215 | |
216 if item.attr.type ~= nil and item.attr.type ~= "jid" and item.attr.type ~= "subscription" and item.attr.type ~= "group" then | |
217 return {"modify", "bad-request", "Type attribute not valid."}; | |
218 end | |
219 | |
220 local tmp = {}; | |
221 orderCheck[item.attr.order] = true; | |
222 | |
223 tmp["type"] = item.attr.type; | |
224 tmp["value"] = item.attr.value; | |
225 tmp["action"] = item.attr.action; | |
226 tmp["order"] = to_number(item.attr.order); | |
227 tmp["presence-in"] = false; | |
228 tmp["presence-out"] = false; | |
229 tmp["message"] = false; | |
230 tmp["iq"] = false; | |
231 | |
232 if #item.tags > 0 then | |
233 for _,tag in ipairs(item.tags) do | |
234 tmp[tag.name] = true; | |
235 end | |
236 end | |
237 | |
238 if tmp.type == "group" then | |
239 local found = false; | |
240 local roster = load_roster(origin.username, origin.host); | |
241 for jid,item in pairs(roster) do | |
242 if item.groups ~= nil then | |
243 for group in pairs(item.groups) do | |
244 if group == tmp.value then | |
245 found = true; | |
246 break; | |
247 end | |
248 end | |
249 if found == true then | |
250 break; | |
251 end | |
252 end | |
253 end | |
254 if found == false then | |
255 return {"cancel", "item-not-found", "Specifed roster group not existing."}; | |
256 end | |
257 elseif tmp.type == "subscription" then | |
258 if tmp.value ~= "both" and | |
259 tmp.value ~= "to" and | |
260 tmp.value ~= "from" and | |
261 tmp.value ~= "none" then | |
262 return {"cancel", "bad-request", "Subscription value must be both, to, from or none."}; | |
263 end | |
264 end | |
265 | |
266 if tmp.action ~= "deny" and tmp.action ~= "allow" then | |
267 return {"cancel", "bad-request", "Action must be either deny or allow."}; | |
268 end | |
269 | |
270 --[[ | |
271 if (privacy_lists.default == name and origin.activePrivacyList == nil) or origin.activePrivacyList == name then | |
272 module:log("debug", "calling sendNeededUnavailablePresences!"); | |
273 -- item is valid and list is active, so send needed unavailable stanzas | |
274 sendNeededUnavailablePersences(origin, tmp); | |
275 end | |
276 ]]-- | |
277 list.items[#list.items + 1] = tmp; | |
278 end | |
279 | |
280 table.sort(list, sortByOrder); | |
281 | |
282 privacy_lists.lists[idx] = list; | |
283 origin.send(st.reply(stanza)); | |
284 if bare_sessions[bare_jid] ~= nil then | |
285 iq = st.iq ( { type = "set", id="push1" } ); | |
286 iq:tag ("query", { xmlns = "jabber:iq:privacy" } ); | |
287 iq:tag ("list", { name = list.name } ):up(); | |
288 iq:up(); | |
289 for resource, session in pairs(bare_sessions[bare_jid].sessions) do | |
290 iq.attr.to = bare_jid.."/"..resource | |
291 session.send(iq); | |
292 end | |
293 else | |
294 return {"cancel", "bad-request", "internal error."}; | |
295 end | |
296 return true; | |
297 end | |
298 | |
299 function getList(privacy_lists, origin, stanza, name) | |
300 local reply = st.reply(stanza); | |
301 reply:tag("query", {xmlns="jabber:iq:privacy"}); | |
302 | |
303 if name == nil then | |
304 reply:tag("active", {name=origin.activePrivacyList or ""}):up(); | |
305 reply:tag("default", {name=privacy_lists.default or ""}):up(); | |
306 if privacy_lists.lists then | |
307 for _,list in ipairs(privacy_lists.lists) do | |
308 reply:tag("list", {name=list.name}):up(); | |
309 end | |
310 end | |
311 else | |
312 local idx = findNamedList(privacy_lists, name); | |
313 if idx ~= nil then | |
314 list = privacy_lists.lists[idx]; | |
315 reply = reply:tag("list", {name=list.name}); | |
316 for _,item in ipairs(list.items) do | |
317 reply:tag("item", {type=item.type, value=item.value, action=item.action, order=item.order}); | |
318 if item["message"] then reply:tag("message"):up(); end | |
319 if item["iq"] then reply:tag("iq"):up(); end | |
320 if item["presence-in"] then reply:tag("presence-in"):up(); end | |
321 if item["presence-out"] then reply:tag("presence-out"):up(); end | |
322 reply:up(); | |
323 end | |
324 else | |
325 return {"cancel", "item-not-found", "Unknown list specified."}; | |
326 end | |
327 end | |
328 | |
329 origin.send(reply); | |
330 return true; | |
331 end | |
12 | 332 |
13 module:hook("iq/bare/jabber:iq:privacy:query", function(data) | 333 module:hook("iq/bare/jabber:iq:privacy:query", function(data) |
14 local origin, stanza = data.origin, data.stanza; | 334 local origin, stanza = data.origin, data.stanza; |
15 | 335 |
16 if not stanza.attr.to then -- only service requests to own bare JID | 336 if stanza.attr.to == nil then -- only service requests to own bare JID |
17 local query = stanza.tags[1]; -- the query element | 337 local query = stanza.tags[1]; -- the query element |
338 local valid = false; | |
18 local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {}; | 339 local privacy_lists = datamanager.load(origin.username, origin.host, "privacy") or {}; |
340 | |
19 if stanza.attr.type == "set" then | 341 if stanza.attr.type == "set" then |
20 -- TODO | 342 if #query.tags == 1 then -- the <query/> element MUST NOT include more than one child element |
343 for _,tag in ipairs(query.tags) do | |
344 if tag.name == "active" or tag.name == "default" then | |
345 if tag.attr.name == nil then -- Client declines the use of active / default list | |
346 valid = declineList(privacy_lists, origin, stanza, tag.name); | |
347 else -- Client requests change of active / default list | |
348 valid = activateList(privacy_lists, origin, stanza, tag.name, tag.attr.name); | |
349 end | |
350 elseif tag.name == "list" and tag.attr.name then -- Client adds / edits a privacy list | |
351 if #tag.tags == 0 then -- Client removes a privacy list | |
352 valid = deleteList(privacy_lists, origin, stanza, tag.attr.name); | |
353 else -- Client edits a privacy list | |
354 valid = createOrReplaceList(privacy_lists, origin, stanza, tag.attr.name, tag.tags); | |
355 end | |
356 end | |
357 end | |
358 end | |
21 elseif stanza.attr.type == "get" then | 359 elseif stanza.attr.type == "get" then |
22 if #query.tags == 0 then -- Client requests names of privacy lists from server | 360 local name = nil; |
23 -- TODO | 361 local listsToRetrieve = 0; |
24 elseif #query.tags == 1 and query.tags[1].name == "list" then -- Client requests a privacy list from server | 362 if #query.tags >= 1 then |
25 -- TODO | 363 for _,tag in ipairs(query.tags) do |
26 else | 364 if tag.name == "list" then -- Client requests a privacy list from server |
27 origin.send(st.error_reply(stanza, "modify", "bad-request")); | 365 name = tag.attr.name; |
28 end | 366 listsToRetrieve = listsToRetrieve + 1; |
29 end | 367 end |
30 end | 368 end |
31 end); | 369 end |
370 if listsToRetrieve == 0 or listsToRetrieve == 1 then | |
371 valid = getList(privacy_lists, origin, stanza, name); | |
372 end | |
373 end | |
374 | |
375 if valid ~= true then | |
376 if valid[0] == nil then | |
377 valid[0] = "cancel"; | |
378 end | |
379 if valid[1] == nil then | |
380 valid[1] = "bad-request"; | |
381 end | |
382 origin.send(st.error_reply(stanza, valid[0], valid[1], valid[2])); | |
383 else | |
384 datamanager.store(origin.username, origin.host, "privacy", privacy_lists); | |
385 end | |
386 return true; | |
387 end | |
388 return false; | |
389 end, 500); | |
390 | |
391 function checkIfNeedToBeBlocked(e, session) | |
392 local origin, stanza = e.origin, e.stanza; | |
393 local privacy_lists = datamanager.load(session.username, session.host, "privacy") or {}; | |
394 local bare_jid = session.username.."@"..session.host; | |
395 | |
396 module:log("debug", "stanza: %s, to: %s, from: %s", tostring(stanza.name), tostring(stanza.attr.to), tostring(stanza.attr.from)); | |
397 | |
398 if stanza.attr.to ~= nil and stanza.attr.from ~= nil then | |
399 if privacy_lists.lists == nil or | |
400 (session.activePrivacyList == nil or session.activePrivacyList == "") and | |
401 (privacy_lists.default == nil or privacy_lists.default == "") | |
402 then | |
403 return; -- Nothing to block, default is Allow all | |
404 end | |
405 if jid_bare(stanza.attr.from) == bare_jid and jid_bare(stanza.attr.to) == bare_jid then | |
406 module:log("debug", "Never block communications from one of a user's resources to another."); | |
407 return; -- from one of a user's resource to another => HANDS OFF! | |
408 end | |
409 | |
410 local idx; | |
411 local list; | |
412 local item; | |
413 local listname = session.activePrivacyList; | |
414 if listname == nil or listname == "" then | |
415 listname = privacy_lists.default; -- no active list selected, use default list | |
416 end | |
417 idx = findNamedList(privacy_lists, listname); | |
418 if idx == nil then | |
419 module:log("error", "given privacy listname not found. name: %s", listname); | |
420 return; | |
421 end | |
422 list = privacy_lists.lists[idx]; | |
423 if list == nil then | |
424 module:log("info", "privacy list index wrong. index: %d", idx); | |
425 return; | |
426 end | |
427 for _,item in ipairs(list.items) do | |
428 local apply = false; | |
429 local block = false; | |
430 if ( | |
431 (stanza.name == "message" and item.message) or | |
432 (stanza.name == "iq" and item.iq) or | |
433 (stanza.name == "presence" and jid_bare(stanza.attr.to) == bare_jid and item["presence-in"]) or | |
434 (stanza.name == "presence" and jid_bare(stanza.attr.from) == bare_jid and item["presence-out"]) or | |
435 (item.message == false and item.iq == false and item["presence-in"] == false and item["presence-in"] == false) | |
436 ) then | |
437 apply = true; | |
438 end | |
439 if apply then | |
440 local evilJid = {}; | |
441 apply = false; | |
442 if jid_bare(stanza.attr.to) == bare_jid then | |
443 module:log("debug", "evil jid is (from): %s", stanza.attr.from); | |
444 evilJid.node, evilJid.host, evilJid.resource = jid_split(stanza.attr.from); | |
445 else | |
446 module:log("debug", "evil jid is (to): %s", stanza.attr.to); | |
447 evilJid.node, evilJid.host, evilJid.resource = jid_split(stanza.attr.to); | |
448 end | |
449 if item.type == "jid" and | |
450 (evilJid.node and evilJid.host and evilJid.resource and item.value == evilJid.node.."@"..evilJid.host.."/"..evilJid.resource) or | |
451 (evilJid.node and evilJid.host and item.value == evilJid.node.."@"..evilJid.host) or | |
452 (evilJid.host and evilJid.resource and item.value == evilJid.host.."/"..evilJid.resource) or | |
453 (evilJid.host and item.value == evilJid.host) then | |
454 apply = true; | |
455 block = (item.action == "deny"); | |
456 elseif item.type == "group" then | |
457 local roster = load_roster(session.username, session.host); | |
458 local groups = roster[evilJid.node .. "@" .. evilJid.host].groups; | |
459 for group in pairs(groups) do | |
460 if group == item.value then | |
461 apply = true; | |
462 block = (item.action == "deny"); | |
463 break; | |
464 end | |
465 end | |
466 elseif item.type == "subscription" and evilJid.node ~= nil and evilJid.host ~= nil then -- we need a valid bare evil jid | |
467 local roster = load_roster(session.username, session.host); | |
468 if roster[evilJid.node .. "@" .. evilJid.host].subscription == item.value then | |
469 apply = true; | |
470 block = (item.action == "deny"); | |
471 end | |
472 elseif item.type == nil then | |
473 apply = true; | |
474 block = (item.action == "deny"); | |
475 end | |
476 end | |
477 if apply then | |
478 if block then | |
479 module:log("info", "stanza blocked: %s, to: %s, from: %s", tostring(stanza.name), tostring(stanza.attr.to), tostring(stanza.attr.from)); | |
480 if stanza.name == "message" then | |
481 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
482 elseif stanza.name == "iq" and (stanza.attr.type == "get" or stanza.attr.type == "set") then | |
483 origin.send(st.error_reply(stanza, "cancel", "service-unavailable")); | |
484 end | |
485 return true; -- stanza blocked ! | |
486 else | |
487 module:log("info", "stanza explicit allowed!") | |
488 return; | |
489 end | |
490 end | |
491 end | |
492 end | |
493 return; | |
494 end | |
495 | |
496 function preCheckIncoming(e) | |
497 local session; | |
498 if e.stanza.attr.to ~= nil then | |
499 local node, host, resource = jid_split(e.stanza.attr.to); | |
500 if node == nil or host == nil then | |
501 return; | |
502 end | |
503 if resource == nil then | |
504 local prio = 0; | |
505 local session_; | |
506 if bare_sessions[node.."@"..host] ~= nil then | |
507 for resource, session_ in pairs(bare_sessions[node.."@"..host].sessions) do | |
508 if session_.priority ~= nil and session_.priority > prio then | |
509 session = session_; | |
510 prio = session_.priority; | |
511 end | |
512 end | |
513 end | |
514 else | |
515 session = full_sessions[node.."@"..host.."/"..resource]; | |
516 end | |
517 if session ~= nil then | |
518 return checkIfNeedToBeBlocked(e, session); | |
519 else | |
520 module:log("debug", "preCheckIncoming: Couldn't get session for jid: %s@%s/%s", tostring(node), tostring(host), tostring(resource)) | |
521 end | |
522 end | |
523 return; | |
524 end | |
525 | |
526 function preCheckOutgoing(e) | |
527 local session = e.origin; | |
528 if e.stanza.attr.from == nil then | |
529 e.stanza.attr.from = session.username .. "@" .. session.host; | |
530 if session.resource ~= nil then | |
531 e.stanza.attr.from = e.stanza.attr.from .. "/" .. session.resource; | |
532 end | |
533 end | |
534 return checkIfNeedToBeBlocked(e, session); | |
535 end | |
536 | |
537 | |
538 module:hook("pre-message/full", preCheckOutgoing, 500); | |
539 module:hook("pre-message/bare", preCheckOutgoing, 500); | |
540 module:hook("pre-message/host", preCheckOutgoing, 500); | |
541 module:hook("pre-iq/full", preCheckOutgoing, 500); | |
542 module:hook("pre-iq/bare", preCheckOutgoing, 500); | |
543 module:hook("pre-iq/host", preCheckOutgoing, 500); | |
544 module:hook("pre-presence/full", preCheckOutgoing, 500); | |
545 module:hook("pre-presence/bare", preCheckOutgoing, 500); | |
546 module:hook("pre-presence/host", preCheckOutgoing, 500); | |
547 | |
548 module:hook("message/full", preCheckIncoming, 500); | |
549 module:hook("message/bare", preCheckIncoming, 500); | |
550 module:hook("message/host", preCheckIncoming, 500); | |
551 module:hook("iq/full", preCheckIncoming, 500); | |
552 module:hook("iq/bare", preCheckIncoming, 500); | |
553 module:hook("iq/host", preCheckIncoming, 500); | |
554 module:hook("presence/full", preCheckIncoming, 500); | |
555 module:hook("presence/bare", preCheckIncoming, 500); | |
556 module:hook("presence/host", preCheckIncoming, 500); | |
557 | |
558 module:log("info", "mod_privacy loaded ..."); |