1 |
|
%%%---------------------------------------------------------------------- |
2 |
|
%%% File : mod_offline_rdbms.erl |
3 |
|
%%% Author : Alexey Shchepin <alexey@process-one.net> |
4 |
|
%%% Purpose : Store and manage offline messages in relational database. |
5 |
|
%%% Created : 5 Jan 2003 by Alexey Shchepin <alexey@process-one.net> |
6 |
|
%%% |
7 |
|
%%% |
8 |
|
%%% ejabberd, Copyright (C) 2002-2011 ProcessOne |
9 |
|
%%% |
10 |
|
%%% This program is free software; you can redistribute it and/or |
11 |
|
%%% modify it under the terms of the GNU General Public License as |
12 |
|
%%% published by the Free Software Foundation; either version 2 of the |
13 |
|
%%% License, or (at your option) any later version. |
14 |
|
%%% |
15 |
|
%%% This program is distributed in the hope that it will be useful, |
16 |
|
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of |
17 |
|
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
18 |
|
%%% General Public License for more details. |
19 |
|
%%% |
20 |
|
%%% You should have received a copy of the GNU General Public License |
21 |
|
%%% along with this program; if not, write to the Free Software |
22 |
|
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
23 |
|
%%% |
24 |
|
%%%---------------------------------------------------------------------- |
25 |
|
|
26 |
|
-module(mod_offline_rdbms). |
27 |
|
-behaviour(mod_offline_backend). |
28 |
|
-export([init/2, |
29 |
|
pop_messages/2, |
30 |
|
fetch_messages/2, |
31 |
|
write_messages/4, |
32 |
|
count_offline_messages/4, |
33 |
|
remove_expired_messages/2, |
34 |
|
remove_old_messages/3, |
35 |
|
remove_user/3, |
36 |
|
remove_domain/2]). |
37 |
|
|
38 |
|
-import(mongoose_rdbms, [prepare/4, execute_successfully/3]). |
39 |
|
|
40 |
|
-include("jlib.hrl"). |
41 |
|
-include("mod_offline.hrl"). |
42 |
|
|
43 |
|
-spec init(mongooseim:host_type(), gen_mod:module_opts()) -> ok. |
44 |
|
init(_HostType, _Opts) -> |
45 |
1 |
prepare_queries(), |
46 |
1 |
ok. |
47 |
|
|
48 |
|
prepare_queries() -> |
49 |
1 |
prepare(offline_insert, offline_message, |
50 |
|
[username, server, timestamp, expire, |
51 |
|
from_jid, packet, permanent_fields], |
52 |
|
<<"INSERT INTO offline_message " |
53 |
|
"(username, server, timestamp, expire," |
54 |
|
" from_jid, packet, permanent_fields) " |
55 |
|
"VALUES (?, ?, ?, ?, ?, ?, ?)">>), |
56 |
1 |
{LimitSQL, LimitMSSQL} = rdbms_queries:get_db_specific_limits_binaries(), |
57 |
1 |
prepare(offline_count_limit, offline_message, |
58 |
|
rdbms_queries:add_limit_arg(limit, [server, username]), |
59 |
|
<<"SELECT ", LimitMSSQL/binary, |
60 |
|
" count(*) FROM offline_message " |
61 |
|
"WHERE server = ? AND username = ? ", LimitSQL/binary>>), |
62 |
1 |
prepare(offline_select, offline_message, |
63 |
|
[server, username, expire], |
64 |
|
<<"SELECT timestamp, from_jid, packet, permanent_fields " |
65 |
|
"FROM offline_message " |
66 |
|
"WHERE server = ? AND username = ? AND " |
67 |
|
"(expire IS null OR expire > ?) " |
68 |
|
"ORDER BY timestamp">>), |
69 |
1 |
prepare(offline_delete, offline_message, |
70 |
|
[server, username], |
71 |
|
<<"DELETE FROM offline_message " |
72 |
|
"WHERE server = ? AND username = ?">>), |
73 |
1 |
prepare(offline_remove_domain, offline_message, |
74 |
|
[server], |
75 |
|
<<"DELETE FROM offline_message WHERE server = ?">>), |
76 |
1 |
prepare(offline_delete_old, offline_message, |
77 |
|
[server, timestamp], |
78 |
|
<<"DELETE FROM offline_message WHERE server = ? AND timestamp < ?">>), |
79 |
1 |
prepare(offline_delete_expired, offline_message, |
80 |
|
[server, expire], |
81 |
|
<<"DELETE FROM offline_message " |
82 |
|
"WHERE server = ? AND expire IS NOT null AND expire < ?">>). |
83 |
|
|
84 |
|
-spec execute_count_offline_messages(mongooseim:host_type(), jid:luser(), jid:lserver(), |
85 |
|
pos_integer()) -> |
86 |
|
mongoose_rdbms:query_result(). |
87 |
|
execute_count_offline_messages(HostType, LUser, LServer, Limit) -> |
88 |
8 |
Args = rdbms_queries:add_limit_arg(Limit, [LServer, LUser]), |
89 |
8 |
execute_successfully(HostType, offline_count_limit, Args). |
90 |
|
|
91 |
|
-spec execute_fetch_offline_messages(mongooseim:host_type(), jid:luser(), jid:lserver(), |
92 |
|
integer()) -> |
93 |
|
mongoose_rdbms:query_result(). |
94 |
|
execute_fetch_offline_messages(HostType, LUser, LServer, ExtTimeStamp) -> |
95 |
100 |
execute_successfully(HostType, offline_select, [LServer, LUser, ExtTimeStamp]). |
96 |
|
|
97 |
|
-spec execute_remove_expired_offline_messages(mongooseim:host_type(), jid:lserver(), integer()) -> |
98 |
|
mongoose_rdbms:query_result(). |
99 |
|
execute_remove_expired_offline_messages(HostType, LServer, ExtTimeStamp) -> |
100 |
:-( |
execute_successfully(HostType, offline_delete_expired, [LServer, ExtTimeStamp]). |
101 |
|
|
102 |
|
-spec execute_remove_old_offline_messages(mongooseim:host_type(), jid:lserver(), integer()) -> |
103 |
|
mongoose_rdbms:query_result(). |
104 |
|
execute_remove_old_offline_messages(HostType, LServer, ExtTimeStamp) -> |
105 |
:-( |
execute_successfully(HostType, offline_delete_old, [LServer, ExtTimeStamp]). |
106 |
|
|
107 |
|
-spec execute_offline_delete(mongooseim:host_type(), jid:luser(), jid:lserver()) -> |
108 |
|
mongoose_rdbms:query_result(). |
109 |
|
execute_offline_delete(HostType, LUser, LServer) -> |
110 |
100 |
execute_successfully(HostType, offline_delete, [LServer, LUser]). |
111 |
|
|
112 |
|
%% Transactions |
113 |
|
|
114 |
|
-spec pop_offline_messages(mongooseim:host_type(), jid:luser(), jid:server(), integer()) -> |
115 |
|
mongoose_rdbms:transaction_result(). |
116 |
|
pop_offline_messages(HostType, LUser, LServer, ExtTimeStamp) -> |
117 |
100 |
F = fun() -> |
118 |
100 |
Res = execute_fetch_offline_messages(HostType, LUser, LServer, ExtTimeStamp), |
119 |
100 |
execute_offline_delete(HostType, LUser, LServer), |
120 |
100 |
Res |
121 |
|
end, |
122 |
100 |
mongoose_rdbms:sql_transaction(HostType, F). |
123 |
|
|
124 |
|
-spec push_offline_messages(mongooseim:host_type(), [list()]) -> |
125 |
|
mongoose_rdbms:transaction_result(). |
126 |
|
push_offline_messages(HostType, Rows) -> |
127 |
8 |
F = fun() -> |
128 |
8 |
[execute_successfully(HostType, offline_insert, Row) |
129 |
8 |
|| Row <- Rows], ok |
130 |
|
end, |
131 |
8 |
mongoose_rdbms:sql_transaction(HostType, F). |
132 |
|
|
133 |
|
%% API functions |
134 |
|
|
135 |
|
-spec pop_messages(mongooseim:host_type(), jid:jid()) -> {ok, [mod_offline:msg()]} | {error, any()}. |
136 |
|
pop_messages(HostType, #jid{} = To) -> |
137 |
100 |
US = {LUser, LServer} = jid:to_lus(To), |
138 |
100 |
ExtTimeStamp = os:system_time(microsecond), |
139 |
100 |
case pop_offline_messages(HostType, LUser, LServer, ExtTimeStamp) of |
140 |
|
{atomic, {selected, Rows}} -> |
141 |
100 |
{ok, rows_to_records(US, To, Rows)}; |
142 |
|
{aborted, Reason} -> |
143 |
:-( |
{error, Reason}; |
144 |
|
{error, Reason} -> |
145 |
:-( |
{error, Reason} |
146 |
|
end. |
147 |
|
|
148 |
|
%% Fetch messages for GDPR |
149 |
|
-spec fetch_messages(mongooseim:host_type(), jid:jid()) -> {ok, [mod_offline:msg()]}. |
150 |
|
fetch_messages(HostType, #jid{} = To) -> |
151 |
:-( |
US = {LUser, LServer} = jid:to_lus(To), |
152 |
:-( |
ExtTimeStamp = os:system_time(microsecond), |
153 |
:-( |
{selected, Rows} = execute_fetch_offline_messages(HostType, LUser, LServer, ExtTimeStamp), |
154 |
:-( |
{ok, rows_to_records(US, To, Rows)}. |
155 |
|
|
156 |
|
-spec write_messages(mongooseim:host_type(), jid:luser(), jid:lserver(), [mod_offline:msg()]) -> |
157 |
|
ok | {error, any()}. |
158 |
|
write_messages(HostType, LUser, LServer, Msgs) -> |
159 |
8 |
Rows = [record_to_row(LUser, LServer, Msg) || Msg <- Msgs], |
160 |
8 |
case push_offline_messages(HostType, Rows) of |
161 |
|
{atomic, ok} -> |
162 |
8 |
ok; |
163 |
|
Other -> |
164 |
:-( |
{error, Other} |
165 |
|
end. |
166 |
|
|
167 |
|
-spec count_offline_messages(mongooseim:host_type(), jid:luser(), jid:lserver(), |
168 |
|
mod_offline:msg_count()) -> |
169 |
|
mod_offline:msg_count(). |
170 |
|
count_offline_messages(HostType, LUser, LServer, Limit) -> |
171 |
8 |
Result = execute_count_offline_messages(HostType, LUser, LServer, Limit), |
172 |
8 |
mongoose_rdbms:selected_to_integer(Result). |
173 |
|
|
174 |
|
-spec remove_expired_messages(mongooseim:host_type(), jid:lserver()) -> {ok, mod_offline:msg_count()}. |
175 |
|
remove_expired_messages(HostType, LServer) -> |
176 |
:-( |
TimeStamp = os:system_time(microsecond), |
177 |
:-( |
Result = execute_remove_expired_offline_messages(HostType, LServer, TimeStamp), |
178 |
:-( |
updated_ok(Result). |
179 |
|
|
180 |
|
-spec remove_old_messages(mongooseim:host_type(), jid:lserver(), mod_offline:timestamp()) -> |
181 |
|
{ok, mod_offline:msg_count()}. |
182 |
|
remove_old_messages(HostType, LServer, TimeStamp) -> |
183 |
:-( |
Result = execute_remove_old_offline_messages(HostType, LServer, TimeStamp), |
184 |
:-( |
updated_ok(Result). |
185 |
|
|
186 |
|
-spec remove_user(mongooseim:host_type(), jid:luser(), jid:lserver()) -> ok. |
187 |
|
remove_user(HostType, LUser, LServer) -> |
188 |
:-( |
execute_offline_delete(HostType, LUser, LServer), |
189 |
:-( |
ok. |
190 |
|
|
191 |
|
-spec remove_domain(mongooseim:host_type(), jid:lserver()) -> ok. |
192 |
|
remove_domain(HostType, Domain) -> |
193 |
:-( |
mongoose_rdbms:execute_successfully(HostType, offline_remove_domain, [Domain]), |
194 |
:-( |
ok. |
195 |
|
|
196 |
|
%% Pure helper functions |
197 |
|
record_to_row(LUser, LServer, |
198 |
|
#offline_msg{timestamp = TimeStamp, expire = Expire, from = From, |
199 |
|
packet = Packet, permanent_fields = PermanentFields}) -> |
200 |
8 |
ExtExpire = maybe_encode_timestamp(Expire), |
201 |
8 |
ExtFrom = jid:to_binary(From), |
202 |
8 |
ExtPacket = exml:to_binary(Packet), |
203 |
8 |
ExtFields = encode_permanent_fields(PermanentFields), |
204 |
8 |
prepare_offline_message(LUser, LServer, TimeStamp, ExtExpire, |
205 |
|
ExtFrom, ExtPacket, ExtFields). |
206 |
|
|
207 |
|
prepare_offline_message(LUser, LServer, ExtTimeStamp, ExtExpire, ExtFrom, ExtPacket, ExtFields) -> |
208 |
8 |
[LUser, LServer, ExtTimeStamp, ExtExpire, ExtFrom, ExtPacket, ExtFields]. |
209 |
|
|
210 |
|
encode_permanent_fields(Fields) -> |
211 |
8 |
term_to_binary(Fields). |
212 |
|
|
213 |
8 |
maybe_encode_timestamp(never) -> null; |
214 |
:-( |
maybe_encode_timestamp(TimeStamp) -> TimeStamp. |
215 |
|
|
216 |
|
rows_to_records(US, To, Rows) -> |
217 |
100 |
[row_to_record(US, To, Row) || Row <- Rows]. |
218 |
|
|
219 |
|
row_to_record(US, To, {ExtTimeStamp, ExtFrom, ExtPacket, ExtPermanentFields}) -> |
220 |
8 |
{ok, Packet} = exml:parse(ExtPacket), |
221 |
8 |
TimeStamp = mongoose_rdbms:result_to_integer(ExtTimeStamp), |
222 |
8 |
From = jid:from_binary(ExtFrom), |
223 |
8 |
PermanentFields = extract_permanent_fields(ExtPermanentFields), |
224 |
8 |
#offline_msg{us = US, timestamp = TimeStamp, expire = never, |
225 |
|
from = From, to = To, packet = Packet, |
226 |
|
permanent_fields = PermanentFields}. |
227 |
|
|
228 |
|
extract_permanent_fields(null) -> |
229 |
:-( |
[]; %% This is needed in transition period when upgrading to MongooseIM above 3.5.0 |
230 |
|
extract_permanent_fields(Escaped) -> |
231 |
8 |
Bin = mongoose_rdbms:unescape_binary(global, Escaped), |
232 |
8 |
binary_to_term(Bin). |
233 |
|
|
234 |
:-( |
updated_ok({updated, Count}) -> {ok, Count}. |