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("mongoose_rsm.hrl"). |
15 |
|
-include("mod_inbox.hrl"). |
16 |
|
-include("mongoose_config_spec.hrl"). |
17 |
|
-include("mongoose_logger.hrl"). |
18 |
|
-include("mongoose_ns.hrl"). |
19 |
|
|
20 |
|
%% gen_mod |
21 |
|
-export([start/2, stop/1, hooks/1, config_spec/0, supported_features/0]). |
22 |
|
|
23 |
|
-export([process_iq/5]). |
24 |
|
|
25 |
|
%% hook handlers |
26 |
|
-export([user_send_message/3, |
27 |
|
filter_local_packet/3, |
28 |
|
inbox_unread_count/3, |
29 |
|
remove_user/3, |
30 |
|
remove_domain/3, |
31 |
|
disco_local_features/3, |
32 |
|
get_personal_data/3 |
33 |
|
]). |
34 |
|
|
35 |
|
-export([process_inbox_boxes/1]). |
36 |
|
-export([config_metrics/1]). |
37 |
|
|
38 |
|
-type message_type() :: one2one | groupchat. |
39 |
|
-type entry_key() :: {LUser :: jid:luser(), |
40 |
|
LServer :: jid:lserver(), |
41 |
|
ToBareJid :: jid:literal_jid()}. |
42 |
|
|
43 |
|
-type get_inbox_params() :: #{ |
44 |
|
start => integer(), |
45 |
|
'end' => integer(), |
46 |
|
order => asc | desc, |
47 |
|
hidden_read => true | false, |
48 |
|
box => binary(), |
49 |
|
limit => undefined | pos_integer(), |
50 |
|
rsm => jlib:rsm_in(), |
51 |
|
filter_on_jid => 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, entry_properties/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(Acc, Params, Extra) -> {ok, Acc} when |
64 |
|
Acc :: gdpr:personal_data(), |
65 |
|
Params :: #{jid := jid:jid()}, |
66 |
|
Extra :: #{host_type := mongooseim:host_type()}. |
67 |
|
get_personal_data(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) -> |
68 |
29 |
Schema = ["jid", "content", "unread_count", "timestamp"], |
69 |
29 |
InboxParams = #{ |
70 |
|
start => 0, |
71 |
|
'end' => erlang:system_time(microsecond), |
72 |
|
order => asc, |
73 |
|
hidden_read => false |
74 |
|
}, |
75 |
29 |
Entries = mod_inbox_backend:get_inbox(HostType, LUser, LServer, InboxParams), |
76 |
29 |
ProcessedEntries = lists:map(fun process_entry/1, Entries), |
77 |
29 |
NewAcc = [{inbox, Schema, ProcessedEntries} | Acc], |
78 |
29 |
{ok, NewAcc}. |
79 |
|
|
80 |
|
process_entry(#{remote_jid := RemJID, |
81 |
|
msg := Content, |
82 |
|
unread_count := UnreadCount, |
83 |
|
timestamp := Timestamp}) -> |
84 |
12 |
TS = calendar:system_time_to_rfc3339(Timestamp, [{offset, "Z"}, {unit, microsecond}]), |
85 |
12 |
{RemJID, Content, UnreadCount, TS}. |
86 |
|
|
87 |
|
%%-------------------------------------------------------------------- |
88 |
|
%% gen_mod callbacks |
89 |
|
%%-------------------------------------------------------------------- |
90 |
|
|
91 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
92 |
|
start(HostType, #{iqdisc := IQDisc, groupchat := MucTypes} = Opts) -> |
93 |
48 |
mod_inbox_backend:init(HostType, Opts), |
94 |
48 |
lists:member(muc, MucTypes) andalso mod_inbox_muc:start(HostType), |
95 |
48 |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm, |
96 |
|
fun ?MODULE:process_iq/5, #{}, IQDisc), |
97 |
48 |
gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm, |
98 |
|
fun mod_inbox_entries:process_iq_conversation/5, #{}, IQDisc), |
99 |
48 |
start_cleaner(HostType, Opts), |
100 |
48 |
ok. |
101 |
|
|
102 |
|
-spec stop(HostType :: mongooseim:host_type()) -> ok. |
103 |
|
stop(HostType) -> |
104 |
48 |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm), |
105 |
48 |
gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm), |
106 |
48 |
stop_cleaner(HostType), |
107 |
48 |
mod_inbox_muc:stop(HostType), |
108 |
48 |
case mongoose_config:get_opt([{modules, HostType}, ?MODULE, backend]) of |
109 |
22 |
rdbms_async -> mod_inbox_rdbms_async:stop(HostType); |
110 |
26 |
_ -> ok |
111 |
|
end. |
112 |
|
|
113 |
|
-spec supported_features() -> [atom()]. |
114 |
|
supported_features() -> |
115 |
50 |
[dynamic_domains]. |
116 |
|
|
117 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
118 |
|
config_spec() -> |
119 |
84 |
Markers = mongoose_chat_markers:chat_marker_names(), |
120 |
84 |
#section{ |
121 |
|
items = #{<<"backend">> => #option{type = atom, validate = {enum, [rdbms, rdbms_async]}}, |
122 |
|
<<"async_writer">> => async_config_spec(), |
123 |
|
<<"reset_markers">> => #list{items = #option{type = binary, |
124 |
|
validate = {enum, Markers}}}, |
125 |
|
<<"groupchat">> => #list{items = #option{type = atom, |
126 |
|
validate = {enum, [muc, muclight]}}}, |
127 |
|
<<"boxes">> => #list{items = #option{type = binary, validate = non_empty}, |
128 |
|
validate = unique}, |
129 |
|
<<"bin_ttl">> => #option{type = integer, validate = non_negative}, |
130 |
|
<<"bin_clean_after">> => #option{type = integer, validate = non_negative, |
131 |
|
process = fun timer:hours/1}, |
132 |
|
<<"delete_domain_limit">> => #option{type = int_or_infinity, |
133 |
|
validate = positive}, |
134 |
|
<<"aff_changes">> => #option{type = boolean}, |
135 |
|
<<"remove_on_kicked">> => #option{type = boolean}, |
136 |
|
<<"iqdisc">> => mongoose_config_spec:iqdisc(), |
137 |
|
<<"max_result_limit">> => #option{type = int_or_infinity, validate = positive} |
138 |
|
}, |
139 |
|
defaults = #{<<"backend">> => rdbms, |
140 |
|
<<"groupchat">> => [muclight], |
141 |
|
<<"boxes">> => [], |
142 |
|
<<"bin_ttl">> => 30, % 30 days |
143 |
|
<<"bin_clean_after">> => timer:hours(1), |
144 |
|
<<"delete_domain_limit">> => infinity, |
145 |
|
<<"aff_changes">> => true, |
146 |
|
<<"remove_on_kicked">> => true, |
147 |
|
<<"reset_markers">> => [<<"displayed">>], |
148 |
|
<<"iqdisc">> => no_queue, |
149 |
|
<<"max_result_limit">> => infinity |
150 |
|
}, |
151 |
|
process = fun ?MODULE:process_inbox_boxes/1 |
152 |
|
}. |
153 |
|
|
154 |
|
async_config_spec() -> |
155 |
84 |
#section{ |
156 |
|
items = #{<<"pool_size">> => #option{type = integer, validate = non_negative}}, |
157 |
|
defaults = #{<<"pool_size">> => 2 * erlang:system_info(schedulers_online)}, |
158 |
|
include = always |
159 |
|
}. |
160 |
|
|
161 |
|
process_inbox_boxes(Config = #{boxes := Boxes}) -> |
162 |
:-( |
false = lists:any(fun(<<"all">>) -> true; |
163 |
:-( |
(<<"inbox">>) -> true; |
164 |
:-( |
(<<"archive">>) -> true; |
165 |
:-( |
(<<"bin">>) -> true; |
166 |
:-( |
(_) -> false |
167 |
|
end, Boxes), |
168 |
:-( |
AllBoxes = [<<"inbox">>, <<"archive">>, <<"bin">> | Boxes ], |
169 |
:-( |
Config#{boxes := AllBoxes}. |
170 |
|
|
171 |
|
%% Cleaner gen_server callbacks |
172 |
|
start_cleaner(HostType, #{bin_ttl := TTL, bin_clean_after := Interval}) -> |
173 |
48 |
WOpts = #{host_type => HostType, action => fun mod_inbox_api:flush_global_bin/2, |
174 |
|
opts => TTL, interval => Interval}, |
175 |
48 |
mongoose_collector:start_common(?MODULE, HostType, WOpts). |
176 |
|
|
177 |
|
stop_cleaner(HostType) -> |
178 |
48 |
Name = gen_mod:get_module_proc(HostType, ?MODULE), |
179 |
48 |
ejabberd_sup:stop_child(Name). |
180 |
|
|
181 |
|
%%%%%%%%%%%%%%%%%%% |
182 |
|
%% Process IQ |
183 |
|
-spec process_iq(Acc :: mongoose_acc:t(), |
184 |
|
From :: jid:jid(), |
185 |
|
To :: jid:jid(), |
186 |
|
IQ :: jlib:iq(), |
187 |
|
Extra :: gen_hook:extra()) -> {stop, mongoose_acc:t()} | {mongoose_acc:t(), jlib:iq()}. |
188 |
|
process_iq(Acc, _From, _To, #iq{type = get, sub_el = SubEl} = IQ, #{host_type := HostType}) -> |
189 |
2 |
Form = build_inbox_form(HostType), |
190 |
2 |
SubElWithForm = SubEl#xmlel{ children = [Form] }, |
191 |
2 |
{Acc, IQ#iq{type = result, sub_el = SubElWithForm}}; |
192 |
|
process_iq(Acc, #jid{luser = LUser, lserver = LServer}, |
193 |
|
_To, #iq{type = set, sub_el = #xmlel{name = <<"empty-bin">>}} = IQ, |
194 |
|
#{host_type := HostType}) -> |
195 |
1 |
TS = mongoose_acc:timestamp(Acc), |
196 |
1 |
NumRemRows = mod_inbox_backend:empty_user_bin(HostType, LServer, LUser, TS), |
197 |
1 |
{Acc, IQ#iq{type = result, sub_el = [build_empty_bin(NumRemRows)]}}; |
198 |
|
process_iq(Acc, From, _To, #iq{type = set, sub_el = QueryEl} = IQ, _Extra) -> |
199 |
1180 |
HostType = mongoose_acc:host_type(Acc), |
200 |
1180 |
LUser = From#jid.luser, |
201 |
1180 |
LServer = From#jid.lserver, |
202 |
1180 |
case query_to_params(HostType, QueryEl) of |
203 |
|
{error, Error, Msg} -> |
204 |
22 |
{Acc, IQ#iq{type = error, sub_el = [mongoose_xmpp_errors:Error(<<"en">>, Msg)]}}; |
205 |
|
Params -> |
206 |
1158 |
List0 = mod_inbox_backend:get_inbox(HostType, LUser, LServer, Params), |
207 |
1158 |
List1 = with_rsm(List0, Params), |
208 |
1158 |
List2 = mongoose_hooks:extend_inbox_result(Acc, List1, IQ), |
209 |
1158 |
forward_messages(Acc, List2, IQ, From), |
210 |
1158 |
Res = IQ#iq{type = result, sub_el = [build_result_iq(List2)]}, |
211 |
1158 |
{Acc, Res} |
212 |
|
end. |
213 |
|
|
214 |
|
-spec with_rsm([inbox_res()], get_inbox_params()) -> [inbox_res()]. |
215 |
|
with_rsm(List, #{order := asc, start := TS, filter_on_jid := BinJid, rsm := #rsm_in{}}) -> |
216 |
2 |
lists:reverse(drop_filter_on_jid(List, BinJid, TS, List)); |
217 |
|
with_rsm(List, #{order := asc, rsm := #rsm_in{}}) -> |
218 |
4 |
lists:reverse(List); |
219 |
|
with_rsm(List, #{order := desc, 'end' := TS, filter_on_jid := BinJid}) -> |
220 |
2 |
drop_filter_on_jid(List, BinJid, TS, List); |
221 |
|
with_rsm(List, _) -> |
222 |
1150 |
List. |
223 |
|
|
224 |
|
%% As IDs must be unique but timestamps are not, and SQL queries and orders by timestamp alone, |
225 |
|
%% we query max+1 and then match to remove the entry that matches the ID given before. |
226 |
|
-spec drop_filter_on_jid([inbox_res()], binary(), integer(), [inbox_res()]) -> [inbox_res()]. |
227 |
|
drop_filter_on_jid(_List, BinJid, TS, [#{remote_jid := BinJid, timestamp := TS} | Rest]) -> |
228 |
4 |
Rest; |
229 |
|
drop_filter_on_jid(List, BinJid, TS, [_ | Rest]) -> |
230 |
:-( |
drop_filter_on_jid(List, BinJid, TS, Rest); |
231 |
|
drop_filter_on_jid(List, _, _, []) -> |
232 |
:-( |
List. |
233 |
|
|
234 |
|
-spec forward_messages(Acc :: mongoose_acc:t(), |
235 |
|
List :: [inbox_res()], |
236 |
|
QueryEl :: jlib:iq(), |
237 |
|
To :: jid:jid()) -> [mongoose_acc:t()]. |
238 |
|
forward_messages(Acc, List, QueryEl, To) when is_list(List) -> |
239 |
1158 |
Msgs = [ build_inbox_message(Acc, El, QueryEl) || El <- List], |
240 |
1158 |
[ send_message(Acc, To, Msg) || Msg <- Msgs]. |
241 |
|
|
242 |
|
-spec send_message(mongoose_acc:t(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
243 |
|
send_message(Acc, To = #jid{lserver = LServer}, Msg) -> |
244 |
960 |
BareTo = jid:to_bare(To), |
245 |
960 |
HostType = mongoose_acc:host_type(Acc), |
246 |
960 |
NewAcc0 = mongoose_acc:new(#{location => ?LOCATION, |
247 |
|
host_type => HostType, |
248 |
|
lserver => LServer, |
249 |
|
element => Msg, |
250 |
|
from_jid => BareTo, |
251 |
|
to_jid => To}), |
252 |
960 |
PermanentFields = mongoose_acc:get_permanent_fields(Acc), |
253 |
960 |
NewAcc = mongoose_acc:set_permanent(PermanentFields, NewAcc0), |
254 |
960 |
ejabberd_sm:route(BareTo, To, NewAcc). |
255 |
|
|
256 |
|
%%%%%%%%%%%%%%%%%%% |
257 |
|
%% Handlers |
258 |
|
-spec user_send_message(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) -> |
259 |
|
mongoose_c2s_hooks:result(). |
260 |
|
user_send_message(Acc, _, _) -> |
261 |
483 |
{From, To, Msg} = mongoose_acc:packet(Acc), |
262 |
483 |
Acc1 = maybe_process_message(Acc, From, To, Msg, outgoing), |
263 |
483 |
{ok, Acc1}. |
264 |
|
|
265 |
|
-spec inbox_unread_count(Acc, Params, Extra) -> {ok, Acc} when |
266 |
|
Acc :: mongoose_acc:t(), |
267 |
|
Params :: #{user := jid:jid()}, |
268 |
|
Extra :: gen_hook:extra(). |
269 |
|
inbox_unread_count(Acc, #{user := User}, _) -> |
270 |
:-( |
Res = mongoose_acc:get(inbox, unread_count, undefined, Acc), |
271 |
:-( |
NewAcc = get_inbox_unread(Res, Acc, User), |
272 |
:-( |
{ok, NewAcc}. |
273 |
|
|
274 |
|
-spec filter_local_packet(FPacketAcc, Params, Extra) -> {ok, FPacketAcc} when |
275 |
|
FPacketAcc :: mongoose_hooks:filter_packet_acc(), |
276 |
|
Params :: map(), |
277 |
|
Extra :: gen_hook:extra(). |
278 |
|
filter_local_packet({From, To, Acc, Msg = #xmlel{name = <<"message">>}}, _, _) -> |
279 |
1626 |
Acc0 = maybe_process_message(Acc, From, To, Msg, incoming), |
280 |
1626 |
{ok, {From, To, Acc0, Msg}}; |
281 |
|
filter_local_packet(FPacketAcc, _, _) -> |
282 |
6026 |
{ok, FPacketAcc}. |
283 |
|
|
284 |
|
-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when |
285 |
|
Acc :: mongoose_acc:t(), |
286 |
|
Params :: #{jid := jid:jid()}, |
287 |
|
Extra :: gen_hook:extra(). |
288 |
|
remove_user(Acc, #{jid := #jid{luser = User, lserver = Server}}, _) -> |
289 |
616 |
HostType = mongoose_acc:host_type(Acc), |
290 |
616 |
mod_inbox_utils:clear_inbox(HostType, User, Server), |
291 |
616 |
{ok, Acc}. |
292 |
|
|
293 |
|
-spec remove_domain(Acc, Params, Extra) -> {ok | stop, Acc} when |
294 |
|
Acc :: mongoose_domain_api:remove_domain_acc(), |
295 |
|
Params :: #{domain := jid:lserver()}, |
296 |
|
Extra :: gen_hook:extra(). |
297 |
|
remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) -> |
298 |
2 |
F = fun() -> |
299 |
2 |
mod_inbox_backend:remove_domain(HostType, Domain), |
300 |
2 |
Acc |
301 |
|
end, |
302 |
2 |
mongoose_domain_api:remove_domain_wrapper(Acc, F, ?MODULE). |
303 |
|
|
304 |
|
-spec disco_local_features(Acc, Params, Extra) -> {ok, Acc} when |
305 |
|
Acc :: mongoose_disco:feature_acc(), |
306 |
|
Params :: map(), |
307 |
|
Extra :: gen_hook:extra(). |
308 |
|
disco_local_features(Acc = #{node := <<>>}, _, _) -> |
309 |
2 |
{ok, mongoose_disco:add_features([?NS_ESL_INBOX], Acc)}; |
310 |
|
disco_local_features(Acc, _, _) -> |
311 |
:-( |
{ok, Acc}. |
312 |
|
|
313 |
|
-spec maybe_process_message(Acc :: mongoose_acc:t(), |
314 |
|
From :: jid:jid(), |
315 |
|
To :: jid:jid(), |
316 |
|
Msg :: exml:element(), |
317 |
|
Dir :: mod_mam_utils:direction()) -> mongoose_acc:t(). |
318 |
|
maybe_process_message(Acc, From, To, Msg, Dir) -> |
319 |
2109 |
Type = mongoose_lib:get_message_type(Acc), |
320 |
2109 |
case should_be_stored_in_inbox(Acc, From, To, Msg, Dir, Type) of |
321 |
|
true -> |
322 |
1746 |
do_maybe_process_message(Acc, From, To, Msg, Dir, Type); |
323 |
|
false -> |
324 |
363 |
Acc |
325 |
|
end. |
326 |
|
|
327 |
|
do_maybe_process_message(Acc, From, To, Msg, Dir, Type) -> |
328 |
|
%% In case of PgSQL we can update inbox and obtain unread_count in one query, |
329 |
|
%% so we put it in accumulator here. |
330 |
|
%% In case of MySQL/MsSQL it costs an extra query, so we fetch it only if necessary |
331 |
|
%% (when push notification is created) |
332 |
1746 |
HostType = mongoose_acc:host_type(Acc), |
333 |
1746 |
case maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, Type) of |
334 |
1746 |
ok -> Acc; |
335 |
|
{ok, UnreadCount} -> |
336 |
:-( |
mongoose_acc:set(inbox, unread_count, UnreadCount, Acc); |
337 |
|
{error, Error} -> |
338 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
339 |
:-( |
?LOG_WARNING(#{what => inbox_process_message_failed, |
340 |
|
from_jid => jid:to_binary(From), to_jid => jid:to_binary(To), |
341 |
:-( |
host_type => HostType, dir => incoming, reason => Error}), |
342 |
:-( |
Acc |
343 |
|
end. |
344 |
|
|
345 |
|
-spec maybe_process_acceptable_message( |
346 |
|
mongooseim:host_type(), jid:jid(), jid:jid(), exml:element(), |
347 |
|
mongoose_acc:t(), mod_mam_utils:direction(), message_type()) -> |
348 |
|
count_res(). |
349 |
|
maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, one2one) -> |
350 |
542 |
process_message(HostType, From, To, Msg, Acc, Dir, one2one); |
351 |
|
maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, groupchat) -> |
352 |
1204 |
case muclight_enabled(HostType) of |
353 |
1090 |
true -> process_message(HostType, From, To, Msg, Acc, Dir, groupchat); |
354 |
114 |
false -> ok |
355 |
|
end. |
356 |
|
|
357 |
|
-spec process_message(HostType :: mongooseim:host_type(), |
358 |
|
From :: jid:jid(), |
359 |
|
To :: jid:jid(), |
360 |
|
Message :: exml:element(), |
361 |
|
Acc :: mongoose_acc:t(), |
362 |
|
Dir :: mod_mam_utils:direction(), |
363 |
|
Type :: message_type()) -> count_res(). |
364 |
|
process_message(HostType, From, To, Message, Acc, outgoing, one2one) -> |
365 |
272 |
mod_inbox_one2one:handle_outgoing_message(HostType, From, To, Message, Acc); |
366 |
|
process_message(HostType, From, To, Message, Acc, incoming, one2one) -> |
367 |
270 |
mod_inbox_one2one:handle_incoming_message(HostType, From, To, Message, Acc); |
368 |
|
process_message(HostType, From, To, Message, Acc, outgoing, groupchat) -> |
369 |
191 |
mod_inbox_muclight:handle_outgoing_message(HostType, From, To, Message, Acc); |
370 |
|
process_message(HostType, From, To, Message, Acc, incoming, groupchat) -> |
371 |
899 |
mod_inbox_muclight:handle_incoming_message(HostType, From, To, Message, Acc); |
372 |
|
process_message(HostType, From, To, Message, _TS, Dir, Type) -> |
373 |
:-( |
?LOG_WARNING(#{what => inbox_unknown_message, |
374 |
|
text => <<"Unknown message was not written into inbox">>, |
375 |
|
exml_packet => Message, |
376 |
|
from_jid => jid:to_binary(From), to_jid => jid:to_binary(To), |
377 |
:-( |
host_type => HostType, dir => Dir, inbox_message_type => Type}), |
378 |
:-( |
ok. |
379 |
|
|
380 |
|
|
381 |
|
%%%%%%%%%%%%%%%%%%% |
382 |
|
%% Stanza builders |
383 |
|
build_empty_bin(Num) -> |
384 |
1 |
#xmlel{name = <<"empty-bin">>, |
385 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX}], |
386 |
|
children = [#xmlel{name = <<"num">>, |
387 |
|
children = [#xmlcdata{content = integer_to_binary(Num)}]}]}. |
388 |
|
|
389 |
|
-spec build_inbox_message(mongoose_acc:t(), inbox_res(), jlib:iq()) -> exml:element(). |
390 |
|
build_inbox_message(Acc, InboxRes, IQ) -> |
391 |
960 |
#xmlel{name = <<"message">>, attrs = [{<<"id">>, mongoose_bin:gen_from_timestamp()}], |
392 |
|
children = [build_result_el(Acc, InboxRes, IQ)]}. |
393 |
|
|
394 |
|
-spec build_result_el(mongoose_acc:t(), inbox_res(), jlib:iq()) -> exml:element(). |
395 |
|
build_result_el(Acc, InboxRes = #{unread_count := Count}, #iq{id = IqId, sub_el = QueryEl}) -> |
396 |
960 |
AccTS = mongoose_acc:timestamp(Acc), |
397 |
960 |
Children = mod_inbox_utils:build_inbox_result_elements(InboxRes, AccTS), |
398 |
960 |
#xmlel{name = <<"result">>, |
399 |
|
attrs = [{<<"xmlns">>, ?NS_ESL_INBOX}, |
400 |
|
{<<"unread">>, integer_to_binary(Count)}, |
401 |
|
{<<"queryid">>, exml_query:attr(QueryEl, <<"queryid">>, IqId)}], |
402 |
|
children = Children}. |
403 |
|
|
404 |
|
-spec build_result_iq([inbox_res()]) -> exml:element(). |
405 |
|
build_result_iq(List) -> |
406 |
1158 |
AllUnread = [ N || #{unread_count := N} <- List, N =/= 0], |
407 |
1158 |
Result = #{<<"count">> => length(List), |
408 |
|
<<"unread-messages">> => lists:sum(AllUnread), |
409 |
|
<<"active-conversations">> => length(AllUnread)}, |
410 |
1158 |
ResultBinary = maps:map(fun(K, V) -> |
411 |
3474 |
#xmlel{name = K, children = [#xmlcdata{content = integer_to_binary(V)}]} |
412 |
|
end, Result), |
413 |
1158 |
ResultSetEl = result_set(List), |
414 |
1158 |
#xmlel{name = <<"fin">>, attrs = [{<<"xmlns">>, ?NS_ESL_INBOX}], |
415 |
|
children = [ResultSetEl | maps:values(ResultBinary)]}. |
416 |
|
|
417 |
|
-spec result_set([inbox_res()]) -> exml:element(). |
418 |
|
result_set([]) -> |
419 |
278 |
#xmlel{name = <<"set">>, attrs = [{<<"xmlns">>, ?NS_RSM}]}; |
420 |
|
result_set([#{remote_jid := FirstBinJid, timestamp := FirstTS} | _] = List) -> |
421 |
880 |
#{remote_jid := LastBinJid, timestamp := LastTS} = lists:last(List), |
422 |
880 |
BFirst = mod_inbox_utils:encode_rsm_id(FirstTS, FirstBinJid), |
423 |
880 |
BLast = mod_inbox_utils:encode_rsm_id(LastTS, LastBinJid), |
424 |
880 |
mod_mam_utils:result_set(BFirst, BLast, undefined, undefined). |
425 |
|
|
426 |
|
%%%%%%%%%%%%%%%%%%% |
427 |
|
%% iq-get |
428 |
|
-spec build_inbox_form(mongooseim:host_type()) -> exml:element(). |
429 |
|
build_inbox_form(HostType) -> |
430 |
2 |
AllBoxes = mod_inbox_utils:all_valid_boxes_for_query(HostType), |
431 |
2 |
OrderOptions = [{<<"Ascending by timestamp">>, <<"asc">>}, |
432 |
|
{<<"Descending by timestamp">>, <<"desc">>}], |
433 |
2 |
Fields = [#{var => <<"start">>, type => <<"text-single">>}, |
434 |
|
#{var => <<"end">>, type => <<"text-single">>}, |
435 |
|
#{var => <<"hidden_read">>, type => <<"text-single">>, values => [<<"false">>]}, |
436 |
|
#{var => <<"order">>, type => <<"list-single">>, values => [<<"desc">>], |
437 |
|
options => OrderOptions}, |
438 |
|
#{var => <<"box">>, type => <<"list-single">>, values => [<<"all">>], |
439 |
|
options => AllBoxes}, |
440 |
|
#{var => <<"archive">>, type => <<"boolean">>, values => [<<"false">>]}], |
441 |
2 |
mongoose_data_forms:form(#{ns => ?NS_ESL_INBOX, fields => Fields}). |
442 |
|
|
443 |
|
%%%%%%%%%%%%%%%%%%% |
444 |
|
%% iq-set |
445 |
|
-spec query_to_params(mongooseim:host_type(), QueryEl :: exml:element()) -> |
446 |
|
get_inbox_params() | {error, atom(), binary()}. |
447 |
|
query_to_params(HostType, QueryEl) -> |
448 |
1180 |
Form = form_to_params(HostType, mongoose_data_forms:find_form(QueryEl)), |
449 |
1180 |
Rsm = create_rsm(HostType, QueryEl), |
450 |
1180 |
build_params(Form, Rsm). |
451 |
|
|
452 |
|
-spec create_rsm(mongooseim:host_type(), exml:element()) -> none | jlib:rsm_in(). |
453 |
|
create_rsm(HostType, QueryEl) -> |
454 |
1180 |
case {jlib:rsm_decode(QueryEl), get_max_result_limit(HostType)} of |
455 |
|
{Rsm, infinity} -> |
456 |
1176 |
Rsm; |
457 |
|
{none, MaxResultLimit} -> |
458 |
2 |
#rsm_in{max = MaxResultLimit}; |
459 |
|
{Rsm = #rsm_in{max = Max}, MaxResultLimit} when is_integer(Max) -> |
460 |
2 |
Rsm#rsm_in{max = min(Max, MaxResultLimit)}; |
461 |
|
{Rsm, MaxResultLimit} -> |
462 |
:-( |
Rsm#rsm_in{max = MaxResultLimit} |
463 |
|
end. |
464 |
|
|
465 |
|
-spec build_params(get_inbox_params() | {error, atom(), binary()}, none | jlib:rsm_in()) -> |
466 |
|
get_inbox_params() | {error, atom(), binary()}. |
467 |
|
build_params({error, Error, Msg}, _) -> |
468 |
14 |
{error, Error, Msg}; |
469 |
|
build_params(_, #rsm_in{max = Max, index = Index}) when Max =:= error; Index =:= error -> |
470 |
2 |
{error, bad_request, <<"bad-request">>}; |
471 |
|
build_params(_, #rsm_in{index = Index}) when Index =/= undefined -> |
472 |
2 |
{error, feature_not_implemented, <<"Inbox does not expose a total count and indexes">>}; |
473 |
|
build_params(Params, none) -> |
474 |
1133 |
Params; |
475 |
|
build_params(Params, #rsm_in{max = Max, id = undefined}) when Max =/= undefined -> |
476 |
15 |
Params#{limit => Max}; |
477 |
|
build_params(Params, Rsm) -> |
478 |
14 |
build_params_with_rsm(Params#{rsm => Rsm}, Rsm). |
479 |
|
|
480 |
|
build_params_with_rsm(Params, #rsm_in{max = Max, id = <<>>, direction = before}) -> |
481 |
4 |
Params#{limit => Max, order => asc, start => 0}; |
482 |
|
build_params_with_rsm(Params, #rsm_in{max = Max, id = <<>>, direction = aft}) -> |
483 |
2 |
maps:remove('end', Params#{limit => Max}); |
484 |
|
build_params_with_rsm(Params, #rsm_in{max = Max, id = Id, direction = Dir}) when is_binary(Id) -> |
485 |
8 |
case {mod_inbox_utils:decode_rsm_id(Id), Dir} of |
486 |
|
{error, _} -> |
487 |
4 |
{error, bad_request, <<"bad-request">>}; |
488 |
|
{{Stamp, Jid}, aft} -> |
489 |
2 |
Params#{limit => expand_limit(Max), filter_on_jid => Jid, 'end' => Stamp}; |
490 |
|
{{Stamp, Jid}, undefined} -> |
491 |
:-( |
Params#{limit => expand_limit(Max), filter_on_jid => Jid, 'end' => Stamp}; |
492 |
|
{{Stamp, Jid}, before} -> |
493 |
2 |
Params#{limit => expand_limit(Max), order => asc, filter_on_jid => Jid, start => Stamp} |
494 |
|
end; |
495 |
|
build_params_with_rsm(Params, _Rsm) -> |
496 |
:-( |
Params. |
497 |
|
|
498 |
|
-spec expand_limit(undefined) -> undefined; |
499 |
|
(integer()) -> integer(). |
500 |
|
expand_limit(undefined) -> |
501 |
2 |
undefined; |
502 |
|
expand_limit(Max) -> |
503 |
2 |
Max + 1. |
504 |
|
|
505 |
|
-spec form_to_params(mongooseim:host_type(), FormEl :: exml:element() | undefined) -> |
506 |
|
get_inbox_params() | {error, bad_request, Msg :: binary()}. |
507 |
|
form_to_params(_, undefined) -> |
508 |
:-( |
#{ order => desc }; |
509 |
|
form_to_params(HostType, FormEl) -> |
510 |
1180 |
#{kvs := ParsedFields} = mongoose_data_forms:parse_form_fields(FormEl), |
511 |
1180 |
?LOG_DEBUG(#{what => inbox_parsed_form_fields, parsed_fields => ParsedFields}), |
512 |
1180 |
fields_to_params(HostType, maps:to_list(ParsedFields), #{ order => desc }). |
513 |
|
|
514 |
|
-spec fields_to_params(mongooseim:host_type(), |
515 |
|
[{Var :: binary(), Values :: [binary()]}], Acc :: get_inbox_params()) -> |
516 |
|
get_inbox_params() | {error, bad_request, Msg :: binary()}. |
517 |
|
fields_to_params(_, [], Acc) -> |
518 |
1166 |
Acc; |
519 |
|
fields_to_params(HostType, [{<<"start">>, [StartISO]} | RFields], Acc) -> |
520 |
11 |
try calendar:rfc3339_to_system_time(binary_to_list(StartISO), [{unit, microsecond}]) of |
521 |
|
StartStamp -> |
522 |
9 |
fields_to_params(HostType, RFields, Acc#{ start => StartStamp }) |
523 |
|
catch error:Error -> |
524 |
2 |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
525 |
:-( |
reason => Error, field => start, value => StartISO}), |
526 |
2 |
{error, bad_request, invalid_field_value(<<"start">>, StartISO)} |
527 |
|
end; |
528 |
|
fields_to_params(HostType, [{<<"end">>, [EndISO]} | RFields], Acc) -> |
529 |
17 |
try calendar:rfc3339_to_system_time(binary_to_list(EndISO), [{unit, microsecond}]) of |
530 |
|
EndStamp -> |
531 |
13 |
fields_to_params(HostType, RFields, Acc#{ 'end' => EndStamp }) |
532 |
|
catch error:Error -> |
533 |
4 |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
534 |
:-( |
reason => Error, field => 'end', value => EndISO}), |
535 |
4 |
{error, bad_request, invalid_field_value(<<"end">>, EndISO)} |
536 |
|
end; |
537 |
|
fields_to_params(HostType, [{<<"order">>, [OrderBin]} | RFields], Acc) -> |
538 |
4 |
case binary_to_order(OrderBin) of |
539 |
|
error -> |
540 |
2 |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
541 |
:-( |
field => order, value => OrderBin}), |
542 |
2 |
{error, bad_request, invalid_field_value(<<"order">>, OrderBin)}; |
543 |
|
Order -> |
544 |
2 |
fields_to_params(HostType, RFields, Acc#{ order => Order }) |
545 |
|
end; |
546 |
|
|
547 |
|
fields_to_params(HostType, [{<<"hidden_read">>, [HiddenRead]} | RFields], Acc) -> |
548 |
1168 |
case mod_inbox_utils:binary_to_bool(HiddenRead) of |
549 |
|
error -> |
550 |
2 |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
551 |
:-( |
field => hidden_read, value => HiddenRead}), |
552 |
2 |
{error, bad_request, invalid_field_value(<<"hidden_read">>, HiddenRead)}; |
553 |
|
Hidden -> |
554 |
1166 |
fields_to_params(HostType, RFields, Acc#{ hidden_read => Hidden }) |
555 |
|
end; |
556 |
|
|
557 |
|
fields_to_params(HostType, [{<<"archive">>, [Value]} | RFields], Acc) -> |
558 |
44 |
case mod_inbox_utils:binary_to_bool(Value) of |
559 |
|
error -> |
560 |
2 |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
561 |
:-( |
field => archive, value => Value}), |
562 |
2 |
{error, bad_request, invalid_field_value(<<"archive">>, Value)}; |
563 |
|
true -> |
564 |
23 |
fields_to_params(HostType, RFields, Acc#{ box => maps:get(box, Acc, <<"archive">>) }); |
565 |
|
false -> |
566 |
19 |
fields_to_params(HostType, RFields, Acc#{ box => maps:get(box, Acc, <<"inbox">>) }) |
567 |
|
end; |
568 |
|
|
569 |
|
fields_to_params(HostType, [{<<"box">>, [Value]} | RFields], Acc) -> |
570 |
192 |
case validate_box(HostType, Value) of |
571 |
|
false -> |
572 |
:-( |
?LOG_WARNING(#{what => inbox_invalid_form_field, |
573 |
:-( |
field => box, value => Value}), |
574 |
:-( |
{error, bad_request, invalid_field_value(<<"box">>, Value)}; |
575 |
|
true -> |
576 |
192 |
fields_to_params(HostType, RFields, Acc#{ box => Value }) |
577 |
|
end; |
578 |
|
|
579 |
|
fields_to_params(_, [{Invalid, [InvalidFieldVal]} | _], _) -> |
580 |
2 |
?LOG_WARNING(#{what => inbox_invalid_form_field, reason => unknown_field, |
581 |
:-( |
field => Invalid, value => InvalidFieldVal}), |
582 |
2 |
{error, bad_request, <<"Unknown inbox form field=", Invalid/binary, ", value=", InvalidFieldVal/binary>>}. |
583 |
|
|
584 |
|
-spec binary_to_order(binary()) -> asc | desc | error. |
585 |
:-( |
binary_to_order(<<"desc">>) -> desc; |
586 |
2 |
binary_to_order(<<"asc">>) -> asc; |
587 |
2 |
binary_to_order(_) -> error. |
588 |
|
|
589 |
|
validate_box(HostType, Box) -> |
590 |
192 |
AllBoxes = mod_inbox_utils:all_valid_boxes_for_query(HostType), |
591 |
192 |
lists:member(Box, AllBoxes). |
592 |
|
|
593 |
|
invalid_field_value(Field, Value) -> |
594 |
12 |
<<"Invalid inbox form field value, field=", Field/binary, ", value=", Value/binary>>. |
595 |
|
|
596 |
|
%%%%%%%%%%%%%%%%%%% |
597 |
|
%% Helpers |
598 |
|
get_inbox_unread(Value, Acc, _) when is_integer(Value) -> |
599 |
:-( |
Acc; |
600 |
|
get_inbox_unread(undefined, Acc, To) -> |
601 |
|
%% TODO this value should be bound to a stanza reference inside Acc |
602 |
:-( |
InterlocutorJID = mongoose_acc:from_jid(Acc), |
603 |
:-( |
InboxEntryKey = mod_inbox_utils:build_inbox_entry_key(To, InterlocutorJID), |
604 |
:-( |
HostType = mongoose_acc:host_type(Acc), |
605 |
:-( |
{ok, Count} = mod_inbox_backend:get_inbox_unread(HostType, InboxEntryKey), |
606 |
:-( |
mongoose_acc:set(inbox, unread_count, Count, Acc). |
607 |
|
|
608 |
|
hooks(HostType) -> |
609 |
96 |
[ |
610 |
|
{disco_local_features, HostType, fun ?MODULE:disco_local_features/3, #{}, 99}, |
611 |
|
{remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50}, |
612 |
|
{remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50}, |
613 |
|
{user_send_message, HostType, fun ?MODULE:user_send_message/3, #{}, 70}, |
614 |
|
{filter_local_packet, HostType, fun ?MODULE:filter_local_packet/3, #{}, 90}, |
615 |
|
{inbox_unread_count, HostType, fun ?MODULE:inbox_unread_count/3, #{}, 80}, |
616 |
|
{get_personal_data, HostType, fun ?MODULE:get_personal_data/3, #{}, 50} |
617 |
|
]. |
618 |
|
|
619 |
|
get_groupchat_types(HostType) -> |
620 |
1204 |
gen_mod:get_module_opt(HostType, ?MODULE, groupchat). |
621 |
|
|
622 |
|
get_max_result_limit(HostType) -> |
623 |
1180 |
gen_mod:get_module_opt(HostType, ?MODULE, max_result_limit, infinity). |
624 |
|
|
625 |
|
-spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}]. |
626 |
|
config_metrics(HostType) -> |
627 |
12 |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). |
628 |
|
|
629 |
|
-spec muclight_enabled(HostType :: mongooseim:host_type()) -> boolean(). |
630 |
|
muclight_enabled(HostType) -> |
631 |
1204 |
Groupchats = get_groupchat_types(HostType), |
632 |
1204 |
lists:member(muclight, Groupchats). |
633 |
|
|
634 |
|
%%%%%%%%%%%%%%%%%%% |
635 |
|
%% Message Predicates |
636 |
|
-spec should_be_stored_in_inbox( |
637 |
|
mongoose_acc:t(), jid:jid(), jid:jid(), exml:element(), mod_mam_utils:direction(), message_type()) -> |
638 |
|
boolean(). |
639 |
|
should_be_stored_in_inbox(Acc, From, To, Msg, Dir, Type) -> |
640 |
2109 |
mod_mam_utils:is_archivable_message(?MODULE, Dir, Msg, true) |
641 |
2002 |
andalso mod_inbox_entries:should_be_stored_in_inbox(Msg) |
642 |
2002 |
andalso inbox_owner_exists(Acc, From, To, Dir, Type). |
643 |
|
|
644 |
|
-spec inbox_owner_exists(mongoose_acc:t(), |
645 |
|
From :: jid:jid(), |
646 |
|
To ::jid:jid(), |
647 |
|
mod_mam_utils:direction(), |
648 |
|
message_type()) -> boolean(). |
649 |
|
inbox_owner_exists(Acc, _, To, incoming, MessageType) -> % filter_local_packet |
650 |
1521 |
HostType = mongoose_acc:host_type(Acc), |
651 |
1521 |
mongoose_lib:does_local_user_exist(HostType, To, MessageType); |
652 |
|
inbox_owner_exists(Acc, From, _, outgoing, _) -> % user_send_message |
653 |
481 |
HostType = mongoose_acc:host_type(Acc), |
654 |
481 |
ejabberd_auth:does_user_exist(HostType, From, stored). |