./ct_report/coverage/mod_mam_utils.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author Uvarov Michael <arcusfelis@gmail.com>
3 %%% @copyright (C) 2013, Uvarov Michael
4 %%% @doc General functions for MAM.
5 %%% @end
6 %%%-------------------------------------------------------------------
7 -module(mod_mam_utils).
8 %% Time
9 -export([maybe_microseconds/1]).
10
11 %% UID
12 -export([get_or_generate_mam_id/1,
13 generate_message_id/1,
14 encode_compact_uuid/2,
15 decode_compact_uuid/1,
16 mess_id_to_external_binary/1,
17 external_binary_to_mess_id/1,
18 wrapper_id/0]).
19
20 %% XML
21 -export([maybe_add_arcid_elems/4,
22 maybe_log_deprecation/1,
23 is_arcid_elem_for/3,
24 replace_arcid_elem/4,
25 replace_x_user_element/4,
26 append_arcid_elem/4,
27 delete_arcid_elem/3,
28 delete_x_user_element/1,
29 packet_to_x_user_jid/1,
30 get_one_of_path/2,
31 get_one_of_path/3,
32 is_archivable_message/4,
33 has_message_retraction/2,
34 get_retract_id/2,
35 get_origin_id/1,
36 is_groupchat/1,
37 should_page_be_flipped/1,
38 tombstone/2,
39 wrap_message/6,
40 wrap_message/7,
41 result_set/4,
42 result_query/2,
43 result_prefs/4,
44 make_fin_element/7,
45 parse_prefs/1,
46 form_borders_decode/1,
47 form_decode_optimizations/1,
48 is_mam_result_message/1,
49 make_metadata_element/0,
50 make_metadata_element/4,
51 features/2]).
52
53 %% Forms
54 -export([
55 message_form/3,
56 form_to_text/1
57 ]).
58
59 %% Text search
60 -export([
61 normalize_search_text/1,
62 normalize_search_text/2,
63 packet_to_search_body/2,
64 has_full_text_search/2
65 ]).
66
67 %% JID serialization
68 -export([jid_to_opt_binary/2,
69 expand_minified_jid/2]).
70
71 %% Other
72 -export([maybe_integer/2,
73 maybe_min/2,
74 maybe_max/2,
75 maybe_last/1,
76 apply_start_border/2,
77 apply_end_border/2,
78 bare_jid/1,
79 full_jid/1,
80 calculate_msg_id_borders/3,
81 calculate_msg_id_borders/4,
82 maybe_encode_compact_uuid/2,
83 wait_shaper/4,
84 check_for_item_not_found/3,
85 maybe_reverse_messages/2,
86 get_msg_id_and_timestamp/1,
87 lookup_specific_messages/4,
88 is_mam_muc_enabled/2]).
89
90 %% Ejabberd
91 -export([send_message/4,
92 maybe_set_client_xmlns/2,
93 is_jid_in_user_roster/3]).
94
95 %% Shared logic
96 -export([check_result_for_policy_violation/2,
97 lookup/3,
98 lookup_first_and_last_messages/4,
99 lookup_first_and_last_messages/5,
100 incremental_delete_domain/5,
101 db_message_codec/2, db_jid_codec/2]).
102
103 -callback extra_fin_element(mongooseim:host_type(),
104 mam_iq:lookup_params(),
105 exml:element()) -> exml:element().
106
107 -ignore_xref([behaviour_info/1, append_arcid_elem/4, delete_arcid_elem/3,
108 get_one_of_path/3, is_arcid_elem_for/3, maybe_encode_compact_uuid/2,
109 maybe_last/1, result_query/2, send_message/4, wrap_message/7, wrapper_id/0]).
110
111 %-define(MAM_INLINE_UTILS, true).
112
113 -ifdef(MAM_INLINE_UTILS).
114 -compile({inline, [
115 is_valid_message/4,
116 is_valid_message_type/3,
117 encode_compact_uuid/2,
118 get_one_of_path/3,
119 delay/2,
120 forwarded/3,
121 result/4,
122 valid_behavior/1]}).
123 -endif.
124
125 -include("jlib.hrl").
126 -include_lib("exml/include/exml.hrl").
127
128 -ifdef(TEST).
129 -include_lib("eunit/include/eunit.hrl").
130 -export([is_valid_message/4]).
131 -endif.
132
133 -include("mod_mam.hrl").
134 -include("mongoose_rsm.hrl").
135 -include("mongoose_ns.hrl").
136
137 -define(MAYBE_BIN(X), (is_binary(X) orelse (X) =:= undefined)).
138 -define(BIGINT_MAX, 16#7fffffffffffffff). % 9223372036854775807, (2^63 - 1)
139
140 -export_type([direction/0, retraction_id/0, retraction_info/0]).
141
142 %% ----------------------------------------------------------------------
143 %% Datetime types
144 -type ne_binary() :: <<_:8, _:_*8>>.
145 -type iso8601_datetime_binary() :: ne_binary().
146 %% Microseconds from 01.01.1970
147 -type unix_timestamp() :: mod_mam:unix_timestamp().
148
149 -type archive_behaviour() :: mod_mam:archive_behaviour().
150 -type archive_behaviour_bin() :: binary(). % `<<"roster">> | <<"always">> | <<"never">>'.
151
152 -type direction() :: incoming | outgoing.
153 -type retraction_id() :: {origin_id | stanza_id, binary()}.
154 -type retraction_info() :: #{retract_on := origin_id | stanza_id,
155 packet := exml:element(),
156 message_id := mod_mam:message_id(),
157 origin_id := null | binary()}.
158
159 %% -----------------------------------------------------------------------
160 %% Time
161
162 %% @doc Return a unix timestamp in microseconds.
163 %%
164 %% "maybe" means, that the function may return `undefined'.
165 %% @end
166 -spec maybe_microseconds(iso8601_datetime_binary()) -> unix_timestamp();
167 (<<>>) -> undefined.
168
:-(
maybe_microseconds(<<>>) -> undefined;
169 maybe_microseconds(ISODateTime) ->
170 201 try calendar:rfc3339_to_system_time(binary_to_list(ISODateTime), [{unit, microsecond}])
171 12 catch error:_Error -> undefined
172 end.
173
174 %% -----------------------------------------------------------------------
175 %% UID
176
177 -spec get_or_generate_mam_id(mongoose_acc:t()) -> integer().
178 get_or_generate_mam_id(Acc) ->
179 2459 case mongoose_acc:get(mam, mam_id, undefined, Acc) of
180 undefined ->
181 2438 CandidateStamp = mongoose_acc:timestamp(Acc),
182 2438 generate_message_id(CandidateStamp);
183 ExtMessId ->
184 21 mod_mam_utils:external_binary_to_mess_id(ExtMessId)
185 end.
186
187 -spec generate_message_id(integer()) -> integer().
188 generate_message_id(CandidateStamp) ->
189 3041 NodeNum = mongoose_node_num:node_num(),
190 3041 UniqueStamp = mongoose_mam_id:next_unique(CandidateStamp),
191 3041 encode_compact_uuid(UniqueStamp, NodeNum).
192
193 %% @doc Create a message ID (UID).
194 %%
195 %% It removes a leading 0 from 64-bit binary representation.
196 %% It puts node id as a last byte.
197 %% The maximum date, that can be encoded is `{{4253, 5, 31}, {22, 20, 37}}'.
198 -spec encode_compact_uuid(integer(), integer()) -> integer().
199 encode_compact_uuid(Microseconds, NodeNum)
200 when is_integer(Microseconds), is_integer(NodeNum) ->
201 12242 (Microseconds bsl 8) + NodeNum.
202
203
204 %% @doc Extract date and node id from a message id.
205 -spec decode_compact_uuid(integer()) -> {integer(), byte()}.
206 decode_compact_uuid(Id) ->
207 4754 Microseconds = Id bsr 8,
208 4754 NodeNum = Id band 255,
209 4754 {Microseconds, NodeNum}.
210
211
212 %% @doc Encode a message ID to pass it to the user.
213 -spec mess_id_to_external_binary(integer()) -> binary().
214 mess_id_to_external_binary(MessID) when is_integer(MessID) ->
215 9944 integer_to_binary(MessID, 32).
216
217 %% @doc Decode a message ID received from the user.
218 -spec external_binary_to_mess_id(binary()) -> integer().
219 external_binary_to_mess_id(BExtMessID) when is_binary(BExtMessID) ->
220 700 try binary_to_integer(BExtMessID, 32) of
221 665 MessId when is_integer(MessId), MessId =< ?BIGINT_MAX -> MessId;
222 14 _ -> throw(invalid_stanza_id)
223 21 catch error:badarg -> throw(invalid_stanza_id)
224 end.
225
226 %% -----------------------------------------------------------------------
227 %% XML
228
229 -spec maybe_add_arcid_elems(To :: jid:simple_jid() | jid:jid(),
230 MessID :: binary(), Packet :: exml:element(),
231 AddStanzaid :: boolean()) ->
232 AlteredPacket :: exml:element().
233 maybe_add_arcid_elems(To, MessID, Packet, AddStanzaid) ->
234 1863 BareTo = jid:to_bare_binary(To),
235 1863 case AddStanzaid of
236 true ->
237 1849 replace_arcid_elem(<<"stanza-id">>, BareTo, MessID, Packet);
238 14 _ -> Packet
239 end.
240
241 maybe_log_deprecation(_IQ) ->
242 1982 ok. %% May be reused for future MAM versions.
243
244 %% @doc Return true, if the first element points on `By'.
245 -spec is_arcid_elem_for(ElemName :: binary(), exml:element(), By :: binary()) -> boolean().
246 is_arcid_elem_for(<<"archived">>, #xmlel{name = <<"archived">>, attrs=As}, By) ->
247
:-(
lists:member({<<"by">>, By}, As);
248 is_arcid_elem_for(<<"stanza-id">>, #xmlel{name = <<"stanza-id">>, attrs=As}, By) ->
249 85 lists:member({<<"by">>, By}, As) andalso
250
:-(
lists:member({<<"xmlns">>, ?NS_STANZAID}, As);
251 is_arcid_elem_for(_, _, _) ->
252 2152 false.
253
254 -spec replace_arcid_elem(ElemName :: binary(), By :: binary(), Id :: binary(),
255 Packet :: exml:element()) -> exml:element().
256 replace_arcid_elem(ElemName, By, Id, Packet) ->
257 1856 append_arcid_elem(ElemName, By, Id,
258 delete_arcid_elem(ElemName, By, Packet)).
259
260 -spec append_arcid_elem(ElemName :: binary(), By :: binary(), Id :: binary(),
261 Packet :: exml:element()) ->exml:element().
262 append_arcid_elem(<<"stanza-id">>, By, Id, Packet) ->
263 1856 Archived = #xmlel{
264 name = <<"stanza-id">>,
265 attrs=[{<<"by">>, By}, {<<"id">>, Id}, {<<"xmlns">>, ?NS_STANZAID}]},
266 1856 xml:append_subtags(Packet, [Archived]);
267 append_arcid_elem(ElemName, By, Id, Packet) ->
268
:-(
Archived = #xmlel{
269 name = ElemName,
270 attrs=[{<<"by">>, By}, {<<"id">>, Id}]},
271
:-(
xml:append_subtags(Packet, [Archived]).
272
273 -spec delete_arcid_elem(ElemName :: binary(), By :: binary(), exml:element()) -> exml:element().
274 delete_arcid_elem(ElemName, By, Packet=#xmlel{children=Cs}) ->
275 1856 Packet#xmlel{children=[C || C <- Cs, not is_arcid_elem_for(ElemName, C, By)]}.
276
277
278 is_x_user_element(#xmlel{name = <<"x">>, attrs = As}) ->
279 416 lists:member({<<"xmlns">>, ?NS_MUC_USER}, As);
280 is_x_user_element(_) ->
281 875 false.
282
283 -spec replace_x_user_element(FromJID :: jid:jid(), Role :: mod_muc:role(),
284 Affiliation :: mod_muc:affiliation(), exml:element()) -> exml:element().
285 replace_x_user_element(FromJID, Role, Affiliation, Packet) ->
286 603 append_x_user_element(FromJID, Role, Affiliation,
287 delete_x_user_element(Packet)).
288
289 append_x_user_element(FromJID, Role, Affiliation, Packet) ->
290 603 ItemElem = x_user_item(FromJID, Role, Affiliation),
291 603 X = #xmlel{
292 name = <<"x">>,
293 attrs = [{<<"xmlns">>, ?NS_MUC_USER}],
294 children = [ItemElem]},
295 603 xml:append_subtags(Packet, [X]).
296
297 x_user_item(FromJID, Role, Affiliation) ->
298 603 #xmlel{
299 name = <<"item">>,
300 attrs = [{<<"affiliation">>, atom_to_binary(Affiliation, latin1)},
301 {<<"jid">>, jid:to_binary(FromJID)},
302 {<<"role">>, atom_to_binary(Role, latin1)}]}.
303
304 -spec delete_x_user_element(exml:element()) -> exml:element().
305 delete_x_user_element(Packet=#xmlel{children=Cs}) ->
306 703 Packet#xmlel{children=[C || C <- Cs, not is_x_user_element(C)]}.
307
308 -spec packet_to_x_user_jid(exml:element()) -> jid:jid() | error | undefined.
309 packet_to_x_user_jid(#xmlel{children=Cs}) ->
310 122 case [C || C <- Cs, is_x_user_element(C)] of
311 14 [] -> undefined;
312 [X|_] ->
313 108 case exml_query:path(X, [{element, <<"item">>}, {attr, <<"jid">>}]) of
314
:-(
undefined -> undefined;
315 108 BinaryJid -> jid:from_binary(BinaryJid)
316 end
317 end.
318
319 -spec get_one_of_path(_, list(T)) -> T when T :: any().
320 get_one_of_path(Elem, List) ->
321 1408 get_one_of_path(Elem, List, <<>>).
322
323
324 -spec get_one_of_path(_, list(T), T) -> T when T :: any().
325 get_one_of_path(Elem, [H|T], Def) ->
326 2116 case exml_query:path(Elem, H) of
327 1416 undefined -> get_one_of_path(Elem, T, Def);
328 700 Val -> Val
329 end;
330 get_one_of_path(_Elem, [], Def) ->
331 708 Def.
332
333
334 %% @doc In order to be archived, the message must be of type "normal", "chat" or "groupchat".
335 %% It also must include a body or chat marker, as long as it doesn't include
336 %% "result", "delay" or "no-store" elements.
337 %% @end
338 -spec is_archivable_message(module(), direction(), exml:element(), boolean()) -> boolean().
339 is_archivable_message(Mod, Dir, Packet=#xmlel{name = <<"message">>}, ArchiveChatMarkers) ->
340 6204 Type = exml_query:attr(Packet, <<"type">>, <<"normal">>),
341 6204 is_valid_message_type(Mod, Dir, Type) andalso
342 6022 is_valid_message(Mod, Dir, Packet, ArchiveChatMarkers);
343 is_archivable_message(_, _, _, _) ->
344 9198 false.
345
346 276 is_valid_message_type(_, _, <<"normal">>) -> true;
347 3187 is_valid_message_type(_, _, <<"chat">>) -> true;
348 1460 is_valid_message_type(mod_inbox, _, <<"groupchat">>) -> true;
349 1099 is_valid_message_type(_, incoming, <<"groupchat">>) -> true;
350 182 is_valid_message_type(_, _, _) -> false.
351
352 is_valid_message(_Mod, _Dir, Packet, ArchiveChatMarkers) ->
353 6022 Body = exml_query:subelement(Packet, <<"body">>, false),
354 6022 ChatMarker = ArchiveChatMarkers
355 2440 andalso has_chat_marker(Packet),
356 6022 Retract = get_retract_id(Packet) =/= none,
357 %% Used in MAM
358 6022 Result = exml_query:subelement(Packet, <<"result">>, false),
359 %% Used in mod_offline
360 6022 Delay = exml_query:subelement(Packet, <<"delay">>, false),
361 %% Message Processing Hints (XEP-0334)
362 6022 NoStore = exml_query:path(Packet, [{element_with_ns, <<"no-store">>, ?NS_HINTS}], false),
363 %% Message Processing Hints (XEP-0334)
364 6022 Store = exml_query:path(Packet, [{element_with_ns, <<"store">>, ?NS_HINTS}], false),
365
366 6022 has_any([Store, Body, ChatMarker, Retract]) andalso not has_any([Result, Delay, NoStore]).
367
368 has_any(Elements) ->
369 11788 lists:any(fun(El) -> El =/= false end, Elements).
370
371 has_chat_marker(Packet) ->
372 2440 mongoose_chat_markers:has_chat_markers(Packet).
373
374 -spec get_retract_id(false, exml:element()) -> none;
375 (true, exml:element()) -> none | retraction_id().
376 get_retract_id(true = _Enabled, Packet) ->
377 10144 get_retract_id(Packet);
378 get_retract_id(false, _Packet) ->
379 28 none.
380
381 -spec get_retract_id(exml:element()) -> none | retraction_id().
382 get_retract_id(Packet) ->
383 16166 case exml_query:path(Packet, [{element_with_ns, <<"apply-to">>, ?NS_FASTEN}], none) of
384 16012 none -> none;
385 Fasten ->
386 154 case {exml_query:path(Fasten, [{element, <<"retract">>}, {attr, <<"xmlns">>}], none),
387 exml_query:path(Fasten, [{attr, <<"id">>}], none)} of
388
:-(
{none, _} -> none;
389 14 {_, none} -> none;
390 98 {?NS_RETRACT, OriginId} -> {origin_id, OriginId};
391 42 {?NS_ESL_RETRACT, StanzaId} -> {stanza_id, StanzaId}
392 end
393 end.
394
395 get_origin_id(Packet) ->
396 3356 exml_query:path(Packet, [{element_with_ns, <<"origin-id">>, ?NS_STANZAID},
397 {attr, <<"id">>}], none).
398
399 is_groupchat(<<"groupchat">>) ->
400 147 true;
401 is_groupchat(_) ->
402 2312 false.
403
404 -spec should_page_be_flipped(exml:element()) -> boolean().
405 should_page_be_flipped(Packet) ->
406 1408 case exml_query:path(Packet, [{element, <<"flip-page">>}], none) of
407 1366 none -> false;
408 42 _ -> true
409 end.
410
411 -spec maybe_reverse_messages(mam_iq:lookup_params(), [mod_mam:message_row()]) ->
412 [mod_mam:message_row()].
413 42 maybe_reverse_messages(#{flip_page := true}, Messages) -> lists:reverse(Messages);
414 1282 maybe_reverse_messages(#{flip_page := false}, Messages) -> Messages.
415
416 -spec get_msg_id_and_timestamp(mod_mam:message_row()) -> {binary(), binary()}.
417 get_msg_id_and_timestamp(#{id := MsgID}) ->
418 56 {Microseconds, _NodeMessID} = decode_compact_uuid(MsgID),
419 56 TS = calendar:system_time_to_rfc3339(Microseconds, [{offset, "Z"}, {unit, microsecond}]),
420 56 ExtID = mess_id_to_external_binary(MsgID),
421 56 {ExtID, list_to_binary(TS)}.
422
423 -spec lookup_specific_messages(mongooseim:host_type(),
424 mam_iq:lookup_params(),
425 [mod_mam:message_id()],
426 fun()) -> [mod_mam:message_row()] | {error, item_not_found}.
427 lookup_specific_messages(HostType, Params, IDs, FetchFun) ->
428 42 {FinalOffset, AccumulatedMessages} = lists:foldl(
429 fun(ID, {_AccOffset, AccMsgs}) ->
430 98 {ok, {_, OffsetForID, MessagesForID}} = FetchFun(HostType, Params#{message_id => ID}),
431 98 {OffsetForID, AccMsgs ++ MessagesForID}
432 end,
433 {0, []}, IDs),
434
435 42 Result = determine_result(Params, FinalOffset, AccumulatedMessages),
436 42 case length(IDs) == length(AccumulatedMessages) of
437 28 true -> Result;
438 14 false -> {error, item_not_found}
439 end.
440
441 determine_result(#{is_simple := true}, _Offset, Messages) ->
442 14 {ok, {undefined, undefined, Messages}};
443 determine_result(#{}, Offset, Messages) ->
444 28 {ok, {length(Messages), Offset, Messages}}.
445
446 tombstone(RetractionInfo = #{packet := Packet}, LocJid) ->
447 42 Packet#xmlel{children = [retracted_element(RetractionInfo, LocJid)]}.
448
449 -spec retracted_element(retraction_info(), jid:jid()) -> exml:element().
450 retracted_element(#{retract_on := origin_id,
451 origin_id := OriginID}, _LocJid) ->
452 21 Timestamp = calendar:system_time_to_rfc3339(erlang:system_time(second), [{offset, "Z"}]),
453 21 #xmlel{name = <<"retracted">>,
454 attrs = [{<<"xmlns">>, ?NS_RETRACT},
455 {<<"stamp">>, list_to_binary(Timestamp)}],
456 children = [#xmlel{name = <<"origin-id">>,
457 attrs = [{<<"xmlns">>, ?NS_STANZAID},
458 {<<"id">>, OriginID}]}
459 ]};
460 retracted_element(#{retract_on := stanza_id,
461 message_id := MessID} = Env, LocJid) ->
462 21 Timestamp = calendar:system_time_to_rfc3339(erlang:system_time(second), [{offset, "Z"}]),
463 21 StanzaID = mod_mam_utils:mess_id_to_external_binary(MessID),
464 21 MaybeOriginId = maybe_append_origin_id(Env),
465 21 #xmlel{name = <<"retracted">>,
466 attrs = [{<<"xmlns">>, ?NS_ESL_RETRACT},
467 {<<"stamp">>, list_to_binary(Timestamp)}],
468 children = [#xmlel{name = <<"stanza-id">>,
469 attrs = [{<<"xmlns">>, ?NS_STANZAID},
470 {<<"id">>, StanzaID},
471 {<<"by">>, jid:to_bare_binary(LocJid)}]} |
472 MaybeOriginId
473 ]}.
474
475 -spec maybe_append_origin_id(retraction_info()) -> [exml:element()].
476 maybe_append_origin_id(#{origin_id := OriginID}) when is_binary(OriginID), <<>> =/= OriginID ->
477 21 [#xmlel{name = <<"origin-id">>, attrs = [{<<"xmlns">>, ?NS_STANZAID}, {<<"id">>, OriginID}]}];
478 maybe_append_origin_id(_) ->
479
:-(
[].
480
481 %% @doc Forms `<forwarded/>' element, according to the XEP.
482 -spec wrap_message(MamNs :: binary(), Packet :: exml:element(), QueryID :: binary(),
483 MessageUID :: term(), TS :: jlib:rfc3339_string(),
484 SrcJID :: jid:jid()) -> Wrapper :: exml:element().
485 wrap_message(MamNs, Packet, QueryID, MessageUID, TS, SrcJID) ->
486 4514 wrap_message(MamNs, Packet, QueryID, MessageUID, wrapper_id(), TS, SrcJID).
487
488 -spec wrap_message(MamNs :: binary(), Packet :: exml:element(), QueryID :: binary(),
489 MessageUID :: term(), WrapperI :: binary(),
490 TS :: jlib:rfc3339_string(),
491 SrcJID :: jid:jid()) -> Wrapper :: exml:element().
492 wrap_message(MamNs, Packet, QueryID, MessageUID, WrapperID, TS, SrcJID) ->
493 4514 #xmlel{ name = <<"message">>,
494 attrs = [{<<"id">>, WrapperID}],
495 children = [result(MamNs, QueryID, MessageUID,
496 [forwarded(Packet, TS, SrcJID)])] }.
497
498 -spec forwarded(exml:element(), jlib:rfc3339_string(), jid:jid())
499 -> exml:element().
500 forwarded(Packet, TS, SrcJID) ->
501 4514 #xmlel{
502 name = <<"forwarded">>,
503 attrs = [{<<"xmlns">>, ?NS_FORWARD}],
504 %% Two places to include SrcJID:
505 %% - delay.from - optional XEP-0297 (TODO: depricate adding it?)
506 %% - message.from - required XEP-0313
507 %% Also, mod_mam_muc will replace it again with SrcJID
508 children = [delay(TS, SrcJID), replace_from_attribute(SrcJID, Packet)]}.
509
510 -spec delay(jlib:rfc3339_string(), jid:jid()) -> exml:element().
511 delay(TS, SrcJID) ->
512 4514 jlib:timestamp_to_xml(TS, SrcJID, <<>>).
513
514 replace_from_attribute(From, Packet=#xmlel{attrs = Attrs}) ->
515 4514 Attrs1 = lists:keydelete(<<"from">>, 1, Attrs),
516 4514 Attrs2 = [{<<"from">>, jid:to_binary(From)} | Attrs1],
517 4514 Packet#xmlel{attrs = Attrs2}.
518
519 %% @doc Generates tag `<result />'.
520 %% This element will be added in each forwarded message.
521 -spec result(binary(), _, MessageUID :: binary(), Children :: [exml:element(), ...])
522 -> exml:element().
523 result(MamNs, QueryID, MessageUID, Children) when is_list(Children) ->
524 %% <result xmlns='urn:xmpp:mam:tmp' queryid='f27' id='28482-98726-73623' />
525 4514 #xmlel{
526 name = <<"result">>,
527 3296 attrs = [{<<"queryid">>, QueryID} || QueryID =/= undefined, QueryID =/= <<>>] ++
528 [{<<"xmlns">>, MamNs},
529 {<<"id">>, MessageUID}],
530 children = Children}.
531
532
533 %% @doc Generates `<set />' tag.
534 %%
535 %% This element will be added into "iq/query".
536 %% @end
537 -spec result_set(FirstId :: binary() | undefined,
538 LastId :: binary() | undefined,
539 FirstIndexI :: non_neg_integer() | undefined,
540 CountI :: non_neg_integer() | undefined) -> exml:element().
541 result_set(FirstId, LastId, undefined, undefined)
542 when ?MAYBE_BIN(FirstId), ?MAYBE_BIN(LastId) ->
543 %% Simple response
544 1060 FirstEl = [#xmlel{name = <<"first">>,
545 children = [#xmlcdata{content = FirstId}]
546 }
547 1060 || FirstId =/= undefined],
548 1060 LastEl = [#xmlel{name = <<"last">>,
549 children = [#xmlcdata{content = LastId}]
550 }
551 1060 || LastId =/= undefined],
552 1060 #xmlel{
553 name = <<"set">>,
554 attrs = [{<<"xmlns">>, ?NS_RSM}],
555 children = FirstEl ++ LastEl};
556 result_set(FirstId, LastId, FirstIndexI, CountI)
557 when ?MAYBE_BIN(FirstId), ?MAYBE_BIN(LastId) ->
558 1135 FirstEl = [#xmlel{name = <<"first">>,
559 attrs = [{<<"index">>, integer_to_binary(FirstIndexI)}],
560 children = [#xmlcdata{content = FirstId}]
561 }
562 1135 || FirstId =/= undefined],
563 1135 LastEl = [#xmlel{name = <<"last">>,
564 children = [#xmlcdata{content = LastId}]
565 }
566 1135 || LastId =/= undefined],
567 1135 CountEl = #xmlel{
568 name = <<"count">>,
569 children = [#xmlcdata{content = integer_to_binary(CountI)}]},
570 1135 #xmlel{
571 name = <<"set">>,
572 attrs = [{<<"xmlns">>, ?NS_RSM}],
573 children = FirstEl ++ LastEl ++ [CountEl]}.
574
575
576 -spec result_query(jlib:xmlcdata() | exml:element(), binary()) -> exml:element().
577 result_query(SetEl, Namespace) ->
578 35 #xmlel{
579 name = <<"query">>,
580 attrs = [{<<"xmlns">>, Namespace}],
581 children = [SetEl]}.
582
583 -spec result_prefs(DefaultMode :: archive_behaviour(),
584 AlwaysJIDs :: [jid:literal_jid()],
585 NeverJIDs :: [jid:literal_jid()],
586 Namespace :: binary()) -> exml:element().
587 result_prefs(DefaultMode, AlwaysJIDs, NeverJIDs, Namespace) ->
588 469 AlwaysEl = #xmlel{name = <<"always">>,
589 children = encode_jids(AlwaysJIDs)},
590 469 NeverEl = #xmlel{name = <<"never">>,
591 children = encode_jids(NeverJIDs)},
592 469 #xmlel{
593 name = <<"prefs">>,
594 attrs = [{<<"xmlns">>, Namespace},
595 {<<"default">>, atom_to_binary(DefaultMode, utf8)}],
596 children = [AlwaysEl, NeverEl]
597 }.
598
599
600 -spec encode_jids([binary() | string()]) -> [exml:element()].
601 encode_jids(JIDs) ->
602 938 [#xmlel{name = <<"jid">>, children = [#xmlcdata{content = JID}]}
603 938 || JID <- JIDs].
604
605
606 %% MAM v0.4.1 and above
607 -spec make_fin_element(mongooseim:host_type(),
608 mam_iq:lookup_params(),
609 binary(),
610 boolean(),
611 boolean(),
612 exml:element(),
613 module()) ->
614 exml:element().
615 make_fin_element(HostType, Params, MamNs, IsComplete, IsStable, ResultSetEl, ExtFinMod) ->
616 1324 FinEl = #xmlel{
617 name = <<"fin">>,
618 attrs = [{<<"xmlns">>, MamNs}]
619 960 ++ [{<<"complete">>, <<"true">>} || IsComplete]
620
:-(
++ [{<<"stable">>, <<"false">>} || not IsStable],
621 children = [ResultSetEl]},
622 1324 maybe_transform_fin_elem(ExtFinMod, HostType, Params, FinEl).
623
624 maybe_transform_fin_elem(undefined, _HostType, _Params, FinEl) ->
625 1324 FinEl;
626 maybe_transform_fin_elem(Module, HostType, Params, FinEl) ->
627
:-(
Module:extra_fin_element(HostType, Params, FinEl).
628
629 -spec make_metadata_element() -> exml:element().
630 make_metadata_element() ->
631 14 #xmlel{
632 name = <<"metadata">>,
633 attrs = [{<<"xmlns">>, ?NS_MAM_06}]}.
634
635 -spec make_metadata_element(binary(), binary(), binary(), binary()) -> exml:element().
636 make_metadata_element(FirstMsgID, FirstMsgTS, LastMsgID, LastMsgTS) ->
637 28 #xmlel{
638 name = <<"metadata">>,
639 attrs = [{<<"xmlns">>, ?NS_MAM_06}],
640 children = [#xmlel{name = <<"start">>,
641 attrs = [{<<"id">>, FirstMsgID}, {<<"timestamp">>, FirstMsgTS}]},
642 #xmlel{name = <<"end">>,
643 attrs = [{<<"id">>, LastMsgID}, {<<"timestamp">>, LastMsgTS}]}]
644 }.
645
646 -spec parse_prefs(PrefsEl :: exml:element()) -> mod_mam:preference().
647 parse_prefs(El = #xmlel{ name = <<"prefs">> }) ->
648 308 Default = exml_query:attr(El, <<"default">>),
649 308 AlwaysJIDs = parse_jid_list(El, <<"always">>),
650 308 NeverJIDs = parse_jid_list(El, <<"never">>),
651 308 {valid_behavior(Default), AlwaysJIDs, NeverJIDs}.
652
653
654 -spec valid_behavior(archive_behaviour_bin()) -> archive_behaviour().
655 98 valid_behavior(<<"always">>) -> always;
656 98 valid_behavior(<<"never">>) -> never;
657 112 valid_behavior(<<"roster">>) -> roster.
658
659
660 -spec parse_jid_list(exml:element(), binary()) -> [jid:literal_jid()].
661 parse_jid_list(El, Name) ->
662 616 case exml_query:subelement(El, Name) of
663
:-(
undefined -> [];
664 #xmlel{children = JIDEls} ->
665 %% Ignore cdata between jid elements
666 616 MaybeJids = [binary_jid_to_lower(exml_query:cdata(JIDEl))
667 616 || JIDEl <- JIDEls, is_jid_element(JIDEl)],
668 616 skip_bad_jids(MaybeJids)
669 end.
670
671 is_jid_element(#xmlel{name = <<"jid">>}) ->
672 364 true;
673 is_jid_element(_) -> %% ignore cdata
674
:-(
false.
675
676 %% @doc Normalize JID to be used when comparing JIDs in DB
677 binary_jid_to_lower(BinJid) when is_binary(BinJid) ->
678 364 Jid = jid:from_binary(BinJid),
679 364 case jid:to_lower(Jid) of
680 error ->
681
:-(
error;
682 LowerJid ->
683 364 jid:to_binary(LowerJid)
684 end.
685
686 skip_bad_jids(MaybeJids) ->
687 616 [Jid || Jid <- MaybeJids, is_binary(Jid)].
688
689 -spec form_borders_decode(mongoose_data_forms:kv_map()) -> 'undefined' | mod_mam:borders().
690 form_borders_decode(KVs) ->
691 1408 AfterID = form_field_mess_id(KVs, <<"after-id">>),
692 1408 BeforeID = form_field_mess_id(KVs, <<"before-id">>),
693 1408 FromID = form_field_mess_id(KVs, <<"from-id">>),
694 1387 ToID = form_field_mess_id(KVs, <<"to-id">>),
695 1387 borders(AfterID, BeforeID, FromID, ToID).
696
697
698 -spec borders(AfterID :: 'undefined' | non_neg_integer(),
699 BeforeID :: 'undefined' | non_neg_integer(),
700 FromID :: 'undefined' | non_neg_integer(),
701 ToID :: 'undefined' | non_neg_integer()
702 ) -> 'undefined' | mod_mam:borders().
703 borders(undefined, undefined, undefined, undefined) ->
704 1282 undefined;
705 borders(AfterID, BeforeID, FromID, ToID) ->
706 105 #mam_borders{
707 after_id = AfterID,
708 before_id = BeforeID,
709 from_id = FromID,
710 to_id = ToID
711 }.
712
713 -spec form_field_mess_id(mongoose_data_forms:kv_map(), binary()) -> 'undefined' | integer().
714 form_field_mess_id(KVs, Name) ->
715 5611 case KVs of
716 168 #{Name := [BExtMessID]} -> external_binary_to_mess_id(BExtMessID);
717 5443 #{} -> undefined
718 end.
719
720 -spec form_decode_optimizations(mongoose_data_forms:kv_map()) -> boolean().
721 form_decode_optimizations(#{<<"simple">> := [<<"true">>]}) ->
722 182 true;
723 form_decode_optimizations(#{}) ->
724 1198 false.
725
726 is_mam_result_message(Packet = #xmlel{name = <<"message">>}) ->
727 10 Ns = maybe_get_result_namespace(Packet),
728 10 is_mam_namespace(Ns);
729 is_mam_result_message(_) ->
730
:-(
false.
731
732 maybe_get_result_namespace(Packet) ->
733 10 exml_query:path(Packet, [{element, <<"result">>}, {attr, <<"xmlns">>}], <<>>).
734
735 is_mam_namespace(NS) ->
736 10 lists:member(NS, mam_features()).
737
738 features(Module, HostType) ->
739 69 mam_features() ++ retraction_features(Module, HostType)
740 ++ groupchat_features(Module, HostType).
741
742 mam_features() ->
743 79 [?NS_MAM_04, ?NS_MAM_06, ?NS_MAM_EXTENDED].
744
745 retraction_features(Module, HostType) ->
746 69 case has_message_retraction(Module, HostType) of
747 55 true -> [?NS_RETRACT, ?NS_RETRACT_TOMBSTONE, ?NS_ESL_RETRACT];
748 14 false -> [?NS_RETRACT]
749 end.
750
751 groupchat_features(mod_mam_pm = Module, HostType) ->
752 49 case gen_mod:get_module_opt(HostType, mod_mam, backend) of
753
:-(
cassandra -> [];
754 _ ->
755 49 case gen_mod:get_module_opt(HostType, Module, archive_groupchats) of
756 7 true -> [?NS_MAM_GC_FIELD, ?NS_MAM_GC_AVAILABLE];
757 42 false -> [?NS_MAM_GC_FIELD]
758 end
759 end;
760 groupchat_features(_, _) ->
761 20 [].
762
763 %% -----------------------------------------------------------------------
764 %% Forms
765
766 -spec message_form(Mod :: mod_mam_pm | mod_mam_muc,
767 HostType :: mongooseim:host_type(), binary()) ->
768 exml:element().
769 message_form(Module, HostType, MamNs) ->
770 35 Fields = message_form_fields(Module, HostType, MamNs),
771 35 Form = mongoose_data_forms:form(#{ns => MamNs, fields => Fields}),
772 35 result_query(Form, MamNs).
773
774 message_form_fields(Mod, HostType, <<"urn:xmpp:mam:1">>) ->
775 28 TextSearch =
776 case has_full_text_search(Mod, HostType) of
777 21 true -> [#{type => <<"text-single">>,
778 var => <<"{https://erlang-solutions.com/}full-text-search">>}];
779 7 false -> []
780 end,
781 28 [#{type => <<"jid-single">>, var => <<"with">>},
782 #{type => <<"text-single">>, var => <<"start">>},
783 #{type => <<"text-single">>, var => <<"end">>} | TextSearch];
784 message_form_fields(Mod, HostType, <<"urn:xmpp:mam:2">>) ->
785 7 TextSearch =
786 case has_full_text_search(Mod, HostType) of
787 7 true -> [#{type => <<"text-single">>,
788 var => <<"{https://erlang-solutions.com/}full-text-search">>}];
789
:-(
false -> []
790 end,
791 7 [#{type => <<"jid-single">>, var => <<"with">>},
792 #{type => <<"text-single">>, var => <<"start">>},
793 #{type => <<"text-single">>, var => <<"end">>},
794 #{type => <<"text-single">>, var => <<"before-id">>},
795 #{type => <<"text-single">>, var => <<"after-id">>},
796 #{type => <<"boolean">>, var => <<"include-groupchat">>},
797 #{type => <<"list-multi">>, var => <<"ids">>,
798 validate => #{method => open, datatype => <<"xs:string">>}} | TextSearch].
799
800 -spec form_to_text(_) -> 'undefined' | binary().
801 form_to_text(#{<<"full-text-search">> := [Text]}) ->
802 49 Text;
803 form_to_text(#{}) ->
804 1359 undefined.
805
806 %% -----------------------------------------------------------------------
807 %% Text search tokenization
808 %% -----------------------------------------------------------------------
809
810 %% -----------------------------------------------------------------------
811 %% @doc
812 %% Normalize given text to improve text search in some MAM backends.
813 %% This normalization involves making text all lowercase, replacing some word separators
814 %% ([, .:;-?!]) with given one (by default "%") and removing all unicode characters that are
815 %% considered non-alphanumerical.
816 %% For example, text: "My cat, was eaten by: my dog?!? Why...?!?" will be normalized as:
817 %% "my%cat%was%eaten%by%my%dog%why"
818 %% @end
819 %% -----------------------------------------------------------------------
820 -spec normalize_search_text(binary() | undefined) -> binary() | undefined.
821 normalize_search_text(Text) ->
822 1576 normalize_search_text(Text, <<"%">>).
823
824 -spec normalize_search_text(binary() | undefined, binary()) -> binary() | undefined.
825 normalize_search_text(undefined, _WordSeparator) ->
826 1534 undefined;
827 normalize_search_text(Text, WordSeparator) ->
828 10395 BodyString = unicode:characters_to_list(Text),
829 10395 LowerBody = string:to_lower(BodyString),
830 10395 ReOpts = [{return, list}, global, unicode, ucp],
831 10395 Re0 = re:replace(LowerBody, "[, .:;-?!]+", " ", ReOpts),
832 10395 Re1 = re:replace(Re0, "([^\\w ]+)|(^\\s+)|(\\s+$)", "", ReOpts),
833 10395 Re2 = re:replace(Re1, "\s+", unicode:characters_to_list(WordSeparator), ReOpts),
834 10395 unicode:characters_to_binary(Re2).
835
836 -spec packet_to_search_body(Enabled :: boolean(),
837 Packet :: exml:element()) -> binary().
838 packet_to_search_body(true, Packet) ->
839 10353 BodyValue = exml_query:path(Packet, [{element, <<"body">>}, cdata], <<>>),
840 10353 mod_mam_utils:normalize_search_text(BodyValue, <<" ">>);
841 packet_to_search_body(false, _Packet) ->
842
:-(
<<>>.
843
844 -spec has_full_text_search(Module :: mod_mam_pm | mod_mam_muc,
845 HostType :: mongooseim:host_type()) -> boolean().
846 has_full_text_search(Module, HostType) ->
847 17413 gen_mod:get_module_opt(HostType, Module, full_text_search).
848
849 %% Message retraction
850
851 -spec has_message_retraction(Module :: mod_mam_pm | mod_mam_muc,
852 HostType :: mongooseim:host_type()) -> boolean().
853 has_message_retraction(Module, HostType) ->
854 17447 gen_mod:get_module_opt(HostType, Module, message_retraction).
855
856 %% -----------------------------------------------------------------------
857 %% JID serialization
858
859 -spec jid_to_opt_binary(UserJID :: jid:jid(), JID :: jid:jid()
860 ) -> jid:literal_jid().
861 jid_to_opt_binary(#jid{lserver = LServer},
862 #jid{lserver = LServer, luser = <<>>, lresource = <<>>}) ->
863
:-(
<<$:>>;
864 jid_to_opt_binary(#jid{lserver = LServer, luser = LUser},
865 #jid{lserver = LServer, luser = LUser, lresource = <<>>}) ->
866 2286 <<>>;
867 jid_to_opt_binary(#jid{lserver = LServer, luser = LUser},
868 #jid{lserver = LServer, luser = LUser, lresource = LResource}) ->
869 1074 <<$/, LResource/binary>>;
870 jid_to_opt_binary(#jid{lserver = LServer},
871 #jid{lserver = LServer, luser = LUser, lresource = <<>>}) ->
872 %% Both clients are on the same server.
873 8842 <<LUser/binary>>;
874 jid_to_opt_binary(#jid{lserver = LServer},
875 #jid{lserver = LServer, luser = <<>>, lresource = LResource}) ->
876 %% Both clients are on the same server.
877
:-(
<<$:, $/, LResource/binary>>;
878 jid_to_opt_binary(#jid{lserver = LServer},
879 #jid{lserver = LServer, luser = LUser, lresource = LResource}) ->
880 %% Both clients are on the same server.
881 973 <<LUser/binary, $/, LResource/binary>>;
882 jid_to_opt_binary(_,
883 #jid{lserver = LServer, luser = LUser, lresource = <<>>}) ->
884 325 <<LServer/binary, $:, LUser/binary>>;
885 jid_to_opt_binary(_,
886 #jid{lserver = LServer, luser = LUser, lresource = LResource}) ->
887 147 <<LServer/binary, $@, LUser/binary, $/, LResource/binary>>.
888
889
890 -spec expand_minified_jid(UserJID :: jid:jid(),
891 OptJID :: jid:literal_jid()) -> jid:literal_jid().
892 expand_minified_jid(#jid{lserver = LServer, luser = LUser}, <<>>) ->
893 438 <<LUser/binary, $@, LServer/binary>>;
894 expand_minified_jid(#jid{lserver = LServer, luser = <<>>}, <<$/, LResource/binary>>) ->
895
:-(
<<LServer/binary, $/, LResource/binary>>;
896 expand_minified_jid(#jid{lserver = LServer, luser = LUser}, <<$/, LResource/binary>>) ->
897 1926 <<LUser/binary, $@, LServer/binary, $/, LResource/binary>>;
898 expand_minified_jid(UserJID, Encoded) ->
899 486 Part = binary:match(Encoded, [<<$@>>, <<$/>>, <<$:>>]),
900 486 expand_minified_jid(Part, UserJID, Encoded).
901
902 -spec expand_minified_jid('nomatch' | {non_neg_integer(), 1}, jid:jid(),
903 Encoded :: jid:luser() | binary()) -> binary().
904 expand_minified_jid(nomatch, #jid{lserver = ThisServer}, LUser) ->
905 22 <<LUser/binary, $@, ThisServer/binary>>;
906 expand_minified_jid({Pos, 1}, #jid{lserver = ThisServer}, Encoded) ->
907 464 case Encoded of
908 <<$:, $/, LResource/binary>> ->
909
:-(
<<ThisServer/binary, $/, LResource/binary>>;
910 <<$:>> ->
911
:-(
ThisServer;
912 <<LServer:Pos/binary, $:>> ->
913
:-(
<<LServer/binary>>;
914 <<LServer:Pos/binary, $:, LUser/binary>> ->
915 48 <<LUser/binary, $@, LServer/binary>>;
916 <<LServer:Pos/binary, $@, $/, LResource/binary>> ->
917
:-(
<<LServer/binary, $/, LResource/binary>>;
918 <<LServer:Pos/binary, $@, Tail/binary>> ->
919 59 [LUser, LResource] = binary:split(Tail, <<$/>>),
920 59 <<LUser/binary, $@, LServer/binary, $/, LResource/binary>>;
921 <<LUser:Pos/binary, $/, LResource/binary>> ->
922 357 <<LUser/binary, $@, ThisServer/binary, $/, LResource/binary>>
923 end.
924
925 -ifdef(TEST).
926
927 jid_to_opt_binary_test_() ->
928 check_stringprep(),
929 UserJID = jid:from_binary(<<"alice@room">>),
930 [?_assertEqual(JID,
931 (expand_minified_jid(UserJID,
932 jid_to_opt_binary(UserJID, jid:from_binary(JID)))))
933 || JID <- test_jids()].
934
935 test_jids() ->
936 [<<"alice@room">>,
937 <<"alice@room/computer">>,
938 <<"alice@street/mobile">>,
939 <<"bob@room">>,
940 <<"bob@room/mobile">>,
941 <<"bob@street">>,
942 <<"bob@street/mobile">>].
943
944 check_stringprep() ->
945 is_loaded_application(jid) orelse start_stringprep().
946
947 start_stringprep() ->
948 EJ = code:lib_dir(mongooseim),
949 code:add_path(filename:join([EJ, "..", "..", "deps", "jid", "ebin"])),
950 {ok, _} = application:ensure_all_started(jid).
951
952 is_loaded_application(AppName) when is_atom(AppName) ->
953 lists:keymember(AppName, 1, application:loaded_applications()).
954
955 -endif.
956
957 %% -----------------------------------------------------------------------
958 %% Other
959 -spec bare_jid(undefined | jid:jid()) -> undefined | binary().
960
:-(
bare_jid(undefined) -> undefined;
961 bare_jid(JID) ->
962
:-(
jid:to_bare_binary(jid:to_lower(JID)).
963
964 -spec full_jid(jid:jid()) -> binary().
965 full_jid(JID) ->
966
:-(
jid:to_binary(jid:to_lower(JID)).
967
968 -spec maybe_integer(binary(), Default :: integer()) -> integer().
969 708 maybe_integer(<<>>, Def) -> Def;
970 maybe_integer(Bin, _Def) when is_binary(Bin) ->
971 700 binary_to_integer(Bin).
972
973 -spec apply_start_border('undefined' | mod_mam:borders(), undefined | integer()) ->
974 undefined | integer().
975 apply_start_border(undefined, StartID) ->
976 1471 StartID;
977 apply_start_border(#mam_borders{after_id=AfterID, from_id=FromID}, StartID) ->
978 105 maybe_max(maybe_next_id(AfterID), maybe_max(FromID, StartID)).
979
980
981 -spec apply_end_border('undefined' | mod_mam:borders(), undefined | integer()) ->
982 undefined | integer().
983 apply_end_border(undefined, EndID) ->
984 1471 EndID;
985 apply_end_border(#mam_borders{before_id=BeforeID, to_id=ToID}, EndID) ->
986 105 maybe_min(maybe_previous_id(BeforeID), maybe_min(ToID, EndID)).
987
988 -spec calculate_msg_id_borders(mod_mam:borders() | undefined,
989 mod_mam:unix_timestamp() | undefined,
990 mod_mam:unix_timestamp() | undefined) -> R when
991 R :: {integer() | undefined, integer() | undefined}.
992 calculate_msg_id_borders(Borders, Start, End) ->
993
:-(
StartID = maybe_encode_compact_uuid(Start, 0),
994
:-(
EndID = maybe_encode_compact_uuid(End, 255),
995
:-(
{apply_start_border(Borders, StartID),
996 apply_end_border(Borders, EndID)}.
997
998 -spec calculate_msg_id_borders(RSM, Borders, Start, End) -> R when
999 RSM :: jlib:rsm_in() | undefined,
1000 Borders :: mod_mam:borders() | undefined,
1001 Start :: mod_mam:unix_timestamp() | undefined,
1002 End :: mod_mam:unix_timestamp() | undefined,
1003 R :: {integer() | undefined, integer() | undefined}.
1004 calculate_msg_id_borders(undefined, Borders, Start, End) ->
1005
:-(
calculate_msg_id_borders(Borders, Start, End);
1006 calculate_msg_id_borders(#rsm_in{id = undefined}, Borders, Start, End) ->
1007
:-(
calculate_msg_id_borders(Borders, Start, End);
1008 calculate_msg_id_borders(#rsm_in{direction = aft, id = Id}, Borders, Start, End)
1009 when Id =/= undefined ->
1010
:-(
{StartId, EndId} = mod_mam_utils:calculate_msg_id_borders(Borders, Start, End),
1011
:-(
{mod_mam_utils:maybe_max(StartId, Id), EndId};
1012 calculate_msg_id_borders(#rsm_in{direction = before, id = Id}, Borders, Start, End)
1013 when Id =/= undefined ->
1014
:-(
{StartId, EndId} = mod_mam_utils:calculate_msg_id_borders(Borders, Start, End),
1015
:-(
{StartId, mod_mam_utils:maybe_min(EndId, Id)}.
1016
1017 -spec maybe_encode_compact_uuid(mod_mam:unix_timestamp() | undefined, integer()) ->
1018 undefined | integer().
1019 maybe_encode_compact_uuid(undefined, _) ->
1020
:-(
undefined;
1021 maybe_encode_compact_uuid(Microseconds, NodeID) ->
1022
:-(
mod_mam_utils:encode_compact_uuid(Microseconds, NodeID).
1023
1024
1025 -spec maybe_min('undefined' | integer(), undefined | integer()) -> integer().
1026 maybe_min(undefined, Y) ->
1027 168 Y;
1028 maybe_min(X, undefined) ->
1029 42 X;
1030 maybe_min(X, Y) ->
1031
:-(
min(X, Y).
1032
1033
1034 -spec maybe_max('undefined' | integer(), undefined | integer()) -> integer().
1035 maybe_max(undefined, Y) ->
1036 105 Y;
1037 maybe_max(X, undefined) ->
1038 105 X;
1039 maybe_max(X, Y) ->
1040
:-(
max(X, Y).
1041
1042 -spec maybe_last([T]) -> undefined | {ok, T}.
1043 21 maybe_last([]) -> undefined;
1044 49 maybe_last([_|_] = L) -> {ok, lists:last(L)}.
1045
1046 -spec maybe_next_id('undefined' | non_neg_integer()) -> 'undefined' | pos_integer().
1047 maybe_next_id(undefined) ->
1048
:-(
undefined;
1049 maybe_next_id(X) ->
1050 105 X + 1.
1051
1052 -spec maybe_previous_id('undefined' | non_neg_integer()) -> 'undefined' | integer().
1053 maybe_previous_id(undefined) ->
1054 63 undefined;
1055 maybe_previous_id(X) ->
1056 42 X - 1.
1057
1058
1059 %% @doc Returns true, if the current page is the final one in the result set.
1060 %% If there are more pages with messages, than this function returns false.
1061 %%
1062 %% PageSize - maximum number of messages extracted in one lookup.
1063 %% TotalCount - total number of messages in the Result Set.
1064 %% Result Set - is subset of all messages in user's archive,
1065 %% in a specified time period.
1066 %% MessageRows - stuff we are about to send to the user.
1067 %% Params - lookup parameters, coming from mam_iq module.
1068 %%
1069 %% TotalCount and Offset can be undefined, in case we use IsSimple=true.
1070 %% IsSimple=true tells the server not to do heavy `SELECT COUNT(*)' queries.
1071 %%
1072 %% TODO It is possible to set complete flag WITH IsSimple=true,
1073 %% if we select one extra message from archive, but don't send it to the client.
1074 %% It's the most efficient way to query archive, if the client side does
1075 %% not care about the total number of messages and if it's stateless
1076 %% (i.e. web interface).
1077 %% Handles case when we have TotalCount and Offset as integers
1078 -spec is_complete_result_page_using_offset(Params, Result) ->
1079 boolean() when
1080 Params :: mam_iq:lookup_params(),
1081 Result :: mod_mam:lookup_result_map().
1082 is_complete_result_page_using_offset(#{page_size := PageSize} = Params,
1083 #{total_count := TotalCount, offset := Offset,
1084 messages := MessageRows})
1085 when is_integer(TotalCount), is_integer(Offset) ->
1086 1135 case maps:get(ordering_direction, Params, forward) of
1087 forward ->
1088 953 is_most_recent_page(PageSize, TotalCount, Offset, MessageRows);
1089 backward ->
1090 182 Offset =:= 0
1091 end.
1092
1093 %% @doc Returns true, if the current page contains the most recent messages.
1094 %% If there are some more recent messages in archive, this function returns false.
1095 -spec is_most_recent_page(PageSize, TotalCount, Offset, MessageRows) -> boolean() when
1096 PageSize :: non_neg_integer(),
1097 TotalCount :: non_neg_integer()|undefined,
1098 Offset :: non_neg_integer()|undefined,
1099 MessageRows :: list().
1100 is_most_recent_page(PageSize, _TotalCount, _Offset, MessageRows)
1101 when length(MessageRows) < PageSize ->
1102 743 true;
1103 is_most_recent_page(PageSize, TotalCount, Offset, MessageRows)
1104 when is_integer(TotalCount), is_integer(Offset),
1105 length(MessageRows) =:= PageSize ->
1106 %% Number of messages on skipped pages from the beginning plus the current page
1107 210 PagedCount = Offset + PageSize,
1108 210 TotalCount =:= PagedCount; %% false means full page but not the last one in the result set
1109 is_most_recent_page(_PageSize, _TotalCount, _Offset, _MessageRows) ->
1110 %% When is_integer(TotalCount), is_integer(Offset)
1111 %% it's not possible case: the page is bigger than page size.
1112 %% Otherwise either TotalCount or Offset is undefined because of optimizations.
1113
:-(
false.
1114
1115 -spec maybe_set_client_xmlns(boolean(), exml:element()) -> exml:element().
1116 maybe_set_client_xmlns(true, Packet) ->
1117 4514 xml:replace_tag_attr(<<"xmlns">>, <<"jabber:client">>, Packet);
1118 maybe_set_client_xmlns(false, Packet) ->
1119
:-(
Packet.
1120
1121 -spec action_to_shaper_name(mam_iq:action()) -> atom().
1122 action_to_shaper_name(Action) ->
1123 1968 list_to_atom(atom_to_list(Action) ++ "_shaper").
1124
1125 -spec action_to_global_shaper_name(mam_iq:action()) -> atom().
1126 action_to_global_shaper_name(Action) ->
1127 1968 list_to_atom(atom_to_list(Action) ++ "_global_shaper").
1128
1129 -spec wait_shaper(mongooseim:host_type(), jid:server(), mam_iq:action(), jid:jid()) ->
1130 continue | {error, max_delay_reached}.
1131 wait_shaper(HostType, Host, Action, From) ->
1132 1968 case mongoose_shaper:wait(
1133 HostType, Host, action_to_shaper_name(Action), From, 1) of
1134 continue ->
1135 1968 mongoose_shaper:wait(
1136 global, Host, action_to_global_shaper_name(Action), From, 1);
1137 {error, max_delay_reached} ->
1138
:-(
{error, max_delay_reached}
1139 end.
1140
1141 %% -----------------------------------------------------------------------
1142 %% Ejabberd
1143
1144 -spec send_message(mod_mam:message_row(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t().
1145 send_message(_Row, From, To, Mess) ->
1146 4514 ejabberd_sm:route(From, To, Mess).
1147
1148 -spec is_jid_in_user_roster(mongooseim:host_type(), jid:jid(), jid:jid()) -> boolean().
1149 is_jid_in_user_roster(HostType, #jid{} = ToJID, #jid{} = RemJID) ->
1150 84 RemBareJID = jid:to_bare(RemJID),
1151 84 {Subscription, _G} = mongoose_hooks:roster_get_jid_info(HostType, ToJID, RemBareJID),
1152 84 Subscription == from orelse Subscription == both.
1153
1154 %% @doc Returns a UUIDv4 canonical form binary.
1155 -spec wrapper_id() -> binary().
1156 wrapper_id() ->
1157 4514 uuid:uuid_to_string(uuid:get_v4(), binary_standard).
1158
1159
1160 -spec check_result_for_policy_violation(Params, Result) -> Result when
1161 Params :: mam_iq:lookup_params(),
1162 Result :: {ok, mod_mam:lookup_result()}
1163 | {error, 'policy-violation'}
1164 | {error, Reason :: term()}.
1165 check_result_for_policy_violation(
1166 _Params = #{limit_passed := LimitPassed,
1167 max_result_limit := MaxResultLimit},
1168 Result = {ok, {TotalCount, Offset, _MessageRows}})
1169 when is_integer(TotalCount), is_integer(Offset) ->
1170 1135 case is_policy_violation(TotalCount, Offset, MaxResultLimit, LimitPassed) of
1171 true ->
1172
:-(
{error, 'policy-violation'};
1173 false ->
1174 1135 Result
1175 end;
1176 check_result_for_policy_violation(_Params, Result) ->
1177 392 Result.
1178
1179 is_policy_violation(TotalCount, Offset, MaxResultLimit, LimitPassed) ->
1180 1135 TotalCount - Offset > MaxResultLimit andalso not LimitPassed.
1181
1182 %% @doc Check for XEP-313 `item-not-found' error condition,
1183 %% that is if a message ID passed in a `before'/`after' query is actually present in the archive.
1184 %% See https://xmpp.org/extensions/xep-0313.html#query-paging for details.
1185 %%
1186 %% In a backend it's reasonable to query for PageSize + 1 messages,
1187 %% so that once the interval endpoint with requested ID is discarded we actually
1188 %% return (up to) PageSize messages.
1189 %% @end
1190 -spec check_for_item_not_found(RSM, PageSize, LookupResult) -> R when
1191 RSM :: jlib:rsm_in() | undefined,
1192 PageSize :: non_neg_integer(),
1193 LookupResult :: mod_mam:lookup_result(),
1194 R :: {ok, mod_mam:lookup_result()} | {error, item_not_found}.
1195 check_for_item_not_found(#rsm_in{direction = before, id = ID},
1196 _PageSize, {TotalCount, Offset, MessageRows}) ->
1197 70 case maybe_last(MessageRows) of
1198 {ok, #{id := ID}} ->
1199 49 {ok, {TotalCount, Offset, lists:droplast(MessageRows)}};
1200 undefined ->
1201 21 {error, item_not_found}
1202 end;
1203 check_for_item_not_found(#rsm_in{direction = aft, id = ID},
1204 _PageSize, {TotalCount, Offset, MessageRows0}) ->
1205 105 case MessageRows0 of
1206 [#{id := ID} | MessageRows] ->
1207 84 {ok, {TotalCount, Offset, MessageRows}};
1208 _ ->
1209 21 {error, item_not_found}
1210 end.
1211
1212 -spec lookup(HostType :: mongooseim:host_type(),
1213 Params :: mam_iq:lookup_params(),
1214 F :: fun()) ->
1215 {ok, mod_mam:lookup_result_map()} | {error, Reason :: term()}.
1216 lookup(HostType, Params, F) ->
1217 1457 F1 = patch_fun_to_make_result_as_map(F),
1218 1457 process_lookup_with_complete_check(HostType, Params, F1).
1219
1220 process_lookup_with_complete_check(HostType, Params = #{is_simple := true}, F) ->
1221 259 process_simple_lookup_with_complete_check(HostType, Params, F);
1222 process_lookup_with_complete_check(HostType, Params, F) ->
1223 1198 case F(HostType, Params) of
1224 {ok, Result} ->
1225 1135 IsComplete = is_complete_result_page_using_offset(Params, Result),
1226 1135 {ok, Result#{is_complete => IsComplete}};
1227 Other ->
1228 63 Other
1229 end.
1230
1231 -spec lookup_first_and_last_messages(mongooseim:host_type(), mod_mam:archive_id(),
1232 jid:jid(), fun()) ->
1233 {mod_mam:message_row(), mod_mam:message_row()} | {error, term()} | empty_archive.
1234 lookup_first_and_last_messages(HostType, ArcID, ArcJID, F) ->
1235 21 lookup_first_and_last_messages(HostType, ArcID, ArcJID, ArcJID, F).
1236
1237 -spec lookup_first_and_last_messages(mongooseim:host_type(), mod_mam:archive_id(), jid:jid(),
1238 jid:jid(), fun()) ->
1239 {mod_mam:message_row(), mod_mam:message_row()} | {error, term()} | empty_archive.
1240 lookup_first_and_last_messages(HostType, ArcID, CallerJID, OwnerJID, F) ->
1241 42 FirstMsgParams = create_lookup_params(undefined, forward, ArcID, CallerJID, OwnerJID),
1242 42 LastMsgParams = create_lookup_params(#rsm_in{direction = before},
1243 backward, ArcID, CallerJID, OwnerJID),
1244 42 case lookup(HostType, FirstMsgParams, F) of
1245 {ok, #{messages := [FirstMsg]}} ->
1246 28 case lookup(HostType, LastMsgParams, F) of
1247 28 {ok, #{messages := [LastMsg]}} -> {FirstMsg, LastMsg};
1248
:-(
ErrorLast -> ErrorLast
1249 end;
1250 14 {ok, #{messages := []}} -> empty_archive;
1251
:-(
ErrorFirst -> ErrorFirst
1252 end.
1253
1254 -spec create_lookup_params(jlib:rsm_in() | undefined,
1255 backward | forward,
1256 mod_mam:archive_id(),
1257 jid:jid(),
1258 jid:jid()) -> mam_iq:lookup_params().
1259 create_lookup_params(RSM, Direction, ArcID, CallerJID, OwnerJID) ->
1260 84 #{now => erlang:system_time(microsecond),
1261 is_simple => true,
1262 rsm => RSM,
1263 max_result_limit => 1,
1264 archive_id => ArcID,
1265 owner_jid => OwnerJID,
1266 search_text => undefined,
1267 with_jid => undefined,
1268 start_ts => undefined,
1269 page_size => 1,
1270 end_ts => undefined,
1271 borders => undefined,
1272 flip_page => false,
1273 ordering_direction => Direction,
1274 limit_passed => true,
1275 caller_jid => CallerJID,
1276 message_ids => undefined}.
1277
1278 patch_fun_to_make_result_as_map(F) ->
1279 1457 fun(HostType, Params) -> result_to_map(F(HostType, Params)) end.
1280
1281 result_to_map({ok, {TotalCount, Offset, MessageRows}}) ->
1282 1394 {ok, #{total_count => TotalCount, offset => Offset, messages => MessageRows}};
1283 result_to_map(Other) ->
1284 63 Other.
1285
1286 %% We query an extra message by changing page_size.
1287 %% After that we remove this message from the result set when returning.
1288 process_simple_lookup_with_complete_check(HostType, Params = #{page_size := PageSize}, F) ->
1289 259 Params2 = Params#{page_size => PageSize + 1},
1290 259 case F(HostType, Params2) of
1291 {ok, Result} ->
1292 259 {ok, set_complete_result_page_using_extra_message(PageSize, Params, Result)};
1293 Other ->
1294
:-(
Other
1295 end.
1296
1297 set_complete_result_page_using_extra_message(PageSize, Params, Result = #{messages := MessageRows}) ->
1298 259 case length(MessageRows) =:= (PageSize + 1) of
1299 true ->
1300 91 Result#{is_complete => false, messages => remove_extra_message(Params, MessageRows)};
1301 false ->
1302 168 Result#{is_complete => true}
1303 end.
1304
1305 remove_extra_message(Params, Messages) ->
1306 91 case maps:get(ordering_direction, Params, forward) of
1307 forward ->
1308 35 lists:droplast(Messages);
1309 backward ->
1310 56 tl(Messages)
1311 end.
1312
1313 -spec db_jid_codec(mongooseim:host_type(), module()) -> module().
1314 db_jid_codec(HostType, Module) ->
1315 17378 gen_mod:get_module_opt(HostType, Module, db_jid_format).
1316
1317 -spec db_message_codec(mongooseim:host_type(), module()) -> module().
1318 db_message_codec(HostType, Module) ->
1319 17378 gen_mod:get_module_opt(HostType, Module, db_message_format).
1320
1321 -spec incremental_delete_domain(
1322 mongooseim:host_type(), jid:lserver(), non_neg_integer(), [atom()], non_neg_integer()) ->
1323 non_neg_integer().
1324 incremental_delete_domain(_HostType, _Domain, _Limit, [], TotalDeleted) ->
1325 5 TotalDeleted;
1326 incremental_delete_domain(HostType, Domain, Limit, [Query | MoreQueries] = AllQueries, TotalDeleted) ->
1327 26 R1 = mongoose_rdbms:execute_successfully(HostType, Query, [Domain]),
1328 26 case is_removing_done(R1, Limit) of
1329 {done, N} ->
1330 11 incremental_delete_domain(HostType, Domain, Limit, MoreQueries, N + TotalDeleted);
1331 {remove_more, N} ->
1332 15 incremental_delete_domain(HostType, Domain, Limit, AllQueries, N + TotalDeleted)
1333 end.
1334
1335 -spec is_removing_done(LastResult :: {updated, non_neg_integer()}, Limit :: non_neg_integer()) ->
1336 {done | remove_more, non_neg_integer()}.
1337 is_removing_done({updated, N}, Limit) when N < Limit ->
1338 11 {done, N};
1339 is_removing_done({updated, N}, _)->
1340 15 {remove_more, N}.
1341
1342 -spec is_mam_muc_enabled(jid:lserver(), mongooseim:host_type()) -> boolean().
1343 is_mam_muc_enabled(MucDomain, HostType) ->
1344 607 HostPattern = mongoose_config:get_opt([{modules, HostType}, mod_mam_muc, host]),
1345 607 {ok, #{subdomain_pattern := SubDomainPattern}} =
1346 mongoose_domain_api:get_subdomain_info(MucDomain),
1347 607 HostPattern =:= SubDomainPattern.
Line Hits Source