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

processone / ejabberd / 1296

19 Jan 2026 11:25AM UTC coverage: 33.562% (+0.09%) from 33.468%
1296

push

github

badlop
mod_conversejs: Cosmetic change: sort paths alphabetically

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

11245 existing lines in 174 files now uncovered.

15580 of 46421 relevant lines covered (33.56%)

1074.56 hits per line

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

38.16
/src/mod_mam_mnesia.erl
1
%%%-------------------------------------------------------------------
2
%%% File    : mod_mam_mnesia.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-2026   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_mnesia).
26

27
-behaviour(mod_mam).
28
-behaviour(ejabberd_db_serialize).
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/6,
33
         remove_from_archive/3,
34
         is_empty_for_user/2, is_empty_for_room/3, delete_old_messages_batch/5,
35
         transform/1]).
36
-export([serialize/3, deserialize_start/1, deserialize/2]).
37

38
-include_lib("stdlib/include/ms_transform.hrl").
39
-include_lib("xmpp/include/xmpp.hrl").
40

41
-include("logger.hrl").
42
-include("mod_mam.hrl").
43
-include("ejabberd_db_serialize.hrl").
44

45
-define(BIN_GREATER_THAN(A, B),
46
        ((A > B andalso byte_size(A) == byte_size(B))
47
         orelse byte_size(A) > byte_size(B))).
48
-define(BIN_LESS_THAN(A, B),
49
        ((A < B andalso byte_size(A) == byte_size(B))
50
         orelse byte_size(A) < byte_size(B))).
51

52
-define(TABLE_SIZE_LIMIT, 2000000000). % A bit less than 2 GiB.
53

54
%%%===================================================================
55
%%% API
56
%%%===================================================================
57
init(_Host, _Opts) ->
58
    try
103✔
59
        {atomic, _} = ejabberd_mnesia:create(
103✔
60
                        ?MODULE, archive_msg,
61
                        [{disc_only_copies, [node()]},
62
                         {type, bag},
63
                         {attributes, record_info(fields, archive_msg)}]),
64
        {atomic, _} = ejabberd_mnesia:create(
103✔
65
                        ?MODULE, archive_prefs,
66
                        [{disc_only_copies, [node()]},
67
                         {attributes, record_info(fields, archive_prefs)}]),
68
        ok
103✔
69
    catch _:{badmatch, _} ->
70
            {error, db_failure}
×
71
    end.
72

73
remove_user(LUser, LServer) ->
74
    US = {LUser, LServer},
283✔
75
    F = fun () ->
283✔
76
                mnesia:delete({archive_msg, US}),
283✔
77
                mnesia:delete({archive_prefs, US})
283✔
78
        end,
79
    mnesia:transaction(F).
283✔
80

81
remove_room(_LServer, LName, LHost) ->
UNCOV
82
    remove_user(LName, LHost).
222✔
83

84
remove_from_archive(LUser, LHost, Key) when is_binary(LUser) ->
85
    remove_from_archive({LUser, LHost}, LHost, Key);
×
86
remove_from_archive(US, _LServer, none) ->
87
    case mnesia:transaction(fun () -> mnesia:delete({archive_msg, US}) end) of
×
88
        {atomic, _} -> ok;
×
89
        {aborted, Reason} -> {error, Reason}
×
90
    end;
