1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : mod_offline.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : Store and manage offline messages |
5 |
|
%%% See : XEP-0160: Best Practices for Handling Offline Messages |
6 |
|
%%% Created : 5 Jan 2003 by Alexey Shchepin <alexey@process-one.net> |
7 |
|
%%% |
8 |
|
%%% |
9 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
10 |
|
%%% |
11 |
|
%%% This program is free software; you can redistribute it and/or |
12 |
|
%%% modify it under the terms of the GNU General Public License as |
13 |
|
%%% published by the Free Software Foundation; either version 2 of the |
14 |
|
%%% License, or (at your option) any later version. |
15 |
|
%%% |
16 |
|
%%% This program is distributed in the hope that it will be useful, |
17 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
18 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
19 |
|
%%% General Public License for more details. |
20 |
|
%%% |
21 |
|
%%% You should have received a copy of the GNU General Public License |
22 |
|
%%% along with this program; if not, write to the Free Software |
23 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
24 |
|
%%% |
25 |
|
%%%---------------------------------------------------------------------- |
26 |
|
|
27 |
|
-module(mod_offline). |
28 |
|
-author('alexey@process-one.net'). |
29 |
|
-xep([{xep, 160}, {version, "1.0.1"}]). |
30 |
|
-xep([{xep, 23}, {version, "1.3"}]). |
31 |
|
-xep([{xep, 22}, {version, "1.4"}]). |
32 |
|
-xep([{xep, 85}, {version, "2.1"}]). |
33 |
|
-behaviour(gen_mod). |
34 |
|
-behaviour(mongoose_module_metrics). |
35 |
|
|
36 |
|
%% gen_mod handlers |
37 |
|
-export([start/2, stop/1, hooks/1, config_spec/0, supported_features/0]). |
38 |
|
|
39 |
|
%% Hook handlers |
40 |
|
-export([inspect_packet/3, |
41 |
|
pop_offline_messages/3, |
42 |
|
remove_user/3, |
43 |
|
remove_domain/3, |
44 |
|
disco_features/3, |
45 |
|
determine_amp_strategy/3, |
46 |
|
amp_failed_event/3, |
47 |
|
get_personal_data/3]). |
48 |
|
|
49 |
|
%% Admin API |
50 |
|
-export([remove_expired_messages/2, |
51 |
|
remove_old_messages/3]). |
52 |
|
|
53 |
|
%% Internal exports |
54 |
|
-export([start_link/3]). |
55 |
|
|
56 |
|
%% gen_server callbacks |
57 |
|
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, |
58 |
|
terminate/2, code_change/3]). |
59 |
|
|
60 |
|
%% helpers to be used from backend moudules |
61 |
|
-export([is_expired_message/2]). |
62 |
|
|
63 |
|
-export([config_metrics/1]). |
64 |
|
|
65 |
|
-ignore_xref([ |
66 |
|
behaviour_info/1, code_change/3, handle_call/3, handle_cast/2, |
67 |
|
handle_info/2, init/1, start_link/3, terminate/2 |
68 |
|
]). |
69 |
|
|
70 |
|
-include("mongoose.hrl"). |
71 |
|
-include("jlib.hrl"). |
72 |
|
-include("amp.hrl"). |
73 |
|
-include("mod_offline.hrl"). |
74 |
|
-include("mongoose_config_spec.hrl"). |
75 |
|
|
76 |
|
%% default value for the maximum number of user messages |
77 |
|
-define(MAX_USER_MESSAGES, infinity). |
78 |
|
|
79 |
|
-type msg() :: #offline_msg{us :: {jid:luser(), jid:lserver()}, |
80 |
|
timestamp :: integer(), |
81 |
|
expire :: integer() | never, |
82 |
|
from :: jid:jid(), |
83 |
|
to :: jid:jid(), |
84 |
|
packet :: exml:element()}. |
85 |
|
|
86 |
|
-export_type([msg/0]). |
87 |
|
|
88 |
|
-type poppers() :: monitored_map:t({jid:luser(), jid:lserver()}, pid()). |
89 |
|
|
90 |
|
-record(state, {host_type :: mongooseim:host_type(), |
91 |
|
access_max_user_messages :: atom(), |
92 |
|
message_poppers = monitored_map:new() :: poppers() |
93 |
|
}). |
94 |
|
|
95 |
|
-type state() :: #state{}. |
96 |
|
|
97 |
|
%% Types used in backend callbacks |
98 |
|
-type msg_count() :: non_neg_integer(). |
99 |
|
-type timestamp() :: integer(). |
100 |
|
|
101 |
|
-export_type([msg_count/0, timestamp/0]). |
102 |
|
|
103 |
|
%% gen_mod callbacks |
104 |
|
%% ------------------------------------------------------------------ |
105 |
|
|
106 |
|
-spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
107 |
|
start(HostType, #{access_max_user_messages := AccessMaxOfflineMsgs} = Opts) -> |
108 |
24 |
mod_offline_backend:init(HostType, Opts), |
109 |
24 |
start_worker(HostType, AccessMaxOfflineMsgs), |
110 |
24 |
ok. |
111 |
|
|
112 |
|
-spec stop(mongooseim:host_type()) -> ok. |
113 |
|
stop(Host) -> |
114 |
24 |
stop_worker(Host), |
115 |
24 |
ok. |
116 |
|
|
117 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
118 |
|
config_spec() -> |
119 |
208 |
#section{ |
120 |
|
items = #{<<"access_max_user_messages">> => #option{type = atom, |
121 |
|
validate = access_rule}, |
122 |
|
<<"backend">> => #option{type = atom, |
123 |
|
validate = {module, mod_offline}}, |
124 |
|
<<"store_groupchat_messages">> => #option{type = boolean} |
125 |
|
}, |
126 |
|
defaults = #{<<"access_max_user_messages">> => max_user_offline_messages, |
127 |
|
<<"store_groupchat_messages">> => false, |
128 |
|
<<"backend">> => mnesia |
129 |
|
} |
130 |
|
}. |
131 |
|
|
132 |
:-( |
supported_features() -> [dynamic_domains]. |
133 |
|
|
134 |
|
-spec hooks(mongooseim:host_type()) -> gen_hook:hook_list(). |
135 |
|
hooks(HostType) -> |
136 |
48 |
DefaultHooks = [ |
137 |
|
{offline_message_hook, HostType, fun ?MODULE:inspect_packet/3, #{}, 50}, |
138 |
|
{resend_offline_messages_hook, HostType, fun ?MODULE:pop_offline_messages/3, #{}, 50}, |
139 |
|
{remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50}, |
140 |
|
{remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50}, |
141 |
|
{anonymous_purge_hook, HostType, fun ?MODULE:remove_user/3, #{}, 50}, |
142 |
|
{disco_sm_features, HostType, fun ?MODULE:disco_features/3, #{}, 50}, |
143 |
|
{disco_local_features, HostType, fun ?MODULE:disco_features/3, #{}, 50}, |
144 |
|
{amp_determine_strategy, HostType, fun ?MODULE:determine_amp_strategy/3, #{}, 30}, |
145 |
|
{failed_to_store_message, HostType, fun ?MODULE:amp_failed_event/3, #{}, 30}, |
146 |
|
{get_personal_data, HostType, fun ?MODULE:get_personal_data/3, #{}, 50} |
147 |
|
], |
148 |
48 |
case gen_mod:get_module_opt(HostType, ?MODULE, store_groupchat_messages) of |
149 |
|
true -> |
150 |
4 |
GroupChatHook = {offline_groupchat_message_hook, |
151 |
|
HostType, fun ?MODULE:inspect_packet/3, #{}, 50}, |
152 |
4 |
[GroupChatHook | DefaultHooks]; |
153 |
44 |
_ -> DefaultHooks |
154 |
|
end. |
155 |
|
|
156 |
|
%% Server side functions |
157 |
|
%% ------------------------------------------------------------------ |
158 |
|
|
159 |
|
-spec amp_failed_event(Acc, Params, Extra) -> {ok, Acc} when |
160 |
|
Acc :: mongoose_acc:t(), |
161 |
|
Params :: map(), |
162 |
|
Extra :: gen_hook:extra(). |
163 |
|
amp_failed_event(Acc, _Params, _Extra) -> |
164 |
8 |
{ok, mod_amp:check_packet(Acc, offline_failed)}. |
165 |
|
|
166 |
|
handle_offline_msg(HostType, Acc, #offline_msg{us=US} = Msg, AccessMaxOfflineMsgs) -> |
167 |
128 |
{LUser, LServer} = US, |
168 |
128 |
Msgs = receive_all(US, [{Acc, Msg}]), |
169 |
128 |
MaxOfflineMsgs = get_max_user_messages(HostType, AccessMaxOfflineMsgs, LUser, LServer), |
170 |
128 |
Len = length(Msgs), |
171 |
128 |
case is_message_count_threshold_reached(HostType, MaxOfflineMsgs, LUser, LServer, Len) of |
172 |
|
false -> |
173 |
127 |
write_messages(HostType, LUser, LServer, Msgs); |
174 |
|
true -> |
175 |
1 |
discard_warn_sender(Msgs) |
176 |
|
end. |
177 |
|
|
178 |
|
write_messages(HostType, LUser, LServer, Msgs) -> |
179 |
127 |
MsgsWithoutAcc = [Msg || {_Acc, Msg} <- Msgs], |
180 |
127 |
case mod_offline_backend:write_messages(HostType, LUser, LServer, MsgsWithoutAcc) of |
181 |
|
ok -> |
182 |
119 |
[mod_amp:check_packet(Acc, archived) || {Acc, _Msg} <- Msgs], |
183 |
119 |
ok; |
184 |
|
{error, Reason} -> |
185 |
8 |
?LOG_ERROR(#{what => offline_write_failed, |
186 |
|
text => <<"Failed to write offline messages">>, |
187 |
|
reason => Reason, |
188 |
:-( |
user => LUser, server => LServer, msgs => Msgs}), |
189 |
8 |
discard_warn_sender(Msgs) |
190 |
|
end. |
191 |
|
|
192 |
|
-spec is_message_count_threshold_reached(mongooseim:host_type(), integer() | infinity, |
193 |
|
jid:luser(), jid:lserver(), integer()) -> |
194 |
|
boolean(). |
195 |
|
is_message_count_threshold_reached(_HostType, infinity, _LUser, _LServer, _Len) -> |
196 |
:-( |
false; |
197 |
|
is_message_count_threshold_reached(_HostType, MaxOfflineMsgs, _LUser, _LServer, Len) |
198 |
|
when Len > MaxOfflineMsgs -> |
199 |
:-( |
true; |
200 |
|
is_message_count_threshold_reached(HostType, MaxOfflineMsgs, LUser, LServer, Len) -> |
201 |
|
%% Only count messages if needed. |
202 |
128 |
MaxArchivedMsg = MaxOfflineMsgs - Len, |
203 |
|
%% Maybe do not need to count all messages in archive |
204 |
128 |
MaxArchivedMsg < mod_offline_backend:count_offline_messages(HostType, LUser, LServer, |
205 |
|
MaxArchivedMsg + 1). |
206 |
|
|
207 |
|
|
208 |
|
get_max_user_messages(HostType, AccessRule, LUser, LServer) -> |
209 |
128 |
case acl:match_rule(HostType, LServer, AccessRule, jid:make_noprep(LUser, LServer, <<>>)) of |
210 |
128 |
Max when is_integer(Max) -> Max; |
211 |
:-( |
infinity -> infinity; |
212 |
:-( |
_ -> ?MAX_USER_MESSAGES |
213 |
|
end. |
214 |
|
|
215 |
|
receive_all(US, Msgs) -> |
216 |
214 |
receive |
217 |
|
{_Acc, #offline_msg{us=US}} = Msg -> |
218 |
86 |
receive_all(US, [Msg | Msgs]) |
219 |
|
after 0 -> |
220 |
128 |
Msgs |
221 |
|
end. |
222 |
|
|
223 |
|
%% Supervision |
224 |
|
%% ------------------------------------------------------------------ |
225 |
|
|
226 |
|
start_worker(HostType, AccessMaxOfflineMsgs) -> |
227 |
24 |
Proc = srv_name(HostType), |
228 |
24 |
ChildSpec = |
229 |
|
{Proc, |
230 |
|
{?MODULE, start_link, [Proc, HostType, AccessMaxOfflineMsgs]}, |
231 |
|
permanent, 5000, worker, [?MODULE]}, |
232 |
24 |
ejabberd_sup:start_child(ChildSpec). |
233 |
|
|
234 |
|
stop_worker(HostType) -> |
235 |
24 |
Proc = srv_name(HostType), |
236 |
24 |
ejabberd_sup:stop_child(Proc). |
237 |
|
|
238 |
|
start_link(Name, HostType, AccessMaxOfflineMsgs) -> |
239 |
24 |
gen_server:start_link({local, Name}, ?MODULE, [HostType, AccessMaxOfflineMsgs], []). |
240 |
|
|
241 |
|
srv_name() -> |
242 |
983 |
mod_offline. |
243 |
|
|
244 |
|
srv_name(HostType) -> |
245 |
983 |
gen_mod:get_module_proc(HostType, srv_name()). |
246 |
|
|
247 |
|
%%==================================================================== |
248 |
|
%% gen_server callbacks |
249 |
|
%%==================================================================== |
250 |
|
|
251 |
|
-spec init(list()) -> {ok, state()}. |
252 |
|
init([HostType, AccessMaxOfflineMsgs]) -> |
253 |
24 |
{ok, #state{host_type = HostType, |
254 |
|
access_max_user_messages = AccessMaxOfflineMsgs}}. |
255 |
|
|
256 |
|
-spec handle_call(Request :: any(), {pid(), any()}, state()) -> {reply, Result, state()} |
257 |
|
when Result :: ok | {ok, [msg()]} | {error, any()}. |
258 |
|
handle_call({pop_offline_messages, JID}, {Pid, _}, State = #state{host_type = HostType}) -> |
259 |
721 |
Result = mod_offline_backend:pop_messages(HostType, JID), |
260 |
721 |
NewPoppers = monitored_map:put(jid:to_lus(JID), Pid, Pid, State#state.message_poppers), |
261 |
721 |
{reply, Result, State#state{message_poppers = NewPoppers}}; |
262 |
|
handle_call(Request, From, State) -> |
263 |
:-( |
?UNEXPECTED_CALL(Request, From), |
264 |
:-( |
{reply, ok, State}. |
265 |
|
|
266 |
|
-spec handle_cast(any(), state()) -> {noreply, state()}. |
267 |
|
handle_cast(Msg, State) -> |
268 |
:-( |
?UNEXPECTED_CAST(Msg), |
269 |
:-( |
{noreply, State}. |
270 |
|
|
271 |
|
-spec handle_info(any(), state()) -> {noreply, state()}. |
272 |
|
handle_info({'DOWN', _MonitorRef, _Type, _Object, _Info} = Msg, State) -> |
273 |
702 |
NewPoppers = monitored_map:handle_info(Msg, State#state.message_poppers), |
274 |
702 |
{noreply, State#state{message_poppers = NewPoppers}}; |
275 |
|
handle_info({Acc, Msg = #offline_msg{us = US}}, |
276 |
|
State = #state{host_type = HostType, |
277 |
|
access_max_user_messages = AccessMaxOfflineMsgs}) -> |
278 |
128 |
handle_offline_msg(HostType, Acc, Msg, AccessMaxOfflineMsgs), |
279 |
128 |
case monitored_map:find(US, State#state.message_poppers) of |
280 |
|
{ok, Pid} -> |
281 |
45 |
Pid ! new_offline_messages; |
282 |
83 |
error -> ok |
283 |
|
end, |
284 |
128 |
{noreply, State}; |
285 |
|
handle_info(Msg, State) -> |
286 |
:-( |
?UNEXPECTED_INFO(Msg), |
287 |
:-( |
{noreply, State}. |
288 |
|
|
289 |
|
-spec terminate(any(), state()) -> ok. |
290 |
|
terminate(_Reason, _State) -> |
291 |
:-( |
ok. |
292 |
|
|
293 |
|
-spec code_change(any(), state(), any()) -> {ok, state()}. |
294 |
|
code_change(_OldVsn, State, _Extra) -> |
295 |
:-( |
{ok, State}. |
296 |
|
|
297 |
|
%% Handlers |
298 |
|
%% ------------------------------------------------------------------ |
299 |
|
|
300 |
|
%% This function should be called only from a hook |
301 |
|
%% Calling it directly is dangerous and may store unwanted messages |
302 |
|
%% in the offline storage (e.g. messages of type error) |
303 |
|
-spec inspect_packet(Acc, Params, Extra) -> {ok | stop, Acc} when |
304 |
|
Acc :: mongoose_acc:t(), |
305 |
|
Params :: map(), |
306 |
|
Extra :: gen_hook:extra(). |
307 |
|
inspect_packet(Acc, #{from := From, to := To, packet := Packet}, _Extra) -> |
308 |
214 |
case check_event_chatstates(Acc, From, To, Packet) of |
309 |
|
true -> |
310 |
214 |
Acc1 = store_packet(Acc, From, To, Packet), |
311 |
214 |
{stop, Acc1}; |
312 |
|
false -> |
313 |
:-( |
{ok, Acc} |
314 |
|
end. |
315 |
|
|
316 |
|
-spec store_packet(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
317 |
|
store_packet(Acc, From, To = #jid{luser = LUser, lserver = LServer}, |
318 |
|
Packet = #xmlel{children = Els}) -> |
319 |
214 |
TimeStamp = get_or_build_timestamp_from_packet(Packet), |
320 |
214 |
Expire = find_x_expire(TimeStamp, Els), |
321 |
214 |
HostType = mongoose_acc:host_type(Acc), |
322 |
214 |
Pid = srv_name(HostType), |
323 |
214 |
PermanentFields = mongoose_acc:get_permanent_fields(Acc), |
324 |
214 |
Msg = #offline_msg{us = {LUser, LServer}, |
325 |
|
timestamp = TimeStamp, |
326 |
|
expire = Expire, |
327 |
|
from = From, |
328 |
|
to = To, |
329 |
|
packet = jlib:remove_delay_tags(Packet), |
330 |
|
permanent_fields = PermanentFields}, |
331 |
214 |
Pid ! {Acc, Msg}, |
332 |
214 |
mongoose_acc:set(offline, stored, true, Acc). |
333 |
|
|
334 |
|
-spec get_or_build_timestamp_from_packet(exml:element()) -> integer(). |
335 |
|
get_or_build_timestamp_from_packet(Packet) -> |
336 |
214 |
case exml_query:path(Packet, [{element, <<"delay">>}, {attr, <<"stamp">>}]) of |
337 |
|
undefined -> |
338 |
212 |
erlang:system_time(microsecond); |
339 |
|
Stamp -> |
340 |
2 |
try |
341 |
2 |
calendar:rfc3339_to_system_time(binary_to_list(Stamp), [{unit, microsecond}]) |
342 |
|
catch |
343 |
:-( |
error:_Error -> erlang:system_time(microsecond) |
344 |
|
end |
345 |
|
end. |
346 |
|
|
347 |
|
%% Check if the packet has any content about XEP-0022 or XEP-0085 |
348 |
|
check_event_chatstates(Acc, From, To, Packet) -> |
349 |
214 |
#xmlel{children = Els} = Packet, |
350 |
214 |
case find_x_event_chatstates(Els, {false, false, false}) of |
351 |
|
%% There wasn't any x:event or chatstates subelements |
352 |
|
{false, false, _} -> |
353 |
214 |
true; |
354 |
|
%% There a chatstates subelement and other stuff, but no x:event |
355 |
|
{false, CEl, true} when CEl /= false -> |
356 |
:-( |
true; |
357 |
|
%% There was only a subelement: a chatstates |
358 |
|
{false, CEl, false} when CEl /= false -> |
359 |
|
%% Don't allow offline storage |
360 |
:-( |
false; |
361 |
|
%% There was an x:event element, and maybe also other stuff |
362 |
|
{El, _, _} when El /= false -> |
363 |
:-( |
inspect_xevent(Acc, From, To, Packet, El) |
364 |
|
end. |
365 |
|
|
366 |
|
inspect_xevent(Acc, From, To, Packet, XEvent) -> |
367 |
:-( |
case exml_query:subelement(XEvent, <<"id">>) of |
368 |
|
undefined -> |
369 |
:-( |
case exml_query:subelement(XEvent, <<"offline">>) of |
370 |
|
undefined -> |
371 |
:-( |
true; |
372 |
|
_ -> |
373 |
:-( |
ejabberd_router:route(To, From, Acc, patch_offline_message(Packet)), |
374 |
:-( |
true |
375 |
|
end; |
376 |
|
_ -> |
377 |
:-( |
false |
378 |
|
end. |
379 |
|
|
380 |
|
patch_offline_message(Packet) -> |
381 |
:-( |
ID = case exml_query:attr(Packet, <<"id">>, <<>>) of |
382 |
:-( |
<<"">> -> #xmlel{name = <<"id">>}; |
383 |
:-( |
S -> #xmlel{name = <<"id">>, children = [#xmlcdata{content = S}]} |
384 |
|
end, |
385 |
:-( |
Packet#xmlel{children = [x_elem(ID)]}. |
386 |
|
|
387 |
|
x_elem(ID) -> |
388 |
:-( |
#xmlel{ |
389 |
|
name = <<"x">>, |
390 |
|
attrs = [{<<"xmlns">>, ?NS_EVENT}], |
391 |
|
children = [ID, #xmlel{name = <<"offline">>}]}. |
392 |
|
|
393 |
|
%% Check if the packet has subelements about XEP-0022, XEP-0085 or other |
394 |
|
find_x_event_chatstates([], Res) -> |
395 |
214 |
Res; |
396 |
|
find_x_event_chatstates([#xmlcdata{} | Els], Res) -> |
397 |
:-( |
find_x_event_chatstates(Els, Res); |
398 |
|
find_x_event_chatstates([El | Els], {A, B, C}) -> |
399 |
222 |
case exml_query:attr(El, <<"xmlns">>, <<>>) of |
400 |
:-( |
?NS_EVENT -> find_x_event_chatstates(Els, {El, B, C}); |
401 |
:-( |
?NS_CHATSTATES -> find_x_event_chatstates(Els, {A, El, C}); |
402 |
222 |
_ -> find_x_event_chatstates(Els, {A, B, true}) |
403 |
|
end. |
404 |
|
|
405 |
|
find_x_expire(_, []) -> |
406 |
212 |
never; |
407 |
|
find_x_expire(TimeStamp, [#xmlcdata{} | Els]) -> |
408 |
:-( |
find_x_expire(TimeStamp, Els); |
409 |
|
find_x_expire(TimeStamp, [El | Els]) -> |
410 |
220 |
case exml_query:attr(El, <<"xmlns">>, <<>>) of |
411 |
|
?NS_EXPIRE -> |
412 |
2 |
Val = exml_query:attr(El, <<"seconds">>, <<>>), |
413 |
2 |
try binary_to_integer(Val) of |
414 |
|
Int when Int > 0 -> |
415 |
2 |
ExpireMicroSeconds = erlang:convert_time_unit(Int, second, microsecond), |
416 |
2 |
TimeStamp + ExpireMicroSeconds; |
417 |
|
_ -> |
418 |
:-( |
never |
419 |
|
catch |
420 |
:-( |
error:badarg -> never |
421 |
|
end; |
422 |
|
_ -> |
423 |
218 |
find_x_expire(TimeStamp, Els) |
424 |
|
end. |
425 |
|
|
426 |
|
-spec pop_offline_messages(Acc, Params, Extra) -> {ok, Acc} when |
427 |
|
Acc :: mongoose_acc:t(), |
428 |
|
Params :: map(), |
429 |
|
Extra :: gen_hook:extra(). |
430 |
|
pop_offline_messages(Acc, #{jid := JID}, _Extra) -> |
431 |
721 |
{ok, mongoose_acc:append(offline, messages, offline_messages(Acc, JID), Acc)}. |
432 |
|
|
433 |
|
-spec offline_messages(mongoose_acc:t(), jid:jid()) -> |
434 |
|
[{route, jid:jid(), jid:jid(), mongoose_acc:t()}]. |
435 |
|
offline_messages(Acc, #jid{lserver = LServer} = JID) -> |
436 |
721 |
HostType = mongoose_acc:host_type(Acc), |
437 |
721 |
case pop_messages(HostType, JID) of |
438 |
|
{ok, Rs} -> |
439 |
721 |
lists:map(fun(R) -> |
440 |
143 |
Packet = resend_offline_message_packet(LServer, R), |
441 |
143 |
compose_offline_message(R, Packet, Acc) |
442 |
|
end, Rs); |
443 |
|
{error, Reason} -> |
444 |
:-( |
?LOG_WARNING(#{what => offline_pop_failed, reason => Reason, acc => Acc}), |
445 |
:-( |
[] |
446 |
|
end. |
447 |
|
|
448 |
|
-spec pop_messages(mongooseim:host_type(), jid:jid()) -> {ok, [msg()]} | {error, any()}. |
449 |
|
pop_messages(HostType, JID) -> |
450 |
721 |
case gen_server:call(srv_name(HostType), {pop_offline_messages, jid:to_bare(JID)}) of |
451 |
|
{ok, RsAll} -> |
452 |
721 |
TimeStamp = erlang:system_time(microsecond), |
453 |
721 |
Rs = skip_expired_messages(TimeStamp, lists:keysort(#offline_msg.timestamp, RsAll)), |
454 |
721 |
{ok, Rs}; |
455 |
|
Other -> |
456 |
:-( |
Other |
457 |
|
end. |
458 |
|
|
459 |
|
-spec remove_user(Acc, Params, Extra) -> {ok, Acc} when |
460 |
|
Acc :: mongoose_acc:t(), |
461 |
|
Params :: map(), |
462 |
|
Extra :: gen_hook:extra(). |
463 |
|
remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) -> |
464 |
436 |
mod_offline_backend:remove_user(HostType, LUser, LServer), |
465 |
436 |
{ok, Acc}. |
466 |
|
|
467 |
|
-spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when |
468 |
|
Acc :: mongoose_domain_api:remove_domain_acc(), |
469 |
|
Params :: map(), |
470 |
|
Extra :: gen_hook:extra(). |
471 |
|
remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) -> |
472 |
1 |
case mongoose_lib:is_exported(mod_offline_backend, remove_domain, 2) of |
473 |
|
true -> |
474 |
1 |
mod_offline_backend:remove_domain(HostType, Domain); |
475 |
|
false -> |
476 |
:-( |
ok |
477 |
|
end, |
478 |
1 |
{ok, Acc}. |
479 |
|
|
480 |
|
-spec disco_features(Acc, Params, Extra) -> {ok, Acc} when |
481 |
|
Acc :: mongoose_disco:feature_acc(), |
482 |
|
Params :: map(), |
483 |
|
Extra :: gen_hook:extra(). |
484 |
|
disco_features(Acc = #{node := <<>>}, _Params, _Extra) -> |
485 |
49 |
{ok, mongoose_disco:add_features([?NS_FEATURE_MSGOFFLINE], Acc)}; |
486 |
|
disco_features(Acc = #{node := ?NS_FEATURE_MSGOFFLINE}, _Params, _Extra) -> |
487 |
|
%% override all lesser features... |
488 |
:-( |
{ok, Acc#{result := []}}; |
489 |
|
disco_features(Acc, _Params, _Extra) -> |
490 |
:-( |
{ok, Acc}. |
491 |
|
|
492 |
|
-spec determine_amp_strategy(Acc, Params, Extra) -> {ok, Acc} when |
493 |
|
Acc :: mod_amp:amp_strategy(), |
494 |
|
Params :: map(), |
495 |
|
Extra :: gen_hook:extra(). |
496 |
|
determine_amp_strategy(Strategy = #amp_strategy{deliver = [none]}, #{to := ToJID, event := initial_check}, _Params) -> |
497 |
64 |
case ejabberd_auth:does_user_exist(ToJID) of |
498 |
32 |
true -> {ok, Strategy#amp_strategy{deliver = [stored, none]}}; |
499 |
32 |
false -> {ok, Strategy} |
500 |
|
end; |
501 |
|
determine_amp_strategy(Strategy, _Params, _Extra) -> |
502 |
160 |
{ok, Strategy}. |
503 |
|
|
504 |
|
-spec get_personal_data(Acc, Params, Extra) -> {ok | stop, Acc} when |
505 |
|
Acc :: gdpr:personal_data(), |
506 |
|
Params :: #{jid := jid:jid()}, |
507 |
|
Extra :: gen_hook:extra(). |
508 |
|
get_personal_data(Acc, #{jid := JID}, #{host_type := HostType}) -> |
509 |
38 |
{ok, Messages} = mod_offline_backend:fetch_messages(HostType, JID), |
510 |
38 |
{ok, [{offline, ["timestamp", "from", "to", "packet"], |
511 |
|
offline_messages_to_gdpr_format(Messages)} | Acc]}. |
512 |
|
|
513 |
|
offline_messages_to_gdpr_format(MsgList) -> |
514 |
38 |
[offline_msg_to_gdpr_format(Msg) || Msg <- MsgList]. |
515 |
|
|
516 |
|
offline_msg_to_gdpr_format(#offline_msg{timestamp = TimeStamp, from = From, |
517 |
|
to = To, packet = Packet}) -> |
518 |
3 |
SystemTime = erlang:convert_time_unit(TimeStamp, microsecond, second), |
519 |
3 |
UTCTime = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]), |
520 |
3 |
UTC = list_to_binary(UTCTime), |
521 |
3 |
{UTC, jid:to_binary(From), jid:to_bare_binary(To), exml:to_binary(Packet)}. |
522 |
|
|
523 |
|
skip_expired_messages(TimeStamp, Rs) -> |
524 |
721 |
[R || R <- Rs, not is_expired_message(TimeStamp, R)]. |
525 |
|
|
526 |
|
is_expired_message(_TimeStamp, #offline_msg{expire=never}) -> |
527 |
143 |
false; |
528 |
|
is_expired_message(TimeStamp, #offline_msg{expire=ExpireTimeStamp}) -> |
529 |
:-( |
ExpireTimeStamp < TimeStamp. |
530 |
|
|
531 |
|
compose_offline_message(#offline_msg{from = From, to = To, permanent_fields = PermanentFields}, |
532 |
|
Packet, Acc0) -> |
533 |
143 |
Acc1 = mongoose_acc:set_permanent(PermanentFields, Acc0), |
534 |
143 |
Acc = mongoose_acc:update_stanza(#{element => Packet, from_jid => From, to_jid => To}, Acc1), |
535 |
143 |
{route, From, To, Acc}. |
536 |
|
|
537 |
|
resend_offline_message_packet(LServer, |
538 |
|
#offline_msg{timestamp=TimeStamp, packet = Packet}) -> |
539 |
143 |
add_timestamp(TimeStamp, LServer, Packet). |
540 |
|
|
541 |
|
add_timestamp(undefined, _LServer, Packet) -> |
542 |
:-( |
Packet; |
543 |
|
add_timestamp(TimeStamp, LServer, Packet) -> |
544 |
143 |
TimeStampXML = timestamp_xml(LServer, TimeStamp), |
545 |
143 |
xml:append_subtags(Packet, [TimeStampXML]). |
546 |
|
|
547 |
|
timestamp_xml(LServer, Time) -> |
548 |
143 |
FromJID = jid:make_noprep(<<>>, LServer, <<>>), |
549 |
143 |
TS = calendar:system_time_to_rfc3339(Time, [{offset, "Z"}, {unit, microsecond}]), |
550 |
143 |
jlib:timestamp_to_xml(TS, FromJID, <<"Offline Storage">>). |
551 |
|
|
552 |
|
-spec remove_expired_messages(mongooseim:host_type(), jid:lserver()) -> {ok, msg_count()} | {error, any()}. |
553 |
|
remove_expired_messages(HostType, LServer) -> |
554 |
9 |
Result = mod_offline_backend:remove_expired_messages(HostType, LServer), |
555 |
9 |
mongoose_lib:log_if_backend_error(Result, ?MODULE, ?LINE, [HostType]), |
556 |
9 |
Result. |
557 |
|
|
558 |
|
-spec remove_old_messages(mongooseim:host_type(), jid:lserver(), non_neg_integer()) -> |
559 |
|
{ok, msg_count()} | {error, any()}. |
560 |
|
remove_old_messages(HostType, LServer, HowManyDays) -> |
561 |
9 |
Timestamp = fallback_timestamp(HowManyDays, erlang:system_time(microsecond)), |
562 |
9 |
Result = mod_offline_backend:remove_old_messages(HostType, LServer, Timestamp), |
563 |
9 |
mongoose_lib:log_if_backend_error(Result, ?MODULE, ?LINE, [HostType, Timestamp]), |
564 |
9 |
Result. |
565 |
|
|
566 |
|
%% Warn senders that their messages have been discarded: |
567 |
|
discard_warn_sender(Msgs) -> |
568 |
9 |
lists:foreach( |
569 |
|
fun({Acc, #offline_msg{from=From, to=To, packet=Packet}}) -> |
570 |
9 |
ErrText = <<"Your contact offline message queue is full." |
571 |
|
" The message has been discarded.">>, |
572 |
9 |
Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), |
573 |
9 |
mod_amp:check_packet(Acc, offline_failed), |
574 |
9 |
{Acc1, Err} = jlib:make_error_reply( |
575 |
|
Acc, Packet, mongoose_xmpp_errors:resource_constraint(Lang, ErrText)), |
576 |
9 |
ejabberd_router:route(To, From, Acc1, Err) |
577 |
|
end, Msgs). |
578 |
|
|
579 |
|
fallback_timestamp(HowManyDays, TS_MicroSeconds) -> |
580 |
9 |
HowManySeconds = HowManyDays * 86400, |
581 |
9 |
HowManyMicroSeconds = erlang:convert_time_unit(HowManySeconds, second, microsecond), |
582 |
9 |
TS_MicroSeconds - HowManyMicroSeconds. |
583 |
|
|
584 |
|
config_metrics(HostType) -> |
585 |
24 |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]). |