• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

processone / ejabberd / 1227

02 Dec 2025 11:38AM UTC coverage: 33.6% (+0.06%) from 33.536%
1227

push

github

prefiks
Add db serialization to mod_muc_sql

0 of 38 new or added lines in 1 file covered. (0.0%)

10614 existing lines in 161 files now uncovered.

15526 of 46208 relevant lines covered (33.6%)

1072.13 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.7
/src/mod_mam_sql.erl
1
%%%-------------------------------------------------------------------
2
%%% File    : mod_mam_sql.erl
3
%%% Author  : Evgeny Khramtsov <ekhramtsov@process-one.net>
4
%%% Created : 15 Apr 2016 by Evgeny Khramtsov <ekhramtsov@process-one.net>
5
%%%
6
%%%
7
%%% ejabberd, Copyright (C) 2002-2025   ProcessOne
8
%%%
9
%%% This program is free software; you can redistribute it and/or
10
%%% modify it under the terms of the GNU General Public License as
11
%%% published by the Free Software Foundation; either version 2 of the
12
%%% License, or (at your option) any later version.
13
%%%
14
%%% This program is distributed in the hope that it will be useful,
15
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
16
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17
%%% General Public License for more details.
18
%%%
19
%%% You should have received a copy of the GNU General Public License along
20
%%% with this program; if not, write to the Free Software Foundation, Inc.,
21
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22
%%%
23
%%%----------------------------------------------------------------------
24

25
-module(mod_mam_sql).
26

27

28
-behaviour(mod_mam).
29
-behaviour(ejabberd_db_serialize).
30

31
%% API
32
-export([init/2, remove_user/2, remove_room/3, delete_old_messages/3,
33
         extended_fields/1, store/10, write_prefs/4, get_prefs/2, select/7, export/1, remove_from_archive/3,
34
         is_empty_for_user/2, is_empty_for_room/3, select_with_mucsub/6,
35
         delete_old_messages_batch/4, count_messages_to_delete/3]).
36
-export([sql_schemas/0]).
37
-export([serialize/3, deserialize_start/1, deserialize/2]).
38

39
-include_lib("stdlib/include/ms_transform.hrl").
40
-include_lib("xmpp/include/xmpp.hrl").
41
-include("mod_mam.hrl").
42
-include("logger.hrl").
43
-include("ejabberd_sql_pt.hrl").
44
-include("mod_muc_room.hrl").
45
-include("ejabberd_db_serialize.hrl").
46

47
%%%===================================================================
48
%%% API
49
%%%===================================================================
50
init(Host, _Opts) ->
UNCOV
51
    ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
6✔
UNCOV
52
    ok.
6✔
53

54
sql_schemas() ->
UNCOV
55
    [#sql_schema{
6✔
56
        version = 2,
57
        tables =
58
            [#sql_table{
59
                name = <<"archive">>,
60
                columns =
61
                    [#sql_column{name = <<"username">>, type = text},
62
                     #sql_column{name = <<"server_host">>, type = text},
63
                     #sql_column{name = <<"timestamp">>, type = bigint},
64
                     #sql_column{name = <<"peer">>, type = text},
65
                     #sql_column{name = <<"bare_peer">>, type = text},
66
                     #sql_column{name = <<"xml">>, type = {text, big}},
67
                     #sql_column{name = <<"txt">>, type = {text, big}},
68
                     #sql_column{name = <<"id">>, type = bigserial},
69
                     #sql_column{name = <<"kind">>, type = {text, 10}},
70
                     #sql_column{name = <<"nick">>, type = text},
71
                     #sql_column{name = <<"origin_id">>, type = text},
72
                     #sql_column{name = <<"created_at">>, type = timestamp,
73
                                 default = true}],
74
                indices = [#sql_index{
75
                              columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]},
76
                           #sql_index{
77
                              columns = [<<"server_host">>, <<"username">>, <<"peer">>]},
78
                           #sql_index{
79
                              columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]},
80
                           #sql_index{
81
                              columns = [<<"server_host">>, <<"timestamp">>]},
82
                           #sql_index{
83
                              columns = [<<"server_host">>, <<"username">>, <<"origin_id">>]}
84
                          ],
85
                post_create =
86
                    fun(#sql_schema_info{db_type = mysql}) ->
UNCOV
87
                            [<<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>];
2✔
88
                       (_) ->
UNCOV
89
                            []
2✔
90
                    end},
91
             #sql_table{
92
                name = <<"archive_prefs">>,
93
                columns =
94
                    [#sql_column{name = <<"username">>, type = text},
95
                     #sql_column{name = <<"server_host">>, type = text},
96
                     #sql_column{name = <<"def">>, type = text},
97
                     #sql_column{name = <<"always">>, type = text},
98
                     #sql_column{name = <<"never">>, type = text},
99
                     #sql_column{name = <<"created_at">>, type = timestamp,
100
                                 default = true}],
101
                indices = [#sql_index{
102
                              columns = [<<"server_host">>, <<"username">>],
103
                              unique = true}]}],
104
        update =
105
            [{add_column, <<"archive">>, <<"origin_id">>},
106
             {create_index, <<"archive">>,
107
              [<<"server_host">>, <<"username">>, <<"origin_id">>]}
108
            ]},
109
     #sql_schema{
110
        version = 1,
111
        tables =
