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