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]). |
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 |
|
start(HostType, Opts) -> |
109 |
10 |
AccessMaxOfflineMsgs = gen_mod:get_opt(access_max_user_messages, Opts, |
110 |
|
max_user_offline_messages), |
111 |
10 |
mod_offline_backend:init(HostType, Opts), |
112 |
10 |
start_worker(HostType, AccessMaxOfflineMsgs), |
113 |
10 |
ejabberd_hooks:add(hooks(HostType)), |
114 |
10 |
ok. |
115 |
|
|
116 |
|
stop(Host) -> |
117 |
10 |
ejabberd_hooks:delete(hooks(Host)), |
118 |
10 |
stop_worker(Host), |
119 |
10 |
ok. |
120 |
|
|
121 |
|
-spec config_spec() -> mongoose_config_spec:config_section(). |
122 |
|
config_spec() -> |
123 |
160 |
#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 |
|
<<"riak">> => riak_config_spec() |
129 |
|
} |
130 |
|
}. |
131 |
|
|
132 |
|
riak_config_spec() -> |
133 |
160 |
#section{items = #{<<"bucket_type">> => #option{type = binary, |
134 |
|
validate = non_empty}}, |
135 |
|
wrap = none |
136 |
|
}. |
137 |
|
|
138 |
:-( |
supported_features() -> [dynamic_domains]. |
139 |
|
|
140 |
|
hooks(HostType) -> |
141 |
20 |
DefaultHooks = [ |
142 |
|
{offline_message_hook, HostType, ?MODULE, inspect_packet, 50}, |
143 |
|
{resend_offline_messages_hook, HostType, ?MODULE, pop_offline_messages, 50}, |
144 |
|
{remove_user, HostType, ?MODULE, remove_user, 50}, |
145 |
|
{remove_domain, HostType, ?MODULE, remove_domain, 50}, |
146 |
|
{anonymous_purge_hook, HostType, ?MODULE, remove_user, 50}, |
147 |
|
{disco_sm_features, HostType, ?MODULE, disco_features, 50}, |
148 |
|
{disco_local_features, HostType, ?MODULE, disco_features, 50}, |
149 |
|
{amp_determine_strategy, HostType, ?MODULE, determine_amp_strategy, 30}, |
150 |
|
{failed_to_store_message, HostType, ?MODULE, amp_failed_event, 30}, |
151 |
|
{get_personal_data, HostType, ?MODULE, get_personal_data, 50} |
152 |
|
], |
153 |
20 |
case gen_mod:get_module_opt(HostType, ?MODULE, store_groupchat_messages, false) of |
154 |
|
true -> |
155 |
2 |
GroupChatHook = {offline_groupchat_message_hook, |
156 |
|
HostType, ?MODULE, inspect_packet, 50}, |
157 |
2 |
[GroupChatHook | DefaultHooks]; |
158 |
18 |
_ -> DefaultHooks |
159 |
|
end. |
160 |
|
|
161 |
|
|
162 |
|
|
163 |
|
%% Server side functions |
164 |
|
%% ------------------------------------------------------------------ |
165 |
|
|
166 |
|
amp_failed_event(Acc) -> |
167 |
17 |
mod_amp:check_packet(Acc, offline_failed). |
168 |
|
|
169 |
|
handle_offline_msg(HostType, Acc, #offline_msg{us=US} = Msg, AccessMaxOfflineMsgs) -> |
170 |
109 |
{LUser, LServer} = US, |
171 |
109 |
Msgs = receive_all(US, [{Acc, Msg}]), |
172 |
109 |
MaxOfflineMsgs = get_max_user_messages(HostType, AccessMaxOfflineMsgs, LUser, LServer), |
173 |
109 |
Len = length(Msgs), |
174 |
109 |
case is_message_count_threshold_reached(HostType, MaxOfflineMsgs, LUser, LServer, Len) of |
175 |
|
false -> |
176 |
108 |
write_messages(HostType, LUser, LServer, Msgs); |
177 |
|
true -> |
178 |
1 |
discard_warn_sender(Msgs) |
179 |
|
end. |
180 |
|
|
181 |
|
write_messages(HostType, LUser, LServer, Msgs) -> |
182 |
108 |
MsgsWithoutAcc = [Msg || {_Acc, Msg} <- Msgs], |
183 |
108 |
case mod_offline_backend:write_messages(HostType, LUser, LServer, MsgsWithoutAcc) of |
184 |
|
ok -> |
185 |
100 |
[mod_amp:check_packet(Acc, archived) || {Acc, _Msg} <- Msgs], |
186 |
100 |
ok; |
187 |
|
{error, Reason} -> |
188 |
8 |
?LOG_ERROR(#{what => offline_write_failed, |
189 |
|
text => <<"Failed to write offline messages">>, |
190 |
|
reason => Reason, |
191 |
:-( |
user => LUser, server => LServer, msgs => Msgs}), |
192 |
8 |
discard_warn_sender(Msgs) |
193 |
|
end. |
194 |
|
|
195 |
|
-spec is_message_count_threshold_reached(mongooseim:host_type(), integer() | infinity, |
196 |
|
jid:luser(), jid:lserver(), integer()) -> |
197 |
|
boolean(). |
198 |
|
is_message_count_threshold_reached(_HostType, infinity, _LUser, _LServer, _Len) -> |
199 |
:-( |
false; |
200 |
|
is_message_count_threshold_reached(_HostType, MaxOfflineMsgs, _LUser, _LServer, Len) |
201 |
|
when Len > MaxOfflineMsgs -> |
202 |
:-( |
true; |
203 |
|
is_message_count_threshold_reached(HostType, MaxOfflineMsgs, LUser, LServer, Len) -> |
204 |
|
%% Only count messages if needed. |
205 |
109 |
MaxArchivedMsg = MaxOfflineMsgs - Len, |
206 |
|
%% Maybe do not need to count all messages in archive |
207 |
109 |
MaxArchivedMsg < mod_offline_backend:count_offline_messages(HostType, LUser, LServer, |
208 |
|
MaxArchivedMsg + 1). |
209 |
|
|
210 |
|
|
211 |
|
get_max_user_messages(HostType, AccessRule, LUser, LServer) -> |
212 |
109 |
case acl:match_rule(HostType, LServer, AccessRule, jid:make_noprep(LUser, LServer, <<>>)) of |
213 |
109 |
Max when is_integer(Max) -> Max; |
214 |
:-( |
infinity -> infinity; |
215 |
:-( |
_ -> ?MAX_USER_MESSAGES |
216 |
|
end. |
217 |
|
|
218 |
|
receive_all(US, Msgs) -> |
219 |
186 |
receive |
220 |
|
{_Acc, #offline_msg{us=US}} = Msg -> |
221 |
77 |
receive_all(US, [Msg | Msgs]) |
222 |
|
after 0 -> |
223 |
109 |
Msgs |
224 |
|
end. |
225 |
|
|
226 |
|
%% Supervision |
227 |
|
%% ------------------------------------------------------------------ |
228 |
|
|
229 |
|
start_worker(HostType, AccessMaxOfflineMsgs) -> |
230 |
10 |
Proc = srv_name(HostType), |
231 |
10 |
ChildSpec = |
232 |
|
{Proc, |
233 |
|
{?MODULE, start_link, [Proc, HostType, AccessMaxOfflineMsgs]}, |
234 |
|
permanent, 5000, worker, [?MODULE]}, |
235 |
10 |
ejabberd_sup:start_child(ChildSpec). |
236 |
|
|
237 |
|
stop_worker(HostType) -> |
238 |
10 |
Proc = srv_name(HostType), |
239 |
10 |
ejabberd_sup:stop_child(Proc). |
240 |
|
|
241 |
|
start_link(Name, HostType, AccessMaxOfflineMsgs) -> |
242 |
10 |
gen_server:start_link({local, Name}, ?MODULE, [HostType, AccessMaxOfflineMsgs], []). |
243 |
|
|
244 |
|
srv_name() -> |
245 |
556 |
mod_offline. |
246 |
|
|
247 |
|
srv_name(HostType) -> |
248 |
556 |
gen_mod:get_module_proc(HostType, srv_name()). |
249 |
|
|
250 |
|
%%==================================================================== |
251 |
|
%% gen_server callbacks |
252 |
|
%%==================================================================== |
253 |
|
|
254 |
|
-spec init(list()) -> {ok, state()}. |
255 |
|
init([HostType, AccessMaxOfflineMsgs]) -> |
256 |
10 |
{ok, #state{host_type = HostType, |
257 |
|
access_max_user_messages = AccessMaxOfflineMsgs}}. |
258 |
|
|
259 |
|
-spec handle_call(Request :: any(), {pid(), any()}, state()) -> {reply, Result, state()} |
260 |
|
when Result :: ok | {ok, [msg()]} | {error, any()}. |
261 |
|
handle_call({pop_offline_messages, JID}, {Pid, _}, State = #state{host_type = HostType}) -> |
262 |
350 |
Result = mod_offline_backend:pop_messages(HostType, JID), |
263 |
350 |
NewPoppers = monitored_map:put(jid:to_lus(JID), Pid, Pid, State#state.message_poppers), |
264 |
350 |
{reply, Result, State#state{message_poppers = NewPoppers}}; |
265 |
|
handle_call(Request, From, State) -> |
266 |
:-( |
?UNEXPECTED_CALL(Request, From), |
267 |
:-( |
{reply, ok, State}. |
268 |
|
|
269 |
|
-spec handle_cast(any(), state()) -> {noreply, state()}. |
270 |
|
handle_cast(Msg, State) -> |
271 |
:-( |
?UNEXPECTED_CAST(Msg), |
272 |
:-( |
{noreply, State}. |
273 |
|
|
274 |
|
-spec handle_info(any(), state()) -> {noreply, state()}. |
275 |
|
handle_info({'DOWN', _MonitorRef, _Type, _Object, _Info} = Msg, State) -> |
276 |
341 |
NewPoppers = monitored_map:handle_info(Msg, State#state.message_poppers), |
277 |
341 |
{noreply, State#state{message_poppers = NewPoppers}}; |
278 |
|
handle_info({Acc, Msg = #offline_msg{us = US}}, |
279 |
|
State = #state{host_type = HostType, |
280 |
|
access_max_user_messages = AccessMaxOfflineMsgs}) -> |
281 |
109 |
handle_offline_msg(HostType, Acc, Msg, AccessMaxOfflineMsgs), |
282 |
109 |
case monitored_map:find(US, State#state.message_poppers) of |
283 |
|
{ok, Pid} -> |
284 |
25 |
Pid ! new_offline_messages; |
285 |
84 |
error -> ok |
286 |
|
end, |
287 |
109 |
{noreply, State}; |
288 |
|
handle_info(Msg, State) -> |
289 |
:-( |
?UNEXPECTED_INFO(Msg), |
290 |
:-( |
{noreply, State}. |
291 |
|
|
292 |
|
-spec terminate(any(), state()) -> ok. |
293 |
|
terminate(_Reason, _State) -> |
294 |
:-( |
ok. |
295 |
|
|
296 |
|
-spec code_change(any(), state(), any()) -> {ok, state()}. |
297 |
|
code_change(_OldVsn, State, _Extra) -> |
298 |
:-( |
{ok, State}. |
299 |
|
|
300 |
|
%% Handlers |
301 |
|
%% ------------------------------------------------------------------ |
302 |
|
|
303 |
|
%% This function should be called only from a hook |
304 |
|
%% Calling it directly is dangerous and may store unwanted messages |
305 |
|
%% in the offline storage (e.g. messages of type error) |
306 |
|
-spec inspect_packet(mongoose_acc:t(), jid:jid(), jid:jid(), exml:element()) -> mongoose_acc:t(). |
307 |
|
inspect_packet(Acc, From, To, Packet) -> |
308 |
186 |
case check_event_chatstates(Acc, From, To, Packet) of |
309 |
|
true -> |
310 |
186 |
Acc1 = store_packet(Acc, From, To, Packet), |
311 |
186 |
{stop, Acc1}; |
312 |
|
false -> |
313 |
:-( |
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 |
186 |
TimeStamp = get_or_build_timestamp_from_packet(Packet), |
320 |
186 |
Expire = find_x_expire(TimeStamp, Els), |
321 |
186 |
HostType = mongoose_acc:host_type(Acc), |
322 |
186 |
Pid = srv_name(HostType), |
323 |
186 |
PermanentFields = mongoose_acc:get_permanent_fields(Acc), |
324 |
186 |
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 |
186 |
Pid ! {Acc, Msg}, |
332 |
186 |
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 |
186 |
case exml_query:path(Packet, [{element, <<"delay">>}, {attr, <<"stamp">>}]) of |
337 |
|
undefined -> |
338 |
145 |
erlang:system_time(microsecond); |
339 |
|
Stamp -> |
340 |
41 |
try |
341 |
41 |
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 |
186 |
#xmlel{children = Els} = Packet, |
350 |
186 |
case find_x_event_chatstates(Els, {false, false, false}) of |
351 |
|
%% There wasn't any x:event or chatstates subelements |
352 |
|
{false, false, _} -> |
353 |
186 |
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 |
186 |
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 |
230 |
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 |
230 |
_ -> find_x_event_chatstates(Els, {A, B, true}) |
403 |
|
end. |
404 |
|
|
405 |
|
find_x_expire(_, []) -> |
406 |
184 |
never; |
407 |
|
find_x_expire(TimeStamp, [#xmlcdata{} | Els]) -> |
408 |
:-( |
find_x_expire(TimeStamp, Els); |
409 |
|
find_x_expire(TimeStamp, [El | Els]) -> |
410 |
228 |
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 |
226 |
find_x_expire(TimeStamp, Els) |
424 |
|
end. |
425 |
|
|
426 |
|
-spec pop_offline_messages(mongoose_acc:t(), jid:jid()) -> mongoose_acc:t(). |
427 |
|
pop_offline_messages(Acc, JID) -> |
428 |
350 |
mongoose_acc:append(offline, messages, offline_messages(Acc, JID), Acc). |
429 |
|
|
430 |
|
-spec offline_messages(mongoose_acc:t(), jid:jid()) -> |
431 |
|
[{route, jid:jid(), jid:jid(), mongoose_acc:t()}]. |
432 |
|
offline_messages(Acc, #jid{lserver = LServer} = JID) -> |
433 |
350 |
HostType = mongoose_acc:host_type(Acc), |
434 |
350 |
case pop_messages(HostType, JID) of |
435 |
|
{ok, Rs} -> |
436 |
350 |
lists:map(fun(R) -> |
437 |
136 |
Packet = resend_offline_message_packet(LServer, R), |
438 |
136 |
compose_offline_message(R, Packet, Acc) |
439 |
|
end, Rs); |
440 |
|
{error, Reason} -> |
441 |
:-( |
?LOG_WARNING(#{what => offline_pop_failed, reason => Reason, acc => Acc}), |
442 |
:-( |
[] |
443 |
|
end. |
444 |
|
|
445 |
|
-spec pop_messages(mongooseim:host_type(), jid:jid()) -> {ok, [msg()]} | {error, any()}. |
446 |
|
pop_messages(HostType, JID) -> |
447 |
350 |
case gen_server:call(srv_name(HostType), {pop_offline_messages, jid:to_bare(JID)}) of |
448 |
|
{ok, RsAll} -> |
449 |
350 |
TimeStamp = erlang:system_time(microsecond), |
450 |
350 |
Rs = skip_expired_messages(TimeStamp, lists:keysort(#offline_msg.timestamp, RsAll)), |
451 |
350 |
{ok, Rs}; |
452 |
|
Other -> |
453 |
:-( |
Other |
454 |
|
end. |
455 |
|
|
456 |
|
-spec remove_user(mongoose_acc:t(), jid:luser(), jid:lserver()) -> mongoose_acc:t(). |
457 |
|
remove_user(Acc, LUser, LServer) -> |
458 |
132 |
HostType = mongoose_acc:host_type(Acc), |
459 |
132 |
mod_offline_backend:remove_user(HostType, LUser, LServer), |
460 |
132 |
Acc. |
461 |
|
|
462 |
|
-spec remove_domain(mongoose_hooks:simple_acc(), |
463 |
|
mongooseim:host_type(), jid:lserver()) -> |
464 |
|
mongoose_hooks:simple_acc(). |
465 |
|
remove_domain(Acc, HostType, Domain) -> |
466 |
:-( |
case backend_module:is_exported(mod_offline_backend, remove_domain, 2) of |
467 |
|
true -> |
468 |
:-( |
mod_offline_backend:remove_domain(HostType, Domain); |
469 |
|
false -> |
470 |
:-( |
ok |
471 |
|
end, |
472 |
:-( |
Acc. |
473 |
|
|
474 |
|
-spec disco_features(mongoose_disco:feature_acc()) -> mongoose_disco:feature_acc(). |
475 |
|
disco_features(Acc = #{node := <<>>}) -> |
476 |
65 |
mongoose_disco:add_features([?NS_FEATURE_MSGOFFLINE], Acc); |
477 |
|
disco_features(Acc = #{node := ?NS_FEATURE_MSGOFFLINE}) -> |
478 |
|
%% override all lesser features... |
479 |
:-( |
Acc#{result := []}; |
480 |
|
disco_features(Acc) -> |
481 |
:-( |
Acc. |
482 |
|
|
483 |
|
-spec determine_amp_strategy(amp_strategy(), jid:jid(), jid:jid(), exml:element(), amp_event()) -> |
484 |
|
amp_strategy(). |
485 |
|
determine_amp_strategy(Strategy = #amp_strategy{deliver = [none]}, |
486 |
|
_FromJID, ToJID, _Packet, initial_check) -> |
487 |
64 |
case ejabberd_auth:does_user_exist(ToJID) of |
488 |
32 |
true -> Strategy#amp_strategy{deliver = [stored, none]}; |
489 |
32 |
false -> Strategy |
490 |
|
end; |
491 |
|
determine_amp_strategy(Strategy, _, _, _, _) -> |
492 |
160 |
Strategy. |
493 |
|
|
494 |
|
-spec get_personal_data(gdpr:personal_data(), mongooseim:host_type(), jid:jid()) -> gdpr:personal_data(). |
495 |
|
get_personal_data(Acc, HostType, #jid{} = JID) -> |
496 |
32 |
{ok, Messages} = mod_offline_backend:fetch_messages(HostType, JID), |
497 |
32 |
[ {offline, ["timestamp", "from", "to", "packet"], |
498 |
|
offline_messages_to_gdpr_format(Messages)} | Acc]. |
499 |
|
|
500 |
|
offline_messages_to_gdpr_format(MsgList) -> |
501 |
32 |
[offline_msg_to_gdpr_format(Msg) || Msg <- MsgList]. |
502 |
|
|
503 |
|
offline_msg_to_gdpr_format(#offline_msg{timestamp = TimeStamp, from = From, |
504 |
|
to = To, packet = Packet}) -> |
505 |
3 |
SystemTime = erlang:convert_time_unit(TimeStamp, microsecond, second), |
506 |
3 |
UTCTime = calendar:system_time_to_rfc3339(SystemTime, [{offset, "Z"}]), |
507 |
3 |
UTC = list_to_binary(UTCTime), |
508 |
3 |
{UTC, jid:to_binary(From), jid:to_binary(jid:to_bare(To)), exml:to_binary(Packet)}. |
509 |
|
|
510 |
|
skip_expired_messages(TimeStamp, Rs) -> |
511 |
350 |
[R || R <- Rs, not is_expired_message(TimeStamp, R)]. |
512 |
|
|
513 |
|
is_expired_message(_TimeStamp, #offline_msg{expire=never}) -> |
514 |
135 |
false; |
515 |
|
is_expired_message(TimeStamp, #offline_msg{expire=ExpireTimeStamp}) -> |
516 |
6 |
ExpireTimeStamp < TimeStamp. |
517 |
|
|
518 |
|
compose_offline_message(#offline_msg{from = From, to = To, permanent_fields = PermanentFields}, |
519 |
|
Packet, Acc0) -> |
520 |
136 |
Acc1 = mongoose_acc:set_permanent(PermanentFields, Acc0), |
521 |
136 |
Acc = mongoose_acc:update_stanza(#{element => Packet, from_jid => From, to_jid => To}, Acc1), |
522 |
136 |
{route, From, To, Acc}. |
523 |
|
|
524 |
|
resend_offline_message_packet(LServer, |
525 |
|
#offline_msg{timestamp=TimeStamp, packet = Packet}) -> |
526 |
136 |
add_timestamp(TimeStamp, LServer, Packet). |
527 |
|
|
528 |
|
add_timestamp(undefined, _LServer, Packet) -> |
529 |
:-( |
Packet; |
530 |
|
add_timestamp(TimeStamp, LServer, Packet) -> |
531 |
136 |
TimeStampXML = timestamp_xml(LServer, TimeStamp), |
532 |
136 |
xml:append_subtags(Packet, [TimeStampXML]). |
533 |
|
|
534 |
|
timestamp_xml(LServer, Time) -> |
535 |
136 |
FromJID = jid:make_noprep(<<>>, LServer, <<>>), |
536 |
136 |
TS = calendar:system_time_to_rfc3339(Time, [{offset, "Z"}, {unit, microsecond}]), |
537 |
136 |
jlib:timestamp_to_xml(TS, FromJID, <<"Offline Storage">>). |
538 |
|
|
539 |
|
-spec remove_expired_messages(mongooseim:host_type(), jid:lserver()) -> {ok, msg_count()} | {error, any()}. |
540 |
|
remove_expired_messages(HostType, LServer) -> |
541 |
1 |
Result = mod_offline_backend:remove_expired_messages(HostType, LServer), |
542 |
1 |
mongoose_lib:log_if_backend_error(Result, ?MODULE, ?LINE, [HostType]), |
543 |
1 |
Result. |
544 |
|
|
545 |
|
-spec remove_old_messages(mongooseim:host_type(), jid:lserver(), non_neg_integer()) -> |
546 |
|
{ok, msg_count()} | {error, any()}. |
547 |
|
remove_old_messages(HostType, LServer, HowManyDays) -> |
548 |
1 |
Timestamp = fallback_timestamp(HowManyDays, erlang:system_time(microsecond)), |
549 |
1 |
Result = mod_offline_backend:remove_old_messages(HostType, LServer, Timestamp), |
550 |
1 |
mongoose_lib:log_if_backend_error(Result, ?MODULE, ?LINE, [HostType, Timestamp]), |
551 |
1 |
Result. |
552 |
|
|
553 |
|
%% Warn senders that their messages have been discarded: |
554 |
|
discard_warn_sender(Msgs) -> |
555 |
9 |
lists:foreach( |
556 |
|
fun({Acc, #offline_msg{from=From, to=To, packet=Packet}}) -> |
557 |
9 |
ErrText = <<"Your contact offline message queue is full." |
558 |
|
" The message has been discarded.">>, |
559 |
9 |
Lang = exml_query:attr(Packet, <<"xml:lang">>, <<>>), |
560 |
9 |
amp_failed_event(Acc), |
561 |
9 |
{Acc1, Err} = jlib:make_error_reply( |
562 |
|
Acc, Packet, mongoose_xmpp_errors:resource_constraint(Lang, ErrText)), |
563 |
9 |
ejabberd_router:route(To, From, Acc1, Err) |
564 |
|
end, Msgs). |
565 |
|
|
566 |
|
fallback_timestamp(HowManyDays, TS_MicroSeconds) -> |
567 |
1 |
HowManySeconds = HowManyDays * 86400, |
568 |
1 |
HowManyMicroSeconds = erlang:convert_time_unit(HowManySeconds, second, microsecond), |
569 |
1 |
TS_MicroSeconds - HowManyMicroSeconds. |
570 |
|
|
571 |
|
config_metrics(HostType) -> |
572 |
6 |
OptsToReport = [{backend, mnesia}], %list of tuples {option, default_value} |
573 |
6 |
mongoose_module_metrics:opts_for_module(HostType, ?MODULE, OptsToReport). |