./ct_report/coverage/mod_blocking.COVER.html

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 202 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]}.
Line Hits Source