1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author Uvarov Michael <arcusfelis@gmail.com> |
3 |
|
%%% @copyright (C) 2013, Uvarov Michael |
4 |
|
%%% @doc XEP-0313: Message Archive Management |
5 |
|
%%% |
6 |
|
%%% The module uses several backend modules: |
7 |
|
%%% |
8 |
|
%%% <ul> |
9 |
|
%%% <li>Preference manager ({@link mod_mam_muc_rdbms_prefs});</li> |
10 |
|
%%% <li>Writer ({@link mod_mam_muc_rdbms_arch} or {@link mod_mam_muc_rdbms_async_pool_writer});</li> |
11 |
|
%%% <li>Archive manager ({@link mod_mam_muc_rdbms_arch});</li> |
12 |
|
%%% <li>User's ID generator ({@link mod_mam_muc_user}).</li> |
13 |
|
%%% </ul> |
14 |
|
%%% |
15 |
|
%%% Preferences can be also stored in Mnesia ({@link mod_mam_mnesia_prefs}). |
16 |
|
%%% This module handles MUC archives. |
17 |
|
%%% |
18 |
|
%%% This module should be started for each host. |
19 |
|
%%% Message archivation is not shaped here (use standard support for this). |
20 |
|
%%% MAM's IQs are shaped inside {@link shaper_srv}. |
21 |
|
%%% |
22 |
|
%%% Message identifiers (or UIDs in the spec) are generated based on: |
23 |
|
%%% |
24 |
|
%%% <ul> |
25 |
|
%%% <li>date (using `timestamp()');</li> |
26 |
|
%%% <li>node number (using {@link ejabberd_node_id}).</li> |
27 |
|
%%% </ul> |
28 |
|
%%% @end |
29 |
|
%%%------------------------------------------------------------------- |
30 |
|
-module(mod_mam_muc). |
31 |
|
-xep([{xep, 313}, {version, "0.4.1"}]). |
32 |
|
-xep([{xep, 313}, {version, "0.5"}]). |
33 |
|
-xep([{xep, 45}, {version, "1.25"}]). |
34 |
|
%% ---------------------------------------------------------------------- |
35 |
|
%% Exports |
36 |
|
|
37 |
|
%% Client API |
38 |
|
-export([delete_archive/2, |
39 |
|
archive_size/2, |
40 |
|
archive_id/2]). |
41 |
|
|
42 |
|
%% gen_mod handlers |
43 |
|
-export([start/2, stop/1, supported_features/0]). |
44 |
|
|
45 |
|
%% ejabberd room handlers |
46 |
|
-export([disco_muc_features/1, |
47 |
|
filter_room_packet/3, |
48 |
|
room_process_mam_iq/5, |
49 |
|
forget_room/4]). |
50 |
|
|
51 |
|
%% gdpr callback |
52 |
|
-export([get_personal_data/3]). |
53 |
|
|
54 |
|
%% private |
55 |
|
-export([archive_message_for_ct/1]). |
56 |
|
-export([lookup_messages/2]). |
57 |
|
-export([archive_id_int/2]). |
58 |
|
|
59 |
|
-ignore_xref([archive_id/2, archive_message_for_ct/1, archive_size/2, behaviour_info/1, |
60 |
|
delete_archive/2, disco_muc_features/1, filter_room_packet/3, |
61 |
|
forget_room/4, get_personal_data/3, start/2, stop/1, supported_features/0]). |
62 |
|
|
63 |
|
%% ---------------------------------------------------------------------- |
64 |
|
%% Imports |
65 |
|
|
66 |
|
%% UID |
67 |
|
-import(mod_mam_utils, |
68 |
|
[generate_message_id/1, |
69 |
|
decode_compact_uuid/1]). |
70 |
|
|
71 |
|
%% XML |
72 |
|
-import(mod_mam_utils, |
73 |
|
[maybe_add_arcid_elems/4, |
74 |
|
replace_x_user_element/4, |
75 |
|
delete_x_user_element/1, |
76 |
|
packet_to_x_user_jid/1, |
77 |
|
wrap_message/6, |
78 |
|
result_set/4, |
79 |
|
result_query/2, |
80 |
|
result_prefs/4, |
81 |
|
make_fin_message/5, |
82 |
|
make_fin_element/7, |
83 |
|
parse_prefs/1, |
84 |
|
borders_decode/1, |
85 |
|
features/2]). |
86 |
|
|
87 |
|
%% Forms |
88 |
|
-import(mod_mam_utils, |
89 |
|
[message_form/3]). |
90 |
|
|
91 |
|
%% Other |
92 |
|
-import(mod_mam_utils, |
93 |
|
[mess_id_to_external_binary/1, |
94 |
|
is_complete_result_page/4]). |
95 |
|
|
96 |
|
-include_lib("mongoose.hrl"). |
97 |
|
-include_lib("jlib.hrl"). |
98 |
|
-include_lib("exml/include/exml.hrl"). |
99 |
|
|
100 |
|
-callback is_complete_message(Module :: atom(), Dir :: atom(), Packet :: any()) -> |
101 |
|
boolean(). |
102 |
|
|
103 |
|
%% ---------------------------------------------------------------------- |
104 |
|
%% Other types |
105 |
|
-type packet() :: any(). |
106 |
|
-type row_batch() :: {TotalCount :: non_neg_integer(), |
107 |
|
Offset :: non_neg_integer(), |
108 |
|
MessageRows :: [row()]}. |
109 |
|
-type row() :: mod_mam:message_row(). |
110 |
|
-type host_type() :: mongooseim:host_type(). |
111 |
|
-type muc_action() :: atom(). |
112 |
|
|
113 |
|
-export_type([row/0, row_batch/0]). |
114 |
|
|
115 |
|
%% ---------------------------------------------------------------------- |
116 |
|
%% API |
117 |
|
|
118 |
|
-spec get_personal_data(gdpr:personal_data(), mongooseim:host_type(), jid:jid()) -> |
119 |
|
gdpr:personal_data(). |
120 |
|
get_personal_data(Acc, HostType, ArcJID) -> |
121 |
:-( |
Schema = ["id", "message"], |
122 |
:-( |
Entries = mongoose_hooks:get_mam_muc_gdpr_data(HostType, ArcJID), |
123 |
:-( |
[{mam_muc, Schema, Entries} | Acc]. |
124 |
|
|
125 |
|
-spec delete_archive(jid:server(), jid:user()) -> ok. |
126 |
|
delete_archive(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
127 |
4 |
?LOG_DEBUG(#{what => mam_delete_room, room => RoomName, sub_host => MucHost}), |
128 |
4 |
ArcJID = jid:make(RoomName, MucHost, <<>>), |
129 |
4 |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
130 |
4 |
ArcID = archive_id_int(HostType, ArcJID), |
131 |
4 |
remove_archive(HostType, ArcID, ArcJID), |
132 |
4 |
ok. |
133 |
|
|
134 |
|
-spec archive_size(jid:server(), jid:user()) -> integer(). |
135 |
|
archive_size(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
136 |
:-( |
ArcJID = jid:make(RoomName, MucHost, <<>>), |
137 |
:-( |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
138 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
139 |
:-( |
archive_size(HostType, ArcID, ArcJID). |
140 |
|
|
141 |
|
-spec archive_id(jid:server(), jid:user()) -> integer(). |
142 |
|
archive_id(MucHost, RoomName) when is_binary(MucHost), is_binary(RoomName) -> |
143 |
:-( |
ArcJID = jid:make(RoomName, MucHost, <<>>), |
144 |
:-( |
HostType = mod_muc_light_utils:room_jid_to_host_type(ArcJID), |
145 |
:-( |
archive_id_int(HostType, ArcJID). |
146 |
|
|
147 |
|
%% ---------------------------------------------------------------------- |
148 |
|
%% gen_mod callbacks |
149 |
|
%% Starting and stopping functions for MUC archives |
150 |
|
|
151 |
|
-spec start(HostType :: host_type(), Opts :: list()) -> any(). |
152 |
|
start(HostType, Opts) -> |
153 |
6 |
?LOG_DEBUG(#{what => mam_muc_starting}), |
154 |
6 |
ensure_metrics(HostType), |
155 |
6 |
ejabberd_hooks:add(hooks(HostType)), |
156 |
6 |
add_iq_handlers(HostType, Opts), |
157 |
6 |
ok. |
158 |
|
|
159 |
|
-spec stop(HostType :: host_type()) -> any(). |
160 |
|
stop(HostType) -> |
161 |
6 |
?LOG_DEBUG(#{what => mam_muc_stopping}), |
162 |
6 |
ejabberd_hooks:delete(hooks(HostType)), |
163 |
6 |
remove_iq_handlers(HostType), |
164 |
6 |
ok. |
165 |
|
|
166 |
|
-spec supported_features() -> [atom()]. |
167 |
|
supported_features() -> |
168 |
:-( |
[dynamic_domains]. |
169 |
|
|
170 |
|
%% ---------------------------------------------------------------------- |
171 |
|
%% hooks and handlers for MUC |
172 |
|
|
173 |
|
-spec disco_muc_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). |
174 |
|
disco_muc_features(Acc = #{host_type := HostType, node := <<>>}) -> |
175 |
6 |
mongoose_disco:add_features(features(?MODULE, HostType), Acc); |
176 |
|
disco_muc_features(Acc) -> |
177 |
:-( |
Acc. |
178 |
|
|
179 |
|
%% @doc Handle public MUC-message. |
180 |
|
-spec filter_room_packet(Packet :: packet(), HostType :: host_type(), |
181 |
|
EventData :: mod_muc:room_event_data()) -> packet(). |
182 |
|
filter_room_packet(Packet, HostType, EventData = #{}) -> |
183 |
:-( |
?LOG_DEBUG(#{what => mam_room_packet, text => <<"Incoming room packet">>, |
184 |
:-( |
packet => Packet, event_data => EventData}), |
185 |
:-( |
IsArchivable = is_archivable_message(HostType, incoming, Packet), |
186 |
:-( |
case IsArchivable of |
187 |
|
true -> |
188 |
:-( |
#{from_nick := FromNick, from_jid := FromJID, room_jid := RoomJID, |
189 |
|
role := Role, affiliation := Affiliation, timestamp := TS} = EventData, |
190 |
:-( |
archive_room_packet(HostType, Packet, FromNick, FromJID, |
191 |
|
RoomJID, Role, Affiliation, TS); |
192 |
:-( |
false -> Packet |
193 |
|
end. |
194 |
|
|
195 |
|
%% @doc Archive without validation. |
196 |
|
-spec archive_room_packet(HostType :: host_type(), |
197 |
|
Packet :: packet(), FromNick :: jid:user(), |
198 |
|
FromJID :: jid:jid(), RoomJID :: jid:jid(), |
199 |
|
Role :: mod_muc:role(), Affiliation :: mod_muc:affiliation(), |
200 |
|
TS :: integer()) -> packet(). |
201 |
|
archive_room_packet(HostType, Packet, FromNick, FromJID = #jid{}, |
202 |
|
RoomJID = #jid{}, Role, Affiliation, TS) -> |
203 |
:-( |
ArcID = archive_id_int(HostType, RoomJID), |
204 |
|
%% Occupant JID <room@service/nick> |
205 |
:-( |
SrcJID = jid:replace_resource(RoomJID, FromNick), |
206 |
:-( |
IsInteresting = |
207 |
|
case get_behaviour(HostType, ArcID, RoomJID, SrcJID) of |
208 |
:-( |
always -> true; |
209 |
:-( |
never -> false; |
210 |
:-( |
roster -> true |
211 |
|
end, |
212 |
:-( |
case IsInteresting of |
213 |
|
true -> |
214 |
:-( |
MessID = generate_message_id(TS), |
215 |
:-( |
Packet1 = replace_x_user_element(FromJID, Role, Affiliation, Packet), |
216 |
:-( |
OriginID = mod_mam_utils:get_origin_id(Packet), |
217 |
:-( |
Params = #{message_id => MessID, |
218 |
|
archive_id => ArcID, |
219 |
|
local_jid => RoomJID, |
220 |
|
remote_jid => FromJID, |
221 |
|
source_jid => SrcJID, |
222 |
|
origin_id => OriginID, |
223 |
|
direction => incoming, |
224 |
|
packet => Packet1}, |
225 |
|
%% Packet to be broadcasted and packet to be archived are |
226 |
|
%% not 100% the same |
227 |
:-( |
Result = archive_message(HostType, Params), |
228 |
:-( |
case Result of |
229 |
|
ok -> |
230 |
:-( |
ExtID = mess_id_to_external_binary(MessID), |
231 |
:-( |
ShouldAdd = mod_mam_params:add_stanzaid_element(?MODULE, HostType), |
232 |
:-( |
maybe_add_arcid_elems(RoomJID, ExtID, Packet, ShouldAdd); |
233 |
:-( |
{error, _} -> Packet |
234 |
|
end; |
235 |
:-( |
false -> Packet |
236 |
|
end. |
237 |
|
|
238 |
|
%% @doc `To' is an account or server entity hosting the archive. |
239 |
|
%% Servers that archive messages on behalf of local users SHOULD expose archives |
240 |
|
%% to the user on their bare JID (i.e. `From.luser'), |
241 |
|
%% while a MUC service might allow MAM queries to be sent to the room's bare JID |
242 |
|
%% (i.e `To.luser'). |
243 |
|
-spec room_process_mam_iq(Acc :: mongoose_acc:t(), |
244 |
|
From :: jid:jid(), |
245 |
|
To :: jid:jid(), |
246 |
|
IQ :: jlib:iq(), |
247 |
|
Extra :: map()) -> {mongoose_acc:t(), jlib:iq() | ignore}. |
248 |
|
room_process_mam_iq(Acc, From, To, IQ, #{host_type := HostType}) -> |
249 |
:-( |
mod_mam_utils:maybe_log_deprecation(IQ), |
250 |
:-( |
Action = mam_iq:action(IQ), |
251 |
:-( |
MucAction = action_to_muc_action(Action), |
252 |
:-( |
case check_action_allowed(HostType, Acc, To#jid.lserver, Action, MucAction, From, To) of |
253 |
|
ok -> |
254 |
:-( |
case mod_mam_utils:wait_shaper(HostType, To#jid.lserver, MucAction, From) of |
255 |
|
ok -> |
256 |
:-( |
handle_error_iq(Acc, HostType, To, Action, |
257 |
|
handle_mam_iq(HostType, Action, From, To, IQ)); |
258 |
|
{error, max_delay_reached} -> |
259 |
:-( |
mongoose_metrics:update(HostType, modMucMamDroppedIQ, 1), |
260 |
:-( |
{Acc, return_max_delay_reached_error_iq(IQ)} |
261 |
|
end; |
262 |
|
{error, Reason} -> |
263 |
:-( |
?LOG_WARNING(#{what => action_not_allowed, |
264 |
|
action => Action, acc => Acc, reason => Reason, |
265 |
:-( |
can_access_room => can_access_room(HostType, Acc, From, To)}), |
266 |
:-( |
{Acc, return_action_not_allowed_error_iq(Reason, IQ)} |
267 |
|
end. |
268 |
|
|
269 |
|
-spec forget_room(map(), host_type(), jid:lserver(), binary()) -> map(). |
270 |
|
forget_room(Acc, _HostType, MucServer, RoomName) -> |
271 |
4 |
delete_archive(MucServer, RoomName), |
272 |
4 |
Acc. |
273 |
|
|
274 |
|
%% ---------------------------------------------------------------------- |
275 |
|
%% Internal functions |
276 |
|
|
277 |
|
-spec check_action_allowed(host_type(), mongoose_acc:t(), jid:lserver(), mam_iq:action(), muc_action(), |
278 |
|
jid:jid(), jid:jid()) -> ok | {error, binary()}. |
279 |
|
check_action_allowed(HostType, Acc, Domain, Action, MucAction, From, To) -> |
280 |
:-( |
case acl:match_rule(HostType, Domain, MucAction, From, default) of |
281 |
:-( |
allow -> ok; |
282 |
:-( |
deny -> {false, <<"Blocked by service policy.">>}; |
283 |
:-( |
default -> check_room_action_allowed_by_default(HostType, Acc, Action, From, To) |
284 |
|
end. |
285 |
|
|
286 |
|
-spec action_to_muc_action(mam_iq:action()) -> atom(). |
287 |
|
action_to_muc_action(Action) -> |
288 |
:-( |
list_to_atom("muc_" ++ atom_to_list(Action)). |
289 |
|
|
290 |
|
-spec check_room_action_allowed_by_default(HostType :: host_type(), |
291 |
|
Acc :: mongoose_acc:t(), |
292 |
|
Action :: mam_iq:action(), |
293 |
|
From :: jid:jid(), |
294 |
|
To :: jid:jid()) -> ok | {error, binary()}. |
295 |
|
check_room_action_allowed_by_default(HostType, Acc, Action, From, To) -> |
296 |
:-( |
case mam_iq:action_type(Action) of |
297 |
|
set -> |
298 |
:-( |
case is_room_owner(HostType, Acc, From, To) of |
299 |
:-( |
true -> ok; |
300 |
:-( |
false -> {error, <<"Not a room owner.">>} |
301 |
|
end; |
302 |
|
get -> |
303 |
:-( |
case can_access_room(HostType, Acc, From, To) of |
304 |
:-( |
true -> ok; |
305 |
:-( |
false -> {error, <<"Not allowed to enter the room.">>} |
306 |
|
end |
307 |
|
end. |
308 |
|
|
309 |
|
-spec is_room_owner(HostType :: host_type(), |
310 |
|
Acc :: mongoose_acc:t(), |
311 |
|
UserJid :: jid:jid(), |
312 |
|
RoomJid :: jid:jid()) -> boolean(). |
313 |
|
is_room_owner(HostType, Acc, UserJid, RoomJid) -> |
314 |
:-( |
mongoose_hooks:is_muc_room_owner(HostType, Acc, UserJid, RoomJid). |
315 |
|
|
316 |
|
%% @doc Return true if user element should be removed from results |
317 |
|
-spec is_user_identity_hidden(HostType :: host_type(), |
318 |
|
UserJid :: jid:jid(), |
319 |
|
RoomJid :: jid:jid()) -> boolean(). |
320 |
|
is_user_identity_hidden(HostType, UserJid, RoomJid) -> |
321 |
:-( |
case mongoose_hooks:can_access_identity(HostType, RoomJid, UserJid) of |
322 |
:-( |
CanAccess when is_boolean(CanAccess) -> not CanAccess |
323 |
|
end. |
324 |
|
|
325 |
|
-spec can_access_room(HostType :: host_type(), |
326 |
|
Acc :: mongoose_acc:t(), |
327 |
|
UserJid :: jid:jid(), |
328 |
|
RoomJid :: jid:jid()) -> boolean(). |
329 |
|
can_access_room(HostType, Acc, UserJid, RoomJid) -> |
330 |
:-( |
mongoose_hooks:can_access_room(HostType, Acc, RoomJid, UserJid). |
331 |
|
|
332 |
|
-spec handle_mam_iq(HostType :: host_type(), mam_iq:action(), |
333 |
|
From :: jid:jid(), jid:jid(), jlib:iq()) -> |
334 |
|
jlib:iq() | {error, any(), jlib:iq()} | ignore. |
335 |
|
handle_mam_iq(HostType, Action, From, To, IQ) -> |
336 |
:-( |
case Action of |
337 |
|
mam_get_prefs -> |
338 |
:-( |
handle_get_prefs(HostType, To, IQ); |
339 |
|
mam_set_prefs -> |
340 |
:-( |
handle_set_prefs(HostType, To, IQ); |
341 |
|
mam_set_message_form -> |
342 |
:-( |
handle_set_message_form(HostType, From, To, IQ); |
343 |
|
mam_get_message_form -> |
344 |
:-( |
handle_get_message_form(HostType, From, To, IQ) |
345 |
|
end. |
346 |
|
|
347 |
|
-spec handle_set_prefs(host_type(), jid:jid(), jlib:iq()) -> |
348 |
|
jlib:iq() | {error, any(), jlib:iq()}. |
349 |
|
handle_set_prefs(HostType, ArcJID = #jid{}, |
350 |
|
IQ = #iq{sub_el = PrefsEl}) -> |
351 |
:-( |
{DefaultMode, AlwaysJIDs, NeverJIDs} = parse_prefs(PrefsEl), |
352 |
:-( |
?LOG_DEBUG(#{what => mam_muc_set_prefs, archive_jid => ArcJID, |
353 |
|
default_mode => DefaultMode, |
354 |
:-( |
always_jids => AlwaysJIDs, never_jids => NeverJIDs, iq => IQ}), |
355 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
356 |
:-( |
Res = set_prefs(HostType, ArcID, ArcJID, DefaultMode, AlwaysJIDs, NeverJIDs), |
357 |
:-( |
handle_set_prefs_result(Res, DefaultMode, AlwaysJIDs, NeverJIDs, IQ). |
358 |
|
|
359 |
|
handle_set_prefs_result(ok, DefaultMode, AlwaysJIDs, NeverJIDs, IQ) -> |
360 |
:-( |
ResultPrefsEl = result_prefs(DefaultMode, AlwaysJIDs, NeverJIDs, IQ#iq.xmlns), |
361 |
:-( |
IQ#iq{type = result, sub_el = [ResultPrefsEl]}; |
362 |
|
handle_set_prefs_result({error, Reason}, |
363 |
|
_DefaultMode, _AlwaysJIDs, _NeverJIDs, IQ) -> |
364 |
:-( |
return_error_iq(IQ, Reason). |
365 |
|
|
366 |
|
-spec handle_get_prefs(host_type(), jid:jid(), jlib:iq()) -> |
367 |
|
jlib:iq() | {error, any(), jlib:iq()}. |
368 |
|
handle_get_prefs(HostType, ArcJID=#jid{}, IQ=#iq{}) -> |
369 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
370 |
:-( |
Res = get_prefs(HostType, ArcID, ArcJID, always), |
371 |
:-( |
handle_get_prefs_result(ArcJID, Res, IQ). |
372 |
|
|
373 |
|
handle_get_prefs_result(ArcJID, {DefaultMode, AlwaysJIDs, NeverJIDs}, IQ) -> |
374 |
:-( |
?LOG_DEBUG(#{what => mam_muc_get_prefs_result, archive_jid => ArcJID, |
375 |
|
default_mode => DefaultMode, |
376 |
:-( |
always_jids => AlwaysJIDs, never_jids => NeverJIDs, iq => IQ}), |
377 |
:-( |
ResultPrefsEl = result_prefs(DefaultMode, AlwaysJIDs, NeverJIDs, IQ#iq.xmlns), |
378 |
:-( |
IQ#iq{type = result, sub_el = [ResultPrefsEl]}; |
379 |
|
handle_get_prefs_result(_ArcJID, {error, Reason}, IQ) -> |
380 |
:-( |
return_error_iq(IQ, Reason). |
381 |
|
|
382 |
|
-spec handle_set_message_form(HostType :: host_type(), |
383 |
|
From :: jid:jid(), ArcJID :: jid:jid(), |
384 |
|
IQ :: jlib:iq()) -> |
385 |
|
jlib:iq() | ignore | {error, term(), jlib:iq()}. |
386 |
|
handle_set_message_form(HostType, #jid{} = From, #jid{} = ArcJID, IQ) -> |
387 |
:-( |
ArcID = archive_id_int(HostType, ArcJID), |
388 |
:-( |
ResLimit = mod_mam_params:max_result_limit(?MODULE, HostType), |
389 |
:-( |
DefLimit = mod_mam_params:default_result_limit(?MODULE, HostType), |
390 |
:-( |
ExtMod = mod_mam_params:extra_params_module(?MODULE, HostType), |
391 |
:-( |
Params0 = mam_iq:form_to_lookup_params(IQ, ResLimit, DefLimit, ExtMod), |
392 |
:-( |
Params = mam_iq:lookup_params_with_archive_details(Params0, ArcID, ArcJID, From), |
393 |
:-( |
Result = lookup_messages(HostType, Params), |
394 |
:-( |
handle_lookup_result(Result, HostType, From, IQ, Params). |
395 |
|
|
396 |
|
-spec handle_lookup_result({ok, mod_mam:lookup_result()} | {error, term()}, |
397 |
|
host_type(), jid:jid(), jlib:iq(), map()) -> |
398 |
|
jlib:iq() | ignore | {error, term(), jlib:iq()}. |
399 |
|
handle_lookup_result(Result, HostType, From, IQ, #{owner_jid := ArcJID} = Params) -> |
400 |
:-( |
case Result of |
401 |
|
{error, Reason} -> |
402 |
:-( |
report_issue(Reason, mam_muc_lookup_failed, ArcJID, IQ), |
403 |
:-( |
return_error_iq(IQ, Reason); |
404 |
|
{ok, Res} -> |
405 |
:-( |
send_messages_and_iq_result(Res, HostType, From, IQ, Params) |
406 |
|
end. |
407 |
|
|
408 |
|
send_messages_and_iq_result({TotalCount, Offset, MessageRows}, HostType, From, |
409 |
|
#iq{xmlns = MamNs, sub_el = QueryEl} = IQ, |
410 |
|
#{owner_jid := ArcJID} = Params) -> |
411 |
|
%% Forward messages |
412 |
:-( |
QueryID = exml_query:attr(QueryEl, <<"queryid">>, <<>>), |
413 |
:-( |
{FirstMessID, LastMessID} = forward_messages(HostType, From, ArcJID, MamNs, |
414 |
|
QueryID, MessageRows, true), |
415 |
|
|
416 |
|
%% Make fin iq |
417 |
:-( |
IsComplete = is_complete_result_page(TotalCount, Offset, MessageRows, Params), |
418 |
:-( |
IsStable = true, |
419 |
:-( |
ResultSetEl = result_set(FirstMessID, LastMessID, Offset, TotalCount), |
420 |
:-( |
ExtFinMod = mod_mam_params:extra_fin_element_module(?MODULE, HostType), |
421 |
:-( |
FinElem = make_fin_element(HostType, Params, IQ#iq.xmlns, IsComplete, IsStable, ResultSetEl, ExtFinMod), |
422 |
:-( |
IQ#iq{type = result, sub_el = [FinElem]}. |
423 |
|
|
424 |
|
forward_messages(HostType, From, ArcJID, MamNs, QueryID, MessageRows, SetClientNs) -> |
425 |
|
%% Forward messages |
426 |
:-( |
{FirstMessID, LastMessID, HideUser} = |
427 |
|
case MessageRows of |
428 |
:-( |
[] -> {undefined, undefined, undefined}; |
429 |
:-( |
[_ | _] -> {message_row_to_ext_id(hd(MessageRows)), |
430 |
|
message_row_to_ext_id(lists:last(MessageRows)), |
431 |
|
is_user_identity_hidden(HostType, From, ArcJID)} |
432 |
|
end, |
433 |
:-( |
SendModule = mod_mam_params:send_message_mod(?MODULE, HostType), |
434 |
:-( |
[send_message(SendModule, Row, ArcJID, From, |
435 |
|
message_row_to_xml(MamNs, From, HideUser, SetClientNs, Row, |
436 |
|
QueryID)) |
437 |
:-( |
|| Row <- MessageRows], |
438 |
:-( |
{FirstMessID, LastMessID}. |
439 |
|
|
440 |
|
send_message(SendModule, Row, ArcJID, From, Packet) -> |
441 |
:-( |
mam_send_message:call_send_message(SendModule, Row, ArcJID, From, Packet). |
442 |
|
|
443 |
|
-spec handle_get_message_form(host_type(), jid:jid(), jid:jid(), jlib:iq()) -> |
444 |
|
jlib:iq(). |
445 |
|
handle_get_message_form(HostType, |
446 |
|
_From = #jid{}, _ArcJID = #jid{}, IQ = #iq{}) -> |
447 |
:-( |
return_message_form_iq(HostType, IQ). |
448 |
|
|
449 |
|
%% ---------------------------------------------------------------------- |
450 |
|
%% Backend wrappers |
451 |
|
|
452 |
|
-spec archive_id_int(HostType :: host_type(), ArcJID :: jid:jid()) -> |
453 |
|
integer() | undefined. |
454 |
|
archive_id_int(HostType, ArcJID = #jid{}) -> |
455 |
4 |
mongoose_hooks:mam_muc_archive_id(HostType, ArcJID). |
456 |
|
|
457 |
|
-spec archive_size(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
458 |
|
ArcJID ::jid:jid()) -> non_neg_integer(). |
459 |
|
archive_size(HostType, ArcID, ArcJID = #jid{}) -> |
460 |
:-( |
mongoose_hooks:mam_muc_archive_size(HostType, ArcID, ArcJID). |
461 |
|
|
462 |
|
-spec get_behaviour(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
463 |
|
LocJID :: jid:jid(), RemJID :: jid:jid()) -> any(). |
464 |
|
get_behaviour(HostType, ArcID, LocJID = #jid{}, RemJID = #jid{}) -> |
465 |
:-( |
mongoose_hooks:mam_muc_get_behaviour(HostType, ArcID, LocJID, RemJID). |
466 |
|
|
467 |
|
-spec set_prefs(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
468 |
|
ArcJID :: jid:jid(), DefaultMode :: mod_mam:archive_behaviour(), |
469 |
|
AlwaysJIDs :: [jid:literal_jid()], |
470 |
|
NeverJIDs :: [jid:literal_jid()]) -> any(). |
471 |
|
set_prefs(HostType, ArcID, ArcJID, DefaultMode, AlwaysJIDs, NeverJIDs) -> |
472 |
:-( |
mongoose_hooks:mam_muc_set_prefs(HostType, ArcID, ArcJID, DefaultMode, |
473 |
|
AlwaysJIDs, NeverJIDs). |
474 |
|
|
475 |
|
%% @doc Load settings from the database. |
476 |
|
-spec get_prefs(HostType :: host_type(), ArcID :: mod_mam:archive_id(), |
477 |
|
ArcJID :: jid:jid(), GlobalDefaultMode :: mod_mam:archive_behaviour()) |
478 |
|
-> mod_mam:preference() | {error, Reason :: term()}. |
479 |
|
get_prefs(HostType, ArcID, ArcJID, GlobalDefaultMode) -> |
480 |
:-( |
mongoose_hooks:mam_muc_get_prefs(HostType, GlobalDefaultMode, ArcID, ArcJID). |
481 |
|
|
482 |
|
-spec remove_archive(host_type(), mod_mam:archive_id() | undefined, |
483 |
|
jid:jid()) -> ok. |
484 |
|
remove_archive(HostType, ArcID, ArcJID = #jid{}) -> |
485 |
4 |
mongoose_hooks:mam_muc_remove_archive(HostType, ArcID, ArcJID), |
486 |
4 |
ok. |
487 |
|
|
488 |
|
%% See description in mod_mam. |
489 |
|
-spec lookup_messages(HostType :: host_type(), Params :: map()) -> |
490 |
|
{ok, mod_mam:lookup_result()} |
491 |
|
| {error, 'policy-violation'} |
492 |
|
| {error, Reason :: term()}. |
493 |
|
lookup_messages(HostType, Params) -> |
494 |
:-( |
Result = lookup_messages_without_policy_violation_check(HostType, Params), |
495 |
|
%% If a query returns a number of stanzas greater than this limit and the |
496 |
|
%% client did not specify a limit using RSM then the server should return |
497 |
|
%% a policy-violation error to the client. |
498 |
:-( |
mod_mam_utils:check_result_for_policy_violation(Params, Result). |
499 |
|
|
500 |
|
lookup_messages_without_policy_violation_check(HostType, |
501 |
|
#{search_text := SearchText} = Params) -> |
502 |
:-( |
case SearchText /= undefined andalso |
503 |
:-( |
not mod_mam_params:has_full_text_search(?MODULE, HostType) of |
504 |
|
true -> %% Use of disabled full text search |
505 |
:-( |
{error, 'not-supported'}; |
506 |
|
false -> |
507 |
:-( |
mongoose_hooks:mam_muc_lookup_messages(HostType, Params) |
508 |
|
end. |
509 |
|
|
510 |
|
archive_message_for_ct(Params = #{local_jid := RoomJid}) -> |
511 |
:-( |
HostType = mod_muc_light_utils:room_jid_to_host_type(RoomJid), |
512 |
:-( |
archive_message(HostType, Params). |
513 |
|
|
514 |
|
-spec archive_message(host_type(), mod_mam:archive_message_params()) -> ok | {error, timeout}. |
515 |
|
archive_message(HostType, Params) -> |
516 |
:-( |
mongoose_hooks:mam_muc_archive_message(HostType, Params). |
517 |
|
|
518 |
|
%% ---------------------------------------------------------------------- |
519 |
|
%% Helpers |
520 |
|
|
521 |
|
-spec message_row_to_xml(binary(), jid:jid(), boolean(), boolean(), row(), binary() | undefined) -> |
522 |
|
exml:element(). |
523 |
|
message_row_to_xml(MamNs, ReceiverJID, HideUser, SetClientNs, |
524 |
|
#{id := MessID, jid := SrcJID, packet := Packet}, QueryID) -> |
525 |
:-( |
{Microseconds, _NodeMessID} = decode_compact_uuid(MessID), |
526 |
:-( |
TS = calendar:system_time_to_rfc3339(erlang:convert_time_unit(Microseconds, microsecond, second), [{offset, "Z"}]), |
527 |
:-( |
BExtMessID = mess_id_to_external_binary(MessID), |
528 |
:-( |
Packet1 = maybe_delete_x_user_element(HideUser, ReceiverJID, Packet), |
529 |
:-( |
Packet2 = mod_mam_utils:maybe_set_client_xmlns(SetClientNs, Packet1), |
530 |
:-( |
Packet3 = replace_from_to_attributes(SrcJID, Packet2), |
531 |
:-( |
wrap_message(MamNs, Packet3, QueryID, BExtMessID, TS, SrcJID). |
532 |
|
|
533 |
|
maybe_delete_x_user_element(true, ReceiverJID, Packet) -> |
534 |
:-( |
PacketJID = packet_to_x_user_jid(Packet), |
535 |
:-( |
case jid:are_bare_equal(ReceiverJID, PacketJID) of |
536 |
|
false -> |
537 |
:-( |
delete_x_user_element(Packet); |
538 |
|
true -> %% expose identity for user's own messages |
539 |
:-( |
Packet |
540 |
|
end; |
541 |
|
maybe_delete_x_user_element(false, _ReceiverJID, Packet) -> |
542 |
:-( |
Packet. |
543 |
|
|
544 |
|
%% From XEP-0313: |
545 |
|
%% When sending out the archives to a requesting client, the 'to' of the |
546 |
|
%% forwarded stanza MUST be empty, and the 'from' MUST be the occupant JID |
547 |
|
%% of the sender of the archived message. |
548 |
|
%% However, Smack crashes if 'to' is present, so it is removed. |
549 |
|
replace_from_to_attributes(SrcJID, Packet = #xmlel{attrs = Attrs}) -> |
550 |
:-( |
NewAttrs = jlib:replace_from_to_attrs(jid:to_binary(SrcJID), undefined, Attrs), |
551 |
:-( |
Packet#xmlel{attrs = NewAttrs}. |
552 |
|
|
553 |
|
-spec message_row_to_ext_id(row()) -> binary(). |
554 |
|
message_row_to_ext_id(#{id := MessID}) -> |
555 |
:-( |
mess_id_to_external_binary(MessID). |
556 |
|
|
557 |
|
-spec handle_error_iq(mongoose_acc:t(), host_type(), jid:jid(), atom(), |
558 |
|
{error, term(), jlib:iq()} | jlib:iq() | ignore) -> {mongoose_acc:t(), jlib:iq() | ignore}. |
559 |
|
handle_error_iq(Acc, HostType, _To, _Action, {error, _Reason, IQ}) -> |
560 |
:-( |
mongoose_metrics:update(HostType, modMucMamDroppedIQ, 1), |
561 |
:-( |
{Acc, IQ}; |
562 |
|
handle_error_iq(Acc, _HostType, _To, _Action, IQ) -> |
563 |
:-( |
{Acc, IQ}. |
564 |
|
|
565 |
|
return_error_iq(IQ, {Reason, {stacktrace, _Stacktrace}}) -> |
566 |
:-( |
return_error_iq(IQ, Reason); |
567 |
|
return_error_iq(IQ, timeout) -> |
568 |
:-( |
{error, timeout, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:service_unavailable(<<"en">>, <<"Timeout in mod_mam_muc">>)]}}; |
569 |
|
return_error_iq(IQ, item_not_found) -> |
570 |
:-( |
{error, item_not_found, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:item_not_found()]}}; |
571 |
|
return_error_iq(IQ, not_implemented) -> |
572 |
:-( |
{error, not_implemented, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:feature_not_implemented(<<"en">>, <<"From mod_mam_muc">>)]}}; |
573 |
|
return_error_iq(IQ, missing_with_jid) -> |
574 |
:-( |
Error = mongoose_xmpp_errors:bad_request(<<"en">>, |
575 |
|
<<"Limited set of queries allowed in the conversation mode.", |
576 |
|
"Missing with_jid filter">>), |
577 |
:-( |
{error, bad_request, IQ#iq{type = error, sub_el = [Error]}}; |
578 |
|
return_error_iq(IQ, Reason) -> |
579 |
:-( |
{error, Reason, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:internal_server_error()]}}. |
580 |
|
|
581 |
|
-spec return_action_not_allowed_error_iq(Reason :: binary(), jlib:iq()) -> jlib:iq(). |
582 |
|
return_action_not_allowed_error_iq(Reason, IQ) -> |
583 |
:-( |
ErrorEl = jlib:stanza_errort(<<"">>, <<"cancel">>, <<"not-allowed">>, |
584 |
|
<<"en">>, <<"The action is not allowed. ", Reason/binary>>), |
585 |
:-( |
IQ#iq{type = error, sub_el = [ErrorEl]}. |
586 |
|
|
587 |
|
-spec return_max_delay_reached_error_iq(jlib:iq()) -> jlib:iq(). |
588 |
|
return_max_delay_reached_error_iq(IQ) -> |
589 |
|
%% Message not found. |
590 |
:-( |
ErrorEl = mongoose_xmpp_errors:resource_constraint( |
591 |
|
<<"en">>, <<"The action is cancelled because of flooding.">>), |
592 |
:-( |
IQ#iq{type = error, sub_el = [ErrorEl]}. |
593 |
|
|
594 |
|
return_message_form_iq(HostType, IQ) -> |
595 |
:-( |
IQ#iq{type = result, sub_el = [message_form(?MODULE, HostType, IQ#iq.xmlns)]}. |
596 |
|
|
597 |
|
% the stacktrace is a big lie |
598 |
|
report_issue({Reason, {stacktrace, Stacktrace}}, Issue, ArcJID, IQ) -> |
599 |
:-( |
report_issue(Reason, Stacktrace, Issue, ArcJID, IQ); |
600 |
|
report_issue(Reason, Issue, ArcJID, IQ) -> |
601 |
:-( |
report_issue(Reason, [], Issue, ArcJID, IQ). |
602 |
|
|
603 |
|
report_issue(item_not_found, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
604 |
:-( |
expected; |
605 |
|
report_issue(missing_with_jid, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
606 |
:-( |
expected; |
607 |
|
report_issue(not_implemented, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
608 |
:-( |
expected; |
609 |
|
report_issue(timeout, _Stacktrace, _Issue, _ArcJID, _IQ) -> |
610 |
:-( |
expected; |
611 |
|
report_issue(Reason, Stacktrace, Issue, #jid{lserver = LServer, luser = LUser}, IQ) -> |
612 |
:-( |
?LOG_ERROR(#{what => mam_muc_error, issue => Issue, reason => Reason, |
613 |
:-( |
user => LUser, server => LServer, iq => IQ, stacktrace => Stacktrace}). |
614 |
|
|
615 |
|
-spec is_archivable_message(HostType :: host_type(), |
616 |
|
Dir :: incoming | outgoing, |
617 |
|
Packet :: exml:element()) -> boolean(). |
618 |
|
is_archivable_message(HostType, Dir, Packet) -> |
619 |
:-( |
{M, F} = mod_mam_params:is_archivable_message_fun(?MODULE, HostType), |
620 |
:-( |
ArchiveChatMarkers = mod_mam_params:archive_chat_markers(?MODULE, HostType), |
621 |
:-( |
erlang:apply(M, F, [?MODULE, Dir, Packet, ArchiveChatMarkers]). |
622 |
|
|
623 |
|
-spec hooks(host_type()) -> [ejabberd_hooks:hook()]. |
624 |
|
hooks(HostType) -> |
625 |
12 |
[{disco_muc_features, HostType, ?MODULE, disco_muc_features, 99}, |
626 |
|
{filter_room_packet, HostType, ?MODULE, filter_room_packet, 60}, |
627 |
|
{forget_room, HostType, ?MODULE, forget_room, 90}, |
628 |
|
{get_personal_data, HostType, ?MODULE, get_personal_data, 50} |
629 |
|
| mongoose_metrics_mam_hooks:get_mam_muc_hooks(HostType)]. |
630 |
|
|
631 |
|
add_iq_handlers(HostType, Opts) -> |
632 |
6 |
IQDisc = gen_mod:get_opt(iqdisc, Opts, parallel), |
633 |
6 |
MUCSubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host, |
634 |
|
mod_muc:default_host()), |
635 |
|
|
636 |
6 |
gen_iq_handler:add_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
637 |
|
?NS_MAM_04, mod_muc_iq, |
638 |
|
fun ?MODULE:room_process_mam_iq/5, |
639 |
|
#{}, IQDisc), |
640 |
6 |
gen_iq_handler:add_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
641 |
|
?NS_MAM_06, mod_muc_iq, |
642 |
|
fun ?MODULE:room_process_mam_iq/5, |
643 |
|
#{}, IQDisc), |
644 |
6 |
ok. |
645 |
|
|
646 |
|
remove_iq_handlers(HostType) -> |
647 |
6 |
MUCSubdomainPattern = gen_mod:get_module_opt(HostType, ?MODULE, host, |
648 |
|
mod_muc:default_host()), |
649 |
6 |
gen_iq_handler:remove_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
650 |
|
?NS_MAM_04, mod_muc_iq), |
651 |
6 |
gen_iq_handler:remove_iq_handler_for_subdomain(HostType, MUCSubdomainPattern, |
652 |
|
?NS_MAM_06, mod_muc_iq), |
653 |
6 |
ok. |
654 |
|
|
655 |
|
ensure_metrics(HostType) -> |
656 |
6 |
lists:foreach(fun(Name) -> |
657 |
54 |
mongoose_metrics:ensure_metric(HostType, Name, spiral) |
658 |
|
end, |
659 |
|
spirals()). |
660 |
|
|
661 |
|
spirals() -> |
662 |
6 |
[modMucMamPrefsSets, |
663 |
|
modMucMamPrefsGets, |
664 |
|
modMucMamArchiveRemoved, |
665 |
|
modMucMamLookups, |
666 |
|
modMucMamForwarded, |
667 |
|
modMucMamArchived, |
668 |
|
modMucMamFlushed, |
669 |
|
modMucMamDropped, |
670 |
|
modMucMamDroppedIQ]. |