112
            [#sql_table{
113
                name = <<"archive">>,
114
                columns =
115
                    [#sql_column{name = <<"username">>, type = text},
116
                     #sql_column{name = <<"server_host">>, type = text},
117
                     #sql_column{name = <<"timestamp">>, type = bigint},
118
                     #sql_column{name = <<"peer">>, type = text},
119
                     #sql_column{name = <<"bare_peer">>, type = text},
120
                     #sql_column{name = <<"xml">>, type = {text, big}},
121
                     #sql_column{name = <<"txt">>, type = {text, big}},
122
                     #sql_column{name = <<"id">>, type = bigserial},
123
                     #sql_column{name = <<"kind">>, type = {text, 10}},
124
                     #sql_column{name = <<"nick">>, type = text},
125
                     #sql_column{name = <<"created_at">>, type = timestamp,
126
                                 default = true}],
127
                indices = [#sql_index{
128
                              columns = [<<"server_host">>, <<"username">>, <<"timestamp">>]},
129
                           #sql_index{
130
                              columns = [<<"server_host">>, <<"username">>, <<"peer">>]},
131
                           #sql_index{
132
                              columns = [<<"server_host">>, <<"username">>, <<"bare_peer">>]},
133
                           #sql_index{
134
                              columns = [<<"server_host">>, <<"timestamp">>]}
135
                          ],
136
                post_create =
137
                    fun(#sql_schema_info{db_type = mysql}) ->
138
                            ejabberd_sql:sql_query_t(
×
139
                              <<"CREATE FULLTEXT INDEX i_archive_txt ON archive(txt);">>);
140
                       (_) ->
141
                            ok
×
142
                    end},
143
             #sql_table{
144
                name = <<"archive_prefs">>,
145
                columns =
146
                    [#sql_column{name = <<"username">>, type = text},
147
                     #sql_column{name = <<"server_host">>, type = text},
148
                     #sql_column{name = <<"def">>, type = text},
149
                     #sql_column{name = <<"always">>, type = text},
150
                     #sql_column{name = <<"never">>, type = text},
151
                     #sql_column{name = <<"created_at">>, type = timestamp,
152
                                 default = true}],
153
                indices = [#sql_index{
154
                              columns = [<<"server_host">>, <<"username">>],
155
                              unique = true}]}]}].
156

