./ct_report/coverage/mod_inbox.COVER.html

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 47 Schema = ["jid", "content", "unread_count", "timestamp"],
69 47 InboxParams = #{
70 start => 0,
71 'end' => erlang:system_time(microsecond),
72 order => asc,
73 hidden_read => false
74 },
75 47 Entries = mod_inbox_backend:get_inbox(HostType, LUser, LServer, InboxParams),
76 47 ProcessedEntries = lists:map(fun process_entry/1, Entries),
77 47 NewAcc = [{inbox, Schema, ProcessedEntries} | Acc],
78 47 {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 56 mod_inbox_backend:init(HostType, Opts),
94 56 lists:member(muc, MucTypes) andalso mod_inbox_muc:start(HostType),
95 56 gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm,
96 fun ?MODULE:process_iq/5, #{}, IQDisc),
97 56 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 56 start_cleaner(HostType, Opts),
100 56 ok.
101
102 -spec stop(HostType :: mongooseim:host_type()) -> ok.
103 stop(HostType) ->
104 56 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX, ejabberd_sm),
105 56 gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_ESL_INBOX_CONVERSATION, ejabberd_sm),
106 56 stop_cleaner(HostType),
107 56 mod_inbox_muc:stop(HostType),
108 56 case mongoose_config:get_opt([{modules, HostType}, ?MODULE, backend]) of
109 24 rdbms_async -> mod_inbox_rdbms_async:stop(HostType);
110 32 _ -> ok
111 end.
112
113 -spec supported_features() -> [atom()].
114 supported_features() ->
115
:-(
[dynamic_domains].
116
117 -spec config_spec() -> mongoose_config_spec:config_section().
118 config_spec() ->
119 106 Markers = mongoose_chat_markers:chat_marker_names(),
120 106 #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 106 #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 56 WOpts = #{host_type => HostType, action => fun mod_inbox_api:flush_global_bin/2,
174 opts => TTL, interval => Interval},
175 56 mongoose_collector:start_common(?MODULE, HostType, WOpts).
176
177 stop_cleaner(HostType) ->
178 56 Name = gen_mod:get_module_proc(HostType, ?MODULE),
179 56 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 1125 HostType = mongoose_acc:host_type(Acc),
200 1125 LUser = From#jid.luser,
201 1125 LServer = From#jid.lserver,
202 1125 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 1103 List0 = mod_inbox_backend:get_inbox(HostType, LUser, LServer, Params),
207 1103 List1 = with_rsm(List0, Params),
208 1103 List2 = mongoose_hooks:extend_inbox_result(Acc, List1, IQ),
209 1103 forward_messages(Acc, List2, IQ, From),
210 1103 Res = IQ#iq{type = result, sub_el = [build_result_iq(List2)]},
211 1103 {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 1095 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 1103 Msgs = [ build_inbox_message(Acc, El, QueryEl) || El <- List],
240 1103 [ 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 976 BareTo = jid:to_bare(To),
245 976 HostType = mongoose_acc:host_type(Acc),
246 976 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 976 PermanentFields = mongoose_acc:get_permanent_fields(Acc),
253 976 NewAcc = mongoose_acc:set_permanent(PermanentFields, NewAcc0),
254 976 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 539 {From, To, Msg} = mongoose_acc:packet(Acc),
262 539 Acc1 = maybe_process_message(Acc, From, To, Msg, outgoing),
263 539 {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 120 Res = mongoose_acc:get(inbox, unread_count, undefined, Acc),
271 120 NewAcc = get_inbox_unread(Res, Acc, User),
272 120 {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 1777 Acc0 = maybe_process_message(Acc, From, To, Msg, incoming),
280 1777 {ok, {From, To, Acc0, Msg}};
281 filter_local_packet(FPacketAcc, _, _) ->
282 6413 {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 638 HostType = mongoose_acc:host_type(Acc),
290 638 mod_inbox_utils:clear_inbox(HostType, User, Server),
291 638 {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 48 {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 2316 Type = mongoose_lib:get_message_type(Acc),
320 2316 case should_be_stored_in_inbox(Acc, From, To, Msg, Dir, Type) of
321 true ->
322 1916 do_maybe_process_message(Acc, From, To, Msg, Dir, Type);
323 false ->
324 400 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 1916 HostType = mongoose_acc:host_type(Acc),
333 1916 case maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, Type) of
334 1916 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 614 process_message(HostType, From, To, Msg, Acc, Dir, one2one);
351 maybe_process_acceptable_message(HostType, From, To, Msg, Acc, Dir, groupchat) ->
352 1302 case muclight_enabled(HostType) of
353 1188 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 308 mod_inbox_one2one:handle_outgoing_message(HostType, From, To, Message, Acc);
366 process_message(HostType, From, To, Message, Acc, incoming, one2one) ->
367 306 mod_inbox_one2one:handle_incoming_message(HostType, From, To, Message, Acc);
368 process_message(HostType, From, To, Message, Acc, outgoing, groupchat) ->
369 211 mod_inbox_muclight:handle_outgoing_message(HostType, From, To, Message, Acc);
370 process_message(HostType, From, To, Message, Acc, incoming, groupchat) ->
371 977 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 976 #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 976 AccTS = mongoose_acc:timestamp(Acc),
397 976 Children = mod_inbox_utils:build_inbox_result_elements(InboxRes, AccTS),
398 976 #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 1103 AllUnread = [ N || #{unread_count := N} <- List, N =/= 0],
407 1103 Result = #{<<"count">> => length(List),
408 <<"unread-messages">> => lists:sum(AllUnread),
409 <<"active-conversations">> => length(AllUnread)},
410 1103 ResultBinary = maps:map(fun(K, V) ->
411 3309 #xmlel{name = K, children = [#xmlcdata{content = integer_to_binary(V)}]}
412 end, Result),
413 1103 ResultSetEl = result_set(List),
414 1103 #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 210 #xmlel{name = <<"set">>, attrs = [{<<"xmlns">>, ?NS_RSM}]};
420 result_set([#{remote_jid := FirstBinJid, timestamp := FirstTS} | _] = List) ->
421 893 #{remote_jid := LastBinJid, timestamp := LastTS} = lists:last(List),
422 893 BFirst = mod_inbox_utils:encode_rsm_id(FirstTS, FirstBinJid),
423 893 BLast = mod_inbox_utils:encode_rsm_id(LastTS, LastBinJid),
424 893 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 1125 Form = form_to_params(HostType, mongoose_data_forms:find_form(QueryEl)),
449 1125 Rsm = create_rsm(HostType, QueryEl),
450 1125 build_params(Form, Rsm).
451
452 -spec create_rsm(mongooseim:host_type(), exml:element()) -> none | jlib:rsm_in().
453 create_rsm(HostType, QueryEl) ->
454 1125 case {jlib:rsm_decode(QueryEl), get_max_result_limit(HostType)} of
455 {Rsm, infinity} ->
456 1121 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 1080 Params;
475 build_params(Params, #rsm_in{max = Max, id = undefined}) when Max =/= undefined ->
476 13 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 1125 #{kvs := ParsedFields} = mongoose_data_forms:parse_form_fields(FormEl),
511 1125 ?LOG_DEBUG(#{what => inbox_parsed_form_fields, parsed_fields => ParsedFields}),
512 1125 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 1111 Acc;
519 fields_to_params(HostType, [{<<"start">>, [StartISO]} | RFields], Acc) ->
520 12 try calendar:rfc3339_to_system_time(binary_to_list(StartISO), [{unit, microsecond}]) of
521 StartStamp ->
522 10 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 16 try calendar:rfc3339_to_system_time(binary_to_list(EndISO), [{unit, microsecond}]) of
530 EndStamp ->
531 12 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 1113 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 1111 fields_to_params(HostType, RFields, Acc#{ hidden_read => Hidden })
555 end;
556
557 fields_to_params(HostType, [{<<"archive">>, [Value]} | RFields], Acc) ->
558 40 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 21 fields_to_params(HostType, RFields, Acc#{ box => maps:get(box, Acc, <<"archive">>) });
565 false ->
566 17 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 120 InterlocutorJID = mongoose_acc:from_jid(Acc),
603 120 InboxEntryKey = mod_inbox_utils:build_inbox_entry_key(To, InterlocutorJID),
604 120 HostType = mongoose_acc:host_type(Acc),
605 120 {ok, Count} = mod_inbox_backend:get_inbox_unread(HostType, InboxEntryKey),
606 120 mongoose_acc:set(inbox, unread_count, Count, Acc).
607
608 hooks(HostType) ->
609 112 [
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 1302 gen_mod:get_module_opt(HostType, ?MODULE, groupchat).
621
622 get_max_result_limit(HostType) ->
623 1125 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 1302 Groupchats = get_groupchat_types(HostType),
632 1302 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 2316 mod_mam_utils:is_archivable_message(?MODULE, Dir, Msg, true)
641 2194 andalso mod_inbox_entries:should_be_stored_in_inbox(Msg)
642 2194 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 1657 HostType = mongoose_acc:host_type(Acc),
651 1657 mongoose_lib:does_local_user_exist(HostType, To, MessageType);
652 inbox_owner_exists(Acc, From, _, outgoing, _) -> % user_send_message
653 537 HostType = mongoose_acc:host_type(Acc),
654 537 ejabberd_auth:does_user_exist(HostType, From, stored).
Line Hits Source