91
remove_from_archive(US, _LServer, #jid{} = WithJid) ->
92
    Peer = jid:remove_resource(jid:split(WithJid)),
×
93
    F = fun () ->
×
94
            Msgs = mnesia:select(
×
95
                     archive_msg,
96
                     ets:fun2ms(
97
                       fun(#archive_msg{us = US1, bare_peer = Peer1} = Msg)
98
                          when US1 == US, Peer1 == Peer -> Msg
99
                       end)),
100
            lists:foreach(fun mnesia:delete_object/1, Msgs)
×
101
        end,
102
    case mnesia:transaction(F) of
×
103
        {atomic, _} -> ok;
×
104
        {aborted, Reason} -> {error, Reason}
×
105
    end;
106
remove_from_archive(US, _LServer, StanzaId) ->
107
    Timestamp = misc:usec_to_now(StanzaId),
×
108
    F = fun () ->
×
109
        Msgs = mnesia:select(
×
110
            archive_msg,
111
            ets:fun2ms(
112
                fun(#archive_msg{us = US1, timestamp = Timestamp1} = Msg)
113
                       when US1 == US, Timestamp1 == Timestamp -> Msg
114
                end)),
115
        lists:foreach(fun mnesia:delete_object/1, Msgs)
×
116
        end,
117
    case mnesia:transaction(F) of
×
118
        {atomic, _} -> ok;
×
119
        {aborted, Reason} -> {error, Reason}
×
120
    end.
121

122
delete_old_messages(global, TimeStamp, Type) ->
123
    mnesia:change_table_copy_type(archive_msg, node(), disc_copies),
×
124
    Result = delete_old_user_messages(mnesia:dirty_first(archive_msg), TimeStamp, Type),
×
125
    mnesia:change_table_copy_type(archive_msg, node(), disc_only_copies),
×
126
    Result.
×
127

128
delete_old_user_messages('$end_of_table', _TimeStamp, _Type) ->
129
    ok;
×
130
delete_old_user_messages(User, TimeStamp, Type) ->
131
    F = fun() ->
×
132
                Msgs = mnesia:read(archive_msg, User),
×
133
                Keep = lists:filter(
×
134
                         fun(#archive_msg{timestamp = MsgTS,
135
                                          type = MsgType}) ->
136
                                 MsgTS >= TimeStamp orelse (Type /= all andalso
×
137
                                                            Type /= MsgType)
×
138
                         end, Msgs),
139
                if length(Keep) < length(Msgs) ->
×
140
                        mnesia:delete({archive_msg, User}),
×
141
                        lists:foreach(fun(Msg) -> mnesia:write(Msg) end, Keep);
×
142
                   true ->
143
                        ok
×
144
                end
145
        end,
146
    NextRecord = mnesia:dirty_next(archive_msg, User),
×
147
    case mnesia:transaction(F) of
×
148
        {atomic, ok} ->
149
            delete_old_user_messages(NextRecord, TimeStamp, Type);
×
150
        {aborted, Err} ->
151
            ?ERROR_MSG("Cannot delete old MAM messages: ~ts", [Err]),
×
152
            Err
×
153
    end.
154

155
delete_batch('$end_of_table', _LServer, _TS, _Type, Num) ->
156
    {Num, '$end_of_table'};
×
157
delete_batch(LastUS, _LServer, _TS, _Type, 0) ->
158
    {0, LastUS};
×
159
delete_batch(none, LServer, TS, Type, Num) ->
160
    delete_batch(mnesia:first(archive_msg), LServer, TS, Type, Num);
×
161
delete_batch({_, LServer2} = LastUS, LServer, TS, Type, Num) when LServer /= LServer2 ->
162
    delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Num);
×
163
delete_batch(LastUS, LServer, TS, Type, Num) ->
164
    Left =
×
165
    lists:foldl(
166
        fun(_, 0) ->
167
            0;
×
168
           (#archive_msg{timestamp = TS2, type = Type2} = O, Num2) when TS2 < TS, (Type == all orelse Type == Type2) ->
169
               mnesia:delete_object(O),
×
170
               Num2 - 1;
×
171
           (_, Num2) ->
172
               Num2
×
173
        end, Num, mnesia:wread({archive_msg, LastUS})),
174
    case Left of
×
175
        0 -> {0, LastUS};
×
176
        _ -> delete_batch(mnesia:next(archive_msg, LastUS), LServer, TS, Type, Left)
×
177
    end.
178

179
delete_old_messages_batch(LServer, TimeStamp, Type, Batch, LastUS) ->
180
    R = mnesia:transaction(
×
181
        fun() ->
182
            {Num, NextUS} = delete_batch(LastUS, LServer, TimeStamp, Type, Batch),
×
183
            {Batch - Num, NextUS}
×
184
        end),
185
    case R of
×
186
        {atomic, {Num, State}} ->
187
            {ok, State, Num};
×
188
        {aborted, Err} ->
189
            {error, Err}
×
190
    end.
191

192
extended_fields(_) ->
UNCOV
193
    [].
12✔
194

195
store(Pkt, _, {LUser, LServer}, Type, Peer, Nick, _Dir, TS,
196
      OriginID, Retract) ->
UNCOV
197
    case Retract of
548✔
198
        {true, RID} ->
UNCOV
199
            mnesia:transaction(
22✔
200
              fun () ->
UNCOV
201
                      {PUser, PServer, _} = jid:tolower(Peer),
22✔
UNCOV
202
                      Msgs = mnesia:select(
22✔
203
                               archive_msg,
204
                               ets:fun2ms(
205
                                 fun(#archive_msg{
206
                                        us = US1,
207
                                        bare_peer = Peer1,
208
                                        origin_id = OriginID1} = Msg)
209
                                       when US1 == {LUser, LServer},
210
                                            Peer1 == {PUser, PServer, <<>>},
211
                                            OriginID1 == RID -> Msg
212
                                 end)),
UNCOV
213
                      lists:foreach(fun mnesia:delete_object/1, Msgs)
22✔
214
              end);
UNCOV
215
        false -> ok
526✔
216
    end,
UNCOV
217
    case {mnesia:table_info(archive_msg, disc_only_copies),
548✔
218
          mnesia:table_info(archive_msg, memory)} of
219
        {[_|_], TableSize} when TableSize > ?TABLE_SIZE_LIMIT ->
220
            ?ERROR_MSG("MAM archives too large, won't store message for ~ts@~ts",
×
221
                       [LUser, LServer]),
×
222
            {error, overflow};
×
223
        _ ->
UNCOV
224
            LPeer = {PUser, PServer, _} = jid:tolower(Peer),
548✔
UNCOV
225
            F = fun() ->
548✔
UNCOV
226
                        mnesia:write(
558✔
227
                          #archive_msg{us = {LUser, LServer},
228
                                       id = integer_to_binary(TS),
229
                                       timestamp = misc:usec_to_now(TS),
230
                                       peer = LPeer,
231
                                       bare_peer = {PUser, PServer, <<>>},
232
                                       type = Type,
233
                                       nick = Nick,
234
                                       packet = Pkt,
235
                                       origin_id = OriginID})
