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
, 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 {};
131 local send_available
= not is_blocking
and {};
133 -- Because blocking someone currently also blocks the ability to reject
134 -- subscription requests, we'll preemptively reject such
135 local remove_pending
= is_blocking
and {};
137 for item
in action
:childtags("item") do
138 local jid
= jid_prep(item
.attr
.jid
);
140 origin
.send(st_error_reply(stanza
, "modify", "jid-malformed"));
143 item
.attr
.jid
= jid
; -- echo back prepped
146 if is_contact_subscribed(username
, module
.host
, jid
) then
147 send_unavailable
[jid
] = true;
148 elseif is_contact_pending_in(username
, module
.host
, jid
) then
149 remove_pending
[jid
] = true;
151 elseif is_contact_subscribed(username
, module
.host
, jid
) then
152 send_available
[jid
] = true;
156 if is_blocking
and not next(new
) then
157 -- <block/> element does not contain at least one <item/> child element
158 origin
.send(st_error_reply(stanza
, "modify", "bad-request"));
162 local blocklist
= cache
[username
] or get_blocklist(username
);
164 local new_blocklist
= {
165 -- We set the [false] key to something as a signal not to migrate privacy lists
166 [false] = blocklist
[false] or { created
= now
; };
168 if type(blocklist
[false]) == "table" then
169 new_blocklist
[false].modified
= now
;
172 if is_blocking
or next(new
) then
173 for jid
, t
in pairs(blocklist
) do
174 if jid
then new_blocklist
[jid
] = t
; end
176 for jid
in pairs(new
) do
177 new_blocklist
[jid
] = is_blocking
;
179 -- else empty the blocklist
182 local ok
, err
= set_blocklist(username
, new_blocklist
);
184 origin
.send(st
.reply(stanza
));
186 origin
.send(st_error_reply(stanza
, "wait", "internal-server-error", err
));
191 for jid
in pairs(send_unavailable
) do
192 -- Check that this JID isn't already blocked, i.e. this is not a change
193 if not blocklist
[jid
] then
194 for _
, session
in pairs(sessions
[username
].sessions
) do
195 if session
.presence
then
196 module
:send(st
.presence({ type = "unavailable", to
= jid
, from
= session
.full_jid
}));
202 if next(remove_pending
) then
203 local roster
= load_roster(username
, module
.host
);
204 for jid
in pairs(remove_pending
) do
205 roster
[false].pending
[jid
] = nil;
207 save_roster(username
, module
.host
, roster
);
208 -- Not much we can do about save failing here
211 local user_bare
= username
.. "@" .. module
.host
;
212 for jid
in pairs(send_available
) do
213 module
:send(st
.presence({ type = "probe", to
= user_bare
, from
= jid
}));
217 local blocklist_push
= st
.iq({ type = "set", id
= "blocklist-push" })
218 :add_child(action
); -- I am lazy
220 for _
, session
in pairs(sessions
[username
].sessions
) do
221 if session
.interested_blocklist
then
222 blocklist_push
.attr
.to
= session
.full_jid
;
223 session
.send(blocklist_push
);
230 module
:hook("iq-set/self/urn:xmpp:blocking:block", edit_blocklist
, -1);
231 module
:hook("iq-set/self/urn:xmpp:blocking:unblock", edit_blocklist
, -1);
233 -- Cache invalidation, solved!
234 module
:hook_global("user-deleted", function (event
)
235 if event
.host
== module
.host
then
236 cache2
:set(event
.username
, nil);
237 cache
[event
.username
] = nil;
242 module
:hook("iq-error/self/blocklist-push", function (event
)
243 local origin
, stanza
= event
.origin
, event
.stanza
;
244 local _
, condition
, text
= stanza
:get_error();
245 local log = (origin
.log or module
._log
);
246 log("warn", "Client returned an error in response to notification from mod_%s: %s%s%s",
247 module
.name
, condition
, text
and ": " or "", text
or "");
251 local function is_blocked(user
, jid
)
252 local blocklist
= cache
[user
] or get_blocklist(user
);
253 if blocklist
[jid
] then return true; end
254 local node
, host
= jid_split(jid
);
255 return blocklist
[host
] or node
and blocklist
[node
..'@'..host
];
258 -- Event handlers for bouncing or dropping stanzas
259 local function drop_stanza(event
)
260 local stanza
= event
.stanza
;
261 local attr
= stanza
.attr
;
262 local to
, from
= attr
.to
, attr
.from
;
263 to
= to
and jid_split(to
);
265 return is_blocked(to
, from
);
269 local function bounce_stanza(event
)
270 local origin
, stanza
= event
.origin
, event
.stanza
;
271 if drop_stanza(event
) then
272 origin
.send(st_error_reply(stanza
, "cancel", "service-unavailable"));
277 local function bounce_iq(event
)
278 local type = event
.stanza
.attr
.type;
279 if type == "set" or type == "get" then
280 return bounce_stanza(event
);
282 return drop_stanza(event
); -- result or error
285 local function bounce_message(event
)
286 local stanza
= event
.stanza
;
287 local type = stanza
.attr
.type;
288 if type == "chat" or not type or type == "normal" then
289 if full_sessions
[stanza
.attr
.to
] then
291 return drop_stanza(event
);
293 return bounce_stanza(event
);
295 return drop_stanza(event
); -- drop headlines, groupchats etc
298 local function drop_outgoing(event
)
299 local origin
, stanza
= event
.origin
, event
.stanza
;
300 local username
= origin
.username
or jid_split(stanza
.attr
.from
);
301 if not username
then return end
302 local to
= stanza
.attr
.to
;
303 if to
then return is_blocked(username
, to
); end
304 -- nil 'to' means a self event, don't bock those
307 local function bounce_outgoing(event
)
308 local origin
, stanza
= event
.origin
, event
.stanza
;
309 local type = stanza
.attr
.type;
310 if type == "error" or stanza
.name
== "iq" and type == "result" then
311 return drop_outgoing(event
);
313 if drop_outgoing(event
) then
314 origin
.send(st_error_reply(stanza
, "cancel", "not-acceptable", "You have blocked this JID")
315 :tag("blocked", { xmlns
= "urn:xmpp:blocking:errors" }));
320 -- Hook all the events!
321 local prio_in
, prio_out
= 100, 100;
322 module
:hook("presence/bare", drop_stanza
, prio_in
);
323 module
:hook("presence/full", drop_stanza
, prio_in
);
325 module
:hook("message/bare", bounce_message
, prio_in
);
326 module
:hook("message/full", bounce_message
, prio_in
);
328 module
:hook("iq/bare", bounce_iq
, prio_in
);
329 module
:hook("iq/full", bounce_iq
, prio_in
);
331 module
:hook("pre-message/bare", bounce_outgoing
, prio_out
);
332 module
:hook("pre-message/full", bounce_outgoing
, prio_out
);
333 module
:hook("pre-message/host", bounce_outgoing
, prio_out
);
335 module
:hook("pre-presence/bare", bounce_outgoing
, -1);
336 module
:hook("pre-presence/host", bounce_outgoing
, -1);
337 module
:hook("pre-presence/full", bounce_outgoing
, prio_out
);
339 module
:hook("pre-iq/bare", bounce_outgoing
, prio_out
);
340 module
:hook("pre-iq/full", bounce_outgoing
, prio_out
);
341 module
:hook("pre-iq/host", bounce_outgoing
, prio_out
);