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