236
                end,
UNCOV
237
            case mnesia:transaction(F) of
548✔
238
                {atomic, ok} ->
UNCOV
239
                    ok;
548✔
240
                {aborted, Err} ->
241
                    ?ERROR_MSG("Cannot add message to MAM archive of ~ts@~ts: ~ts",
×
242
                               [LUser, LServer, Err]),
×
243
                    Err
×
244
            end
245
    end.
246

247
write_prefs(_LUser, _LServer, Prefs, _ServerHost) ->
UNCOV
248
    mnesia:dirty_write(Prefs).
322✔
249

250
get_prefs(LUser, LServer) ->
UNCOV
251
    case mnesia:dirty_read(archive_prefs, {LUser, LServer}) of
243✔
252
        [Prefs] ->
UNCOV
253
            {ok, Prefs};
226✔
254
        _ ->
UNCOV
255
            error
17✔
256
    end.
257

258
select(_LServer, JidRequestor,
259
       #jid{luser = LUser, lserver = LServer} = JidArchive,
260
       Query, RSM, MsgType) ->
UNCOV
261
    Start = proplists:get_value(start, Query),
404✔
UNCOV
262
    End = proplists:get_value('end', Query),
404✔
UNCOV
263
    With = proplists:get_value(with, Query),
404✔
UNCOV
264
    LWith = if With /= undefined -> jid:tolower(With);
404✔
UNCOV
265
               true -> undefined
372✔
266
            end,
UNCOV
267
    MS = make_matchspec(LUser, LServer, Start, End, LWith),
404✔
UNCOV
268
    Msgs = mnesia:dirty_select(archive_msg, MS),
