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