2 -- Copyright (C) 2009-2010 Matthew Wild
3 -- Copyright (C) 2009-2010 Waqas Hussain
4 -- Copyright (C) 2014-2015 Kim Alvefur
6 -- This project is MIT/X11 licensed. Please see the
7 -- COPYING file in the source package for more information.
9 -- This module implements XEP-0191: Blocking Command
12 local user_exists
= require
"core.usermanager".user_exists
;
13 local rostermanager
= require
"core.rostermanager";
14 local is_contact_subscribed
= rostermanager
.is_contact_subscribed
;
15 local is_contact_pending_in
= rostermanager
.is_contact_pending_in
;
16 local load_roster
= rostermanager
.load_roster
;
17 local save_roster
= rostermanager
.save_roster
;
18 local st
= require
"util.stanza";
19 local st_error_reply
= st
.error_reply
;
20 local jid_prep
= require
"util.jid".prep
;
21 local jid_split
= require
"util.jid".split
;
23 local storage
= module
:open_store();
24 local sessions
= prosody
.hosts
[module
.host
].sessions
;
25 local full_sessions
= prosody
.full_sessions
;
27 -- First level cache of blocklists by username.
28 -- Weak table so may randomly expire at any time.
29 local cache
= setmetatable({}, { __mode
= "v" });
31 -- Second level of caching, keeps a fixed number of items, also anchors
32 -- items in the above cache.
34 -- The size of this affects how often we will need to load a blocklist from
35 -- disk, which we want to avoid during routing. On the other hand, we don't
36 -- want to use too much memory either, so this can be tuned by advanced
37 -- users. TODO use science to figure out a better default, 64 is just a guess.
38 local cache_size
= module
:get_option_number("blocklist_cache_size", 64);
39 local cache2
= require
"util.cache".new(cache_size
);
41 local null_blocklist
= {};
43 module
:add_feature("urn:xmpp:blocking");
45 local function set_blocklist(username
, blocklist
)
46 local ok
, err
= storage
:set(username
, blocklist
);
50 -- Successful save, update the cache
51 cache2
:set(username
, blocklist
);
52 cache
[username
] = blocklist
;
56 -- Migrates from the old mod_privacy storage
57 local function migrate_privacy_list(username
)
58 local legacy_data
= module
:open_store("privacy"):get(username
);
59 if not legacy_data
or not legacy_data
.lists
or not legacy_data
.default
then return; end
60 local default_list
= legacy_data
.lists
[legacy_data
.default
];
61 if not default_list
or not default_list
.items
then return; end
63 local migrated_data
= { [false] = { created
= os
.time(); migrated
= "privacy" }};
65 module
:log("info", "Migrating blocklist from mod_privacy storage for user '%s'", username
);
66 for _
, item
in ipairs(default_list
.items
) do
67 if item
.type == "jid" and item
.action
== "deny" then
68 local jid
= jid_prep(item
.value
);
70 module
:log("warn", "Invalid JID in privacy store for user '%s' not migrated: %s", username
, tostring(item
.value
));
72 migrated_data
[jid
] = true;
76 set_blocklist(username
, migrated_data
);
80 local function get_blocklist(username
)
81 local blocklist
= cache2
:get(username
);
83 if not user_exists(username
, module
.host
) then
84 return null_blocklist
;
86 blocklist
= storage
:get(username
);
88 blocklist
= migrate_privacy_list(username
);
91 blocklist
= { [false] = { created
= os
.time(); }; };
93 cache2
:set(username
, blocklist
);
95 cache
[username
] = blocklist
;
99 module
:hook("iq-get/self/urn:xmpp:blocking:blocklist", function (event
)
100 local origin
, stanza
= event
.origin
, event
.stanza
;
101 local username
= origin
.username
;
102 local reply
= st
.reply(stanza
):tag("blocklist", { xmlns
= "urn:xmpp:blocking" });
103 local blocklist
= cache
[username
] or get_blocklist(username
);
104 for jid
in pairs(blocklist
) do
106 reply
:tag("item", { jid
= jid
}):up();
109 origin
.interested_blocklist
= true; -- Gets notified about changes
114 -- Add or remove some jid(s) from the blocklist
115 -- We want this to be atomic and not do a partial update
116 local function edit_blocklist(event
)
117 local now
= os
.time();
118 local origin
, stanza
= event
.origin
, event
.stanza
;
119 local username
= origin
.username
;
120 local action
= stanza
.tags
[1]; -- "block" or "unblock"
121 local is_blocking
= action
.name
== "block" and now
or nil; -- nil if unblocking
122 local new
= {}; -- JIDs to block depending or unblock on action
126 -- > When the user blocks communications with the contact, the user's
127 -- > server MUST send unavailable presence information to the contact (but
128 -- > only if the contact is allowed to receive presence notifications [...]
129 -- So contacts we need to do that for are added to the set below.
130 local send_unavailable
= is_blocking
and {};
132 -- Because blocking someone currently also blocks the ability to reject
133 -- subscription requests, we'll preemptively reject such
134 local remove_pending
= is_blocking
and {};
136 for item
in action
:childtags("item") do
137 local jid
= jid_prep(item
.attr
.jid
);
139 origin
.send(st_error_reply(stanza
, "modify", "jid-malformed"));
142 item
.attr
.jid
= jid
; -- echo back prepped
145 if is_contact_subscribed(username
, module
.host
, jid
) then
146 send_unavailable
[jid
] = true;
147 elseif is_contact_pending_in(username
, module
.host
, jid
) then
148 remove_pending
[jid
] = true;
153 if is_blocking
and not next(new
) then
154 -- <block/> element does not contain at least one <item/> child element
155 origin
.send(st_error_reply(stanza
, "modify", "bad-request"));
159 local blocklist
= cache
[username
] or get_blocklist(username
);
161 local new_blocklist
= {
162 -- We set the [false] key to someting as a signal not to migrate privacy lists
163 [false] = blocklist
[false] or { created
= now
; };
165 if type(blocklist
[false]) == "table" then
166 new_blocklist
[false].modified
= now
;
169 if is_blocking
or next(new
) then
170 for jid
, t
in pairs(blocklist
) do
171 if jid
then new_blocklist
[jid
] = t
; end
173 for jid
in pairs(new
) do
174 new_blocklist
[jid
] = is_blocking
;
176 -- else empty the blocklist
179 local ok
, err
= set_blocklist(username
, new_blocklist
);
181 origin
.send(st
.reply(stanza
));
183 origin
.send(st_error_reply(stanza
, "wait", "internal-server-error", err
));
188 for jid
in pairs(send_unavailable
) do
189 if not blocklist
[jid
] then
190 for _
, session
in pairs(sessions
[username
].sessions
) do
191 if session
.presence
then
192 module
:send(st
.presence({ type = "unavailable", to
= jid
, from
= session
.full_jid
}));
198 if next(remove_pending
) then
199 local roster
= load_roster(username
, module
.host
);
200 for jid
in pairs(remove_pending
) do
201 roster
[false].pending
[jid
] = nil;
203 save_roster(username
, module
.host
, roster
);
204 -- Not much we can do about save failing here
208 local blocklist_push
= st
.iq({ type = "set", id
= "blocklist-push" })
209 :add_child(action
); -- I am lazy
211 for _
, session
in pairs(sessions
[username
].sessions
) do
212 if session
.interested_blocklist
then
213 blocklist_push
.attr
.to
= session
.full_jid
;
214 session
.send(blocklist_push
);
221 module
:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist
, -1);
222 module
:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist
, -1);
224 -- Cache invalidation, solved!
225 module
:hook_global("user-deleted", function (event
)
226 if event
.host
== module
.host
then
227 cache2
:set(event
.username
, nil);
228 cache
[event
.username
] = nil;
233 module
:hook("iq-error/self/blocklist-push", function (event
)
234 local origin
, stanza
= event
.origin
, event
.stanza
;
235 local _
, condition
, text
= stanza
:get_error();
236 local log = (origin
.log or module
._log
);
237 log("warn", "Client returned an error in response to notification from mod_%s: %s%s%s",
238 module
.name
, condition
, text
and ": " or "", text
or "");
242 local function is_blocked(user
, jid
)
243 local blocklist
= cache
[user
] or get_blocklist(user
);
244 if blocklist
[jid
] then return true; end
245 local node
, host
= jid_split(jid
);
246 return blocklist
[host
] or node
and blocklist
[node
..'@'..host
];
249 -- Event handlers for bouncing or dropping stanzas
250 local function drop_stanza(event
)
251 local stanza
= event
.stanza
;
252 local attr
= stanza
.attr
;
253 local to
, from
= attr
.to
, attr
.from
;
254 to
= to
and jid_split(to
);
256 return is_blocked(to
, from
);
260 local function bounce_stanza(event
)
261 local origin
, stanza
= event
.origin
, event
.stanza
;
262 if drop_stanza(event
) then
263 origin
.send(st_error_reply(stanza
, "cancel", "service-unavailable"));
268 local function bounce_iq(event
)
269 local type = event
.stanza
.attr
.type;
270 if type == "set" or type == "get" then
271 return bounce_stanza(event
);
273 return drop_stanza(event
); -- result or error
276 local function bounce_message(event
)
277 local stanza
= event
.stanza
;
278 local type = stanza
.attr
.type;
279 if type == "chat" or not type or type == "normal" then
280 if full_sessions
[stanza
.attr
.to
] then
282 return drop_stanza(event
);
284 return bounce_stanza(event
);
286 return drop_stanza(event
); -- drop headlines, groupchats etc
289 local function drop_outgoing(event
)
290 local origin
, stanza
= event
.origin
, event
.stanza
;
291 local username
= origin
.username
or jid_split(stanza
.attr
.from
);
292 if not username
then return end
293 local to
= stanza
.attr
.to
;
294 if to
then return is_blocked(username
, to
); end
295 -- nil 'to' means a self event, don't bock those
298 local function bounce_outgoing(event
)
299 local origin
, stanza
= event
.origin
, event
.stanza
;
300 local type = stanza
.attr
.type;
301 if type == "error" or stanza
.name
== "iq" and type == "result" then
302 return drop_outgoing(event
);
304 if drop_outgoing(event
) then
305 origin
.send(st_error_reply(stanza
, "cancel", "not-acceptable", "You have blocked this JID")
306 :tag("blocked", { xmlns
= "urn:xmpp:blocking:errors" }));
311 -- Hook all the events!
312 local prio_in
, prio_out
= 100, 100;
313 module
:hook("presence/bare", drop_stanza
, prio_in
);
314 module
:hook("presence/full", drop_stanza
, prio_in
);
316 module
:hook("message/bare", bounce_message
, prio_in
);
317 module
:hook("message/full", bounce_message
, prio_in
);
319 module
:hook("iq/bare", bounce_iq
, prio_in
);
320 module
:hook("iq/full", bounce_iq
, prio_in
);
322 module
:hook("pre-message/bare", bounce_outgoing
, prio_out
);
323 module
:hook("pre-message/full", bounce_outgoing
, prio_out
);
324 module
:hook("pre-message/host", bounce_outgoing
, prio_out
);
326 module
:hook("pre-presence/bare", bounce_outgoing
, -1);
327 module
:hook("pre-presence/host", bounce_outgoing
, -1);
328 module
:hook("pre-presence/full", bounce_outgoing
, prio_out
);
330 module
:hook("pre-iq/bare", bounce_outgoing
, prio_out
);
331 module
:hook("pre-iq/full", bounce_outgoing
, prio_out
);
332 module
:hook("pre-iq/host", bounce_outgoing
, prio_out
);