404✔
UNCOV
269
    SortedMsgs = lists:keysort(#archive_msg.timestamp, Msgs),
404✔
UNCOV
270
    {FilteredMsgs, IsComplete} = filter_by_rsm(SortedMsgs, RSM),
404✔
UNCOV
271
    Count = length(Msgs),
404✔
UNCOV
272
    Result = {lists:flatmap(
404✔
273
                fun(Msg) ->
UNCOV
274
                        case mod_mam:msg_to_el(
1,486✔
275
                               Msg, MsgType, JidRequestor, JidArchive) of
276
                            {ok, El} ->
UNCOV
277
                                [{Msg#archive_msg.id,
1,486✔
278
                                  binary_to_integer(Msg#archive_msg.id),
279
                                  El}];
280
                            {error, _} ->
281
                                []
×
282
                        end
283
                end, FilteredMsgs), IsComplete, Count},
UNCOV
284
    erlang:garbage_collect(),
404✔
UNCOV
285
    Result.
404✔
286

287
is_empty_for_user(LUser, LServer) ->
288
    mnesia:dirty_read(archive_msg, {LUser, LServer}) == [].
×
289

290
is_empty_for_room(_LServer, LName, LHost) ->
291
    is_empty_for_user(LName, LHost).
×
292

293
%%%===================================================================
294
%%% Internal functions
295
%%%===================================================================
296
make_matchspec(LUser, LServer, Start, undefined, With) ->
297
    %% List is always greater than a tuple
UNCOV
298
    make_matchspec(LUser, LServer, Start, [], With);
404✔
299
make_matchspec(LUser, LServer, Start, End, {_, _, <<>>} = With) ->
300
    ets:fun2ms(
UNCOV
301
      fun(#archive_msg{timestamp = TS,
16✔
302
                       us = US,
303
                       bare_peer = BPeer} = Msg)
304
            when Start =< TS, End >= TS,
305
                 US == {LUser, LServer},
306
                 BPeer == With ->
307
              Msg
308
      end);
309
make_matchspec(LUser, LServer, Start, End, {_, _, _} = With) ->
310
    ets:fun2ms(
UNCOV
311
      fun(#archive_msg{timestamp = TS,
16✔
312
                       us = US,
313
                       peer = Peer} = Msg)
314
            when Start =< TS, End >= TS,
315
                 US == {LUser, LServer},
316
                 Peer == With ->
317
              Msg
318
      end);
319
make_matchspec(LUser, LServer, Start, End, undefined) ->
320
    ets:fun2ms(
UNCOV
321
      fun(#archive_msg{timestamp = TS,
372✔
322
                       us = US,
323
                       peer = Peer} = Msg)
324
            when Start =< TS, End >= TS,
325
                 US == {LUser, LServer} ->
326
              Msg
327
      end).
328

329
filter_by_rsm(Msgs, undefined) ->
UNCOV
330
    {Msgs, true};
