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