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