./ct_report/coverage/mod_mam_rdbms_arch.COVER.html

1 %%%-------------------------------------------------------------------
2 %%% @author Uvarov Michael <arcusfelis@gmail.com>
3 %%% @copyright (C) 2013, Uvarov Michael
4 %%% @doc RDBMS backend for Message Archive Management.
5 %%% @end
6 %%%-------------------------------------------------------------------
7 -module(mod_mam_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_pm_gdpr_data/3]).
30
31 %% Called from mod_mam_rdbms_async_pool_writer
32 -export([prepare_message/2, retract_message/2, prepare_insert/2]).
33
34 -ignore_xref([behaviour_info/1, remove_archive/4, remove_domain/3]).
35
36 -type host_type() :: mongooseim:host_type().
37
38 %% ----------------------------------------------------------------------
39 %% Imports
40
41 -include("mongoose.hrl").
42 -include("jlib.hrl").
43 -include_lib("exml/include/exml.hrl").
44 -include("mongoose_rsm.hrl").
45 -include("mongoose_mam.hrl").
46
47 %% ----------------------------------------------------------------------
48 %% Types
49
50 -type env_vars() :: #{
51 host_type := host_type(),
52 archive_jid := jid:jid(),
53 table := atom(),
54 index_hint_fn := fun((env_vars()) -> mam_lookup_sql:sql_part()),
55 columns_sql_fn := fun((mam_lookup_sql:query_type()) -> mam_lookup_sql:sql_part()),
56 column_to_id_fn := fun((mam_lookup_sql:column()) -> string()),
57 lookup_fn := mam_lookup_sql:lookup_query_fn(),
58 decode_row_fn := fun((Row :: tuple(), env_vars()) -> Decoded :: tuple()),
59 has_message_retraction := boolean(),
60 has_full_text_search := boolean(),
61 db_jid_codec := module(),
62 db_message_codec := module()
63 }.
64
65 -export_type([env_vars/0]).
66
67 %% ----------------------------------------------------------------------
68 %% gen_mod callbacks
69 %% Starting and stopping functions for users' archives
70
71 -spec start(host_type(), _) -> ok.
72 start(HostType, Opts) ->
73 1 start_hooks(HostType, Opts),
74 1 register_prepared_queries(),
75 1 ok.
76
77 -spec stop(host_type()) -> ok.
78 stop(HostType) ->
79
:-(
stop_hooks(HostType).
80
81 -spec supported_features() -> [atom()].
82 supported_features() ->
83 1 [dynamic_domains].
84
85 -spec get_mam_pm_gdpr_data(ejabberd_gen_mam_archive:mam_pm_gdpr_data(),
86 host_type(), jid:jid()) ->
87 ejabberd_gen_mam_archive:mam_pm_gdpr_data().
88 get_mam_pm_gdpr_data(Acc, HostType,
89 #jid{luser = LUser, lserver = LServer} = ArcJID) ->
90
:-(
case mod_mam:archive_id(LServer, LUser) of
91 undefined ->
92
:-(
Acc;
93 ArcID ->
94
:-(
Env = env_vars(HostType, ArcJID),
95
:-(
{selected, Rows} = extract_gdpr_messages(Env, ArcID),
96
:-(
[uniform_to_gdpr(row_to_uniform_format(Row, Env)) || Row <- Rows] ++ Acc
97 end.
98
99 -spec uniform_to_gdpr(mod_mam:message_row()) -> tuple().
100 uniform_to_gdpr(#{id := MessID, jid := RemoteJID, packet := Packet}) ->
101
:-(
{integer_to_binary(MessID), jid:to_binary(RemoteJID), exml:to_binary(Packet)}.
102
103 %% ----------------------------------------------------------------------
104 %% Add hooks for mod_mam
105
106 -spec start_hooks(host_type(), _) -> ok.
107 start_hooks(HostType, _Opts) ->
108 1 ejabberd_hooks:add(hooks(HostType)).
109
110 -spec stop_hooks(host_type()) -> ok.
111 stop_hooks(HostType) ->
112
:-(
ejabberd_hooks:delete(hooks(HostType)).
113
114 hooks(HostType) ->
115 1 NoWriter = gen_mod:get_module_opt(HostType, ?MODULE, no_writer, false),
116 case NoWriter of
117 true ->
118
:-(
[];
119 false ->
120 1 [{mam_archive_message, HostType, ?MODULE, archive_message, 50}]
121 1 end ++
122 [{mam_archive_size, HostType, ?MODULE, archive_size, 50},
123 {mam_lookup_messages, HostType, ?MODULE, lookup_messages, 50},
124 {mam_remove_archive, HostType, ?MODULE, remove_archive, 50},
125 {remove_domain, HostType, ?MODULE, remove_domain, 50},
126 {get_mam_pm_gdpr_data, HostType, ?MODULE, get_mam_pm_gdpr_data, 50}].
127
128 %% ----------------------------------------------------------------------
129 %% SQL queries
130
131 register_prepared_queries() ->
132 1 prepare_insert(insert_mam_message, 1),
133 1 mongoose_rdbms:prepare(mam_archive_remove, mam_message, [user_id],
134 <<"DELETE FROM mam_message "
135 "WHERE user_id = ?">>),
136 1 mongoose_rdbms:prepare(mam_remove_domain, mam_message, ['mam_server_user.server'],
137 <<"DELETE FROM mam_message "
138 "WHERE user_id IN "
139 "(SELECT id from mam_server_user WHERE server = ?)">>),
140 1 mongoose_rdbms:prepare(mam_remove_domain_prefs, mam_config, ['mam_server_user.server'],
141 <<"DELETE FROM mam_config "
142 "WHERE user_id IN "
143 "(SELECT id from mam_server_user WHERE server = ?)">>),
144 1 mongoose_rdbms:prepare(mam_remove_domain_users, mam_server_user, [server],
145 <<"DELETE FROM mam_server_user WHERE server = ?">>),
146 1 mongoose_rdbms:prepare(mam_make_tombstone, mam_message, [message, user_id, id],
147 <<"UPDATE mam_message SET message = ?, search_body = '' "
148 "WHERE user_id = ? AND id = ?">>),
149 1 {LimitSQL, LimitMSSQL} = rdbms_queries:get_db_specific_limits_binaries(1),
150 1 mongoose_rdbms:prepare(mam_select_messages_to_retract_on_origin_id, mam_message,
151 [user_id, remote_bare_jid, origin_id, direction],
152 <<"SELECT ", LimitMSSQL/binary,
153 " id, message FROM mam_message"
154 " WHERE user_id = ? AND remote_bare_jid = ? "
155 " AND origin_id = ? AND direction = ?"
156 " ORDER BY id DESC ", LimitSQL/binary>>),
157 1 mongoose_rdbms:prepare(mam_select_messages_to_retract_on_stanza_id, mam_message,
158 [user_id, remote_bare_jid, id, direction],
159 <<"SELECT ", LimitMSSQL/binary,
160 " origin_id, message FROM mam_message"
161 " WHERE user_id = ? AND remote_bare_jid = ? "
162 " AND id = ? AND direction = ?"
163 " ORDER BY id DESC ", LimitSQL/binary>>).
164
165 %% ----------------------------------------------------------------------
166 %% Declarative logic
167
168 db_mappings() ->
169 %% One entry per the database field
170 61 [#db_mapping{column = id, param = message_id, format = int},
171 #db_mapping{column = user_id, param = archive_id, format = int},
172 #db_mapping{column = remote_bare_jid, param = remote_jid, format = bare_jid},
173 #db_mapping{column = remote_resource, param = remote_jid, format = jid_resource},
174 #db_mapping{column = direction, param = direction, format = direction},
175 #db_mapping{column = from_jid, param = source_jid, format = jid},
176 #db_mapping{column = origin_id, param = origin_id, format = maybe_string},
177 #db_mapping{column = message, param = packet, format = xml},
178 #db_mapping{column = search_body, param = packet, format = search}].
179
180 lookup_fields() ->
181 %% Describe each possible filtering option
182 12 [#lookup_field{op = equal, column = user_id, param = archive_id, required = true},
183 #lookup_field{op = ge, column = id, param = start_id},
184 #lookup_field{op = le, column = id, param = end_id},
185 #lookup_field{op = equal, column = remote_bare_jid, param = remote_bare_jid},
186 #lookup_field{op = equal, column = remote_resource, param = remote_resource},
187 #lookup_field{op = like, column = search_body, param = norm_search_text, value_maker = search_words}].
188
189 -spec env_vars(host_type(), jid:jid()) -> env_vars().
190 env_vars(HostType, ArcJID) ->
191 %% Please, minimize the usage of the host_type field.
192 %% It's only for passing into RDBMS.
193 72 #{host_type => HostType,
194 archive_jid => ArcJID,
195 table => mam_message,
196 index_hint_fn => fun index_hint_sql/1,
197 columns_sql_fn => fun columns_sql/1,
198 column_to_id_fn => fun column_to_id/1,
199 lookup_fn => fun lookup_query/5,
200 decode_row_fn => fun row_to_uniform_format/2,
201 has_message_retraction => mod_mam_utils:has_message_retraction(mod_mam, HostType),
202 has_full_text_search => mod_mam_utils:has_full_text_search(mod_mam, HostType),
203 db_jid_codec => db_jid_codec(HostType, ?MODULE),
204 db_message_codec => db_message_codec(HostType, ?MODULE)}.
205
206 row_to_uniform_format(Row, Env) ->
207 8 mam_decoder:decode_row(Row, Env).
208
209 -spec index_hint_sql(env_vars()) -> string().
210 index_hint_sql(#{host_type := HostType}) ->
211 1 case mongoose_rdbms:db_engine(HostType) of
212
:-(
mysql -> "USE INDEX(PRIMARY, i_mam_message_rem) ";
213 1 _ -> ""
214 end.
215
216 1 columns_sql(lookup) -> "id, from_jid, message";
217
:-(
columns_sql(count) -> "COUNT(*)".
218
219 %% For each unique column in lookup_fields()
220
:-(
column_to_id(id) -> "i";
221 12 column_to_id(user_id) -> "u";
222
:-(
column_to_id(remote_bare_jid) -> "b";
223
:-(
column_to_id(remote_resource) -> "r";
224
:-(
column_to_id(search_body) -> "s".
225
226 column_names(Mappings) ->
227 1 [Column || #db_mapping{column = Column} <- Mappings].
228
229 %% ----------------------------------------------------------------------
230 %% Options
231
232 -spec db_jid_codec(host_type(), module()) -> module().
233 db_jid_codec(HostType, Module) ->
234 72 gen_mod:get_module_opt(HostType, Module, db_jid_format, mam_jid_mini).
235
236 -spec db_message_codec(host_type(), module()) -> module().
237 db_message_codec(HostType, Module) ->
238 72 gen_mod:get_module_opt(HostType, Module, db_message_format, mam_message_compressed_eterm).
239
240 -spec get_retract_id(exml:element(), env_vars()) -> none | mod_mam_utils:retraction_id().
241 get_retract_id(Packet, #{has_message_retraction := Enabled}) ->
242 60 mod_mam_utils:get_retract_id(Enabled, Packet).
243
244 %% ----------------------------------------------------------------------
245 %% Internal functions and callbacks
246
247 -spec archive_size(Size :: integer(), HostType :: host_type(),
248 ArcId :: mod_mam:archive_id(), ArcJID :: jid:jid()) -> integer().
249 archive_size(Size, HostType, ArcID, ArcJID) when is_integer(Size) ->
250
:-(
Filter = [{equal, user_id, ArcID}],
251
:-(
Env = env_vars(HostType, ArcJID),
252
:-(
Result = lookup_query(count, Env, Filter, unordered, all),
253
:-(
mongoose_rdbms:selected_to_integer(Result).
254
255 -spec archive_message(_Result, host_type(), mod_mam:archive_message_params()) -> ok.
256 archive_message(_Result, HostType, Params = #{local_jid := ArcJID}) ->
257 60 try
258 60 assert_archive_id_provided(Params),
259 60 Env = env_vars(HostType, ArcJID),
260 60 do_archive_message(HostType, Params, Env),
261 60 retract_message(HostType, Params, Env),
262 60 ok
263 catch error:Reason:StackTrace ->
264
:-(
?LOG_ERROR(#{what => archive_message_failed,
265 host_type => HostType, mam_params => Params,
266
:-(
reason => Reason, stacktrace => StackTrace}),
267
:-(
erlang:raise(error, Reason, StackTrace)
268 end.
269
270 do_archive_message(HostType, Params, Env) ->
271 60 Row = mam_encoder:encode_message(Params, Env, db_mappings()),
272 60 {updated, 1} = mongoose_rdbms:execute_successfully(HostType, insert_mam_message, Row).
273
274 %% Retraction logic
275 %% Called after inserting a new message
276 -spec retract_message(host_type(), mod_mam:archive_message_params()) -> ok.
277 retract_message(HostType, #{local_jid := ArcJID} = Params) ->
278
:-(
Env = env_vars(HostType, ArcJID),
279
:-(
retract_message(HostType, Params, Env).
280
281 -spec retract_message(host_type(), mod_mam:archive_message_params(), env_vars()) -> ok.
282 retract_message(HostType, #{archive_id := ArcID, remote_jid := RemJID,
283 direction := Dir, packet := Packet} = Params, Env) ->
284 60 case get_retract_id(Packet, Env) of
285 60 none -> ok;
286 RetractionId ->
287
:-(
Info = get_retraction_info(HostType, ArcID, RemJID, RetractionId, Dir, Env),
288
:-(
make_tombstone(HostType, ArcID, RetractionId, Info, Params, Env)
289 end.
290
291 get_retraction_info(HostType, ArcID, RemJID, RetractionId, Dir, Env) ->
292 %% Code style notice:
293 %% - Add Ext prefix for all externally encoded data
294 %% (in cases, when we usually add Bin, B, S Esc prefixes)
295
:-(
ExtBareRemJID = mam_encoder:encode_jid(jid:to_bare(RemJID), Env),
296
:-(
ExtDir = mam_encoder:encode_direction(Dir),
297
:-(
{selected, Rows} = execute_select_messages_to_retract(
298 HostType, ArcID, ExtBareRemJID, RetractionId, ExtDir),
299
:-(
mam_decoder:decode_retraction_info(Env, Rows, RetractionId).
300
301 make_tombstone(_HostType, ArcID, RetractionId, skip, _Params, _Env) ->
302
:-(
?LOG_INFO(#{what => make_tombstone_failed,
303 text => <<"Message to retract was not found">>,
304
:-(
user_id => ArcID, retraction_context => RetractionId});
305 make_tombstone(HostType, ArcID, _RetractionId,
306 RetractionInfo = #{message_id := MessID}, Params,
307 #{archive_jid := ArcJID} = Env) ->
308
:-(
RetractionInfo1 = mongoose_hooks:mam_retraction(HostType, RetractionInfo, Params),
309
:-(
Tombstone = mod_mam_utils:tombstone(RetractionInfo1, ArcJID),
310
:-(
TombstoneData = mam_encoder:encode_packet(Tombstone, Env),
311
:-(
execute_make_tombstone(HostType, TombstoneData, ArcID, MessID).
312
313 execute_select_messages_to_retract(HostType, ArcID, BareRemJID, {origin_id, OriginID}, Dir) ->
314
:-(
mongoose_rdbms:execute_successfully(HostType, mam_select_messages_to_retract_on_origin_id,
315 [ArcID, BareRemJID, OriginID, Dir]);
316 execute_select_messages_to_retract(HostType, ArcID, BareRemJID, {stanza_id, BinStanzaId}, Dir) ->
317
:-(
StanzaId = mod_mam_utils:external_binary_to_mess_id(BinStanzaId),
318
:-(
mongoose_rdbms:execute_successfully(HostType, mam_select_messages_to_retract_on_stanza_id,
319 [ArcID, BareRemJID, StanzaId, Dir]).
320
321 execute_make_tombstone(HostType, TombstoneData, ArcID, MessID) ->
322
:-(
mongoose_rdbms:execute_successfully(HostType, mam_make_tombstone,
323 [TombstoneData, ArcID, MessID]).
324
325 %% Insert logic
326 -spec prepare_message(host_type(), mod_mam:archive_message_params()) -> list().
327 prepare_message(HostType, Params = #{local_jid := ArcJID}) ->
328
:-(
Env = env_vars(HostType, ArcJID),
329
:-(
mam_encoder:encode_message(Params, Env, db_mappings()).
330
331 -spec prepare_insert(Name :: atom(), NumRows :: pos_integer()) -> ok.
332 prepare_insert(Name, NumRows) ->
333 1 Table = mam_message,
334 1 Fields = column_names(db_mappings()),
335 1 {Query, Fields2} = rdbms_queries:create_bulk_insert_query(Table, Fields, NumRows),
336 1 mongoose_rdbms:prepare(Name, Table, Fields2, Query),
337 1 ok.
338
339 %% Removal logic
340 -spec remove_archive(Acc :: mongoose_acc:t(), HostType :: host_type(),
341 ArcID :: mod_mam:archive_id(),
342 RoomJID :: jid:jid()) -> mongoose_acc:t().
343 remove_archive(Acc, HostType, ArcID, _ArcJID) ->
344
:-(
mongoose_rdbms:execute_successfully(HostType, mam_archive_remove, [ArcID]),
345
:-(
Acc.
346
347 -spec remove_domain(mongoose_hooks:simple_acc(), host_type(), jid:lserver()) ->
348 mongoose_hooks:simple_acc().
349 remove_domain(Acc, HostType, Domain) ->
350
:-(
{atomic, _} = mongoose_rdbms:sql_transaction(HostType, fun() ->
351
:-(
mongoose_rdbms:execute_successfully(HostType, mam_remove_domain, [Domain]),
352
:-(
mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_prefs, [Domain]),
353
:-(
mongoose_rdbms:execute_successfully(HostType, mam_remove_domain_users, [Domain])
354 end),
355
:-(
Acc.
356
357 %% GDPR logic
358 extract_gdpr_messages(Env, ArcID) ->
359
:-(
Filters = [{equal, user_id, ArcID}],
360
:-(
lookup_query(lookup, Env, Filters, asc, all).
361
362 %% Lookup logic
363 -spec lookup_messages(Result :: any(), HostType :: host_type(), Params :: map()) ->
364 {ok, mod_mam:lookup_result()}.
365 lookup_messages({error, _Reason}=Result, _HostType, _Params) ->
366
:-(
Result;
367 lookup_messages(_Result, HostType, Params = #{owner_jid := ArcJID}) ->
368 12 Env = env_vars(HostType, ArcJID),
369 12 ExdParams = mam_encoder:extend_lookup_params(Params, Env),
370 12 Filter = mam_filter:produce_filter(ExdParams, lookup_fields()),
371 12 mam_lookup:lookup(Env, Filter, ExdParams).
372
373 lookup_query(QueryType, Env, Filters, Order, OffsetLimit) ->
374 12 mam_lookup_sql:lookup_query(QueryType, Env, Filters, Order, OffsetLimit).
375
376 assert_archive_id_provided(#{archive_id := ArcID}) when is_integer(ArcID) ->
377 60 ok.
Line Hits Source