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