157
remove_user(LUser, LServer) ->
UNCOV
158
    ejabberd_sql:sql_query(
720✔
159
      LServer,
UNCOV
160
      ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")),
720✔
UNCOV
161
    ejabberd_sql:sql_query(
720✔
162
      LServer,
UNCOV
163
      ?SQL("delete from archive_prefs where username=%(LUser)s and %(LServer)H")).
720✔
164

165
remove_room(LServer, LName, LHost) ->
UNCOV
166
    LUser = jid:encode({LName, LHost, <<>>}),
540✔
UNCOV
167
    remove_user(LUser, LServer).
540✔
168

169
remove_from_archive({LUser, LHost}, LServer, Key) ->
170
    remove_from_archive(jid:encode({LUser, LHost, <<>>}), LServer, Key);
×
171
remove_from_archive(LUser, LServer, none) ->
172
    case ejabberd_sql:sql_query(LServer,
×
173
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H")) of
×
174
        {error, Reason} -> {error, Reason};
×
175
        _ -> ok
×
176
    end;
177
remove_from_archive(LUser, LServer, #jid{} = WithJid) ->
178
    Peer = jid:encode(jid:remove_resource(WithJid)),
×
179
    case ejabberd_sql:sql_query(LServer,
×
180
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and bare_peer=%(Peer)s")) of
×
181
        {error, Reason} -> {error, Reason};
×
182
        _ -> ok
×
183
    end;
184
remove_from_archive(LUser, LServer, StanzaId) ->
185
    case ejabberd_sql:sql_query(LServer,
×
186
                                ?SQL("delete from archive where username=%(LUser)s and %(LServer)H and timestamp=%(StanzaId)d")) of
×
187
        {error, Reason} -> {error, Reason};
×
188
        _ -> ok
×
189
    end.
190

191
count_messages_to_delete(ServerHost, TimeStamp, Type) ->
192
    TS = misc:now_to_usec(TimeStamp),
×
193
    Res =
×
194
    case Type of
195
        all ->
196
            ejabberd_sql:sql_query(
×
197
                ServerHost,
198
                ?SQL("select count(*) from archive"
×
199
                     " where timestamp < %(TS)d and %(ServerHost)H"));
200
        _ ->
201
            SType = misc:atom_to_binary(Type),
×
202
            ejabberd_sql:sql_query(
×
203
                ServerHost,
204
                ?SQL("select @(count(*))d from archive"
×
205
                     " where timestamp < %(TS)d"
206
                     " and kind=%(SType)s"
207
                     " and %(ServerHost)H"))
208
    end,
209
    case Res of
×
210
        {selected, [Count]} ->
211
            {ok, Count};
×
212
        _ ->
213
            error
×
214
    end.
215

216
delete_old_messages_batch(ServerHost, TimeStamp, Type, Batch) ->
217
    TS = misc:now_to_usec(TimeStamp),
×
218
    Res =
×
219
        case Type of
220
            all ->
221
                ejabberd_sql:sql_query(
×
222
                    ServerHost,
223
                    fun(sqlite, _) ->
224
                        ejabberd_sql:sql_query_t(
×
225
                            ?SQL("delete from archive where rowid in "
×
226
                                 "(select rowid from archive where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d)"));
227
                       (mssql, _) ->
228
                           ejabberd_sql:sql_query_t(
×
229
                               ?SQL("delete top(%(Batch)d)§ from archive"
×
230
                                    " where timestamp < %(TS)d and %(ServerHost)H"));
231
                       (_, _) ->
232
                           ejabberd_sql:sql_query_t(
×
233
                               ?SQL("delete from archive"
×
234
                                    " where timestamp < %(TS)d and %(ServerHost)H limit %(Batch)d"))
235
                    end);
236
            _ ->
237
                SType = misc:atom_to_binary(Type),
×
238
                ejabberd_sql:sql_query(
×
239
                    ServerHost,
240
                    fun(sqlite,_)->
241
                        ejabberd_sql:sql_query_t(
×
242
                            ?SQL("delete from archive where rowid in ("
×
243
                                 " select rowid from archive where timestamp < %(TS)d"
244
                                 " and kind=%(SType)s"
245
                                 " and %(ServerHost)H limit %(Batch)d)"));
246
                       (mssql, _)->
247
                           ejabberd_sql:sql_query_t(
×
248
                               ?SQL("delete top(%(Batch)d) from archive"
×
249
                                    " where timestamp < %(TS)d"
250
                                    " and kind=%(SType)s"
251
                                    " and %(ServerHost)H"));
252
                       (_,_)->
253
                           ejabberd_sql:sql_query_t(
×
254
                               ?SQL("delete from archive"
×
255
                                    " where timestamp < %(TS)d"
256
                                    " and kind=%(SType)s"
257
                                    " and %(ServerHost)H limit %(Batch)d"))
258
                    end)
259
        end,
260
    case Res of
×
261
        {updated, Count} ->
262
            {ok, Count};
×
263
        {error, _} = Error ->
264
            Error
×
265
    end.
266

267
delete_old_messages(ServerHost, TimeStamp, Type) ->
268
    TS = misc:now_to_usec(TimeStamp),
×
269
    case Type of
×
270
        all ->
271
            ejabberd_sql:sql_query(
×
272
              ServerHost,
273
              ?SQL("delete from archive"
×
274
                   " where timestamp < %(TS)d and %(ServerHost)H"));
275
        _ ->
276
            SType = misc:atom_to_binary(Type),
×
277
            ejabberd_sql:sql_query(
×
278
              ServerHost,
279
              ?SQL("delete from archive"
×
280
                   " where timestamp < %(TS)d"
281
                   " and kind=%(SType)s"
282
                   " and %(ServerHost)H"))
283
    end,
284
    ok.
×
285

286
extended_fields(LServer) ->
UNCOV
287
    case ejabberd_option:sql_type(LServer) of
36✔
288
        mysql ->
UNCOV
289
            [{withtext, <<"">>},
12✔
290
             #xdata_field{var = <<"{urn:xmpp:fulltext:0}fulltext">>,
291
                          type = 'text-single',
292
                          label = <<"Search the text">>,
293
                          values = []}];
294
        _ ->
UNCOV
295
            []
24✔
296
    end.
297

298
store(Pkt, LServer, {LUser, LHost}, Type, Peer, Nick, _Dir, TS,
299
      OriginID, Retract) ->
UNCOV
300
    SUser = case Type of
1,644✔
UNCOV
301
                chat -> LUser;
1,512✔
UNCOV
302
                groupchat -> jid:encode({LUser, LHost, <<>>})
132✔
303
            end,
UNCOV
304
    BarePeer = jid:encode(
1,644✔
305
                 jid:tolower(
306
                   jid:remove_resource(Peer))),
UNCOV
307
    LPeer = jid:encode(
1,644✔
308
              jid:tolower(Peer)),
UNCOV
309
    Body = fxml:get_subtag_cdata(Pkt, <<"body">>),
1,644✔
UNCOV
310
    SType = misc:atom_to_binary(Type),
1,644✔
UNCOV
311
    SqlType = ejabberd_option:sql_type(LServer),
1,644✔
UNCOV
312
    XML = case mod_mam_opt:compress_xml(LServer) of
1,644✔
313
              true ->
314
                  J1 = case Type of
×
315
                              chat -> jid:encode({LUser, LHost, <<>>});
×
316
                              groupchat -> SUser
×
317
                          end,
318
                  xml_compress:encode(Pkt, J1, LPeer);
×
319
              _ ->
UNCOV
320
                  fxml:element_to_binary(Pkt)
1,644✔
321
          end,
UNCOV
322
    case Retract of
1,644✔
323
        {true, RID} ->
UNCOV
324
            ejabberd_sql:sql_query(
66✔
325
              LServer,
UNCOV
326
              ?SQL("delete from archive"
66✔
327
                   " where username=%(SUser)s"
328
                   " and %(LServer)H"
329
                   " and bare_peer=%(BarePeer)s"
330
                   " and origin_id=%(RID)s"));
UNCOV
331
        false -> ok
1,578✔
332
    end,
UNCOV
333
    case SqlType of
1,644✔
334
        mssql -> case ejabberd_sql:sql_query(
×
335
                   LServer,
336
                   ?SQL_INSERT(
×
337
                      "archive",
338
                      ["username=%(SUser)s",
339
                       "server_host=%(LServer)s",
340
                       "timestamp=%(TS)d",
341
                       "peer=%(LPeer)s",
342
                       "bare_peer=%(BarePeer)s",
343
                       "xml=N%(XML)s",
344
                       "txt=N%(Body)s",
345
                       "kind=%(SType)s",
346
                       "nick=%(Nick)s",
347
                       "origin_id=%(OriginID)s"])) of
348
                {updated, _} ->
349
                    ok;
×
350
                Err ->
351
                    Err
×
352
            end;
UNCOV
353
        _ -> case ejabberd_sql:sql_query(
1,644✔
354
                   LServer,
UNCOV
355
                   ?SQL_INSERT(
1,644✔
356
                      "archive",
357
                      ["username=%(SUser)s",
358
                       "server_host=%(LServer)s",
359
                       "timestamp=%(TS)d",
360
                       "peer=%(LPeer)s",
361
                       "bare_peer=%(BarePeer)s",
362
                       "xml=%(XML)s",
363
                       "txt=%(Body)s",
364
                       "kind=%(SType)s",
365
                       "nick=%(Nick)s",
366
                       "origin_id=%(OriginID)s"])) of
367
                {updated, _} ->
UNCOV
368
                    ok;
1,644✔
369
                Err ->
370
                    Err
×
371
            end
372
    end.
373

374
write_prefs(LUser, _LServer, #archive_prefs{default = Default,
375
                                           never = Never,
376
                                           always = Always},
377
            ServerHost) ->
UNCOV
378
    SDefault = erlang:atom_to_binary(Default, utf8),
966✔
UNCOV
379
    SAlways = misc:term_to_expr(Always),
966✔
UNCOV
380
    SNever = misc:term_to_expr(Never),
966✔
UNCOV
381
    case ?SQL_UPSERT(
966✔
UNCOV
382
            ServerHost,
966✔
383
            "archive_prefs",
384
            ["!username=%(LUser)s",
385
             "!server_host=%(ServerHost)s",
386
             "def=%(SDefault)s",
387
             "always=%(SAlways)s",
388
             "never=%(SNever)s"]) of
389
        ok ->
UNCOV
390
            ok;
966✔
391
        Err ->
392
            Err
×
393
    end.
394

395
get_prefs(LUser, LServer) ->
UNCOV
396
    case ejabberd_sql:sql_query(
720✔
397
           LServer,
UNCOV
398
           ?SQL("select @(def)s, @(always)s, @(never)s from archive_prefs"
720✔
399
                " where username=%(LUser)s and %(LServer)H")) of
400
        {selected, [{SDefault, SAlways, SNever}]} ->
UNCOV
401
            Default = erlang:binary_to_existing_atom(SDefault, utf8),
678✔
UNCOV
402
            Always = ejabberd_sql:decode_term(SAlways),
678✔
UNCOV
403
            Never = ejabberd_sql:decode_term(SNever),
678✔
UNCOV
404
            {ok, #archive_prefs{us = {LUser, LServer},
678✔
405
                    default = Default,
406
                    always = Always,
407
                    never = Never}};
408
        _ ->
UNCOV
409
            error
42✔
410
    end.
411

412
select(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
413
       MAMQuery, RSM, MsgType, Flags) ->
UNCOV
414
    User = case MsgType of
1,176✔
UNCOV
415
               chat -> LUser;
1,152✔
UNCOV
416
               _ -> jid:encode(JidArchive)
24✔
417
           end,
UNCOV
418
    {Query, CountQuery} = make_sql_query(User, LServer, MAMQuery, RSM, none),
1,176✔
UNCOV
419
    do_select_query(LServer, JidRequestor, JidArchive, RSM, MsgType, Query, CountQuery, Flags).
1,176✔
420

421
-spec select_with_mucsub(binary(), jid(), jid(), mam_query:result(),
422
                             #rsm_set{} | undefined, all | only_count | only_messages) ->
423
                                {[{binary(), non_neg_integer(), xmlel()}], boolean(), non_neg_integer()} |
424
                                {error, db_failure}.
425
select_with_mucsub(LServer, JidRequestor, #jid{luser = LUser} = JidArchive,
426
                   MAMQuery, RSM, Flags) ->
UNCOV
427
    Extra = case gen_mod:db_mod(LServer, mod_muc) of
18✔
428
                mod_muc_sql ->
UNCOV
429
                    subscribers_table;
18✔
430
                _ ->
431
                    SubRooms = case mod_muc_admin:find_hosts(LServer) of
×
432
                                   [First|_] ->
433
                                       case mod_muc:get_subscribed_rooms(First, JidRequestor) of
×
434
                                           {ok, L} -> L;
×
435
                                           {error, _} -> []
×
436
                                       end;
437
                                   _ ->
438
                                       []
×
439
                               end,
440
                    [jid:encode(Jid) || {Jid, _, _} <- SubRooms]
×
441
            end,
UNCOV
442
    {Query, CountQuery} = make_sql_query(LUser, LServer, MAMQuery, RSM, Extra),
18✔
UNCOV
443
    do_select_query(LServer, JidRequestor, JidArchive, RSM, chat, Query, CountQuery, Flags).
18✔
444

445
do_select_query(LServer, JidRequestor, #jid{luser = LUser} = JidArchive, RSM,
446
                MsgType, Query, CountQuery, Flags) ->
447
    % TODO from XEP-0313 v0.2: "To conserve resources, a server MAY place a
448
    % reasonable limit on how many stanzas may be pushed to a client in one
449
    % request. If a query returns a number of stanzas greater than this limit
450
    % and the client did not specify a limit using RSM then the server should
451
    % return a policy-violation error to the client." We currently don't do this
452
    % for v0.2 requests, but we do limit #rsm_in.max for v0.3 and newer.
UNCOV
453
    QRes = case Flags of
1,194✔
454
                   all ->
UNCOV
455
                       {ejabberd_sql:sql_query(LServer, Query), ejabberd_sql:sql_query(LServer, CountQuery)};
1,146✔
456
                   only_messages ->
UNCOV
457
                       {ejabberd_sql:sql_query(LServer, Query), {selected, ok, [[<<"0">>]]}};
12✔
458
                   only_count ->
UNCOV
459
                       {{selected, ok, []}, ejabberd_sql:sql_query(LServer, CountQuery)}
36✔
460
               end,
UNCOV
461
    case QRes of
1,194✔
462
        {{selected, _, Res}, {selected, _, [[Count]]}} ->
UNCOV
463
            {Max, Direction, _} = get_max_direction_id(RSM),
1,194✔
UNCOV
464
            {Res1, IsComplete} =
1,194✔
465
            if Max >= 0 andalso Max /= undefined andalso length(Res) > Max ->
UNCOV
466
                if Direction == before ->
288✔
UNCOV
467
                    {lists:nthtail(1, Res), false};
48✔
468
                    true ->
UNCOV
469
                        {lists:sublist(Res, Max), false}
240✔
470
                end;
471
                true ->
UNCOV
472
                    {Res, true}
906✔
473
            end,
UNCOV
474
            MucState = #state{config = #config{anonymous = true}},
1,194✔
UNCOV
475
            JidArchiveS = jid:encode(jid:remove_resource(JidArchive)),
1,194✔
UNCOV
476
            {lists:flatmap(
1,194✔
477
                fun([TS, XML, PeerBin, Kind, Nick]) ->
UNCOV
478
                    case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick,
4,392✔
479
                                         MsgType, JidRequestor, JidArchive) of
480
                        {ok, El} ->
UNCOV
481
                            [{TS, binary_to_integer(TS), El}];
4,392✔
482
                        {error, _} ->
483
                            []
×
484
                    end;
485
                   ([User, TS, XML, PeerBin, Kind, Nick]) when User == LUser ->
486
                       case make_archive_el(JidArchiveS, TS, XML, PeerBin, Kind, Nick,
×
487
                                            MsgType, JidRequestor, JidArchive) of
488
                           {ok, El} ->
489
                               [{TS, binary_to_integer(TS), El}];
×
490
                           {error, _} ->
491
                               []
×
492
                       end;
493
                   ([User, TS, XML, PeerBin, Kind, Nick]) ->
UNCOV
494
                       case make_archive_el(User, TS, XML, PeerBin, Kind, Nick,
66✔
495
                                            {groupchat, member, MucState}, JidRequestor,
496
                                            jid:decode(User)) of
497
                           {ok, El} ->
UNCOV
498
                               mod_mam:wrap_as_mucsub([{TS, binary_to_integer(TS), El}],
66✔
499
                                                      JidRequestor);
500
                           {error, _} ->
501
                               []
×
502
                       end
503
                end, Res1), IsComplete, binary_to_integer(Count)};
504
        _ ->
505
            {[], false, 0}
×
506
    end.
507

508
export(_Server) ->
509
    [{archive_prefs,
×
510
      fun(Host, #archive_prefs{us =
511
                {LUser, LServer},
512
                default = Default,
513
                always = Always,
514
                never = Never})
515
          when LServer == Host ->
516
                SDefault = erlang:atom_to_binary(Default, utf8),
×
517
                SAlways = misc:term_to_expr(Always),
×
518
                SNever = misc:term_to_expr(Never),
×
519
                [?SQL_INSERT(
×
520
                    "archive_prefs",
521
                    ["username=%(LUser)s",
522
                     "server_host=%(LServer)s",
523
                     "def=%(SDefault)s",
524
                     "always=%(SAlways)s",
525
                     "never=%(SNever)s"])];
526
          (_Host, _R) ->
527
              []
×
528
      end},
529
     {archive_msg,
530
      fun([Host | HostTail], #archive_msg{us ={LUser, LServer},
531
                id = _ID, timestamp = TS, peer = Peer,
532
                type = Type, nick = Nick, packet = Pkt, origin_id = OriginID})
533
          when (LServer == Host) or ([LServer] == HostTail)  ->
534
                TStmp = misc:now_to_usec(TS),
×
535
                SUser = case Type of
×
536
                      chat -> LUser;
×
537
                      groupchat -> jid:encode({LUser, LServer, <<>>})
×
538
                    end,
539
                BarePeer = jid:encode(jid:tolower(jid:remove_resource(Peer))),
×
540
                LPeer = jid:encode(jid:tolower(Peer)),
×
541
                XML = fxml:element_to_binary(Pkt),
×
542
                Body = fxml:get_subtag_cdata(Pkt, <<"body">>),
×
543
                SType = misc:atom_to_binary(Type),
×
544
                SqlType = ejabberd_option:sql_type(Host),
×
545
                case SqlType of
×
546
                        mssql -> [?SQL_INSERT(
×
547
                            "archive",
548
                            ["username=%(SUser)s",
549
                             "server_host=%(LServer)s",
550
                             "timestamp=%(TStmp)d",
551
                             "peer=%(LPeer)s",
552
                             "bare_peer=%(BarePeer)s",
553
                             "xml=N%(XML)s",
554
                             "txt=N%(Body)s",
555
                             "kind=%(SType)s",
556
                             "nick=%(Nick)s",
557
                             "origin_id=%(OriginID)s"])];
558
                        _ -> [?SQL_INSERT(
×
559
                            "archive",
560
                            ["username=%(SUser)s",
561
                             "server_host=%(LServer)s",
562
                             "timestamp=%(TStmp)d",
563
                             "peer=%(LPeer)s",
564
                             "bare_peer=%(BarePeer)s",
565
                             "xml=%(XML)s",
566
                             "txt=%(Body)s",
567
                             "kind=%(SType)s",
568
                             "nick=%(Nick)s",
569
                             "origin_id=%(OriginID)s"])]
570
                            end;
571
         (_Host, _R) ->
572
              []
×
573
      end}].
