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