1 |
|
-module(mod_blocking). |
2 |
|
|
3 |
|
-xep([{xep, 191}, {version, "1.3"}]). |
4 |
|
|
5 |
|
-behaviour(gen_mod). |
6 |
|
-behaviour(mongoose_module_metrics). |
7 |
|
|
8 |
|
-export([start/2]). |
9 |
|
-export([stop/1]). |
10 |
|
-export([hooks/1]). |
11 |
|
-export([deps/2]). |
12 |
|
-export([supported_features/0]). |
13 |
|
-export([config_spec/0]). |
14 |
|
|
15 |
|
-export([ |
16 |
|
process_iq_get/3, |
17 |
|
process_iq_set/3, |
18 |
|
disco_local_features/3, |
19 |
|
user_send_iq/3, |
20 |
|
foreign_event/3 |
21 |
|
]). |
22 |
|
|
23 |
|
-include("jlib.hrl"). |
24 |
|
-include("mod_privacy.hrl"). |
25 |
|
|
26 |
|
-type blocking_type() :: block | unblock. |
27 |
|
-type listitem() :: #listitem{}. |
28 |
|
|
29 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
30 |
|
start(_HostType, _Opts) -> |
31 |
3 |
ok. |
32 |
|
|
33 |
|
-spec stop(mongooseim:host_type()) -> ok. |
34 |
|
stop(_HostType) -> |
35 |
3 |
ok. |
36 |
|
|
37 |
|
deps(_HostType, Opts) -> |
38 |
15 |
[{mod_privacy, Opts, hard}]. |
39 |
|
|
40 |
|
-spec supported_features() -> [atom()]. |
41 |
|
supported_features() -> |
42 |
:-( |
[dynamic_domains]. |
43 |
|
|
44 |
|
config_spec() -> |
45 |
200 |
mod_privacy:config_spec(). |
46 |
|
|
47 |
|
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). |
48 |
|
hooks(HostType) -> |
49 |
6 |
[{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}, |
50 |
|
{privacy_iq_get, HostType, fun ?MODULE:process_iq_get/3, #{}, 50}, |
51 |
|
{privacy_iq_set, HostType, fun ?MODULE:process_iq_set/3, #{}, 50} |
52 |
|
| c2s_hooks(HostType)]. |
53 |
|
|
54 |
|
-spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:fn()). |
55 |
|
c2s_hooks(HostType) -> |
56 |
6 |
[ |
57 |
|
{user_send_iq, HostType, fun ?MODULE:user_send_iq/3, #{}, 50}, |
58 |
|
{foreign_event, HostType, fun ?MODULE:foreign_event/3, #{}, 50} |
59 |
|
]. |
60 |
|
|
61 |
|
-spec user_send_iq(mongoose_acc:t(), mongoose_c2s_hooks:params(), map()) -> |
62 |
|
mongoose_c2s_hooks:result(). |
63 |
|
user_send_iq(Acc, #{c2s_data := StateData}, #{host_type := HostType}) -> |
64 |
436 |
case mongoose_iq:info(Acc) of |
65 |
|
{#iq{xmlns = ?NS_BLOCKING, type = Type} = IQ, Acc1} when Type == get; Type == set -> |
66 |
36 |
mod_privacy:do_user_send_iq(Acc1, StateData, HostType, IQ); |
67 |
|
_ -> |
68 |
400 |
{ok, Acc} |
69 |
|
end. |
70 |
|
|
71 |
|
-spec foreign_event(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) -> |
72 |
|
mongoose_c2s_hooks:result(). |
73 |
|
foreign_event(Acc, #{c2s_data := StateData, |
74 |
|
event_type := cast, |
75 |
|
event_tag := ?MODULE, |
76 |
|
event_content := {blocking, UserList, Action, Changed}}, _Extra) -> |
77 |
29 |
{stop, handle_new_blocking_command(Acc, StateData, UserList, Action, Changed)}; |
78 |
|
foreign_event(Acc, _Params, _Extra) -> |
79 |
:-( |
{ok, Acc}. |
80 |
|
|
81 |
|
handle_new_blocking_command(Acc, StateData, UserList, Action, JIDs) -> |
82 |
29 |
blocking_push_to_resources(Action, JIDs, StateData), |
83 |
29 |
blocking_presence_to_contacts(Action, JIDs, StateData), |
84 |
29 |
ToAcc = [{state_mod, {mod_privacy, UserList}}], |
85 |
29 |
mongoose_c2s_acc:to_acc_many(Acc, ToAcc). |
86 |
|
|
87 |
|
-spec blocking_push_to_resources(blocking_type(), [binary()], mongoose_c2s:data()) -> ok. |
88 |
|
blocking_push_to_resources(Action, JIDs, StateData) -> |
89 |
29 |
SubEl = case Action of |
90 |
20 |
block -> blocking_stanza(JIDs, <<"block">>); |
91 |
9 |
unblock -> blocking_stanza(JIDs, <<"unblock">>) |
92 |
|
end, |
93 |
29 |
PrivPushIQ = blocking_iq(SubEl), |
94 |
29 |
T = mongoose_c2s:get_jid(StateData), |
95 |
29 |
F = jid:to_bare(T), |
96 |
29 |
PrivPushEl = jlib:replace_from_to(F, T, jlib:iq_to_xml(PrivPushIQ)), |
97 |
29 |
ejabberd_router:route(F, T, PrivPushEl), |
98 |
29 |
ok. |
99 |
|
|
100 |
|
-spec blocking_presence_to_contacts(blocking_type(), [binary()], mongoose_c2s:data()) -> ok. |
101 |
29 |
blocking_presence_to_contacts(_Action, [], _StateData) -> ok; |
102 |
|
blocking_presence_to_contacts(Action, [Jid | JIDs], StateData) -> |
103 |
33 |
Presences = mod_presence:maybe_get_handler(StateData), |
104 |
33 |
Pres = case Action of |
105 |
|
block -> |
106 |
26 |
mod_presence:presence_unavailable_stanza(); |
107 |
|
unblock -> |
108 |
7 |
mod_presence:get(Presences, last) |
109 |
|
end, |
110 |
33 |
T = jid:from_binary(Jid), |
111 |
33 |
case mod_presence:is_subscribed_to_my_presence(T, jid:to_bare(T), Presences) of |
112 |
|
true -> |
113 |
:-( |
F = jid:to_bare(mongoose_c2s:get_jid(StateData)), |
114 |
:-( |
ejabberd_router:route(F, T, Pres); |
115 |
|
false -> |
116 |
33 |
ok |
117 |
|
end, |
118 |
33 |
blocking_presence_to_contacts(Action, JIDs, StateData). |
119 |
|
|
120 |
|
-spec disco_local_features(mongoose_disco:feature_acc(), map(), map()) -> |
121 |
|
{ok, mongoose_disco:feature_acc()}. |
122 |
|
disco_local_features(Acc = #{node := <<>>}, _, _) -> |
123 |
2 |
{ok, mongoose_disco:add_features([?NS_BLOCKING], Acc)}; |
124 |
|
disco_local_features(Acc, _, _) -> |
125 |
1 |
{ok, Acc}. |
126 |
|
|
127 |
|
-spec process_iq_get(Acc, Params, Extra) -> {ok | stop, Acc} when |
128 |
|
Acc :: mongoose_acc:t(), |
129 |
|
Params :: #{from := jid:jid(), iq := jlib:iq()}, |
130 |
|
Extra :: gen_hook:extra(). |
131 |
|
process_iq_get(Acc, #{from := #jid{luser = LUser, lserver = LServer}, |
132 |
|
iq := #iq{xmlns = ?NS_BLOCKING}}, _) -> |
133 |
8 |
HostType = mongoose_acc:host_type(Acc), |
134 |
8 |
Res = case mod_privacy_backend:get_privacy_list(HostType, LUser, LServer, <<"blocking">>) of |
135 |
|
{error, not_found} -> |
136 |
1 |
{ok, []}; |
137 |
|
{ok, L} -> |
138 |
7 |
{ok, L}; |
139 |
|
E -> |
140 |
:-( |
{error, E} |
141 |
|
end, |
142 |
8 |
IqRes = case Res of |
143 |
|
{ok, Lst} -> |
144 |
8 |
{result, blocking_query_response(Lst)}; |
145 |
|
{error, _} -> |
146 |
:-( |
{error, mongoose_xmpp_errors:internal_server_error()} |
147 |
|
end, |
148 |
8 |
{ok, mongoose_acc:set(hook, result, IqRes, Acc)}; |
149 |
|
process_iq_get(Acc, _, _) -> |
150 |
24 |
{ok, Acc}. |
151 |
|
|
152 |
|
-spec process_iq_set(Acc, Params, Extra) -> {ok | stop, Acc} when |
153 |
|
Acc :: mongoose_acc:t(), |
154 |
|
Params :: #{from := jid:jid(), iq := jlib:iq()}, |
155 |
|
Extra :: gen_hook:extra(). |
156 |
|
process_iq_set(Acc, #{from := From, iq := #iq{xmlns = ?NS_BLOCKING, sub_el = SubEl}}, _) -> |
157 |
|
%% collect needed data |
158 |
28 |
HostType = mongoose_acc:host_type(Acc), |
159 |
28 |
#jid{luser = LUser, lserver = LServer} = From, |
160 |
28 |
#xmlel{name = BType} = SubEl, |
161 |
28 |
Type = parse_command_type(BType), |
162 |
28 |
Usrs = exml_query:paths(SubEl, [{element, <<"item">>}, {attr, <<"jid">>}]), |
163 |
28 |
CurrList = case mod_privacy_backend:get_privacy_list(HostType, LUser, LServer, <<"blocking">>) of |
164 |
|
{ok, List} -> |
165 |
12 |
List; |
166 |
|
{error, not_found} -> |
167 |
16 |
[]; |
168 |
|
{error, Reason} -> |
169 |
:-( |
{error, Reason} |
170 |
|
end, |
171 |
|
%% process |
172 |
28 |
{Acc1, Res} = process_blocking_iq_set(Type, Acc, LUser, LServer, CurrList, Usrs), |
173 |
|
%% respond / notify |
174 |
28 |
{Acc2, Res1} = complete_iq_set(blocking_command, Acc1, LUser, LServer, Res), |
175 |
28 |
{ok, mongoose_acc:set(hook, result, Res1, Acc2)}; |
176 |
|
process_iq_set(Acc, _, _) -> |
177 |
60 |
{ok, Acc}. |
178 |
|
|
179 |
19 |
parse_command_type(<<"block">>) -> block; |
180 |
9 |
parse_command_type(<<"unblock">>) -> unblock. |
181 |
|
|
182 |
|
%% @doc Set IQ must do the following: |
183 |
|
%% * get / create a dedicated privacy list (we call it "blocking") |
184 |
|
%% * modify the list |
185 |
|
%% * set that list as a default (in backend) |
186 |
|
%% * return the list so that c2s can set it as current list |
187 |
|
%% * broadcast (push) message to all the user's resources |
188 |
|
%% * sent 'unavailable' msg to blocked contacts, or 'available' to unblocked |
189 |
|
%% |
190 |
|
-spec process_blocking_iq_set(Type :: block | unblock, Acc :: mongoose_acc:t(), |
191 |
|
LUser:: binary(), LServer:: binary(), |
192 |
|
CurrList :: [listitem()], Users :: [binary()]) -> |
193 |
|
{mongoose_acc:t(), {ok, [binary()], [listitem()], block | unblock | unblock_all} | |
194 |
|
{error, exml:element()}}. |
195 |
|
%% fail if current default list could not be retrieved |
196 |
|
process_blocking_iq_set(_, Acc, _, _, {error, _}, _) -> |
197 |
:-( |
{Acc, {error, mongoose_xmpp_errors:internal_server_error()}}; |
198 |
|
%% reject block request with empty jid list |
199 |
|
process_blocking_iq_set(block, Acc, _, _, _, []) -> |
200 |
1 |
{Acc, {error, mongoose_xmpp_errors:bad_request()}}; |
201 |
|
process_blocking_iq_set(Type, Acc, LUser, LServer, CurrList, Usrs) -> |
202 |
|
%% check who is being added / removed |
203 |
27 |
HostType = mongoose_acc:host_type(Acc), |
204 |
27 |
{NType, Changed, NewList} = blocking_list_modify(Type, Usrs, CurrList), |
205 |
27 |
case mod_privacy_backend:replace_privacy_list(HostType, LUser, LServer, <<"blocking">>, NewList) of |
206 |
|
{error, E} -> |
207 |
:-( |
{error, E}; |
208 |
|
ok -> |
209 |
27 |
case mod_privacy_backend:set_default_list(HostType, LUser, LServer, <<"blocking">>) of |
210 |
|
ok -> |
211 |
27 |
{Acc, {ok, Changed, NewList, NType}}; |
212 |
|
{error, not_found} -> |
213 |
:-( |
{Acc, {error, mongoose_xmpp_errors:item_not_found()}}; |
214 |
|
{error, _Reason} -> |
215 |
:-( |
{Acc, {error, mongoose_xmpp_errors:internal_server_error()}} |
216 |
|
end |
217 |
|
end. |
218 |
|
|
219 |
|
-spec complete_iq_set(atom(), mongoose_acc:t(), term(), term(), term()) -> |
220 |
|
{mongoose_acc:t(), {error, term()} | {result, list() | {result, list(), term()}}}. |
221 |
|
complete_iq_set(blocking_command, Acc, _, _, {error, Reason}) -> |
222 |
1 |
{Acc, {error, Reason}}; |
223 |
|
complete_iq_set(blocking_command, Acc, LUser, LServer, {ok, Changed, List, Type}) -> |
224 |
27 |
UserList = #userlist{name = <<"blocking">>, list = List, needdb = false}, |
225 |
|
% send the list to all users c2s processes (resources) to make it effective immediately |
226 |
27 |
broadcast_blocking_command(Acc, LUser, LServer, UserList, Changed, Type), |
227 |
|
% return a response here so that c2s sets the list in its state |
228 |
27 |
{Acc, {result, [], UserList}}. |
229 |
|
%%complete_iq_set(blocking_command, _, _, _) -> |
230 |
|
%% {result, []}. |
231 |
|
|
232 |
|
-spec blocking_list_modify(Type :: block | unblock, New :: [binary()], Old :: [listitem()]) -> |
233 |
|
{block|unblock|unblock_all, [binary()], [listitem()]}. |
234 |
|
blocking_list_modify(block, Change, Old) -> |
235 |
18 |
N = make_blocking_list(Change), |
236 |
18 |
{_, O} = remove_from(Change, Old), |
237 |
|
%% we treat all items on the "to block" list as changed becase they might have been present |
238 |
|
%% on the old list with different settings |
239 |
|
%% and we need to set order numbers, doesn't matter how but it has to be unique |
240 |
18 |
{block, Change, set_order(N ++ O)}; |
241 |
|
blocking_list_modify(unblock, [], Old) -> |
242 |
|
%% unblock with empty list means unblocking all contacts |
243 |
2 |
Rem = [jid:to_binary(J#listitem.value) || J <- Old], |
244 |
2 |
{unblock_all, Rem, []}; |
245 |
|
blocking_list_modify(unblock, Change, Old) -> |
246 |
7 |
{Removed, O} = remove_from(Change, Old), |
247 |
7 |
{unblock, Removed, O}. |
248 |
|
|
249 |
|
set_order(L) -> |
250 |
18 |
set_order(1, [], L). |
251 |
|
|
252 |
|
set_order(_, N, []) -> |
253 |
18 |
N; |
254 |
|
set_order(Idx, N, [H|T]) -> |
255 |
26 |
set_order(Idx + 1, [H#listitem{order = Idx}|N], T). |
256 |
|
|
257 |
|
remove_from(ToRem, Lst) -> |
258 |
25 |
remove_from(ToRem, [], [], Lst). |
259 |
|
|
260 |
|
remove_from(_, Removed, New, []) -> |
261 |
25 |
{Removed, New}; |
262 |
|
remove_from(ToRem, Removed, New, [H|T]) -> |
263 |
15 |
Bin = jid:to_binary(H#listitem.value), |
264 |
15 |
case lists:member(Bin, ToRem) of |
265 |
|
true -> |
266 |
8 |
remove_from(ToRem, [Bin|Removed], New, T); |
267 |
|
false -> |
268 |
7 |
remove_from(ToRem, Removed, [H|New], T) |
269 |
|
end. |
270 |
|
|
271 |
|
make_blocking_list(L) -> |
272 |
18 |
make_blocking_list([], L). |
273 |
|
|
274 |
|
make_blocking_list(New, []) -> |
275 |
18 |
New; |
276 |
|
make_blocking_list(New, [H|T]) -> |
277 |
24 |
case make_blocking_list_entry(H) of |
278 |
|
false -> |
279 |
:-( |
make_blocking_list(New, T); |
280 |
|
Entry -> |
281 |
24 |
make_blocking_list([Entry|New], T) |
282 |
|
end. |
283 |
|
|
284 |
|
make_blocking_list_entry(J) -> |
285 |
24 |
case jid:from_binary(J) of |
286 |
|
error -> |
287 |
:-( |
false; |
288 |
|
JID -> |
289 |
24 |
#listitem{type = jid, |
290 |
|
match_all = true, |
291 |
|
%% we have to use another action |
292 |
|
%% because c2s has to respond differently based on why we deny |
293 |
|
action = block, |
294 |
|
value = jid:to_lower(JID)} |
295 |
|
end. |
296 |
|
|
297 |
|
%% @doc send iq confirmation to all of the user's resources |
298 |
|
%% if we unblock all contacts then we don't list who's been unblocked |
299 |
|
broadcast_blocking_command(Acc, LUser, LServer, UserList, _Changed, unblock_all) -> |
300 |
2 |
broadcast_blocking_command(Acc, LUser, LServer, UserList, [], unblock); |
301 |
|
broadcast_blocking_command(Acc, LUser, LServer, UserList, Changed, Type) -> |
302 |
27 |
Item = {blocking, UserList, Type, Changed}, |
303 |
27 |
UserPids = ejabberd_sm:get_user_present_pids(LUser, LServer), |
304 |
27 |
HostType = mongoose_acc:host_type(Acc), |
305 |
27 |
mongoose_hooks:privacy_list_push(HostType, LUser, LServer, Item, length(UserPids)), |
306 |
27 |
lists:foreach(fun({_, Pid}) -> mongoose_c2s:cast(Pid, ?MODULE, Item) end, UserPids). |
307 |
|
|
308 |
|
-spec blocking_query_response([mod_privacy:list_name()]) -> exml:element(). |
309 |
|
blocking_query_response(Lst) -> |
310 |
8 |
#xmlel{ |
311 |
|
name = <<"blocklist">>, |
312 |
|
attrs = [{<<"xmlns">>, ?NS_BLOCKING}], |
313 |
6 |
children = [#xmlel{name= <<"item">>, |
314 |
8 |
attrs = [{<<"jid">>, jid:to_binary(J#listitem.value)}]} || J <- Lst]}. |
315 |
|
|
316 |
|
-spec blocking_stanza([binary()], binary()) -> exml:element(). |
317 |
|
blocking_stanza(JIDs, Name) -> |
318 |
29 |
#xmlel{name = Name, |
319 |
|
attrs = [{<<"xmlns">>, ?NS_BLOCKING}], |
320 |
|
children = lists:map( |
321 |
|
fun(JID) -> |
322 |
33 |
#xmlel{name = <<"item">>, attrs = [{<<"jid">>, JID}]} |
323 |
|
end, JIDs)}. |
324 |
|
|
325 |
|
-spec blocking_iq(exml:element()) -> jlib:iq(). |
326 |
|
blocking_iq(SubEl) -> |
327 |
29 |
#iq{type = set, xmlns = ?NS_BLOCKING, id = <<"push">>, sub_el = [SubEl]}. |