1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @copyright (C) 2018, Erlang-Solutions |
3 |
|
%%% @doc |
4 |
|
%%% |
5 |
|
%%% @end |
6 |
|
%%% Created : 30. Jan 2018 13:22 |
7 |
|
%%%------------------------------------------------------------------- |
8 |
|
-module(mod_inbox). |
9 |
|
|
10 |
|
-behaviour(gen_mod). |
11 |
|
-behaviour(mongoose_module_metrics). |
12 |
|
|
13 |
|
-include("jlib.hrl"). |
14 |
|
-include("mod_inbox.hrl"). |
15 |
|
-include("mongoose_config_spec.hrl"). |
16 |
|
-include("mongoose_logger.hrl"). |
17 |
|
-include("mongoose_ns.hrl"). |
18 |
|
|
19 |
|
-export([get_personal_data/3]). |
20 |
|
|
21 |
|
%% gen_mod |
22 |
|
-export([start/2, stop/1, config_spec/0, supported_features/0]). |
23 |
|
|
24 |
|
-export([process_iq/5, |
25 |
|
user_send_packet/4, |
26 |
|
filter_local_packet/1, |
27 |
|
inbox_unread_count/2, |
28 |
|
remove_user/3, |
29 |
|
remove_domain/3, |
30 |
|
disco_local_features/1 |
31 |
|
]). |
32 |
|
|
33 |
|
-ignore_xref([ |
34 |
|
behaviour_info/1, disco_local_features/1, filter_local_packet/1, get_personal_data/3, |
35 |
|
inbox_unread_count/2, remove_domain/3, remove_user/3, user_send_packet/4 |
36 |
|
]). |
37 |
|
|
38 |
|
-export([config_metrics/1]). |
39 |
|
|
40 |
|
-type message_type() :: one2one | groupchat. |
41 |
|
-type entry_key() :: {LUser :: jid:luser(), |
42 |
|
LServer :: jid:lserver(), |
43 |
|
ToBareJid :: jid:literal_jid()}. |
44 |
|
|
45 |
|
-type get_inbox_params() :: #{ |
46 |
|
start => integer(), |
47 |
|
'end' => integer(), |
48 |
|
order => asc | desc, |
49 |
|
hidden_read => true | false, |
50 |
|
archive => boolean() |
51 |
|
}. |
52 |
|
|
53 |
|
-type count_res() :: ok | {ok, non_neg_integer()} | {error, term()}. |
54 |
|
-type write_res() :: ok | {error, any()}. |
55 |
|
|
56 |
|
-export_type([entry_key/0, get_inbox_params/0]). |
57 |
|
-export_type([count_res/0, write_res/0]). |
58 |
|
|
59 |
|
%%-------------------------------------------------------------------- |
60 |
|
%% gdpr callbacks |
61 |
|
%%-------------------------------------------------------------------- |
62 |
|
-spec get_personal_data(gdpr:personal_data(), mongooseim:host_type(), jid:jid()) -> |
63 |
|
gdpr:personal_data(). |
64 |
|
get_personal_data(Acc, HostType, #jid{luser = LUser, lserver = LServer}) -> |
65 |
:-( |
Schema = ["jid", "content", "unread_count", "timestamp"], |
66 |
:-( |
InboxParams = #{ |
67 |
|
start => 0, |
68 |
|
'end' => erlang:system_time(microsecond), |
69 |
|
order => asc, |
70 |
|
hidden_read => false |
71 |
|
}, |
72 |
:-( |
Entries = mod_inbox_backend:get_inbox(HostType, LUser, LServer, InboxParams), |
73 |
:-( |
ProcessedEntries = lists:map(fun process_entry/1, Entries), |
74 |
:-( |
[{inbox, Schema, ProcessedEntries} | Acc]. |
75 |
|
|
76 |
|
process_entry(#{remote_jid := RemJID, |
77 |
|
msg := Content, |
78 |
|
unread_count := UnreadCount, |
79 |
|
timestamp := Timestamp}) -> |
80 |
:-( |
TS = calendar:system_time_to_rfc3339(Timestamp, [{offset, "Z"}, {unit, microsecond}]), |
81 |
:-( |
{RemJID, Content, UnreadCount, TS}. |
82 |
|
|
83 |
|
%%-------------------------------------------------------------------- |
84 |
|
%% gen_mod callbacks |
85 |
|
%%-------------------------------------------------------------------- |
86 |
|
|
87 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
88 |
|
start(HostType, #{iqdisc := IQDisc, groupchat := MucTypes} = Opts) -> |
89 |
:-( |
mod_inbox_backend:init(HostType, Opts), |
90 |
:-( |
lists:member(muc, MucTypes) andalso mod_inbox_muc:start(HostType), |
91 |
:-( |
ejabberd_hooks:add(hooks(HostType)), |
92 |
:-( |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm, |
93 |
|
fun ?MODULE:process_iq/5, #{}, IQDisc), |
94 |
:-( |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm, |
95 |
|
fun mod_inbox_entries:process_iq_conversation/5, #{}, IQDisc), |
96 |
:-( |
ok. |
97 |
|
|
98 |
|
-spec stop(HostType :: mongooseim:host_type()) -> ok. |
99 |
|
stop(HostType) -> |
100 |
:-( |
mod_inbox_muc:stop(HostType), |
101 |
:-( |
case mongoose_config:get_opt([{modules, HostType}, ?MODULE, backend]) of |
102 |
:-( |
rdbms_async -> mod_inbox_rdbms_async:stop(HostType); |
103 |
:-( |
_ -> ok |
104 |
|
end, |
105 |
:-( |
ejabberd_hooks:delete(hooks(HostType)), |
106 |
:-( |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm), |
107 |
:-( |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm). |
108 |
|
|
109 |
|
-spec supported_features() -> [atom()]. |
110 |
|
supported_features() -> |
111 |
:-( |
[dynamic_domains]. |
112 |
|
|
113 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
114 |
|
config_spec() -> |
115 |
164 |
Markers = mongoose_chat_markers:chat_marker_names(), |
116 |
164 |
#section{ |
117 |
|
items = #{<<"backend">> => #option{type = atom, validate = {enum, [rdbms, rdbms_async]}}, |
118 |
|
<<"async_writer">> => async_config_spec(), |
119 |
|
<<"reset_markers">> => #list{items = #option{type = binary, |
120 |
|
validate = {enum, Markers}}}, |
121 |
|
<<"groupchat">> => #list{items = #option{type = atom, |
122 |
|
validate = {enum, [muc, muclight]}}}, |
123 |
|
<<"aff_changes">> => #option{type = boolean}, |
124 |
|
<<"remove_on_kicked">> => #option{type = boolean}, |
125 |
|
<<"iqdisc">> => mongoose_config_spec:iqdisc() |
126 |
|
}, |
127 |
|
defaults = #{<<"backend">> => rdbms, |
128 |
|
<<"groupchat">> => [muclight], |
129 |
|
<<"aff_changes">> => true, |
130 |
|
<<"remove_on_kicked">> => true, |
131 |
|
<<"reset_markers">> => [<<"displayed">>], |
132 |
|
<<"iqdisc">> => no_queue |
133 |
|
}, |
134 |
|
format_items = map |
135 |
|
}. |
136 |
|
|
137 |
|
async_config_spec() -> |
138 |
164 |
#section{ |
139 |
|
items = #{<<"pool_size">> => #option{type = integer, validate = non_negative}}, |
140 |
|
defaults = #{<<"pool_size">> => 2 * erlang:system_info(schedulers_online)}, |
141 |
|
format_items = map, |
142 |
|
include = always |
143 |
|
}. |
144 |
|
|
145 |
|
%%%%%%%%%%%%%%%%%%% |
146 |
|
%% Process IQ |
147 |
|
-spec process_iq(Acc :: mongoose_acc:t(), |
148 |
|
From :: jid:jid(), |
149 |
|
To :: jid:jid(), |
150 |
|
IQ :: jlib:iq(), |
151 |
|
Extra :: map()) -> {stop, mongoose_acc:t()} | {mongoose_acc:t(), jlib:iq()}. |
152 |
|
process_iq(Acc, _From, _To, #iq{type = get, sub_el = SubEl} = IQ, _Extra) -> |
153 |
:-( |
Form = build_inbox_form(), |
154 |
:-( |
SubElWithForm = SubEl#xmlel{ children = [Form] }, |
155 |
:-( |
{Acc, IQ#iq{type = result, sub_el = SubElWithForm}}; |
156 |
|
process_iq(Acc, From, _To, #iq{type = set, id = IqId, sub_el = QueryEl} = IQ, _Extra) -> |
157 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
158 |
:-( |
LUser = From#jid.luser, |
159 |
:-( |
LServer = From#jid.lserver, |
160 |
:-( |
case query_to_params(QueryEl) of |
161 |
|
{error, bad_request, Msg} -> |
162 |
:-( |
{Acc, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:bad_request(<<"en">>, Msg)]}}; |
163 |
|
Params -> |
164 |
:-( |
List = mod_inbox_backend:get_inbox(HostType, LUser, LServer, Params), |
165 |
:-( |
QueryId = exml_query:attr(QueryEl, <<"queryid">>, IqId), |
166 |
:-( |
forward_messages(Acc, List, QueryId, From), |
167 |
:-( |
Res = IQ#iq{type = result, sub_el = [build_result_iq(List)]}, |
168 |
:-( |
{Acc, Res} |
169 |
|
end. |
170 |
|
|
171 |
|
-spec forward_messages(Acc :: mongoose_acc:t(), |
172 |
|
List :: list(inbox_res()), |
173 |
|
QueryId :: id(), |
174 |
|
To :: jid:jid()) -> list(mongoose_acc:t()). |
175 |
|
forward_messages(Acc, List, QueryId, To) when is_list(List) -> |
176 |
:-( |
AccTS = mongoose_acc:timestamp(Acc), |
177 |
:-( |
Msgs = [build_inbox_message(El, QueryId, AccTS) || El <- List], |
178 |
:-( |
[send_message(Acc, To, Msg) || Msg <- Msgs]. |
179 |
|
|
180 |
|
-spec send_message(mongoose_acc:t(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
181 |
|
send_message(Acc, To = #jid{lserver = LServer}, Msg) -> |
182 |
:-( |
BareTo = jid:to_bare(To), |
183 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
184 |
:-( |
NewAcc0 = mongoose_acc:new(#{location => ?LOCATION, |
185 |
|
host_type => HostType, |
186 |
|
lserver => LServer, |
187 |
|
element => Msg, |
188 |
|
from_jid => BareTo, |
189 |
|
to_jid => To}), |
190 |
:-( |
PermanentFields = mongoose_acc:get_permanent_fields(Acc), |
191 |
:-( |
NewAcc = mongoose_acc:set_permanent(PermanentFields, NewAcc0), |
192 |
:-( |
ejabberd_sm:route(BareTo, To, NewAcc). |
193 |
|
|
194 |
|
%%%%%%%%%%%%%%%%%%% |
195 |
|
%% Handlers |
196 |
|
-spec user_send_packet(Acc :: mongoose_acc:t(), From :: jid:jid(), |
197 |
|
To :: jid:jid(), Packet :: exml:element()) -> |
198 |
|
mongoose_acc:t(). |
199 |
|
user_send_packet(Acc, From, To, #xmlel{name = <<"message">>} = Msg) -> |
200 |
:-( |
maybe_process_message(Acc, From, To, Msg, outgoing); |
201 |
|
user_send_packet(Acc, _From, _To, _Packet) -> |
202 |
:-( |
Acc. |
203 |
|
|
204 |
|
-spec inbox_unread_count(Acc :: mongoose_acc:t(), To :: jid:jid()) -> mongoose_acc:t(). |
205 |
|
inbox_unread_count(Acc, To) -> |
206 |
:-( |
Res = mongoose_acc:get(inbox, unread_count, undefined, Acc), |
207 |
:-( |
get_inbox_unread(Res, Acc, To). |
208 |
|
|
209 |
|
-spec filter_local_packet(mongoose_hooks:filter_packet_acc() | drop) -> |
210 |
|
mongoose_hooks:filter_packet_acc() | drop. |
211 |
|
filter_local_packet(drop) -> |
212 |
:-( |
drop; |
213 |
|
filter_local_packet({From, To, Acc, Msg = #xmlel{name = <<"message">>}}) -> |
214 |
:-( |
Acc0 = maybe_process_message(Acc, From, To, Msg, incoming), |
215 |
:-( |
{From, To, Acc0, Msg}; |
216 |
|
filter_local_packet({From, To, Acc, Packet}) -> |
217 |
:-( |
{From, To, Acc, Packet}. |
218 |
|
|
219 |
|
remove_user(Acc, User, Server) -> |
220 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
221 |
:-( |
mod_inbox_utils:clear_inbox(HostType, User, Server), |
222 |
:-( |
Acc. |
223 |
|
|
224 |
|
-spec remove_domain(mongoose_hooks:simple_acc(), |
225 |
|
mongooseim:host_type(), jid:lserver()) -> |
226 |
|
mongoose_hooks:simple_acc(). |
227 |
|
remove_domain(Acc, HostType, Domain) -> |
228 |
:-( |
mod_inbox_backend:remove_domain(HostType, Domain), |
229 |
:-( |
Acc. |
230 |
|
|
231 |
|
-spec disco_local_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). |
232 |
|
disco_local_features(Acc = #{node := <<>>}) -> |
233 |
:-( |
mongoose_disco:add_features([?NS_ESL_INBOX], Acc); |
234 |
|
disco_local_features(Acc) -> |
235 |
:-( |
Acc. |
236 |
|
|
237 |
|
-spec maybe_process_message(Acc :: mongoose_acc:t(), |
238 |
|
From :: jid:jid(), |
239 |
|
To :: jid:jid(), |
240 |
|
Msg :: exml:element(), |
241 |
|
Dir :: mod_mam_utils:direction()) -> mongoose_acc:t(). |
242 |
|
maybe_process_message(Acc, From, To, Msg, Dir) -> |
243 |
:-( |
Type = mongoose_lib:get_message_type(Acc), |
244 |
:-( |
case should_be_stored_in_inbox(Acc, From, To, Msg, Dir, Type) of |
245 |
|
true -> |
246 |
:-( |
do_maybe_process_message(Acc, From, To, Msg, Dir, Type); |
247 |
|
false -> |
248 |
:-( |
Acc |
249 |
|
end. |
250 |
|
|
251 |
|
do_maybe_process_message(Acc, From, To, Msg, Dir, Type) -> |
252 |
|
%% In case of PgSQL we can update inbox and obtain unread_count in one query, |
253 |
|
%% so we put it in accumulator here. |
254 |
|
%% In case of MySQL/MsSQL it costs an extra query, so we fetch it only if necessary |
255 |
|
%% (when push notification is created) |
256 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
257 |
:-( |
case maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, Type) of |
258 |
:-( |
ok -> Acc; |
259 |
|
{ok, UnreadCount} -> |
260 |
:-( |
mongoose_acc:set(inbox, unread_count, UnreadCount, Acc); |
261 |
|
{error, Error} -> |
262 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
263 |
:-( |
?LOG_WARNING(#{what => inbox_process_message_failed, |
264 |
|
from_jid => jid:to_binary(From), to_jid => jid:to_binary(To), |
265 |
:-( |
host_type => HostType, dir => incoming, reason => Error}), |
266 |
:-( |
Acc |
267 |
|
end. |
268 |
|
|
269 |
|
-spec maybe_process_acceptable_message( |
270 |
|
mongooseim:host_type(), jid:jid(), jid:jid(), exml:element(), |
271 |
|
mongoose_acc:t(), mod_mam_utils:direction(), message_type()) -> |
272 |
|
count_res(). |
273 |
|
maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, one2one) -> |
274 |
:-( |
process_message(HostType, From, To, Msg, Acc, Dir, one2one); |
275 |
|
maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, groupchat) -> |
276 |
:-( |
case muclight_enabled(HostType) of |
277 |
:-( |
true -> process_message(HostType, From, To, Msg, Acc, Dir, groupchat); |
278 |
:-( |
false -> ok |
279 |
|
end. |
280 |
|
|
281 |
|
-spec process_message(HostType :: mongooseim:host_type(), |
282 |
|
From :: jid:jid(), |
283 |
|
To :: jid:jid(), |
284 |
|
Message :: exml:element(), |
285 |
|
Acc :: mongoose_acc:t(), |
286 |
|
Dir :: mod_mam_utils:direction(), |
287 |
|
Type :: message_type()) -> count_res(). |
288 |
|
process_message(HostType, From, To, Message, Acc, outgoing, one2one) -> |
289 |
:-( |
mod_inbox_one2one:handle_outgoing_message(HostType, From, To, Message, Acc); |
290 |
|
process_message(HostType, From, To, Message, Acc, incoming, one2one) -> |
291 |
:-( |
mod_inbox_one2one:handle_incoming_message(HostType, From, To, Message, Acc); |
292 |
|
process_message(HostType, From, To, Message, Acc, outgoing, groupchat) -> |
293 |
:-( |
mod_inbox_muclight:handle_outgoing_message(HostType, From, To, Message, Acc); |
294 |
|
process_message(HostType, From, To, Message, Acc, incoming, groupchat) -> |
295 |
:-( |
mod_inbox_muclight:handle_incoming_message(HostType, From, To, Message, Acc); |
296 |
|
process_message(HostType, From, To, Message, _TS, Dir, Type) -> |
297 |
:-( |
?LOG_WARNING(#{what => inbox_unknown_message, |
298 |
|
text => <<"Unknown message was not written into inbox">>, |
299 |
|
exml_packet => Message, |
300 |
|
from_jid => jid:to_binary(From), to_jid => jid:to_binary(To), |
301 |
:-( |
host_type => HostType, dir => Dir, inbox_message_type => Type}), |
302 |
:-( |
ok. |
303 |
|
|
304 |
|
|
305 |
|
%%%%%%%%%%%%%%%%%%% |
306 |
|
%% Stanza builders |
307 |
|
-spec build_inbox_message(inbox_res(), id(), integer()) -> exml:element(). |
308 |
|
build_inbox_message(#{msg := Content, |
309 |
|
unread_count := Count, |
310 |
|
timestamp := Timestamp, |
311 |
|
archive := Archive, |
312 |
|
muted_until := MutedUntil}, QueryId, AccTS) -> |
313 |
:-( |
#xmlel{name = <<"message">>, attrs = [{<<"id">>, mod_inbox_utils:wrapper_id()}], |
314 |
|
children = [build_result_el(Content, QueryId, Count, Timestamp, Archive, MutedUntil, AccTS)]}. |
315 |
|
|
316 |
|
-spec build_result_el(exml:element(), id(), integer(), integer(), boolean(), integer(), integer()) -> exml:element(). |
317 |
|
build_result_el(Msg, QueryId, Count, Timestamp, Archive, MutedUntil, AccTS) -> |
318 |
:-( |
Forwarded = build_forward_el(Msg, Timestamp), |
319 |
:-( |
Properties = mod_inbox_entries:extensions_result(Archive, MutedUntil, AccTS), |
320 |
:-( |
QueryAttr = [{<<"queryid">>, QueryId} || QueryId =/= undefined, QueryId =/= <<>>], |
321 |
:-( |
#xmlel{name = <<"result">>, |
322 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX}, |
323 |
|
{<<"unread">>, integer_to_binary(Count)} | QueryAttr], |
324 |
|
children = [Forwarded | Properties]}. |
325 |
|
|
326 |
|
-spec build_result_iq(get_inbox_res()) -> exml:element(). |
327 |
|
build_result_iq(List) -> |
328 |
:-( |
AllUnread = [ N || #{unread_count := N} <- List, N =/= 0], |
329 |
:-( |
Result = #{<<"count">> => length(List), |
330 |
|
<<"unread-messages">> => lists:sum(AllUnread), |
331 |
|
<<"active-conversations">> => length(AllUnread)}, |
332 |
:-( |
ResultBinary = maps:map(fun(K, V) -> |
333 |
:-( |
build_result_el(K, integer_to_binary(V)) end, Result), |
334 |
:-( |
#xmlel{name = <<"fin">>, attrs = [{<<"xmlns">>, ?NS_ESL_INBOX}], |
335 |
|
children = maps:values(ResultBinary)}. |
336 |
|
|
337 |
|
-spec build_result_el(name_bin(), count_bin()) -> exml:element(). |
338 |
|
build_result_el(Name, CountBin) -> |
339 |
:-( |
#xmlel{name = Name, children = [#xmlcdata{content = CountBin}]}. |
340 |
|
|
341 |
|
-spec build_forward_el(exml:element(), integer()) -> exml:element(). |
342 |
|
build_forward_el(Content, Timestamp) -> |
343 |
:-( |
Delay = build_delay_el(Timestamp), |
344 |
:-( |
#xmlel{name = <<"forwarded">>, attrs = [{<<"xmlns">>, ?NS_FORWARD}], |
345 |
|
children = [Delay, Content]}. |
346 |
|
|
347 |
|
-spec build_delay_el(Timestamp :: integer()) -> exml:element(). |
348 |
|
build_delay_el(Timestamp) -> |
349 |
:-( |
TS = calendar:system_time_to_rfc3339(Timestamp, [{offset, "Z"}, {unit, microsecond}]), |
350 |
:-( |
jlib:timestamp_to_xml(TS, undefined, undefined). |
351 |
|
|
352 |
|
%%%%%%%%%%%%%%%%%%% |
353 |
|
%% iq-get |
354 |
|
-spec build_inbox_form() -> exml:element(). |
355 |
|
build_inbox_form() -> |
356 |
:-( |
OrderOptions = [ |
357 |
|
{<<"Ascending by timestamp">>, <<"asc">>}, |
358 |
|
{<<"Descending by timestamp">>, <<"desc">>} |
359 |
|
], |
360 |
:-( |
FormFields = [ |
361 |
|
jlib:form_field({<<"FORM_TYPE">>, <<"hidden">>, ?NS_ESL_INBOX}), |
362 |
|
text_single_form_field(<<"start">>), |
363 |
|
text_single_form_field(<<"end">>), |
364 |
|
list_single_form_field(<<"order">>, <<"desc">>, OrderOptions), |
365 |
|
text_single_form_field(<<"hidden_read">>, <<"false">>), |
366 |
|
jlib:form_field({<<"archive">>, <<"boolean">>, <<"false">>}) |
367 |
|
], |
368 |
:-( |
#xmlel{name = <<"x">>, |
369 |
|
attrs = [{<<"xmlns">>, ?NS_XDATA}, {<<"type">>, <<"form">>}], |
370 |
|
children = FormFields}. |
371 |
|
|
372 |
|
-spec text_single_form_field(Var :: binary()) -> exml:element(). |
373 |
|
text_single_form_field(Var) -> |
374 |
:-( |
#xmlel{name = <<"field">>, attrs = [{<<"var">>, Var}, {<<"type">>, <<"text-single">>}]}. |
375 |
|
|
376 |
|
-spec text_single_form_field(Var :: binary(), DefaultValue :: binary()) -> exml:element(). |
377 |
|
text_single_form_field(Var, DefaultValue) -> |
378 |
:-( |
#xmlel{name = <<"field">>, |
379 |
|
attrs = [{<<"var">>, Var}, {<<"type">>, <<"text-single">>}, {<<"value">>, DefaultValue}]}. |
380 |
|
|
381 |
|
-spec list_single_form_field(Var :: binary(), |
382 |
|
Default :: binary(), |
383 |
|
Options :: [{Label :: binary(), Value :: binary()}]) -> |
384 |
|
exml:element(). |
385 |
|
list_single_form_field(Var, Default, Options) -> |
386 |
:-( |
Value = form_field_value(Default), |
387 |
:-( |
#xmlel{ |
388 |
|
name = <<"field">>, |
389 |
|
attrs = [{<<"var">>, Var}, {<<"type">>, <<"list-single">>}], |
390 |
:-( |
children = [Value | [ form_field_option(Label, OptValue) || {Label, OptValue} <- Options ]] |
391 |
|
}. |
392 |
|
|
393 |
|
-spec form_field_option(Label :: binary(), Value :: binary()) -> exml:element(). |
394 |
|
form_field_option(Label, Value) -> |
395 |
:-( |
#xmlel{ |
396 |
|
name = <<"option">>, |
397 |
|
attrs = [{<<"label">>, Label}], |
398 |
|
children = [form_field_value(Value)] |
399 |
|
}. |
400 |
|
|
401 |
|
-spec form_field_value(Value :: binary()) -> exml:element(). |
402 |
|
form_field_value(Value) -> |
403 |
:-( |
#xmlel{name = <<"value">>, children = [#xmlcdata{content = Value}]}. |
404 |
|
|
405 |
|
%%%%%%%%%%%%%%%%%%% |
406 |
|
%% iq-set |
407 |
|
-spec query_to_params(QueryEl :: exml:element()) -> |
408 |
|
get_inbox_params() | {error, bad_request, Msg :: binary()}. |
409 |
|
query_to_params(QueryEl) -> |
410 |
:-( |
case form_to_params(exml_query:subelement_with_ns(QueryEl, ?NS_XDATA)) of |
411 |
|
{error, bad_request, Msg} -> |
412 |
:-( |
{error, bad_request, Msg}; |
413 |
|
Params -> |
414 |
:-( |
case maybe_rsm(exml_query:subelement_with_ns(QueryEl, ?NS_RSM)) of |
415 |
:-( |
{error, Msg} -> {error, bad_request, Msg}; |
416 |
:-( |
undefined -> Params; |
417 |
:-( |
Rsm -> Params#{limit => Rsm} |
418 |
|
end |
419 |
|
end. |
420 |
|
|
421 |
|
-spec maybe_rsm(exml:element() | undefined) -> |
422 |
|
undefined | non_neg_integer() | {error, binary()}. |
423 |
|
maybe_rsm(#xmlel{name = <<"set">>, |
424 |
|
children = [#xmlel{name = <<"max">>, |
425 |
|
children = [#xmlcdata{content = Bin}]}]}) -> |
426 |
:-( |
case mod_inbox_utils:maybe_binary_to_positive_integer(Bin) of |
427 |
:-( |
{error, _} -> {error, wrong_rsm_message()}; |
428 |
:-( |
0 -> undefined; |
429 |
:-( |
N -> N |
430 |
|
end; |
431 |
|
maybe_rsm(undefined) -> |
432 |
:-( |
undefined; |
433 |
|
maybe_rsm(_) -> |
434 |
:-( |
{error, wrong_rsm_message()}. |
435 |
|
|
436 |
|
wrong_rsm_message() -> |
437 |
:-( |
<<"bad-request">>. |
438 |
|
|
439 |
|
-spec form_to_params(FormEl :: exml:element() | undefined) -> |
440 |
|
get_inbox_params() | {error, bad_request, Msg :: binary()}. |
441 |
|
form_to_params(undefined) -> |
442 |
:-( |
#{ order => desc }; |
443 |
|
form_to_params(FormEl) -> |
444 |
:-( |
ParsedFields = jlib:parse_xdata_fields(exml_query:subelements(FormEl, <<"field">>)), |
445 |
:-( |
?LOG_DEBUG(#{what => inbox_parsed_form_fields, parsed_fields => ParsedFields}), |
446 |
:-( |
fields_to_params(ParsedFields, #{ order => desc }). |
447 |
|
|
448 |
|
-spec fields_to_params([{Var :: binary(), Values :: [binary()]}], Acc :: get_inbox_params()) -> |
449 |
|
get_inbox_params() | {error, bad_request, Msg :: binary()}. |
450 |
|
fields_to_params([], Acc) -> |
451 |
:-( |
Acc; |
452 |
|
fields_to_params([{<<"start">>, [StartISO]} | RFields], Acc) -> |
453 |
:-( |
try calendar:rfc3339_to_system_time(binary_to_list(StartISO), [{unit, microsecond}]) of |
454 |
|
StartStamp -> |
455 |
:-( |
fields_to_params(RFields, Acc#{ start => StartStamp }) |
456 |
|
catch error:Error -> |
457 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
458 |
:-( |
reason => Error, field => start, value => StartISO}), |
459 |
:-( |
{error, bad_request, invalid_field_value(<<"start">>, StartISO)} |
460 |
|
end; |
461 |
|
fields_to_params([{<<"end">>, [EndISO]} | RFields], Acc) -> |
462 |
:-( |
try calendar:rfc3339_to_system_time(binary_to_list(EndISO), [{unit, microsecond}]) of |
463 |
|
EndStamp -> |
464 |
:-( |
fields_to_params(RFields, Acc#{ 'end' => EndStamp }) |
465 |
|
catch error:Error -> |
466 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
467 |
:-( |
reason => Error, field => 'end', value => EndISO}), |
468 |
:-( |
{error, bad_request, invalid_field_value(<<"end">>, EndISO)} |
469 |
|
end; |
470 |
|
fields_to_params([{<<"order">>, [OrderBin]} | RFields], Acc) -> |
471 |
:-( |
case binary_to_order(OrderBin) of |
472 |
|
error -> |
473 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
474 |
:-( |
field => order, value => OrderBin}), |
475 |
:-( |
{error, bad_request, invalid_field_value(<<"order">>, OrderBin)}; |
476 |
|
Order -> |
477 |
:-( |
fields_to_params(RFields, Acc#{ order => Order }) |
478 |
|
end; |
479 |
|
|
480 |
|
fields_to_params([{<<"hidden_read">>, [HiddenRead]} | RFields], Acc) -> |
481 |
:-( |
case mod_inbox_utils:binary_to_bool(HiddenRead) of |
482 |
|
error -> |
483 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
484 |
:-( |
field => hidden_read, value => HiddenRead}), |
485 |
:-( |
{error, bad_request, invalid_field_value(<<"hidden_read">>, HiddenRead)}; |
486 |
|
Hidden -> |
487 |
:-( |
fields_to_params(RFields, Acc#{ hidden_read => Hidden }) |
488 |
|
end; |
489 |
|
|
490 |
|
fields_to_params([{<<"archive">>, [Value]} | RFields], Acc) -> |
491 |
:-( |
case mod_inbox_utils:binary_to_bool(Value) of |
492 |
|
error -> |
493 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
494 |
:-( |
field => archive, value => Value}), |
495 |
:-( |
{error, bad_request, invalid_field_value(<<"archive">>, Value)}; |
496 |
|
Archive -> |
497 |
:-( |
fields_to_params(RFields, Acc#{ archive => Archive }) |
498 |
|
end; |
499 |
|
|
500 |
|
fields_to_params([{<<"FORM_TYPE">>, _} | RFields], Acc) -> |
501 |
:-( |
fields_to_params(RFields, Acc); |
502 |
|
fields_to_params([{Invalid, [InvalidFieldVal]} | _], _) -> |
503 |
:-( |
?LOG_INFO(#{what => inbox_invalid_form_field, reason => unknown_field, |
504 |
:-( |
field => Invalid, value => InvalidFieldVal}), |
505 |
:-( |
{error, bad_request, <<"Unknown inbox form field=", Invalid/binary, ", value=", InvalidFieldVal/binary>>}. |
506 |
|
|
507 |
|
-spec binary_to_order(binary()) -> asc | desc | error. |
508 |
:-( |
binary_to_order(<<"desc">>) -> desc; |
509 |
:-( |
binary_to_order(<<"asc">>) -> asc; |
510 |
:-( |
binary_to_order(_) -> error. |
511 |
|
|
512 |
|
invalid_field_value(Field, Value) -> |
513 |
:-( |
<<"Invalid inbox form field value, field=", Field/binary, ", value=", Value/binary>>. |
514 |
|
|
515 |
|
%%%%%%%%%%%%%%%%%%% |
516 |
|
%% Helpers |
517 |
|
get_inbox_unread(Value, Acc, _) when is_integer(Value) -> |
518 |
:-( |
Acc; |
519 |
|
get_inbox_unread(undefined, Acc, To) -> |
520 |
|
%% TODO this value should be bound to a stanza reference inside Acc |
521 |
:-( |
InterlocutorJID = mongoose_acc:from_jid(Acc), |
522 |
:-( |
InboxEntryKey = mod_inbox_utils:build_inbox_entry_key(To, InterlocutorJID), |
523 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
524 |
:-( |
{ok, Count} = mod_inbox_backend:get_inbox_unread(HostType, InboxEntryKey), |
525 |
:-( |
mongoose_acc:set(inbox, unread_count, Count, Acc). |
526 |
|
|
527 |
|
hooks(HostType) -> |
528 |
:-( |
[ |
529 |
|
{remove_user, HostType, ?MODULE, remove_user, 50}, |
530 |
|
{remove_domain, HostType, ?MODULE, remove_domain, 50}, |
531 |
|
{user_send_packet, HostType, ?MODULE, user_send_packet, 70}, |
532 |
|
{filter_local_packet, HostType, ?MODULE, filter_local_packet, 90}, |
533 |
|
{inbox_unread_count, HostType, ?MODULE, inbox_unread_count, 80}, |
534 |
|
{get_personal_data, HostType, ?MODULE, get_personal_data, 50}, |
535 |
|
{disco_local_features, HostType, ?MODULE, disco_local_features, 99} |
536 |
|
]. |
537 |
|
|
538 |
|
get_groupchat_types(HostType) -> |
539 |
:-( |
gen_mod:get_module_opt(HostType, ?MODULE, groupchat). |
540 |
|
|
541 |
|
config_metrics(HostType) -> |
542 |
:-( |
OptsToReport = [{backend, rdbms}], %list of tuples {option, defualt_value} |
543 |
:-( |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, OptsToReport). |
544 |
|
|
545 |
|
-spec muclight_enabled(HostType :: mongooseim:host_type()) -> boolean(). |
546 |
|
muclight_enabled(HostType) -> |
547 |
:-( |
Groupchats = get_groupchat_types(HostType), |
548 |
:-( |
lists:member(muclight, Groupchats). |
549 |
|
|
550 |
|
%%%%%%%%%%%%%%%%%%% |
551 |
|
%% Message Predicates |
552 |
|
-spec should_be_stored_in_inbox( |
553 |
|
mongoose_acc:t(), jid:jid(), jid:jid(), exml:element(), mod_mam_utils:direction(), message_type()) -> |
554 |
|
boolean(). |
555 |
|
should_be_stored_in_inbox(Acc, From, To, Msg, Dir, Type) -> |
556 |
:-( |
mod_mam_utils:is_archivable_message(?MODULE, Dir, Msg, true) |
557 |
:-( |
andalso mod_inbox_entries:should_be_stored_in_inbox(Msg) |
558 |
:-( |
andalso inbox_owner_exists(Acc, From, To, Dir, Type). |
559 |
|
|
560 |
|
-spec inbox_owner_exists(mongoose_acc:t(), |
561 |
|
From :: jid:jid(), |
562 |
|
To ::jid:jid(), |
563 |
|
mod_mam_utils:direction(), |
564 |
|
message_type()) -> boolean(). |
565 |
|
inbox_owner_exists(Acc, _, To, incoming, MessageType) -> % filter_local_packet |
566 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
567 |
:-( |
mongoose_lib:does_local_user_exist(HostType, To, MessageType); |
568 |
|
inbox_owner_exists(Acc, From, _, outgoing, _) -> % user_send_packet |
569 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
570 |
:-( |
ejabberd_auth:does_user_exist(HostType, From, stored). |