574

575
is_empty_for_user(LUser, LServer) ->
576
    case ejabberd_sql:sql_query(
×
577
           LServer,
578
           ?SQL("select @(1)d from archive"
×
579
                " where username=%(LUser)s and %(LServer)H limit 1")) of
580
        {selected, [{1}]} ->
581
            false;
×
582
        _ ->
583
            true
×
584
    end.
585

586
is_empty_for_room(LServer, LName, LHost) ->
587
    LUser = jid:encode({LName, LHost, <<>>}),
×
588
    is_empty_for_user(LUser, LServer).
×
589

590
%%%===================================================================
591
%%% Internal functions
592
%%%===================================================================
593
make_sql_query(User, LServer, MAMQuery, RSM, ExtraUsernames) ->
UNCOV
594
    Start = proplists:get_value(start, MAMQuery),
1,194✔
UNCOV
595
    End = proplists:get_value('end', MAMQuery),
1,194✔
UNCOV
596
    With = proplists:get_value(with, MAMQuery),
1,194✔
UNCOV
597
    WithText = proplists:get_value(withtext, MAMQuery),
1,194✔
UNCOV
598
    {Max, Direction, ID} = get_max_direction_id(RSM),
1,194✔
UNCOV
599
    ODBCType = ejabberd_option:sql_type(LServer),
