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