12✔
331
filter_by_rsm(_Msgs, #rsm_set{max = Max}) when Max < 0 ->
332
    {[], true};
×
333
filter_by_rsm(Msgs, #rsm_set{max = Max, before = Before, 'after' = After}) ->
UNCOV
334
    NewMsgs = if is_binary(After), After /= <<"">> ->
392✔
UNCOV
335
                      lists:filter(
80✔
336
                        fun(#archive_msg{id = I}) ->
UNCOV
337
                                ?BIN_GREATER_THAN(I, After)
400✔
338
                        end, Msgs);
339
                 is_binary(Before), Before /= <<"">> ->
UNCOV
340
                      lists:foldl(
86✔
341
                        fun(#archive_msg{id = I} = Msg, Acc)
342
                                when ?BIN_LESS_THAN(I, Before) ->
UNCOV
343
                                [Msg|Acc];
520✔
344
                           (_, Acc) ->
UNCOV
345
                                Acc
240✔
346
                        end, [], Msgs);
347
                 is_binary(Before), Before == <<"">> ->
UNCOV
348
                      lists:reverse(Msgs);
32✔
349
                 true ->
UNCOV
350
                      Msgs
194✔
351
              end,
UNCOV
352
    filter_by_max(NewMsgs, Max).
392✔
353

354
filter_by_max(Msgs, undefined) ->
UNCOV
355
    {Msgs, true};
66✔
356
filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 ->
UNCOV
357
    {lists:sublist(Msgs, Len), length(Msgs) =< Len};
326✔
358
filter_by_max(_Msgs, _Junk) ->
359
    {[], true}.
×
360

361
transform({archive_msg, US, ID, Timestamp, Peer, BarePeer,
362
           Packet, Nick, Type}) ->
363
    #archive_msg{
×
364
       us = US,
365
       id = ID,
366
       timestamp = Timestamp,
367
       peer = Peer,
368
       bare_peer = BarePeer,
369
       packet = Packet,
370
       nick = Nick,
371
       type = Type,
372
       origin_id = <<"">>};
373
transform(Other) ->
374
    Other.
×
375

376

377
serialize(LServer, BatchSize, undefined) ->
378
    ArchiveConv =
×
379
        fun([]) -> skip;
×
380
           ([#archive_msg{us = {U, S}, timestamp = TS, peer = Peer, packet = Xml, nick = Nick, type = Type, origin_id = OriginId}])
381
              when S == LServer ->
382
                {ok, #serialize_mam_v1{
×
383
                       serverhost = LServer,
384
                       username = U,
385
                       timestamp = misc:now_to_usec(TS),
386
                       peer = jid:encode(Peer),
387
                       type = Type,
388
                       nick = Nick,
389
                       origin_id = OriginId,
390
                       packet = fxml:element_to_binary(Xml)
391
                      }};
392
           (_) -> skip
×
393
        end,
394
    PrefsConv =
×
395
        fun([]) -> skip;
×
396
           ([#archive_prefs{us = {U, S}, default = Default, always = Always, never = Never}]) when S == LServer ->
397
                {ok, #serialize_mam_prefs_v1{
×
398
                       serverhost = LServer,
399
                       username = U,
400
                       default = Default,
401
                       always = Always,
402
                       never = Never
403
                      }};
404
           (_) -> skip
×
405
        end,
406
    ejabberd_db_serialize:iter_records([ejabberd_db_serialize:mnesia_iter(archive_msg, ArchiveConv),
×
407
                                        ejabberd_db_serialize:mnesia_iter(archive_prefs, PrefsConv)],
408
                                       [],
409
                                       BatchSize);
410
serialize(_LServer, BatchSize, Key) ->
411
    ejabberd_db_serialize:iter_records(Key, [], BatchSize).
×
412

413

414
deserialize_start(LServer) ->
415
    mnesia:transaction(
×
416
        fun() ->
417
            ArchiveKeys = mnesia:select(archive_msg,
×
418
                                        ets:fun2ms(
419
                                            fun(#archive_msg{us = US}) when element(2, US) == LServer -> US end)),
420
            PrefsKeys = mnesia:select(archive_prefs,
×
421
                                      ets:fun2ms(
422
                                          fun(#archive_prefs{us = US}) when element(2, US) == LServer -> US end)),
423
            lists:foreach(fun(Key) -> mnesia:delete(archive_msg, Key, write) end, ArchiveKeys),
×
424
            lists:foreach(fun(Key) -> mnesia:delete(archive_prefs, Key, write) end, PrefsKeys)
×
425
        end),
426
    ok.
×
427

428

429
deserialize(LServer, Batch) ->
430
    F = fun() ->
×
431
        lists:foldl(
×
432
            fun(_, {error, _} = Err) ->
433
                Err;
×
434
               (#serialize_mam_v1{
435
                   username = LUser,
436
                   timestamp = TS,
437
                   peer = Peer,
438
                   type = Type,
439
                   nick = Nick,
440
                   origin_id = OriginId,
441
                   packet = Xml
442
               }, _) ->
443
                   {PUser, PServer, _} = PeerJ = jid:tolower(jid:decode(Peer)),
×
444
                   mnesia:write(
×
445
                       #archive_msg{
446
                           us = {LUser, LServer},
447
                           id = integer_to_binary(
448
                               TS),
449
                           timestamp = misc:usec_to_now(
450
                               TS),
451
                           peer = PeerJ,
452
                           bare_peer = {PUser, PServer, <<>>},
453
                           type = Type,
454
                           nick = Nick,
455
                           packet = fxml_stream:parse_element(Xml),
456
                           origin_id = OriginId
457
                       });
458
               (#serialize_mam_prefs_v1{
459
                   username = U,
460
                   default = Default,
461
                   always = Always,
462
                   never = Never
463
               },
464
                _) ->
465
                   mnesia:write(
×
466
                       #archive_prefs{us = {U, LServer}, default = Default, always = Always, never = Never})
467
            end,
468
            ok,
469
            Batch)
470
        end,
471
    case mnesia:transaction(F) of
×
472
        {atomic, _} -> ok;
×
473
        {aborted, Reason} ->
474
            {error, iolist_to_binary(io_lib:format("Error when writing archive data: ~p", [Reason]))}
×
475
    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