1 |
|
-module(mod_inbox_entries). |
2 |
|
|
3 |
|
-include("mongoose_ns.hrl"). |
4 |
|
-include("jlib.hrl"). |
5 |
|
-include("mod_inbox.hrl"). |
6 |
|
|
7 |
|
% Inbox extensions |
8 |
|
-export([process_iq_conversation/5]). |
9 |
|
-export([should_be_stored_in_inbox/1]). |
10 |
|
-export([extensions_result/2]). |
11 |
|
|
12 |
|
-type entry_query() :: #{box => binary(), |
13 |
|
unread_count => 0 | 1, |
14 |
|
muted_until => integer()}. |
15 |
|
|
16 |
|
-type get_entry_type() :: full_entry | only_properties. |
17 |
|
-export_type([get_entry_type/0]). |
18 |
|
|
19 |
|
-spec process_iq_conversation(Acc :: mongoose_acc:t(), |
20 |
|
From :: jid:jid(), |
21 |
|
To :: jid:jid(), |
22 |
|
IQ :: jlib:iq(), |
23 |
|
Extra :: map()) -> |
24 |
|
{mongoose_acc:t(), jlib:iq()}. |
25 |
|
process_iq_conversation(Acc, From, _To, #iq{type = get, sub_el = SubEl} = IQ, _Extra) -> |
26 |
:-( |
process_iq_conversation_get(Acc, IQ, From, SubEl); |
27 |
|
process_iq_conversation(Acc, From, _To, #iq{type = set, |
28 |
|
sub_el = #xmlel{name = <<"reset">>} = ResetStanza} = IQ, |
29 |
|
_Extra) -> |
30 |
:-( |
maybe_process_reset_stanza(Acc, From, IQ, ResetStanza); |
31 |
|
process_iq_conversation(Acc, From, _To, #iq{type = set, |
32 |
|
sub_el = #xmlel{name = <<"query">>} = Query} = IQ, |
33 |
|
_Extra) -> |
34 |
:-( |
process_iq_conversation_set(Acc, IQ, From, Query). |
35 |
|
|
36 |
|
-spec process_iq_conversation_get(mongoose_acc:t(), jlib:iq(), jid:jid(), exml:element()) -> |
37 |
|
{mongoose_acc:t(), jlib:iq()}. |
38 |
|
process_iq_conversation_get(Acc, IQ, From, SubEl) -> |
39 |
:-( |
case mod_inbox_utils:extract_attr_jid(SubEl) of |
40 |
|
{error, _} -> |
41 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
42 |
:-( |
Form = build_inbox_entry_form(HostType), |
43 |
:-( |
SubElWithForm = SubEl#xmlel{children = [Form]}, |
44 |
:-( |
{Acc, IQ#iq{type = result, sub_el = SubElWithForm}}; |
45 |
|
EntryJID -> |
46 |
:-( |
FullEntry = maybe_get_full_entry(SubEl), |
47 |
:-( |
get_properties_for_jid(Acc, IQ, From, EntryJID, FullEntry) |
48 |
|
end. |
49 |
|
|
50 |
|
-spec maybe_get_full_entry(exml:element()) -> get_entry_type(). |
51 |
|
maybe_get_full_entry(SubEl) -> |
52 |
:-( |
case exml_query:attr(SubEl, <<"complete">>) of |
53 |
:-( |
<<"true">> -> full_entry; |
54 |
:-( |
_ -> only_properties |
55 |
|
end. |
56 |
|
|
57 |
|
-spec build_inbox_entry_form(mongooseim:host_type()) -> exml:element(). |
58 |
|
build_inbox_entry_form(HostType) -> |
59 |
:-( |
AllBoxes = mod_inbox_utils:all_valid_boxes_for_query(HostType), |
60 |
:-( |
#xmlel{name = <<"x">>, |
61 |
|
attrs = [{<<"xmlns">>, ?NS_XDATA}, |
62 |
|
{<<"type">>, <<"form">>}], |
63 |
|
children = [jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_ESL_INBOX_CONVERSATION}), |
64 |
|
mod_inbox_utils:list_single_form_field(<<"box">>, <<"all">>, AllBoxes), |
65 |
|
jlib:form_field({<<"archive">>, <<"boolean">>, <<"false">>}), |
66 |
|
jlib:form_field({<<"read">>, <<"boolean">>, <<"false">>}), |
67 |
|
jlib:form_field({<<"mute">>, <<"text-single">>, <<"0">>})]}. |
68 |
|
|
69 |
|
fetch_right_query(HostType, InboxEntryKey, only_properties) -> |
70 |
:-( |
mod_inbox_backend:get_entry_properties(HostType, InboxEntryKey); |
71 |
|
fetch_right_query(HostType, InboxEntryKey, full_entry) -> |
72 |
:-( |
mod_inbox_backend:get_full_entry(HostType, InboxEntryKey). |
73 |
|
|
74 |
|
-spec get_properties_for_jid(mongoose_acc:t(), jlib:iq(), jid:jid(), jid:jid(), get_entry_type()) -> |
75 |
|
{mongoose_acc:t(), jlib:iq()}. |
76 |
|
get_properties_for_jid(Acc, IQ, From, EntryJID, QueryType) -> |
77 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
78 |
:-( |
{_, _, BinEntryJID} = InboxEntryKey = mod_inbox_utils:build_inbox_entry_key(From, EntryJID), |
79 |
:-( |
case fetch_right_query(HostType, InboxEntryKey, QueryType) of |
80 |
:-( |
[] -> return_error(Acc, IQ, item_not_found, <<"Entry not found">>); |
81 |
|
Result -> |
82 |
:-( |
Properties = build_result(Acc, Result, IQ, QueryType), |
83 |
:-( |
X = [#xmlel{name = <<"query">>, |
84 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX_CONVERSATION}, |
85 |
|
{<<"jid">>, BinEntryJID}], |
86 |
|
children = Properties}], |
87 |
:-( |
{Acc, IQ#iq{type = result, sub_el = X}} |
88 |
|
end. |
89 |
|
|
90 |
|
-spec process_iq_conversation_set(mongoose_acc:t(), jlib:iq(), jid:jid(), exml:element()) -> |
91 |
|
{mongoose_acc:t(), jlib:iq()}. |
92 |
|
process_iq_conversation_set(Acc, IQ, From, #xmlel{children = Requests} = Query) -> |
93 |
:-( |
case mod_inbox_utils:extract_attr_jid(Query) of |
94 |
|
{error, Msg} -> |
95 |
:-( |
return_error(Acc, IQ, bad_request, Msg); |
96 |
|
EntryJID -> |
97 |
:-( |
extract_requests(Acc, IQ, From, EntryJID, Requests) |
98 |
|
end. |
99 |
|
|
100 |
|
-spec extract_requests(mongoose_acc:t(), jlib:iq(), jid:jid(), jid:jid(), [exml:element()]) -> |
101 |
|
{mongoose_acc:t(), jlib:iq()}. |
102 |
|
extract_requests(Acc, IQ, From, EntryJID, Requests) -> |
103 |
:-( |
case form_to_query(Acc, Requests, #{}) of |
104 |
|
{error, Msg} -> |
105 |
:-( |
return_error(Acc, IQ, bad_request, Msg); |
106 |
|
Params -> |
107 |
:-( |
process_requests(Acc, IQ, From, EntryJID, Params) |
108 |
|
end. |
109 |
|
|
110 |
|
-spec process_requests(mongoose_acc:t(), jlib:iq(), jid:jid(), jid:jid(), entry_query()) -> |
111 |
|
{mongoose_acc:t(), jlib:iq()}. |
112 |
|
process_requests(Acc, IQ, From, EntryJID, Params) -> |
113 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
114 |
:-( |
InboxEntryKey = mod_inbox_utils:build_inbox_entry_key(From, EntryJID), |
115 |
:-( |
case mod_inbox_backend:set_entry_properties(HostType, InboxEntryKey, Params) of |
116 |
|
{error, Msg} -> |
117 |
:-( |
return_error(Acc, IQ, bad_request, Msg); |
118 |
|
Result -> |
119 |
:-( |
forward_result(Acc, IQ, From, InboxEntryKey, Result) |
120 |
|
end. |
121 |
|
|
122 |
|
-spec forward_result(mongoose_acc:t(), jlib:iq(), jid:jid(), mod_inbox:entry_key(), entry_properties()) -> |
123 |
|
{mongoose_acc:t(), jlib:iq()}. |
124 |
|
forward_result(Acc, IQ = #iq{id = IqId}, From, {_, _, ToBareJidBin}, Result) -> |
125 |
:-( |
Properties = build_result(Acc, Result, IQ, only_properties), |
126 |
:-( |
Children = iq_x_wrapper(IQ, ToBareJidBin, Properties), |
127 |
:-( |
Msg = #xmlel{name = <<"message">>, attrs = [{<<"id">>, IqId}], children = Children}, |
128 |
:-( |
Acc1 = ejabberd_router:route(From, jid:to_bare(From), Acc, Msg), |
129 |
:-( |
Res = IQ#iq{type = result, sub_el = []}, |
130 |
:-( |
{Acc1, Res}. |
131 |
|
|
132 |
|
-spec iq_x_wrapper(jlib:iq(), binary(), [exml:element()]) -> [exml:element()]. |
133 |
|
iq_x_wrapper(#iq{id = IqId, sub_el = Query}, Jid, Properties) -> |
134 |
:-( |
[#xmlel{name = <<"x">>, |
135 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX_CONVERSATION}, |
136 |
|
{<<"jid">>, Jid}, |
137 |
|
{<<"queryid">>, exml_query:attr(Query, <<"queryid">>, IqId)}], |
138 |
|
children = Properties}]. |
139 |
|
|
140 |
|
maybe_process_reset_stanza(Acc, From, IQ, ResetStanza) -> |
141 |
:-( |
case mod_inbox_utils:extract_attr_jid(ResetStanza) of |
142 |
|
{error, Msg} -> |
143 |
:-( |
return_error(Acc, IQ, bad_request, Msg); |
144 |
|
InterlocutorJID -> |
145 |
:-( |
process_reset_stanza(Acc, From, IQ, ResetStanza, InterlocutorJID) |
146 |
|
end. |
147 |
|
|
148 |
|
process_reset_stanza(Acc, From, IQ, _ResetStanza, InterlocutorJID) -> |
149 |
:-( |
ok = mod_inbox_utils:reset_unread_count_to_zero(Acc, From, InterlocutorJID), |
150 |
:-( |
Res = IQ#iq{type = result, |
151 |
|
sub_el = [#xmlel{name = <<"reset">>, |
152 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX_CONVERSATION}], |
153 |
|
children = []}]}, |
154 |
:-( |
{Acc, Res}. |
155 |
|
|
156 |
|
%%-------------------------------------------------------------------- |
157 |
|
%% Helpers |
158 |
|
%%-------------------------------------------------------------------- |
159 |
|
|
160 |
|
-spec build_result(mongoose_acc:t(), inbox_res() | entry_properties(), jlib:iq(), get_entry_type()) -> |
161 |
|
[exml:element()]. |
162 |
|
build_result(Acc, Entry = #{unread_count := UnreadCount}, IQ, QueryType) -> |
163 |
:-( |
CurrentTS = mongoose_acc:timestamp(Acc), |
164 |
:-( |
maybe_full_entry(Acc, Entry, IQ, QueryType) ++ |
165 |
|
[ kv_to_el(<<"read">>, mod_inbox_utils:bool_to_binary(0 =:= UnreadCount)) |
166 |
|
| extensions_result(Entry, CurrentTS)]. |
167 |
|
|
168 |
|
maybe_full_entry(Acc, Entry, IQ, full_entry) -> |
169 |
:-( |
Extensions = mongoose_hooks:extend_inbox_message(Acc, Entry, IQ), |
170 |
:-( |
[ mod_inbox_utils:build_forward_el(Entry) | Extensions]; |
171 |
|
maybe_full_entry(_, _, _, _) -> |
172 |
:-( |
[]. |
173 |
|
|
174 |
|
-spec return_error(mongoose_acc:t(), jlib:iq(), bad_request | item_not_found, binary()) -> |
175 |
|
{mongoose_acc:t(), jlib:iq()}. |
176 |
|
return_error(Acc, IQ, Type, Msg) when is_binary(Msg) -> |
177 |
:-( |
{Acc, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:Type(<<"en">>, Msg)]}}. |
178 |
|
|
179 |
|
-spec form_to_query(mongoose_acc:t(), [exml:element()], map()) -> entry_query() | {error, binary()}. |
180 |
|
form_to_query(_, [], Acc) when map_size(Acc) == 0 -> |
181 |
:-( |
{error, <<"no-property">>}; |
182 |
|
form_to_query(_, [], Acc) -> |
183 |
:-( |
Acc; |
184 |
|
form_to_query(MAcc, [#xmlel{name = <<"box">>, children = [#xmlcdata{content = BoxName}]} | Rest], Acc) -> |
185 |
:-( |
case is_box_accepted(MAcc, BoxName) of |
186 |
:-( |
true -> form_to_query(MAcc, Rest, Acc#{box => BoxName}); |
187 |
:-( |
false -> {error, <<"invalid-box">>} |
188 |
|
end; |
189 |
|
form_to_query(MAcc, [#xmlel{name = <<"archive">>, children = [#xmlcdata{content = <<"true">>}]} | Rest], Acc) -> |
190 |
:-( |
form_to_query(MAcc, Rest, Acc#{box => maps:get(box, Acc, <<"archive">>)}); |
191 |
|
form_to_query(MAcc, [#xmlel{name = <<"archive">>, children = [#xmlcdata{content = <<"false">>}]} | Rest], Acc) -> |
192 |
:-( |
form_to_query(MAcc, Rest, Acc#{box => maps:get(box, Acc, <<"inbox">>)}); |
193 |
|
form_to_query(MAcc, [#xmlel{name = <<"read">>, children = [#xmlcdata{content = <<"true">>}]} | Rest], Acc) -> |
194 |
:-( |
form_to_query(MAcc, Rest, Acc#{unread_count => 0}); |
195 |
|
form_to_query(MAcc, [#xmlel{name = <<"read">>, children = [#xmlcdata{content = <<"false">>}]} | Rest], Acc) -> |
196 |
:-( |
form_to_query(MAcc, Rest, Acc#{unread_count => 1}); |
197 |
|
form_to_query(MAcc, [#xmlel{name = <<"mute">>, |
198 |
|
children = [#xmlcdata{content = Value}]} | Rest], Acc) -> |
199 |
:-( |
case mod_inbox_utils:maybe_binary_to_positive_integer(Value) of |
200 |
:-( |
{error, _} -> {error, <<"bad-request">>}; |
201 |
|
0 -> |
202 |
:-( |
form_to_query(MAcc, Rest, Acc#{muted_until => 0}); |
203 |
|
Num when Num > 0 -> |
204 |
:-( |
TS = mongoose_acc:timestamp(MAcc), |
205 |
:-( |
MutedUntilMicroSec = TS + erlang:convert_time_unit(Num, second, microsecond), |
206 |
:-( |
form_to_query(MAcc, Rest, Acc#{muted_until => MutedUntilMicroSec}) |
207 |
|
end; |
208 |
|
form_to_query(_, _, _) -> |
209 |
:-( |
{error, <<"bad-request">>}. |
210 |
|
|
211 |
|
is_box_accepted(MAcc, Box) -> |
212 |
:-( |
HostType = mongoose_acc:host_type(MAcc), |
213 |
:-( |
BoxOptions = gen_mod:get_module_opt(HostType, mod_inbox, boxes), |
214 |
:-( |
lists:member(Box, BoxOptions). |
215 |
|
|
216 |
|
-spec should_be_stored_in_inbox(exml:element()) -> boolean(). |
217 |
|
should_be_stored_in_inbox(Msg) -> |
218 |
:-( |
not is_inbox_update(Msg). |
219 |
|
|
220 |
|
-spec is_inbox_update(exml:element()) -> boolean(). |
221 |
|
is_inbox_update(Msg) -> |
222 |
:-( |
case exml_query:subelement_with_ns(Msg, ?NS_ESL_INBOX_CONVERSATION, undefined) of |
223 |
:-( |
undefined -> false; |
224 |
:-( |
_ -> true |
225 |
|
end. |
226 |
|
|
227 |
|
-spec extensions_result(entry_properties(), integer()) -> [exml:element()]. |
228 |
|
extensions_result(#{box := Box, muted_until := MutedUntil}, AccTS) -> |
229 |
:-( |
[ kv_to_el(<<"box">>, Box), |
230 |
|
kv_to_el(<<"archive">>, is_archive(Box)), |
231 |
|
kv_to_el(<<"mute">>, maybe_muted_until(MutedUntil, AccTS))]. |
232 |
|
|
233 |
|
-spec kv_to_el(binary(), binary()) -> exml:element(). |
234 |
|
kv_to_el(Key, Value) -> |
235 |
:-( |
#xmlel{name = Key, children = [#xmlcdata{content = Value}]}. |
236 |
|
|
237 |
:-( |
is_archive(<<"archive">>) -> <<"true">>; |
238 |
:-( |
is_archive(_) -> <<"false">>. |
239 |
|
|
240 |
|
-spec maybe_muted_until(integer(), integer()) -> binary(). |
241 |
:-( |
maybe_muted_until(0, _) -> <<"0">>; |
242 |
|
maybe_muted_until(MutedUntil, CurrentTS) -> |
243 |
:-( |
case CurrentTS =< MutedUntil of |
244 |
:-( |
true -> list_to_binary(calendar:system_time_to_rfc3339(MutedUntil, [{offset, "Z"}, {unit, microsecond}])); |
245 |
:-( |
false -> <<"0">> |
246 |
|
end. |