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