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([deps/2]). |
11 |
|
-export([supported_features/0]). |
12 |
|
-export([config_spec/0]). |
13 |
|
|
14 |
|
-export([ |
15 |
|
process_iq_get/5, |
16 |
|
process_iq_set/4, |
17 |
|
disco_local_features/1 |
18 |
|
]). |
19 |
|
|
20 |
|
-ignore_xref([disco_local_features/1, process_iq_get/5, process_iq_set/4]). |
21 |
|
|
22 |
|
-include("jlib.hrl"). |
23 |
|
-include("mod_privacy.hrl"). |
24 |
|
|
25 |
|
-type listitem() :: #listitem{}. |
26 |
|
|
27 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
28 |
|
start(HostType, Opts) when is_map(Opts) -> |
29 |
3 |
ejabberd_hooks:add(hooks(HostType)). |
30 |
|
|
31 |
|
-spec stop(mongooseim:host_type()) -> ok. |
32 |
|
stop(HostType) -> |
33 |
3 |
ejabberd_hooks:delete(hooks(HostType)). |
34 |
|
|
35 |
|
deps(_HostType, Opts) -> |
36 |
15 |
[{mod_privacy, Opts, hard}]. |
37 |
|
|
38 |
|
-spec supported_features() -> [atom()]. |
39 |
|
supported_features() -> |
40 |
3 |
[dynamic_domains]. |
41 |
|
|
42 |
|
config_spec() -> |
43 |
152 |
mod_privacy:config_spec(). |
44 |
|
|
45 |
|
hooks(HostType) -> |
46 |
6 |
[{disco_local_features, HostType, ?MODULE, disco_local_features, 99}, |
47 |
|
{privacy_iq_get, HostType, ?MODULE, process_iq_get, 50}, |
48 |
|
{privacy_iq_set, HostType, ?MODULE, process_iq_set, 50}]. |
49 |
|
|
50 |
|
-spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). |
51 |
|
disco_local_features(Acc = #{node := <<>>}) -> |
52 |
2 |
mongoose_disco:add_features([?NS_BLOCKING], Acc); |
53 |
|
disco_local_features(Acc) -> |
54 |
1 |
Acc. |
55 |
|
|
56 |
|
process_iq_get(Acc, _From = #jid{luser = LUser, lserver = LServer}, |
57 |
|
_, #iq{xmlns = ?NS_BLOCKING}, _) -> |
58 |
8 |
HostType = mongoose_acc:host_type(Acc), |
59 |
8 |
Res = case mod_privacy_backend:get_privacy_list(HostType, LUser, LServer, <<"blocking">>) of |
60 |
|
{error, not_found} -> |
61 |
1 |
{ok, []}; |
62 |
|
{ok, L} -> |
63 |
7 |
{ok, L}; |
64 |
|
E -> |
65 |
:-( |
{error, E} |
66 |
|
end, |
67 |
8 |
IqRes = case Res of |
68 |
|
{ok, Lst} -> |
69 |
8 |
{result, blocking_query_response(Lst)}; |
70 |
|
{error, _} -> |
71 |
:-( |
{error, mongoose_xmpp_errors:internal_server_error()} |
72 |
|
end, |
73 |
8 |
mongoose_acc:set(hook, result, IqRes, Acc); |
74 |
|
process_iq_get(Val, _, _, _, _) -> |
75 |
24 |
Val. |
76 |
|
|
77 |
|
process_iq_set(Acc, From, _To, #iq{xmlns = ?NS_BLOCKING, sub_el = SubEl}) -> |
78 |
|
%% collect needed data |
79 |
28 |
HostType = mongoose_acc:host_type(Acc), |
80 |
28 |
#jid{luser = LUser, lserver = LServer} = From, |
81 |
28 |
#xmlel{name = BType} = SubEl, |
82 |
28 |
Type = parse_command_type(BType), |
83 |
28 |
Usrs = exml_query:paths(SubEl, [{element, <<"item">>}, {attr, <<"jid">>}]), |
84 |
28 |
CurrList = case mod_privacy_backend:get_privacy_list(HostType, LUser, LServer, <<"blocking">>) of |
85 |
|
{ok, List} -> |
86 |
12 |
List; |
87 |
|
{error, not_found} -> |
88 |
16 |
[]; |
89 |
|
{error, Reason} -> |
90 |
:-( |
{error, Reason} |
91 |
|
end, |
92 |
|
%% process |
93 |
28 |
{Acc1, Res} = process_blocking_iq_set(Type, Acc, LUser, LServer, CurrList, Usrs), |
94 |
|
%% respond / notify |
95 |
28 |
{Acc2, Res1} = complete_iq_set(blocking_command, Acc1, LUser, LServer, Res), |
96 |
28 |
mongoose_acc:set(hook, result, Res1, Acc2); |
97 |
|
process_iq_set(Val, _, _, _) -> |
98 |
60 |
Val. |
99 |
|
|
100 |
19 |
parse_command_type(<<"block">>) -> block; |
101 |
9 |
parse_command_type(<<"unblock">>) -> unblock. |
102 |
|
|
103 |
|
%% @doc Set IQ must do the following: |
104 |
|
%% * get / create a dedicated privacy list (we call it "blocking") |
105 |
|
%% * modify the list |
106 |
|
%% * set that list as a default (in backend) |
107 |
|
%% * return the list so that c2s can set it as current list |
108 |
|
%% * broadcast (push) message to all the user's resources |
109 |
|
%% * sent 'unavailable' msg to blocked contacts, or 'available' to unblocked |
110 |
|
%% |
111 |
|
-spec process_blocking_iq_set(Type :: block | unblock, Acc :: mongoose_acc:t(), |
112 |
|
LUser:: binary(), LServer:: binary(), |
113 |
|
CurrList :: [listitem()], Users :: [binary()]) -> |
114 |
|
{mongoose_acc:t(), {ok, [binary()], [listitem()], block | unblock | unblock_all} | |
115 |
|
{error, exml:element()}}. |
116 |
|
%% fail if current default list could not be retrieved |
117 |
|
process_blocking_iq_set(_, Acc, _, _, {error, _}, _) -> |
118 |
:-( |
{Acc, {error, mongoose_xmpp_errors:internal_server_error()}}; |
119 |
|
%% reject block request with empty jid list |
120 |
|
process_blocking_iq_set(block, Acc, _, _, _, []) -> |
121 |
1 |
{Acc, {error, mongoose_xmpp_errors:bad_request()}}; |
122 |
|
process_blocking_iq_set(Type, Acc, LUser, LServer, CurrList, Usrs) -> |
123 |
|
%% check who is being added / removed |
124 |
27 |
HostType = mongoose_acc:host_type(Acc), |
125 |
27 |
{NType, Changed, NewList} = blocking_list_modify(Type, Usrs, CurrList), |
126 |
27 |
case mod_privacy_backend:replace_privacy_list(HostType, LUser, LServer, <<"blocking">>, NewList) of |
127 |
|
{error, E} -> |
128 |
:-( |
{error, E}; |
129 |
|
ok -> |
130 |
27 |
case mod_privacy_backend:set_default_list(HostType, LUser, LServer, <<"blocking">>) of |
131 |
|
ok -> |
132 |
27 |
{Acc, {ok, Changed, NewList, NType}}; |
133 |
|
{error, not_found} -> |
134 |
:-( |
{Acc, {error, mongoose_xmpp_errors:item_not_found()}}; |
135 |
|
{error, _Reason} -> |
136 |
:-( |
{Acc, {error, mongoose_xmpp_errors:internal_server_error()}} |
137 |
|
end |
138 |
|
end. |
139 |
|
|
140 |
|
-spec complete_iq_set(atom(), mongoose_acc:t(), term(), term(), term()) -> |
141 |
|
{mongoose_acc:t(), {error, term()} | {result, list() | {result, list(), term()}}}. |
142 |
|
complete_iq_set(blocking_command, Acc, _, _, {error, Reason}) -> |
143 |
1 |
{Acc, {error, Reason}}; |
144 |
|
complete_iq_set(blocking_command, Acc, LUser, LServer, {ok, Changed, List, Type}) -> |
145 |
27 |
UserList = #userlist{name = <<"blocking">>, list = List, needdb = false}, |
146 |
|
% send the list to all users c2s processes (resources) to make it effective immediately |
147 |
27 |
Acc1 = broadcast_blocking_command(Acc, LUser, LServer, UserList, Changed, Type), |
148 |
|
% return a response here so that c2s sets the list in its state |
149 |
27 |
{Acc1, {result, [], UserList}}. |
150 |
|
%%complete_iq_set(blocking_command, _, _, _) -> |
151 |
|
%% {result, []}. |
152 |
|
|
153 |
|
-spec blocking_list_modify(Type :: block | unblock, New :: [binary()], Old :: [listitem()]) -> |
154 |
|
{block|unblock|unblock_all, [binary()], [listitem()]}. |
155 |
|
blocking_list_modify(block, Change, Old) -> |
156 |
18 |
N = make_blocking_list(Change), |
157 |
18 |
{_, O} = remove_from(Change, Old), |
158 |
|
%% we treat all items on the "to block" list as changed becase they might have been present |
159 |
|
%% on the old list with different settings |
160 |
|
%% and we need to set order numbers, doesn't matter how but it has to be unique |
161 |
18 |
{block, Change, set_order(N ++ O)}; |
162 |
|
blocking_list_modify(unblock, [], Old) -> |
163 |
|
%% unblock with empty list means unblocking all contacts |
164 |
2 |
Rem = [jid:to_binary(J#listitem.value) || J <- Old], |
165 |
2 |
{unblock_all, Rem, []}; |
166 |
|
blocking_list_modify(unblock, Change, Old) -> |
167 |
7 |
{Removed, O} = remove_from(Change, Old), |
168 |
7 |
{unblock, Removed, O}. |
169 |
|
|
170 |
|
set_order(L) -> |
171 |
18 |
set_order(1, [], L). |
172 |
|
|
173 |
|
set_order(_, N, []) -> |
174 |
18 |
N; |
175 |
|
set_order(Idx, N, [H|T]) -> |
176 |
26 |
set_order(Idx + 1, [H#listitem{order = Idx}|N], T). |
177 |
|
|
178 |
|
remove_from(ToRem, Lst) -> |
179 |
25 |
remove_from(ToRem, [], [], Lst). |
180 |
|
|
181 |
|
remove_from(_, Removed, New, []) -> |
182 |
25 |
{Removed, New}; |
183 |
|
remove_from(ToRem, Removed, New, [H|T]) -> |
184 |
15 |
Bin = jid:to_binary(H#listitem.value), |
185 |
15 |
case lists:member(Bin, ToRem) of |
186 |
|
true -> |
187 |
8 |
remove_from(ToRem, [Bin|Removed], New, T); |
188 |
|
false -> |
189 |
7 |
remove_from(ToRem, Removed, [H|New], T) |
190 |
|
end. |
191 |
|
|
192 |
|
make_blocking_list(L) -> |
193 |
18 |
make_blocking_list([], L). |
194 |
|
|
195 |
|
make_blocking_list(New, []) -> |
196 |
18 |
New; |
197 |
|
make_blocking_list(New, [H|T]) -> |
198 |
24 |
case make_blocking_list_entry(H) of |
199 |
|
false -> |
200 |
:-( |
make_blocking_list(New, T); |
201 |
|
Entry -> |
202 |
24 |
make_blocking_list([Entry|New], T) |
203 |
|
end. |
204 |
|
|
205 |
|
make_blocking_list_entry(J) -> |
206 |
24 |
case jid:from_binary(J) of |
207 |
|
error -> |
208 |
:-( |
false; |
209 |
|
JID -> |
210 |
24 |
#listitem{type = jid, |
211 |
|
match_all = true, |
212 |
|
%% we have to use another action |
213 |
|
%% because c2s has to respond differently based on why we deny |
214 |
|
action = block, |
215 |
|
value = jid:to_lower(JID)} |
216 |
|
end. |
217 |
|
|
218 |
|
%% @doc send iq confirmation to all of the user's resources |
219 |
|
%% if we unblock all contacts then we don't list who's been unblocked |
220 |
|
broadcast_blocking_command(Acc, LUser, LServer, UserList, _Changed, unblock_all) -> |
221 |
2 |
broadcast_blocking_command(Acc, LUser, LServer, UserList, [], unblock); |
222 |
|
broadcast_blocking_command(Acc, LUser, LServer, UserList, Changed, Type) -> |
223 |
27 |
UserJID = jid:make_noprep(LUser, LServer, <<>>), |
224 |
27 |
Bcast = {blocking, UserList, Type, Changed}, |
225 |
27 |
ejabberd_sm:route(UserJID, UserJID, Acc, {broadcast, Bcast}). |
226 |
|
|
227 |
|
blocking_query_response(Lst) -> |
228 |
8 |
#xmlel{ |
229 |
|
name = <<"blocklist">>, |
230 |
|
attrs = [{<<"xmlns">>, ?NS_BLOCKING}], |
231 |
6 |
children = [#xmlel{name= <<"item">>, |
232 |
8 |
attrs = [{<<"jid">>, jid:to_binary(J#listitem.value)}]} || J <- Lst]}. |