1,194✔
UNCOV
600
    ToString = fun(S) -> ejabberd_sql:to_string_literal(ODBCType, S) end,
1,194✔
UNCOV
601
    LimitClause = if is_integer(Max), Max >= 0, ODBCType /= mssql ->
1,194✔
UNCOV
602
                          [<<" limit ">>, integer_to_binary(Max+1)];
966✔
603
                     true ->
UNCOV
604
                          []
228✔
605
                  end,
UNCOV
606
    TopClause = if is_integer(Max), Max >= 0, ODBCType == mssql ->
1,194✔
607
                          [<<" TOP ">>, integer_to_binary(Max+1)];
×
608
                     true ->
UNCOV
609
                          []
1,194✔
610
                  end,
UNCOV
611
    SubOrderClause = if LimitClause /= []; TopClause /= [] ->
1,194✔
UNCOV
612
                          <<" ORDER BY timestamp DESC ">>;
966✔
613
                     true ->
UNCOV
614
                          []
228✔
615
                  end,
UNCOV
616
    WithTextClause = if is_binary(WithText), WithText /= <<>> ->
1,194✔
617
                             [<<" and match (txt) against (">>,
×
618
                              ToString(WithText), <<")">>];
619
                        true ->
UNCOV
620
                             []
1,194✔
621
                     end,
