1 |
|
%%%------------------------------------------------------------------- |
2 |
|
%%% @author Uvarov Michael <arcusfelis@gmail.com> |
3 |
|
%%% @copyright (C) 2013, Uvarov Michael |
4 |
|
%%% @doc RDBMS backend for MUC Message Archive Management. |
5 |
|
%%% @end |
6 |
|
%%%------------------------------------------------------------------- |
7 |
|
-module(mod_mam_muc_rdbms_arch). |
8 |
|
|
9 |
|
%% ---------------------------------------------------------------------- |
10 |
|
%% Exports |
11 |
|
|
12 |
|
%% gen_mod handlers |
13 |
|
-export([start/2, stop/1, supported_features/0]). |
14 |
|
|
15 |
|
%% MAM hook handlers |
16 |
|
-behaviour(ejabberd_gen_mam_archive). |
17 |
|
-behaviour(gen_mod). |
18 |
|
-behaviour(mongoose_module_metrics). |
19 |
|
|
20 |
|
-callback encode(term()) -> binary(). |
21 |
|
-callback decode(binary()) -> term(). |
22 |
|
|
23 |
|
-export([archive_size/4, |
24 |
|
archive_message/3, |
25 |
|
lookup_messages/3, |
26 |
|
remove_archive/4, |
27 |
|
remove_domain/3]). |
28 |
|
|
29 |
|
-export([get_mam_muc_gdpr_data/3]). |
30 |
|
|
31 |
|
%% Called from mod_mam_muc_rdbms_async_pool_writer |
32 |
|
-export([prepare_message/2, retract_message/2, prepare_insert/2]). |
33 |
|
-export([extend_params_with_sender_id/2]). |
34 |
|
|
35 |
|
-ignore_xref([behaviour_info/1, remove_archive/4, remove_domain/3]). |
36 |
|
|
37 |
|
%% ---------------------------------------------------------------------- |
38 |
|
%% Imports |
39 |
|
|
40 |
|
-include("mongoose.hrl"). |
41 |
|
-include("jlib.hrl"). |
42 |
|
-include_lib("exml/include/exml.hrl"). |
43 |
|
-include("mongoose_rsm.hrl"). |
44 |
|
-include("mongoose_mam.hrl"). |
45 |
|
|
46 |
|
%% ---------------------------------------------------------------------- |
47 |
|
%% Types |
48 |
|
|
49 |
|
-type env_vars() :: mod_mam_rdbms_arch:env_vars(). |
50 |
|
-type host_type() :: mongooseim:host_type(). |
51 |
|
|
52 |
|
%% ---------------------------------------------------------------------- |
53 |
|
%% gen_mod callbacks |
54 |
|
%% Starting and stopping functions for users' archives |
55 |
|
|
56 |
|
-spec start(host_type(), gen_mod:module_opts()) -> ok. |
57 |
|
start(HostType, _Opts) -> |
58 |
:-( |
start_hooks(HostType), |
59 |
:-( |
register_prepared_queries(), |
60 |
:-( |
ok. |
61 |
|
|
62 |
|
-spec stop(host_type()) -> ok. |
63 |
|
stop(HostType) -> |
64 |
:-( |
stop_hooks(HostType). |
65 |
|
|
66 |
|
-spec supported_features() -> [atom()]. |
67 |
|
supported_features() -> |
68 |
:-( |
[dynamic_domains]. |
69 |
|
|
70 |
|
-spec get_mam_muc_gdpr_data(ejabberd_gen_mam_archive:mam_pm_gdpr_data(), |
71 |
|
host_type(), jid:jid()) -> |
72 |
|
ejabberd_gen_mam_archive:mam_muc_gdpr_data(). |
73 |
|
get_mam_muc_gdpr_data(Acc, HostType, #jid{luser = LUser, lserver = LServer} = _UserJID) -> |
74 |
:-( |
case mod_mam:archive_id(LServer, LUser) of |
75 |
|
undefined -> |
76 |
:-( |
Acc; |
77 |
|
SenderID -> |
78 |
|
%% We don't know the real room JID here, use FakeEnv |
79 |
:-( |
FakeEnv = env_vars(HostType, jid:make(<<>>, <<>>, <<>>)), |
80 |
:-( |
{selected, Rows} = extract_gdpr_messages(HostType, SenderID), |
81 |
:-( |
[mam_decoder:decode_muc_gdpr_row(Row, FakeEnv) || Row <- Rows] ++ Acc |
82 |
|
end. |
83 |
|
|
84 |
|
%% ---------------------------------------------------------------------- |
85 |
|
%% Add hooks for mod_mam |
86 |
|
|
87 |
|
-spec start_hooks(host_type()) -> ok. |
88 |
|
start_hooks(HostType) -> |
89 |
:-( |
ejabberd_hooks:add(hooks(HostType)). |
90 |
|
|
91 |
|
-spec stop_hooks(host_type()) -> ok. |
92 |
|
stop_hooks(HostType) -> |
93 |
:-( |
ejabberd_hooks:delete(hooks(HostType)). |
94 |
|
|
95 |
|
hooks(HostType) -> |
96 |
|
case gen_mod:get_module_opt(HostType, ?MODULE, no_writer) of |
97 |
|
true -> |
98 |
:-( |
[]; |
99 |
|
false -> |
100 |
:-( |
[{mam_muc_archive_message, HostType, ?MODULE, archive_message, 50}] |
101 |
:-( |
end ++ |
102 |
|
[{mam_muc_archive_size, HostType, ?MODULE, archive_size, 50}, |
103 |
|
{mam_muc_lookup_messages, HostType, ?MODULE, lookup_messages, 50}, |
104 |
|
{mam_muc_remove_archive, HostType, ?MODULE, remove_archive, 50}, |
105 |
|
{remove_domain, HostType, ?MODULE, remove_domain, 50}, |
106 |
|
{get_mam_muc_gdpr_data, HostType, ?MODULE, get_mam_muc_gdpr_data, 50}]. |
107 |
|
|
108 |
|
%% ---------------------------------------------------------------------- |
109 |
|
%% SQL queries |
110 |
|
|
111 |
|
register_prepared_queries() -> |
112 |
:-( |
prepare_insert(insert_mam_muc_message, 1), |
113 |
:-( |
mongoose_rdbms:prepare(mam_muc_archive_remove, mam_muc_message, [room_id], |
114 |
|
<<"DELETE FROM mam_muc_message " |
115 |
|
"WHERE room_id = ?">>), |
116 |
:-( |
mongoose_rdbms:prepare(mam_muc_remove_domain, mam_muc_message, ['mam_server_user.server'], |
117 |
|
<<"DELETE FROM mam_muc_message " |
118 |
|
"WHERE room_id IN (SELECT id FROM mam_server_user where server = ?)">>), |
119 |
:-( |
mongoose_rdbms:prepare(mam_muc_remove_domain_users, mam_server_user, [server], |
120 |
|
<<"DELETE FROM mam_server_user WHERE server = ?">>), |
121 |
:-( |
mongoose_rdbms:prepare(mam_muc_make_tombstone, mam_muc_message, [message, room_id, id], |
122 |
|
<<"UPDATE mam_muc_message SET message = ?, search_body = '' " |
123 |
|
"WHERE room_id = ? AND id = ?">>), |
124 |
:-( |
{LimitSQL, LimitMSSQL} = rdbms_queries:get_db_specific_limits_binaries(1), |
125 |
:-( |
mongoose_rdbms:prepare(mam_muc_select_messages_to_retract_on_origin_id, mam_muc_message, |
126 |
|
[room_id, sender_id, origin_id], |
127 |
|
<<"SELECT ", LimitMSSQL/binary, |
128 |
|
" id, message FROM mam_muc_message" |
129 |
|
" WHERE room_id = ? AND sender_id = ? " |
130 |
|
" AND origin_id = ?" |
131 |
|
" ORDER BY id DESC ", LimitSQL/binary>>), |
132 |
:-( |
mongoose_rdbms:prepare(mam_muc_select_messages_to_retract_on_stanza_id, mam_muc_message, |
133 |
|
[room_id, sender_id, id], |
134 |
|
<<"SELECT ", LimitMSSQL/binary, |
135 |
|
" origin_id, message FROM mam_muc_message" |
136 |
|
" WHERE room_id = ? AND sender_id = ? " |
137 |
|
" AND id = ?" |
138 |
|
" ORDER BY id DESC ", LimitSQL/binary>>), |
139 |
:-( |
mongoose_rdbms:prepare(mam_muc_extract_gdpr_messages, mam_muc_message, [sender_id], |
140 |
|
<<"SELECT id, message FROM mam_muc_message " |
141 |
|
" WHERE sender_id = ? ORDER BY id">>). |
142 |
|
|
143 |
|
%% ---------------------------------------------------------------------- |
144 |
|
%% Declarative logic |
145 |
|
|
146 |
|
db_mappings() -> |
147 |
:-( |
[#db_mapping{column = id, param = message_id, format = int}, |
148 |
|
#db_mapping{column = room_id, param = archive_id, format = int}, |
149 |
|
#db_mapping{column = sender_id, param = sender_id, format = int}, |
150 |
|
#db_mapping{column = nick_name, param = source_jid, format = jid_resource}, |
151 |
|
#db_mapping{column = origin_id, param = origin_id, format = maybe_string}, |
152 |
|
#db_mapping{column = message, param = packet, format = xml}, |
153 |
|
#db_mapping{column = search_body, param = packet, format = search}]. |
154 |
|
|
155 |
|
lookup_fields() -> |
156 |
:-( |
[#lookup_field{op = equal, column = room_id, param = archive_id, required = true}, |
157 |
|
#lookup_field{op = ge, column = id, param = start_id}, |
158 |
|
#lookup_field{op = le, column = id, param = end_id}, |
159 |
|
#lookup_field{op = equal, column = nick_name, param = remote_resource}, |
160 |
|
#lookup_field{op = like, column = search_body, param = norm_search_text, value_maker = search_words}]. |
161 |
|
|
162 |
|
-spec env_vars(host_type(), jid:jid()) -> env_vars(). |
163 |
|
env_vars(HostType, ArcJID) -> |
164 |
|
%% Please, minimize the usage of the host field. |
165 |
|
%% It's only for passing into RDBMS. |
166 |
:-( |
#{host_type => HostType, |
167 |
|
archive_jid => ArcJID, |
168 |
|
table => mam_muc_message, |
169 |
|
index_hint_fn => fun index_hint_sql/1, |
170 |
|
columns_sql_fn => fun columns_sql/1, |
171 |
|
column_to_id_fn => fun column_to_id/1, |
172 |
|
lookup_fn => fun lookup_query/5, |
173 |
|
decode_row_fn => fun row_to_uniform_format/2, |
174 |
|
has_message_retraction => mod_mam_utils:has_message_retraction(mod_mam_muc, HostType), |
175 |
|
has_full_text_search => mod_mam_utils:has_full_text_search(mod_mam_muc, HostType), |
176 |
|
db_jid_codec => db_jid_codec(HostType, ?MODULE), |
177 |
|
db_message_codec => db_message_codec(HostType, ?MODULE)}. |
178 |
|
|
179 |
|
row_to_uniform_format(Row, Env) -> |
180 |
:-( |
mam_decoder:decode_muc_row(Row, Env). |
181 |
|
|
182 |
|
-spec index_hint_sql(env_vars()) -> string(). |
183 |
:-( |
index_hint_sql(_) -> "". |
184 |
|
|
185 |
:-( |
columns_sql(lookup) -> "id, nick_name, message"; |
186 |
:-( |
columns_sql(count) -> "COUNT(*)". |
187 |
|
|
188 |
:-( |
column_to_id(id) -> "i"; |
189 |
:-( |
column_to_id(room_id) -> "u"; |
190 |
:-( |
column_to_id(nick_name) -> "n"; |
191 |
:-( |
column_to_id(search_body) -> "s". |
192 |
|
|
193 |
|
column_names(Mappings) -> |
194 |
:-( |
[Column || #db_mapping{column = Column} <- Mappings]. |
195 |
|
|
196 |
|
%% ---------------------------------------------------------------------- |
197 |
|
%% Options |
198 |
|
|
199 |
|
-spec db_jid_codec(host_type(), module()) -> module(). |
200 |
|
db_jid_codec(HostType, Module) -> |
201 |
:-( |
gen_mod:get_module_opt(HostType, Module, db_jid_format). |
202 |
|
|
203 |
|
-spec db_message_codec(host_type(), module()) -> module(). |
204 |
|
db_message_codec(HostType, Module) -> |
205 |
:-( |
gen_mod:get_module_opt(HostType, Module, db_message_format). |
206 |
|
|
207 |
|
-spec get_retract_id(exml:element(), env_vars()) -> none | mod_mam_utils:retraction_id(). |
208 |
|
get_retract_id(Packet, #{has_message_retraction := Enabled}) -> |
209 |
:-( |
mod_mam_utils:get_retract_id(Enabled, Packet). |
210 |
|
|
211 |
|
%% ---------------------------------------------------------------------- |
212 |
|
%% Internal functions and callbacks |
213 |
|
|
214 |
|
-spec archive_size(Size :: integer(), HostType :: mongooseim:host_type(), |
215 |
|
ArcId :: mod_mam:archive_id(), ArcJID :: jid:jid()) -> integer(). |
216 |
|
archive_size(Size, HostType, ArcID, ArcJID) when is_integer(Size) -> |
217 |
:-( |
Filter = [{equal, room_id, ArcID}], |
218 |
:-( |
Env = env_vars(HostType, ArcJID), |
219 |
:-( |
Result = lookup_query(count, Env, Filter, unordered, all), |
220 |
:-( |
mongoose_rdbms:selected_to_integer(Result). |
221 |
|
|
222 |
|
extend_params_with_sender_id(HostType, Params = #{remote_jid := SenderJID}) -> |
223 |
:-( |
BareSenderJID = jid:to_bare(SenderJID), |
224 |
:-( |
SenderID = mod_mam:archive_id_int(HostType, BareSenderJID), |
225 |
:-( |
Params#{sender_id => SenderID}. |
226 |
|
|
227 |
|
-spec archive_message(_Result, HostType :: mongooseim:host_type(), |
228 |
|
mod_mam:archive_message_params()) -> ok. |
229 |
|
archive_message(_Result, HostType, Params0 = #{local_jid := ArcJID}) -> |
230 |
:-( |
try |
231 |
:-( |
Params = extend_params_with_sender_id(HostType, Params0), |
232 |
:-( |
Env = env_vars(HostType, ArcJID), |
233 |
:-( |
do_archive_message(HostType, Params, Env), |
234 |
:-( |
retract_message(HostType, Params, Env), |
235 |
:-( |
ok |
236 |
|
catch error:Reason:StackTrace -> |
237 |
:-( |
?LOG_ERROR(#{what => archive_message_failed, |
238 |
|
host_type => HostType, mam_params => Params0, |
239 |
:-( |
reason => Reason, stacktrace => StackTrace}), |
240 |
:-( |
erlang:raise(error, Reason, StackTrace) |
241 |
|
end. |
242 |
|
|
243 |
|
do_archive_message(HostType, Params, Env) -> |
244 |
:-( |
Row = mam_encoder:encode_message(Params, Env, db_mappings()), |
245 |
:-( |
{updated, 1} = mongoose_rdbms:execute_successfully(HostType, insert_mam_muc_message, Row). |
246 |
|
|
247 |
|
%% Retraction logic |
248 |
|
%% Called after inserting a new message |
249 |
|
-spec retract_message(mongooseim:host_type(), mod_mam:archive_message_params()) -> ok. |
250 |
|
retract_message(HostType, #{local_jid := ArcJID} = Params) -> |
251 |
:-( |
Env = env_vars(HostType, ArcJID), |
252 |
:-( |
retract_message(HostType, Params, Env). |
253 |
|
|
254 |
|
-spec retract_message(mongooseim:host_type(), mod_mam:archive_message_params(), env_vars()) -> ok. |
255 |
|
retract_message(HostType, #{archive_id := ArcID, sender_id := SenderID, |
256 |
|
packet := Packet} = Params, Env) -> |
257 |
:-( |
case get_retract_id(Packet, Env) of |
258 |
:-( |
none -> ok; |
259 |
|
RetractionId -> |
260 |
:-( |
Info = get_retraction_info(HostType, ArcID, SenderID, RetractionId, Env), |
261 |
:-( |
make_tombstone(HostType, ArcID, RetractionId, Info, Params, Env) |
262 |
|
end. |
263 |
|
|
264 |
|
get_retraction_info(HostType, ArcID, SenderID, RetractionId, Env) -> |
265 |
:-( |
{selected, Rows} = |
266 |
|
execute_select_messages_to_retract(HostType, ArcID, SenderID, RetractionId), |
267 |
:-( |
mam_decoder:decode_retraction_info(Env, Rows, RetractionId). |
268 |
|
|
269 |
|
make_tombstone(_HostType, ArcID, RetractionId, skip, _Params, _Env) -> |
270 |
:-( |
?LOG_INFO(#{what => make_tombstone_failed, |
271 |
|
text => <<"Message to retract was not found">>, |
272 |
:-( |
user_id => ArcID, retraction_context => RetractionId}); |
273 |
|
make_tombstone(HostType, ArcID, _RetractionId, |
274 |
|
RetractionInfo = #{message_id := MessID}, Params, |
275 |
|
#{archive_jid := ArcJID} = Env) -> |
276 |
:-( |
RetractionInfo1 = mongoose_hooks:mam_muc_retraction(HostType, RetractionInfo, Params), |
277 |
:-( |
Tombstone = mod_mam_utils:tombstone(RetractionInfo1, ArcJID), |
278 |
:-( |
TombstoneData = mam_encoder:encode_packet(Tombstone, Env), |
279 |
:-( |
execute_make_tombstone(HostType, TombstoneData, ArcID, MessID). |
280 |
|
|
281 |
|
execute_select_messages_to_retract(HostType, ArcID, SenderID, {origin_id, OriginID}) -> |
282 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_select_messages_to_retract_on_origin_id, |
283 |
|
[ArcID, SenderID, OriginID]); |
284 |
|
execute_select_messages_to_retract(HostType, ArcID, SenderID, {stanza_id, BinStanzaId}) -> |
285 |
:-( |
StanzaId = mod_mam_utils:external_binary_to_mess_id(BinStanzaId), |
286 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_select_messages_to_retract_on_stanza_id, |
287 |
|
[ArcID, SenderID, StanzaId]). |
288 |
|
|
289 |
|
execute_make_tombstone(HostType, TombstoneData, ArcID, MessID) -> |
290 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_make_tombstone, |
291 |
|
[TombstoneData, ArcID, MessID]). |
292 |
|
|
293 |
|
%% Insert logic |
294 |
|
-spec prepare_message(mongooseim:host_type(), mod_mam:archive_message_params()) -> list(). |
295 |
|
prepare_message(HostType, Params = #{local_jid := ArcJID}) -> |
296 |
:-( |
Env = env_vars(HostType, ArcJID), |
297 |
:-( |
mam_encoder:encode_message(Params, Env, db_mappings()). |
298 |
|
|
299 |
|
-spec prepare_insert(Name :: atom(), NumRows :: pos_integer()) -> ok. |
300 |
|
prepare_insert(Name, NumRows) -> |
301 |
:-( |
Table = mam_muc_message, |
302 |
:-( |
Fields = column_names(db_mappings()), |
303 |
:-( |
{Query, Fields2} = rdbms_queries:create_bulk_insert_query(Table, Fields, NumRows), |
304 |
:-( |
mongoose_rdbms:prepare(Name, Table, Fields2, Query), |
305 |
:-( |
ok. |
306 |
|
|
307 |
|
%% Removal logic |
308 |
|
-spec remove_archive(Acc :: mongoose_acc:t(), HostType :: mongooseim:host_type(), |
309 |
|
ArcID :: mod_mam:archive_id(), |
310 |
|
ArcJID :: jid:jid()) -> mongoose_acc:t(). |
311 |
|
remove_archive(Acc, HostType, ArcID, _ArcJID) -> |
312 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_archive_remove, [ArcID]), |
313 |
:-( |
Acc. |
314 |
|
|
315 |
|
-spec remove_domain(mongoose_hooks:simple_acc(), |
316 |
|
mongooseim:host_type(), jid:lserver()) -> |
317 |
|
mongoose_hooks:simple_acc(). |
318 |
|
remove_domain(Acc, HostType, Domain) -> |
319 |
:-( |
SubHosts = get_subhosts(HostType, Domain), |
320 |
:-( |
{atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() -> |
321 |
:-( |
[remove_domain_trans(HostType, SubHost) || SubHost <- SubHosts] |
322 |
|
end), |
323 |
:-( |
Acc. |
324 |
|
|
325 |
|
remove_domain_trans(HostType, MucHost) -> |
326 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_remove_domain, [MucHost]), |
327 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_remove_domain_users, [MucHost]). |
328 |
|
|
329 |
|
get_subhosts(HostType, Domain) -> |
330 |
:-( |
MucHostPattern = gen_mod:get_module_opt(HostType, mod_muc, host, |
331 |
|
mod_muc:default_host()), |
332 |
:-( |
LightHostPattern = gen_mod:get_module_opt(HostType, mod_muc_light, host, |
333 |
|
mod_muc_light:default_host()), |
334 |
:-( |
MucHost = mongoose_subdomain_utils:get_fqdn(MucHostPattern, Domain), |
335 |
:-( |
LightHost = mongoose_subdomain_utils:get_fqdn(LightHostPattern, Domain), |
336 |
:-( |
lists:usort([MucHost, LightHost]). |
337 |
|
|
338 |
|
%% GDPR logic |
339 |
|
extract_gdpr_messages(HostType, SenderID) -> |
340 |
:-( |
mongoose_rdbms:execute_successfully(HostType, mam_muc_extract_gdpr_messages, [SenderID]). |
341 |
|
|
342 |
|
%% Lookup logic |
343 |
|
-spec lookup_messages(Result :: any(), HostType :: mongooseim:host_type(), Params :: map()) -> |
344 |
|
{ok, mod_mam:lookup_result()}. |
345 |
|
lookup_messages({error, _Reason} = Result, _HostType, _Params) -> |
346 |
:-( |
Result; |
347 |
|
lookup_messages(_Result, HostType, Params = #{owner_jid := ArcJID}) -> |
348 |
:-( |
Env = env_vars(HostType, ArcJID), |
349 |
:-( |
ExdParams = mam_encoder:extend_lookup_params(Params, Env), |
350 |
:-( |
Filter = mam_filter:produce_filter(ExdParams, lookup_fields()), |
351 |
:-( |
mam_lookup:lookup(Env, Filter, ExdParams). |
352 |
|
|
353 |
|
lookup_query(QueryType, Env, Filters, Order, OffsetLimit) -> |
354 |
:-( |
mam_lookup_sql:lookup_query(QueryType, Env, Filters, Order, OffsetLimit). |