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