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