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