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

processone / ejabberd / 1173

28 Oct 2025 11:02AM UTC coverage: 33.768% (-0.02%) from 33.79%
1173

push

github

badlop
CHANGELOG.md: Update to 25.10

15513 of 45940 relevant lines covered (33.77%)

1078.01 hits per line

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

52.78
/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

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

37
-include_lib("stdlib/include/ms_transform.hrl").
38
-include_lib("xmpp/include/xmpp.hrl").
39
-include("mod_mam.hrl").
40
-include("logger.hrl").
41
-include("ejabberd_sql_pt.hrl").
42
-include("mod_muc_room.hrl").
43

44
%%%===================================================================
45
%%% API
46
%%%===================================================================
47
init(Host, _Opts) ->
48
    ejabberd_sql_schema:update_schema(Host, ?MODULE, sql_schemas()),
6✔
49
    ok.
6✔
50

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

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

162
remove_room(LServer, LName, LHost) ->
163
    LUser = jid:encode({LName, LHost, <<>>}),
540✔
164
    remove_user(LUser, LServer).
540✔
165

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

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

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

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

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

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

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

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

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

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

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

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

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

583
is_empty_for_room(LServer, LName, LHost) ->
584
    LUser = jid:encode({LName, LHost, <<>>}),
×
585
    is_empty_for_user(LUser, LServer).
×
586

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

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

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

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

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

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

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