./ct_report/coverage/mod_mam_muc.COVER.html

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