UNCOV
622
    WithClause = case catch jid:tolower(With) of
1,194✔
623
                     {_, _, <<>>} ->
UNCOV
624
                         [<<" and bare_peer=">>,
48✔
625
                          ToString(jid:encode(With))];
626
                     {_, _, _} ->
UNCOV
627
                         [<<" and peer=">>,
48✔
628
                          ToString(jid:encode(With))];
629
                     _ ->
UNCOV
630
                         []
1,098✔
631
                 end,
UNCOV
632
    PageClause = case catch binary_to_integer(ID) of
1,194✔
633
                     I when is_integer(I), I >= 0 ->
UNCOV
634
                         case Direction of
492✔
635
                             before ->
UNCOV
636
                                 [<<" AND timestamp < ">>, ID];
252✔
637
                             'after' ->
UNCOV
638
                                 [<<" AND timestamp > ">>, ID];
240✔
639
                             _ ->
640
                                 []
×
641
                         end;
642
                     _ ->
UNCOV
643
                         []
702✔
644
                 end,
UNCOV
645
    StartClause = case Start of
1,194✔
646
                      {_, _, _} ->
UNCOV
647
                          [<<" and timestamp >= ">>,
12✔
648
                           integer_to_binary(misc:now_to_usec(Start))];
649
                      _ ->
UNCOV
650
                          []
1,182✔
651
                  end,
UNCOV
652
    EndClause = case End of
1,194✔
653
                    {_, _, _} ->
654
                        [<<" and timestamp <= ">>,
×
655
                         integer_to_binary(misc:now_to_usec(End))];
656
                    _ ->
UNCOV
657
                        []
1,194✔
658
                end,
UNCOV
659
    SUser = ToString(User),
1,194✔
UNCOV
660
    SServer = ToString(LServer),
1,194✔
661

UNCOV
662
    HostMatch = case ejabberd_sql:use_multihost_schema() of
1,194✔
663
                    true ->
664
                        [<<" and server_host=", SServer/binary>>];
597✔
665
                    _ ->
UNCOV
666
                        <<"">>
597✔
667
                end,
668

UNCOV
669
    {UserSel, UserWhere} = case ExtraUsernames of
1,194✔
670
                               Users when is_list(Users) ->
671
                                   EscUsers = [ToString(U) || U <- [User | Users]],
×
672
                                   {<<" username,">>,
×
673
                                    [<<" username in (">>, str:join(EscUsers, <<",">>), <<")">>]};
674
                               subscribers_table ->
UNCOV
675
                                   SJid = ToString(jid:encode({User, LServer, <<>>})),
18✔
UNCOV
676
                                   RoomName = case ODBCType of
18✔
677
                                                  sqlite ->
UNCOV
678
                                                      <<"room || '@' || host">>;
6✔
679
                                                  _ ->
UNCOV
680
                                                      <<"concat(room, '@', host)">>
12✔
681
                                              end,
UNCOV
682
                                   {<<" username,">>,
18✔
683
                                    [<<" (username = ">>, SUser,
684
                                        <<" or username in (select ">>, RoomName,
685
                                          <<" from muc_room_subscribers where jid=">>, SJid, HostMatch, <<"))">>]};
686
                               _ ->
UNCOV
687
                                   {<<>>, [<<" username=">>, SUser]}
1,176✔
688
                           end,
689

UNCOV
690
    Query = [<<"SELECT ">>, TopClause, UserSel,
1,194✔
691
             <<" timestamp, xml, peer, kind, nick"
692
               " FROM archive WHERE">>, UserWhere, HostMatch,
693
             WithClause, WithTextClause,
694
             StartClause, EndClause, PageClause],
695

UNCOV
696
    QueryPage =
1,194✔
697
        case Direction of
698
            before ->
699
                % ID can be empty because of
700
                % XEP-0059: Result Set Management
701
                % 2.5 Requesting the Last Page in a Result Set
UNCOV
702
                [<<"SELECT">>, UserSel, <<" timestamp, xml, peer, kind, nick FROM (">>,
348✔
703
                 Query, SubOrderClause,
704
                 LimitClause, <<") AS t ORDER BY timestamp ASC;">>];
705
            _ ->
UNCOV
706
                [Query, <<" ORDER BY timestamp ASC ">>,
846✔
707
                 LimitClause, <<";">>]
708
        end,
UNCOV
709
    {QueryPage,
1,194✔
710
     [<<"SELECT COUNT(*) FROM archive WHERE ">>, UserWhere,
711
      HostMatch, WithClause, WithTextClause,
712
      StartClause, EndClause, <<";">>]}.
713

714
-spec get_max_direction_id(rsm_set() | undefined) ->
715
                                  {integer() | undefined,
716
                                   before | 'after' | undefined,
717
                                   binary()}.
718
get_max_direction_id(RSM) ->
UNCOV
719
    case RSM of
2,388✔
720
        #rsm_set{max = Max, before = Before} when is_binary(Before) ->
UNCOV
721
            {Max, before, Before};
696✔
722
        #rsm_set{max = Max, 'after' = After} when is_binary(After) ->
UNCOV
723
            {Max, 'after', After};
480✔
724
        #rsm_set{max = Max} ->
UNCOV
725
            {Max, undefined, <<>>};
1,140✔
726
        _ ->
UNCOV
727
            {undefined, undefined, <<>>}
72✔
728
    end.
729

730
-spec make_archive_el(binary(), binary(), binary(), binary(), binary(),
731
                      binary(), _, jid(), jid()) ->
732
                             {ok, xmpp_element()} | {error, invalid_jid |
733
                                                     invalid_timestamp |
734
                                                     invalid_xml}.
735
make_archive_el(User, TS, XML, Peer, Kind, Nick, MsgType, JidRequestor, JidArchive) ->
UNCOV
736
    case xml_compress:decode(XML, User, Peer) of
4,458✔
737
        #xmlel{} = El ->
UNCOV
738
            try binary_to_integer(TS) of
4,458✔
739
                TSInt ->
UNCOV
740
                    try jid:decode(Peer) of
4,458✔
741
                        PeerJID ->
UNCOV
742
                            Now = misc:usec_to_now(TSInt),
4,458✔
UNCOV
743
                            PeerLJID = jid:tolower(PeerJID),
4,458✔
UNCOV
744
                            T = case Kind of
4,458✔
745
                                    <<"">> -> chat;
×
746
                                    null -> chat;
×
UNCOV
747
                                    _ -> misc:binary_to_atom(Kind)
4,458✔
748
                                end,
UNCOV
749
                            mod_mam:msg_to_el(
4,458✔
750
                              #archive_msg{timestamp = Now,
751
                                           id = TS,
752
                                           packet = El,
753
                                           type = T,
754
                                           nick = Nick,
755
                                           peer = PeerLJID},
756
                              MsgType, JidRequestor, JidArchive)
757
                    catch _:{bad_jid, _} ->
758
                            ?ERROR_MSG("Malformed 'peer' field with value "
×
759
                                       "'~ts' detected for user ~ts in table "
760
                                       "'archive': invalid JID",
761
                                       [Peer, jid:encode(JidArchive)]),
×
762
                            {error, invalid_jid}
×
763
                    end
764
            catch _:_ ->
765
                    ?ERROR_MSG("Malformed 'timestamp' field with value '~ts' "
×
766
                               "detected for user ~ts in table 'archive': "
767
                               "not an integer",
768
                               [TS, jid:encode(JidArchive)]),
×
769
                    {error, invalid_timestamp}
×
770
            end;
771
        {error, {_, Reason}} ->
772
            ?ERROR_MSG("Malformed 'xml' field with value '~ts' detected "
×
773
                       "for user ~ts in table 'archive': ~ts",
774
                       [XML, jid:encode(JidArchive), Reason]),
×
775
            {error, invalid_xml}
×
776
    end.
777

778
serialize(LServer, BatchSize, undefined) ->
779
    serialize(LServer, BatchSize, {prefs, 0});
×
780
serialize(LServer, BatchSize, {prefs, Offset}) ->
781
    case ejabberd_sql:sql_query(
×
782
           LServer,
783
           ?SQL("select @(username)s, @(def)s, @(always)s, @(never)s from archive_prefs"
×
784
                " where %(LServer)H "
785
                "order by username "
786
                "limit %(BatchSize)d offset %(Offset)d")) of
787
        {selected, Rows} ->
788
            Data = lists:foldl(
×
789
                     fun(_,
790
                         {error, _} =
791
                             Err) ->
792
                             Err;
×
793
                        ({Username, SDefault, SAlways, SNever}, Acc) ->
794
                             try {erlang:binary_to_existing_atom(SDefault, utf8),
×
795
                                  ejabberd_sql:decode_term(SAlways),
796
                                  ejabberd_sql:decode_term(SNever)} of
797
                                 {Default, Always, Never} ->
798
                                     [#serialize_mam_prefs_v1{
×
799
                                        serverhost = LServer,
800
                                        username = Username,
801
                                        default = Default,
802
                                        always = Always,
803
                                        never = Never
804
                                       } | Acc]
805
                             catch
806
                                 _:_ ->
807
                                     {error, io_lib:format(
×
808
                                               "Error when decoding mam prefs for user ~s@~s",
809
                                               [Username, LServer])}
810
                             end
811
                     end,
812
                     [],
813
                     Rows),
814
            case length(Rows) of
×
815
                Val when Val < BatchSize ->
816
                    case serialize(LServer, BatchSize - Val, {mam, 0}) of
×
817
                        {ok, Data2, Next} ->
818
                            {ok, Data ++ Data2, Next};
×
819
                        Err -> Err
×
820
                    end;
821
                Val ->
822
                    {ok, Data, {prefs, Offset + Val}}
×
823
            end;
824
        _ ->
825
            {error, io_lib:format("Error when retrieving list of mam users preferences", [])}
×
826
    end;
827
serialize(LServer, BatchSize, {mam, Offset}) ->
828
    case ejabberd_sql:sql_query(
×
829
           LServer,
830
           ?SQL("select @(username)s, @(timestamp)d, @(peer)s, @(xml)s, @(kind)s, @(nick)s, @(origin_id)s from archive"
×
831
                " where %(LServer)H "
832
                "order by username, timestamp "
833
                "limit %(BatchSize)d offset %(Offset)d")) of
834
        {selected, Rows} ->
835
            Data = lists:map(
×
836
                     fun({Username, Timestamp, Peer, Xml, Kind, Nick, OriginId}) ->
837
                             #serialize_mam_v1{
×
838
                               serverhost = LServer,
839
                               username = Username,
840
                               timestamp = Timestamp,
841
                               peer = Peer,
842
                               type = erlang:binary_to_existing_atom(Kind, utf8),
843
                               nick = Nick,
844
                               origin_id = OriginId,
845
                               packet = Xml
846
                              }
847
                     end,
848
                     Rows),
849
            {ok, Data, {mam, Offset + length(Rows)}};
×
850
        _ ->
851
            {error, io_lib:format("Error when retrieving list of mam users entries", [])}
×
852
    end.
853

854
deserialize_start(LServer) ->
855
    ejabberd_sql:sql_query(
×
856
        LServer,
857
        ?SQL("delete from archive where %(LServer)H")),
×
858
    ejabberd_sql:sql_query(
×
859
        LServer,
860
        ?SQL("delete from archive_prefs where %(LServer)H")).
×
861

862
deserialize(LServer, Batch) ->
863
    F = fun() ->
×
864
        lists:foreach(
×
865
            fun(#serialize_mam_prefs_v1{username = Username, default = Default, always = Always, never = Never}) ->
866
                SDefault = erlang:atom_to_binary(Default, utf8),
×
867
                SAlways = misc:term_to_expr(Always),
×
868
                SNever = misc:term_to_expr(Never),
×
869
                ejabberd_sql:sql_query_t(?SQL_INSERT(
×
870
                    "archive_prefs",
871
                    ["username=%(Username)s",
872
                     "server_host=%(LServer)s",
873
                     "def=%(SDefault)s",
874
                     "always=%(SAlways)s",
875
                     "never=%(SNever)s"]));
876
               (#serialize_mam_v1{username = Username, timestamp = TS, peer = Peer, packet = XML, nick = Nick, type = Type, origin_id = OriginID}) ->
877
                   BarePeer = jid:encode(jid:remove_resource(jid:decode(Peer))),
×
878
                   Pkt = fxml_stream:parse_element(XML),
×
879
                   Body = fxml:get_subtag_cdata(Pkt, <<"body">>),
×
880
                   SType = atom_to_binary(Type),
×
881
                   ejabberd_sql:sql_query_t(
×
882
                       ?SQL_INSERT(
×
883
                           "archive",
884
                           ["username=%(Username)s",
885
                            "server_host=%(LServer)s",
886
                            "timestamp=%(TS)d",
887
                            "peer=%(Peer)s",
888
                            "bare_peer=%(BarePeer)s",
889
                            "xml=%(XML)s",
890
                            "txt=%(Body)s",
891
                            "kind=%(SType)s",
892
                            "nick=%(Nick)s",
893
                            "origin_id=%(OriginID)s"]))
894
            end, Batch)
895
        end,
896
        case ejabberd_sql:sql_transaction(LServer, F) of
×
897
            {atomic, _} -> ok;
×
898
            _ -> {error, io_lib:format("Error when writing archive data", [])}
×
899
        end.
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc