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

processone / ejabberd / 1258

12 Dec 2025 03:57PM UTC coverage: 33.638% (-0.006%) from 33.644%
1258

push

github

badlop
Container: Apply commit a22c88a

ejabberdctl.template: Show meaningful error when ERL_DIST_PORT is in use

15554 of 46240 relevant lines covered (33.64%)

1078.28 hits per line

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

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

26
-module(mod_mam).
27

28
-protocol({xep, 313, '0.6.1', '15.06', "complete", ""}).
29
-protocol({xep, 334, '1.0.0', '16.01', "complete", ""}).
30
-protocol({xep, 359, '0.7.0', '15.09', "complete", ""}).
31
-protocol({xep, 424, '0.4.2', '24.02', "partial", "Tombstones not implemented"}).
32
-protocol({xep, 425, '0.3.0', '24.06', "complete", ""}).
33
-protocol({xep, 441, '0.2.0', '15.06', "complete", ""}).
34
-protocol({xep, 431, '0.2.0', '24.12', "complete", ""}).
35

36
-behaviour(gen_mod).
37

38
%% API
39
-export([start/2, stop/1, reload/3, depends/2, mod_doc/0]).
40

41
-export([sm_receive_packet/1, user_receive_packet/1, user_send_packet/1,
42
         user_send_packet_strip_tag/1, process_iq_v0_2/1, process_iq_v0_3/1,
43
         disco_local_features/5,
44
         disco_sm_features/5, remove_user/2, remove_room/3, mod_opt_type/1,
45
         muc_process_iq/2, muc_filter_message/3, message_is_archived/3,
46
         delete_old_messages/2, get_commands_spec/0, msg_to_el/4,
47
         get_room_config/4, set_room_option/3, offline_message/1, export/1,
48
         mod_options/1, remove_mam_for_user_with_peer/3, remove_mam_for_user/2,
49
         is_empty_for_user/2, is_empty_for_room/3, check_create_room/4,
50
         process_iq/3, store_mam_message/7, make_id/0, wrap_as_mucsub/2, select/7,
51
         is_archiving_enabled/2,
52
         get_mam_count/2,
53
         webadmin_menu_hostuser/4,
54
         webadmin_page_hostuser/4,
55
         get_mam_messages/2, webadmin_user/4,
56
         delete_old_messages_batch/5, delete_old_messages_status/1, delete_old_messages_abort/1,
57
         remove_message_from_archive/3]).
58

59
-import(ejabberd_web_admin, [make_command/4, make_command/2]).
60

61
-include_lib("xmpp/include/xmpp.hrl").
62
-include("logger.hrl").
63
-include("mod_muc_room.hrl").
64
-include("ejabberd_commands.hrl").
65
-include("ejabberd_http.hrl").
66
-include("ejabberd_web_admin.hrl").
67
-include("mod_mam.hrl").
68
-include("translate.hrl").
69

70
-define(DEF_PAGE_SIZE, 50).
71
-define(MAX_PAGE_SIZE, 250).
72

73
-type c2s_state() :: ejabberd_c2s:state().
74
-type count() :: non_neg_integer() | undefined.
75

76
-callback init(binary(), gen_mod:opts()) -> any().
77
-callback remove_user(binary(), binary()) -> any().
78
-callback remove_room(binary(), binary(), binary()) -> any().
79
-callback delete_old_messages(binary() | global,
80
                              erlang:timestamp(),
81
                              all | chat | groupchat) -> any().
82
-callback extended_fields(binary()) -> [mam_query:property() | #xdata_field{}].
83
-callback store(xmlel(), binary(), {binary(), binary()}, chat | groupchat,
84
                jid(), binary(), recv | send, integer(), binary(),
85
                {true, binary()} | false) -> ok | any().
86
-callback write_prefs(binary(), binary(), #archive_prefs{}, binary()) -> ok | any().
87
-callback get_prefs(binary(), binary()) -> {ok, #archive_prefs{}} | error | {error, db_failure}.
88
-callback select(binary(), jid(), jid(), mam_query:result(),
89
                 #rsm_set{} | undefined, chat | groupchat) ->
90
    {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
91
    {error, db_failure}.
92
-callback select(binary(), jid(), jid(), mam_query:result(),
93
                 #rsm_set{} | undefined, chat | groupchat,
94
                 all | only_count | only_messages) ->
95
                    {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
96
                    {error, db_failure}.
97
-callback use_cache(binary()) -> boolean().
98
-callback cache_nodes(binary()) -> [node()].
99
-callback remove_from_archive(binary(), binary(), jid() | none) -> ok | {error, any()}.
100
-callback is_empty_for_user(binary(), binary()) -> boolean().
101
-callback is_empty_for_room(binary(), binary(), binary()) -> boolean().
102
-callback select_with_mucsub(binary(), jid(), jid(), mam_query:result(),
103
                             #rsm_set{} | undefined, all | only_count | only_messages) ->
104
    {[{binary(), non_neg_integer(), xmlel()}], boolean(), count()} |
105
    {error, db_failure}.
106

107
-callback delete_old_messages_batch(binary(), erlang:timestamp(),
108
                                    all | chat | groupchat,
109
                                    pos_integer()) ->
110
    {ok, non_neg_integer()} | {error, term()}.
111

112
-callback delete_old_messages_batch(binary(), erlang:timestamp(),
113
                                    all | chat | groupchat,
114
                                    pos_integer(), any()) ->
115
    {ok, any(), non_neg_integer()} | {error, term()}.
116

117
-optional_callbacks([use_cache/1, cache_nodes/1, select_with_mucsub/6, select/6, select/7,
118
    delete_old_messages_batch/5, delete_old_messages_batch/4]).
119

120
%%%===================================================================
121
%%% API
122
%%%===================================================================
123
start(Host, Opts) ->
124
    case mod_mam_opt:db_type(Opts) of
109✔
125
        mnesia ->
126
            ?WARNING_MSG("Mnesia backend for ~ts is not recommended: "
103✔
127
                         "it's limited to 2GB and often gets corrupted "
128
                         "when reaching this limit. SQL backend is "
129
                         "recommended. Namely, for small servers SQLite "
130
                         "is a preferred choice because it's very easy "
131
                         "to configure.", [?MODULE]);
103✔
132
        _ ->
133
            ok
6✔
134
    end,
135
    Mod = gen_mod:db_mod(Opts, ?MODULE),
109✔
136
    case Mod:init(Host, Opts) of
109✔
137
        ok ->
138
            init_cache(Mod, Host, Opts),
109✔
139
            register_iq_handlers(Host),
109✔
140
            ejabberd_hooks:add(sm_receive_packet, Host, ?MODULE,
109✔
141
                               sm_receive_packet, 50),
142
            ejabberd_hooks:add(user_receive_packet, Host, ?MODULE,
109✔
143
                               user_receive_packet, 88),
144
            ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
109✔
145
                               user_send_packet, 88),
146
            ejabberd_hooks:add(user_send_packet, Host, ?MODULE,
109✔
147
                               user_send_packet_strip_tag, 500),
148
            ejabberd_hooks:add(offline_message_hook, Host, ?MODULE,
109✔
149
                               offline_message, 49),
150
            ejabberd_hooks:add(muc_filter_message, Host, ?MODULE,
109✔
151
                               muc_filter_message, 50),
152
            ejabberd_hooks:add(muc_process_iq, Host, ?MODULE,
109✔
153
                               muc_process_iq, 50),
154
            ejabberd_hooks:add(disco_local_features, Host, ?MODULE,
109✔
155
                               disco_local_features, 50),
156
            ejabberd_hooks:add(disco_sm_features, Host, ?MODULE,
109✔
157
                               disco_sm_features, 50),
158
            ejabberd_hooks:add(remove_user, Host, ?MODULE,
109✔
159
                               remove_user, 50),
160
            ejabberd_hooks:add(get_room_config, Host, ?MODULE,
109✔
161
                               get_room_config, 50),
162
            ejabberd_hooks:add(set_room_option, Host, ?MODULE,
109✔
163
                               set_room_option, 50),
164
            ejabberd_hooks:add(store_mam_message, Host, ?MODULE,
109✔
165
                               store_mam_message, 100),
166
            ejabberd_hooks:add(webadmin_menu_hostuser, Host, ?MODULE,
109✔
167
                               webadmin_menu_hostuser, 50),
168
            ejabberd_hooks:add(webadmin_page_hostuser, Host, ?MODULE,
109✔
169
                               webadmin_page_hostuser, 50),
170
            ejabberd_hooks:add(webadmin_user, Host, ?MODULE,
109✔
171
                               webadmin_user, 50),
172
            case mod_mam_opt:assume_mam_usage(Opts) of
109✔
173
                true ->
174
                    ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
×
175
                                       message_is_archived, 50);
176
                false ->
177
                    ok
109✔
178
            end,
179
            case mod_mam_opt:clear_archive_on_room_destroy(Opts) of
109✔
180
                true ->
181
                    ejabberd_hooks:add(remove_room, Host, ?MODULE,
109✔
182
                                       remove_room, 50);
183
                false ->
184
                    ejabberd_hooks:add(check_create_room, Host, ?MODULE,
×
185
                                       check_create_room, 50)
186
            end,
187
            ejabberd_commands:register_commands(Host, ?MODULE, get_commands_spec()),
109✔
188
            ok;
109✔
189
        Err ->
190
            Err
×
191
    end.
192

193
use_cache(Mod, Host) ->
194
    case erlang:function_exported(Mod, use_cache, 2) of
9,326✔
195
        true -> Mod:use_cache(Host);
×
196
        false -> mod_mam_opt:use_cache(Host)
9,326✔
197
    end.
198

199
cache_nodes(Mod, Host) ->
200
    case erlang:function_exported(Mod, cache_nodes, 1) of
1,529✔
201
        true -> Mod:cache_nodes(Host);
×
202
        false -> ejabberd_cluster:get_nodes()
1,529✔
203
    end.
204

205
init_cache(Mod, Host, Opts) ->
206
    case use_cache(Mod, Host) of
157✔
207
        true ->
208
            ets_cache:new(archive_prefs_cache, cache_opts(Opts));
157✔
209
        false ->
210
            ets_cache:delete(archive_prefs_cache)
×
211
    end.
212

213
cache_opts(Opts) ->
214
    MaxSize = mod_mam_opt:cache_size(Opts),
157✔
215
    CacheMissed = mod_mam_opt:cache_missed(Opts),
157✔
216
    LifeTime = mod_mam_opt:cache_life_time(Opts),
157✔
217
    [{max_size, MaxSize}, {life_time, LifeTime}, {cache_missed, CacheMissed}].
157✔
218

219
stop(Host) ->
220
    unregister_iq_handlers(Host),
109✔
221
    ejabberd_hooks:delete(sm_receive_packet, Host, ?MODULE,
109✔
222
                          sm_receive_packet, 50),
223
    ejabberd_hooks:delete(user_receive_packet, Host, ?MODULE,
109✔
224
                          user_receive_packet, 88),
225
    ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
109✔
226
                          user_send_packet, 88),
227
    ejabberd_hooks:delete(user_send_packet, Host, ?MODULE,
109✔
228
                          user_send_packet_strip_tag, 500),
229
    ejabberd_hooks:delete(offline_message_hook, Host, ?MODULE,
109✔
230
                          offline_message, 49),
231
    ejabberd_hooks:delete(muc_filter_message, Host, ?MODULE,
109✔
232
                          muc_filter_message, 50),
233
    ejabberd_hooks:delete(muc_process_iq, Host, ?MODULE,
109✔
234
                          muc_process_iq, 50),
235
    ejabberd_hooks:delete(disco_local_features, Host, ?MODULE,
109✔
236
                          disco_local_features, 50),
237
    ejabberd_hooks:delete(disco_sm_features, Host, ?MODULE,
109✔
238
                          disco_sm_features, 50),
239
    ejabberd_hooks:delete(remove_user, Host, ?MODULE,
109✔
240
                          remove_user, 50),
241
    ejabberd_hooks:delete(get_room_config, Host, ?MODULE,
109✔
242
                          get_room_config, 50),
243
    ejabberd_hooks:delete(set_room_option, Host, ?MODULE,
109✔
244
                          set_room_option, 50),
245
    ejabberd_hooks:delete(store_mam_message, Host, ?MODULE,
109✔
246
                          store_mam_message, 100),
247
    ejabberd_hooks:delete(webadmin_menu_hostuser, Host, ?MODULE,
109✔
248
                          webadmin_menu_hostuser, 50),
249
    ejabberd_hooks:delete(webadmin_page_hostuser, Host, ?MODULE,
109✔
250
                          webadmin_page_hostuser, 50),
251
    ejabberd_hooks:delete(webadmin_user, Host, ?MODULE,
109✔
252
                          webadmin_user, 50),
253
    case mod_mam_opt:assume_mam_usage(Host) of
109✔
254
        true ->
255
            ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
×
256
                                  message_is_archived, 50);
257
        false ->
258
            ok
109✔
259
    end,
260
    case mod_mam_opt:clear_archive_on_room_destroy(Host) of
109✔
261
        true ->
262
            ejabberd_hooks:delete(remove_room, Host, ?MODULE,
109✔
263
                                  remove_room, 50);
264
        false ->
265
            ejabberd_hooks:delete(check_create_room, Host, ?MODULE,
×
266
                                  check_create_room, 50)
267
    end,
268
    ejabberd_commands:unregister_commands(Host, ?MODULE, get_commands_spec()).
109✔
269

270
reload(Host, NewOpts, OldOpts) ->
271
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
48✔
272
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
48✔
273
    if NewMod /= OldMod ->
48✔
274
            NewMod:init(Host, NewOpts);
×
275
       true ->
276
            ok
48✔
277
    end,
278
    init_cache(NewMod, Host, NewOpts),
48✔
279
    case {mod_mam_opt:assume_mam_usage(NewOpts),
48✔
280
          mod_mam_opt:assume_mam_usage(OldOpts)} of
281
        {true, false} ->
282
            ejabberd_hooks:add(message_is_archived, Host, ?MODULE,
×
283
                               message_is_archived, 50);
284
        {false, true} ->
285
            ejabberd_hooks:delete(message_is_archived, Host, ?MODULE,
×
286
                                  message_is_archived, 50);
287
        _ ->
288
            ok
48✔
289
    end.
290

291
depends(_Host, _Opts) ->
292
    [].
129✔
293

294
-spec register_iq_handlers(binary()) -> ok.
295
register_iq_handlers(Host) ->
296
    gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP,
109✔
297
                                  ?MODULE, process_iq_v0_2),
298
    gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP,
109✔
299
                                  ?MODULE, process_iq_v0_2),
300
    gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_0,
109✔
301
                                  ?MODULE, process_iq_v0_3),
302
    gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_0, ?MODULE,
109✔
303
                                  process_iq_v0_3),
304
    gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_1,
109✔
305
                                  ?MODULE, process_iq_v0_3),
306
    gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_1,
109✔
307
                                  ?MODULE, process_iq_v0_3),
308
    gen_iq_handler:add_iq_handler(ejabberd_local, Host, ?NS_MAM_2,
109✔
309
                                  ?MODULE, process_iq_v0_3),
310
    gen_iq_handler:add_iq_handler(ejabberd_sm, Host, ?NS_MAM_2,
109✔
311
                                  ?MODULE, process_iq_v0_3).
312

313
-spec unregister_iq_handlers(binary()) -> ok.
314
unregister_iq_handlers(Host) ->
315
    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_TMP),
109✔
316
    gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_TMP),
109✔
317
    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_0),
109✔
318
    gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_0),
109✔
319
    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_1),
109✔
320
    gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_1),
109✔
321
    gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_MAM_2),
109✔
322
    gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_MAM_2).
109✔
323

324
-spec remove_user(binary(), binary()) -> ok.
325
remove_user(User, Server) ->
326
    LUser = jid:nodeprep(User),
241✔
327
    LServer = jid:nameprep(Server),
241✔
328
    Mod = gen_mod:db_mod(LServer, ?MODULE),
241✔
329
    Mod:remove_user(LUser, LServer),
241✔
330
    case use_cache(Mod, LServer) of
241✔
331
        true ->
332
            ets_cache:delete(archive_prefs_cache, {LUser, LServer},
241✔
333
                             cache_nodes(Mod, LServer));
334
        false ->
335
            ok
×
336
    end.
337

338
-spec remove_room(binary(), binary(), binary()) -> ok.
339
remove_room(LServer, Name, Host) ->
340
    LName = jid:nodeprep(Name),
762✔
341
    LHost = jid:nameprep(Host),
762✔
342
    Mod = gen_mod:db_mod(LServer, ?MODULE),
762✔
343
    Mod:remove_room(LServer, LName, LHost),
762✔
344
    ok.
762✔
345

346
-spec remove_mam_for_user(binary(), binary()) ->
347
    {ok, binary()} | {error, binary()}.
348
remove_mam_for_user(User, Server) ->
349
    LUser = jid:nodeprep(User),
×
350
    LServer = jid:nameprep(Server),
×
351
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
352
    case Mod:remove_from_archive(LUser, LServer, none) of
×
353
        ok ->
354
            {ok, <<"MAM archive removed">>};
×
355
        {error, Bin} when is_binary(Bin) ->
356
            {error, Bin};
×
357
        {error, _} ->
358
            {error, <<"Db returned error">>}
×
359
    end.
360

361
-spec remove_mam_for_user_with_peer(binary(), binary(), binary()) ->
362
    {ok, binary()} | {error, binary()}.
363
remove_mam_for_user_with_peer(User, Server, Peer) ->
364
    LUser = jid:nodeprep(User),
×
365
    LServer = jid:nameprep(Server),
×
366
    try jid:decode(Peer) of
×
367
        Jid ->
368
            Mod = get_module_host(LServer),
×
369
            case Mod:remove_from_archive(LUser, LServer, Jid) of
×
370
                ok ->
371
                    {ok, <<"MAM archive removed">>};
×
372
                {error, Bin} when is_binary(Bin) ->
373
                    {error, Bin};
×
374
                {error, _} ->
375
                    {error, <<"Db returned error">>}
×
376
            end
377
    catch _:_ ->
378
        {error, <<"Invalid peer JID">>}
×
379
    end.
380

381
-spec remove_message_from_archive(
382
        User :: binary() | {User :: binary(), Host :: binary()},
383
        Server :: binary(), StanzaId :: integer()) ->
384
    ok | {error, binary()}.
385
remove_message_from_archive(User, Server, StanzaId) when is_binary(User) ->
386
    remove_message_from_archive({User, Server}, Server, StanzaId);
×
387
remove_message_from_archive({_User, _Host} = UserHost, Server, StanzaId) ->
388
    Mod = gen_mod:db_mod(Server, ?MODULE),
×
389
    case Mod:remove_from_archive(UserHost, Server, StanzaId) of
×
390
        ok ->
391
            ok;
×
392
        {error, Bin} when is_binary(Bin) ->
393
            {error, Bin};
×
394
        {error, _} ->
395
            {error, <<"Db returned error">>}
×
396
    end.
397

398
get_module_host(LServer) ->
399
    try gen_mod:db_mod(LServer, ?MODULE)
×
400
    catch error:{module_not_loaded, ?MODULE, LServer} ->
401
        gen_mod:db_mod(ejabberd_router:host_of_route(LServer), ?MODULE)
×
402
    end.
403

404
-spec get_room_config([muc_roomconfig:property()], mod_muc_room:state(),
405
                      jid(), binary()) -> [muc_roomconfig:property()].
406
get_room_config(Fields, RoomState, _From, _Lang) ->
407
    Config = RoomState#state.config,
58✔
408
    Fields ++ [{mam, Config#config.mam}].
58✔
409

410
-spec set_room_option({pos_integer(), _}, muc_roomconfig:property(), binary())
411
      -> {pos_integer(), _}.
412
set_room_option(_Acc, {mam, Val}, _Lang) ->
413
    {#config.mam, Val};
40✔
414
set_room_option(Acc, _Property, _Lang) ->
415
    Acc.
×
416

417
-spec sm_receive_packet(stanza()) -> stanza().
418
sm_receive_packet(#message{to = #jid{lserver = LServer}} = Pkt) ->
419
    init_stanza_id(Pkt, LServer);
30,083✔
420
sm_receive_packet(Acc) ->
421
    Acc.
42,773✔
422

423
-spec user_receive_packet({stanza(), c2s_state()}) -> {stanza(), c2s_state()}.
424
user_receive_packet({#message{from = Peer} = Pkt, #{jid := JID} = C2SState}) ->
425
    LUser = JID#jid.luser,
19,597✔
426
    LServer = JID#jid.lserver,
19,597✔
427
    Pkt1 = case should_archive(Pkt, LServer) of
19,597✔
428
               true ->
429
                   case store_msg(Pkt, LUser, LServer, Peer, recv) of
583✔
430
                       ok ->
431
                           mark_stored_msg(Pkt, JID);
416✔
432
                       _ ->
433
                           Pkt
167✔
434
                   end;
435
               _ ->
436
                   Pkt
19,014✔
437
           end,
438
    {Pkt1, C2SState};
19,597✔
439
user_receive_packet(Acc) ->
440
    Acc.
27,779✔
441

442
-spec user_send_packet({stanza(), c2s_state()})
443
      -> {stanza(), c2s_state()}.
444
user_send_packet({#message{to = Peer} = Pkt, #{jid := JID} = C2SState}) ->
445
    LUser = JID#jid.luser,
15,450✔
446
    LServer = JID#jid.lserver,
15,450✔
447
    Pkt1 = init_stanza_id(Pkt, LServer),
15,450✔
448
    Pkt2 = case should_archive(Pkt1, LServer) of
15,450✔
449
               true ->
450
                   case store_msg(xmpp:set_from_to(Pkt1, JID, Peer),
3,337✔
451
                                  LUser, LServer, Peer, send) of
452
                       ok ->
453
                           mark_stored_msg(Pkt1, JID);
240✔
454
                       _ ->
455
                           Pkt1
3,097✔
456
                   end;
457
               false ->
458
                   Pkt1
12,113✔
459
           end,
460
    {Pkt2, C2SState};
15,450✔
461
user_send_packet(Acc) ->
462
    Acc.
27,029✔
463

464
-spec user_send_packet_strip_tag({stanza(), c2s_state()})
465
      -> {stanza(), c2s_state()}.
466
user_send_packet_strip_tag({#message{} = Pkt, #{jid := JID} = C2SState}) ->
467
    LServer = JID#jid.lserver,
15,450✔
468
    Pkt1 = xmpp:del_meta(Pkt, stanza_id),
15,450✔
469
    Pkt2 = strip_my_stanza_id(Pkt1, LServer),
15,450✔
470
    {Pkt2, C2SState};
15,450✔
471
user_send_packet_strip_tag(Acc) ->
472
    Acc.
27,029✔
473

474
-spec offline_message({any(), message()}) -> {any(), message()}.
475
offline_message({_Action, #message{from = Peer, to = To} = Pkt} = Acc) ->
476
    LUser = To#jid.luser,
5,840✔
477
    LServer = To#jid.lserver,
5,840✔
478
    case should_archive(Pkt, LServer) of
5,840✔
479
       true ->
480
           case store_msg(Pkt, LUser, LServer, Peer, recv) of
2,952✔
481
               ok ->
482
                   {archived, mark_stored_msg(Pkt, To)};
1,456✔
483
               _ ->
484
                   Acc
1,496✔
485
           end;
486
       false ->
487
           Acc
2,888✔
488
    end.
489

490
-spec muc_filter_message(message(), mod_muc_room:state(),
491
                         binary()) -> message().
492
muc_filter_message(#message{meta = #{mam_ignore := true}} = Pkt, _MUCState, _FromNick) ->
493
    Pkt;
751✔
494
muc_filter_message(#message{from = From} = Pkt,
495
                   #state{config = Config, jid = RoomJID} = MUCState,
496
                   FromNick) ->
497
    LServer = RoomJID#jid.lserver,
631✔
498
    Pkt1 = init_stanza_id(Pkt, LServer),
631✔
499
    if Config#config.mam ->
631✔
500
            StorePkt = strip_x_jid_tags(Pkt1),
184✔
501
            case store_muc(MUCState, StorePkt, RoomJID, From, FromNick) of
184✔
502
                ok ->
503
                    mark_stored_msg(Pkt1, RoomJID);
176✔
504
                _ ->
505
                    Pkt1
8✔
506
            end;
507
        true ->
508
            Pkt1
447✔
509
    end;
510
muc_filter_message(Acc, _MUCState, _FromNick) ->
511
    Acc.
×
512

513
-spec make_id() -> integer().
514
make_id() ->
515
    erlang:system_time(microsecond).
38,293✔
516

517
-spec get_stanza_id(stanza()) -> integer().
518
get_stanza_id(#message{meta = #{stanza_id := ID}}) ->
519
    ID.
2,368✔
520

521
-spec init_stanza_id(stanza(), binary()) -> stanza().
522
init_stanza_id(#message{meta = #{stanza_id := _ID}} = Pkt, _LServer) ->
523
    Pkt;
5,399✔
524
init_stanza_id(#message{meta = #{from_offline := true}} = Pkt, _LServer) ->
525
    Pkt;
2,472✔
526
init_stanza_id(Pkt, LServer) ->
527
    ID = make_id(),
38,293✔
528
    Pkt1 = strip_my_stanza_id(Pkt, LServer),
38,293✔
529
    xmpp:put_meta(Pkt1, stanza_id, ID).
38,293✔
530

531
-spec set_stanza_id(stanza(), jid(), binary()) -> stanza().
532
set_stanza_id(Pkt, JID, ID) ->
533
    BareJID = jid:remove_resource(JID),
8,392✔
534
    Archived = #mam_archived{by = BareJID, id = ID},
8,392✔
535
    StanzaID = #stanza_id{by = BareJID, id = ID},
8,392✔
536
    NewEls = [Archived, StanzaID|xmpp:get_els(Pkt)],
8,392✔
537
    xmpp:set_els(Pkt, NewEls).
8,392✔
538

539
-spec get_origin_id(stanza()) -> binary().
540
get_origin_id(#message{type = groupchat} = Pkt) ->
541
    integer_to_binary(get_stanza_id(Pkt));
176✔
542
get_origin_id(#message{} = Pkt) ->
543
    case xmpp:get_subtag(Pkt, #origin_id{}) of
2,016✔
544
        #origin_id{id = ID} ->
545
            ID;
×
546
        _ ->
547
            xmpp:get_id(Pkt)
2,016✔
548
    end.
549

550
-spec mark_stored_msg(message(), jid()) -> message().
551
mark_stored_msg(#message{meta = #{stanza_id := ID}} = Pkt, JID) ->
552
    Pkt1 = set_stanza_id(Pkt, JID, integer_to_binary(ID)),
2,288✔
553
    xmpp:put_meta(Pkt1, mam_archived, true).
2,288✔
554

555
% Query archive v0.2
556
process_iq_v0_2(#iq{from = #jid{lserver = LServer},
557
                    to = #jid{lserver = LServer},
558
                    type = get, sub_els = [#mam_query{}]} = IQ) ->
559
    process_iq(LServer, IQ, chat);
368✔
560
process_iq_v0_2(IQ) ->
561
    process_iq(IQ).
512✔
562

563
% Query archive v0.3
564
process_iq_v0_3(#iq{from = #jid{lserver = LServer},
565
                    to = #jid{lserver = LServer},
566
                    type = set, sub_els = [#mam_query{}]} = IQ) ->
567
    process_iq(LServer, IQ, chat);
1,128✔
568
process_iq_v0_3(#iq{from = #jid{lserver = LServer},
569
                    to = #jid{lserver = LServer},
570
                    type = get, sub_els = [#mam_query{}]} = IQ) ->
571
    process_iq(LServer, IQ);
48✔
572
process_iq_v0_3(IQ) ->
573
    process_iq(IQ).
1,544✔
574

575
-spec muc_process_iq(ignore | iq(), mod_muc_room:state()) -> ignore | iq().
576
muc_process_iq(#iq{type = T, lang = Lang,
577
                   from = From,
578
                   sub_els = [#mam_query{xmlns = NS}]} = IQ,
579
               MUCState)
580
  when (T == set andalso (NS /= ?NS_MAM_TMP)) orelse
581
       (T == get andalso NS == ?NS_MAM_TMP) ->
582
    case may_enter_room(From, MUCState) of
40✔
583
        true ->
584
            LServer = MUCState#state.server_host,
40✔
585
            Role = mod_muc_room:get_role(From, MUCState),
40✔
586
            process_iq(LServer, IQ, {groupchat, Role, MUCState});
40✔
587
        false ->
588
            Text = ?T("Only members may query archives of this room"),
×
589
            xmpp:make_error(IQ, xmpp:err_forbidden(Text, Lang))
×
590
    end;
591
muc_process_iq(#iq{type = get,
592
                   sub_els = [#mam_query{xmlns = NS}]} = IQ,
593
               MUCState) when NS /= ?NS_MAM_TMP ->
594
    LServer = MUCState#state.server_host,
×
595
    process_iq(LServer, IQ);
×
596
muc_process_iq(IQ, _MUCState) ->
597
    IQ.
1,727✔
598

599
parse_query(#mam_query{xmlns = ?NS_MAM_TMP,
600
                       start = Start, 'end' = End,
601
                       with = With, withtext = Text}, _Lang) ->
602
    {ok, [{start, Start}, {'end', End},
368✔
603
          {with, With}, {withtext, Text}]};
604
parse_query(#mam_query{xdata = #xdata{}} = Query, Lang) ->
605
    X = xmpp_util:set_xdata_field(
96✔
606
          #xdata_field{var = <<"FORM_TYPE">>,
607
                       type = hidden, values = [?NS_MAM_1]},
608
          Query#mam_query.xdata),
609
    {Fields, WithText} = case lists:keytake(<<"{urn:xmpp:fulltext:0}fulltext">>, #xdata_field.var, X#xdata.fields) of
96✔
610
                             false -> {X#xdata.fields, <<>>};
96✔
611
                             {value, #xdata_field{values = [V]}, F} -> {F, V};
×
612
                             {value, _, F} -> {F, <<>>}
×
613
                         end,
614
    try        mam_query:decode(Fields) of
96✔
615
        Form ->
616
            if WithText /= <<>> ->
96✔
617
                    {ok, lists:keystore(withtext, 1, Form, {withtext, WithText})};
×
618
                true ->
619
                    {ok, Form}
96✔
620
            end
621
    catch _:{mam_query, Why} ->
622
            Txt = mam_query:format_error(Why),
×
623
            {error, xmpp:err_bad_request(Txt, Lang)}
×
624
    end;
625
parse_query(#mam_query{}, _Lang) ->
626
    {ok, []}.
1,072✔
627

628
disco_local_features({error, _Error} = Acc, _From, _To, _Node, _Lang) ->
629
    Acc;
×
630
disco_local_features(Acc, _From, _To, <<"">>, _Lang) ->
631
    Features = case Acc of
1,881✔
632
                   {result, Fs} -> Fs;
1,881✔
633
                   empty -> []
×
634
               end,
635
    {result, [?NS_MESSAGE_RETRACT | Features]};
1,881✔
636
disco_local_features(empty, _From, _To, _Node, Lang) ->
637
    Txt = ?T("No features available"),
×
638
    {error, xmpp:err_item_not_found(Txt, Lang)};
×
639
disco_local_features(Acc, _From, _To, _Node, _Lang) ->
640
    Acc.
1✔
641

642
disco_sm_features(empty, From, To, Node, Lang) ->
643
    disco_sm_features({result, []}, From, To, Node, Lang);
56✔
644
disco_sm_features({result, OtherFeatures},
645
                  #jid{luser = U, lserver = S},
646
                  #jid{luser = U, lserver = S}, <<"">>, _Lang) ->
647
    {result, [?NS_MAM_TMP, ?NS_MAM_0, ?NS_MAM_1, ?NS_MAM_2, ?NS_SID_0,
32✔
648
              ?NS_MESSAGE_RETRACT |
649
              OtherFeatures]};
650
disco_sm_features(Acc, _From, _To, _Node, _Lang) ->
651
    Acc.
24✔
652

653
-spec message_is_archived(boolean(), c2s_state(), message()) -> boolean().
654
message_is_archived(true, _C2SState, _Pkt) ->
655
    true;
×
656
message_is_archived(false, #{lserver := LServer}, Pkt) ->
657
    case mod_mam_opt:assume_mam_usage(LServer) of
×
658
        true ->
659
            is_archived(Pkt, LServer);
×
660
        false ->
661
            false
×
662
    end.
663

664
%%%
665
%%% Commands
666
%%%
667

668
%% @format-begin
669

670
get_mam_count(User, Host) ->
671
    Jid = jid:make(User, Host),
48✔
672
    {_, _, Count} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_count),
48✔
673
    Count.
48✔
674

675
get_mam_messages(User, Host) ->
676
    Jid = jid:make(User, Host),
×
677
    {Messages, _, _} = select(Host, Jid, Jid, [], #rsm_set{}, chat, only_messages),
×
678
    format_user_messages(Messages).
×
679

680
format_user_messages(Messages) ->
681
    lists:map(fun({_ID, _IDInt, Fwd}) ->
×
682
                 El = hd(Fwd#forwarded.sub_els),
×
683
                 FPacket =
×
684
                     ejabberd_web_admin:pretty_print_xml(
685
                         xmpp:encode(El)),
686
                 SFrom = jid:encode(El#message.from),
×
687
                 STo = jid:encode(El#message.to),
×
688
                 Time = format_time(Fwd#forwarded.delay#delay.stamp),
×
689
                 {Time, SFrom, STo, FPacket}
×
690
              end,
691
              Messages).
692

693
format_time(Now) ->
694
    {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Now),
×
695
    str:format("~w-~.2.0w-~.2.0w ~.2.0w:~.2.0w:~.2.0w",
×
696
               [Year, Month, Day, Hour, Minute, Second]).
697

698
webadmin_user(Acc, User, Server, R) ->
699
    Acc
700
    ++ [make_command(get_mam_count,
24✔
701
                     R,
702
                     [{<<"user">>, User}, {<<"host">>, Server}],
703
                     [{result_links, [{value, arg_host, 4, <<"user/", User/binary, "/mam/">>}]}])].
704
%% @format-end
705

706
%%%
707
%%% Commands: Purge
708
%%%
709

710
delete_old_messages_batch(Server, Type, Days, BatchSize, Rate) when Type == <<"chat">>;
711
                                                                  Type == <<"groupchat">>;
712
                                                                  Type == <<"all">> ->
713
    CurrentTime = make_id(),
×
714
    Diff = Days * 24 * 60 * 60 * 1000000,
×
715
    TimeStamp = misc:usec_to_now(CurrentTime - Diff),
×
716
    TypeA = misc:binary_to_atom(Type),
×
717
    LServer = jid:nameprep(Server),
×
718
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
719

720
    case ejabberd_batch:register_task({mam, LServer}, 0, Rate, {LServer, TypeA, TimeStamp, BatchSize, none},
×
721
                                      fun({L, T, St, B, IS} = S) ->
722
                                          case {erlang:function_exported(Mod, delete_old_messages_batch, 4),
×
723
                                                erlang:function_exported(Mod, delete_old_messages_batch, 5)} of
724
                                              {true, _} ->
725
                                                  case Mod:delete_old_messages_batch(L, St, T, B) of
×
726
                                                      {ok, Count} ->
727
                                                          {ok, S, Count, undefined};
×
728
                                                      {error, _} = E ->
729
                                                          E
×
730
                                                  end;
731
                                              {_, true} ->
732
                                                  case Mod:delete_old_messages_batch(L, St, T, B, IS) of
×
733
                                                      {ok, IS2, Count} ->
734
                                                          {ok, {L, St, T, B, IS2}, Count, undefined};
×
735
                                                      {error, _} = E ->
736
                                                          E
×
737
                                                  end;
738
                                              _ ->
739
                                                  {error, not_implemented_for_backend}
×
740
                                          end
741
                                      end) of
742
        ok ->
743
            {ok, ""};
×
744
        {error, in_progress} ->
745
            {error, "Operation in progress"}
×
746
    end.
747
delete_old_messages_status(Server) ->
748
    LServer = jid:nameprep(Server),
×
749
    Msg = case ejabberd_batch:task_status({mam, LServer}) of
×
750
        not_started ->
751
            "Operation not started";
×
752
        {failed, Steps, Error} ->
753
            io_lib:format("Operation failed after deleting ~p messages with error ~p",
×
754
                          [Steps, misc:format_val(Error)]);
755
        {aborted, Steps, _} ->
756
            io_lib:format("Operation was aborted after deleting ~p messages",
×
757
                          [Steps]);
758
        {working, Steps, _} ->
759
            io_lib:format("Operation in progress, deleted ~p messages",
×
760
                          [Steps]);
761
        {completed, Steps, _} ->
762
            io_lib:format("Operation was completed after deleting ~p messages",
×
763
                          [Steps])
764
    end,
765
    lists:flatten(Msg).
×
766

767
delete_old_messages_abort(Server) ->
768
    LServer = jid:nameprep(Server),
×
769
    case ejabberd_batch:abort_task({mam, LServer}) of
×
770
        aborted -> "Operation aborted";
×
771
        not_started -> "No task running"
×
772
    end.
773

774
delete_old_messages(TypeBin, Days) when TypeBin == <<"chat">>;
775
                                        TypeBin == <<"groupchat">>;
776
                                        TypeBin == <<"all">> ->
777
    CurrentTime = make_id(),
×
778
    Diff = Days * 24 * 60 * 60 * 1000000,
×
779
    TimeStamp = misc:usec_to_now(CurrentTime - Diff),
×
780
    Type = misc:binary_to_atom(TypeBin),
×
781
    DBTypes = lists:usort(
×
782
                lists:map(
783
                  fun(Host) ->
784
                          case mod_mam_opt:db_type(Host) of
×
785
                              sql -> {sql, Host};
×
786
                              Other -> {Other, global}
×
787
                          end
788
                  end, ejabberd_option:hosts())),
789
    Results = lists:map(
×
790
                fun({DBType, ServerHost}) ->
791
                        Mod = gen_mod:db_mod(DBType, ?MODULE),
×
792
                        Mod:delete_old_messages(ServerHost, TimeStamp, Type)
×
793
                end, DBTypes),
794
    case lists:filter(fun(Res) -> Res /= ok end, Results) of
×
795
        [] -> ok;
×
796
        [NotOk|_] -> NotOk
×
797
    end;
798
delete_old_messages(_TypeBin, _Days) ->
799
    unsupported_type.
×
800

801
export(LServer) ->
802
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
803
    Mod:export(LServer).
×
804

805
-spec is_empty_for_user(binary(), binary()) -> boolean().
806
is_empty_for_user(User, Server) ->
807
    LUser = jid:nodeprep(User),
×
808
    LServer = jid:nameprep(Server),
×
809
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
810
    Mod:is_empty_for_user(LUser, LServer).
×
811

812
-spec is_empty_for_room(binary(), binary(), binary()) -> boolean().
813
is_empty_for_room(LServer, Name, Host) ->
814
    LName = jid:nodeprep(Name),
×
815
    LHost = jid:nameprep(Host),
×
816
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
817
    Mod:is_empty_for_room(LServer, LName, LHost).
×
818

819
-spec check_create_room(boolean(), binary(), binary(), binary()) -> boolean().
820
check_create_room(Acc, ServerHost, RoomID, Host) ->
821
    Acc and is_empty_for_room(ServerHost, RoomID, Host).
×
822

823
%%%===================================================================
824
%%% Internal functions
825
%%%===================================================================
826

827
process_iq(LServer, #iq{sub_els = [#mam_query{xmlns = NS}]} = IQ) ->
828
    Mod = gen_mod:db_mod(LServer, ?MODULE),
48✔
829
    CommonFields = [{with, undefined},
48✔
830
                    {start, undefined},
831
                    {'end', undefined}],
832
    ExtendedFields = Mod:extended_fields(LServer),
48✔
833
    Fields = mam_query:encode(CommonFields ++ ExtendedFields),
48✔
834
    X = xmpp_util:set_xdata_field(
48✔
835
          #xdata_field{var = <<"FORM_TYPE">>, type = hidden, values = [NS]},
836
          #xdata{type = form, fields = Fields}),
837
    xmpp:make_iq_result(IQ, #mam_query{xmlns = NS, xdata = X}).
48✔
838

839
% Preference setting (both v0.2 & v0.3)
840
process_iq(#iq{type = set, lang = Lang,
841
               sub_els = [#mam_prefs{default = undefined, xmlns = NS}]} = IQ) ->
842
    Why = {missing_attr, <<"default">>, <<"prefs">>, NS},
×
843
    ErrTxt = xmpp:io_format_error(Why),
×
844
    xmpp:make_error(IQ, xmpp:err_bad_request(ErrTxt, Lang));
×
845
process_iq(#iq{from = #jid{luser = LUser, lserver = LServer},
846
               to = #jid{lserver = LServer},
847
               type = set, lang = Lang,
848
               sub_els = [#mam_prefs{xmlns = NS,
849
                                     default = Default,
850
                                     always = Always0,
851
                                     never = Never0}]} = IQ) ->
852
    Access = mod_mam_opt:access_preferences(LServer),
1,288✔
853
    case acl:match_rule(LServer, Access, jid:make(LUser, LServer)) of
1,288✔
854
        allow ->
855
            Always = lists:usort(get_jids(Always0)),
1,288✔
856
            Never = lists:usort(get_jids(Never0)),
1,288✔
857
            case write_prefs(LUser, LServer, LServer, Default, Always, Never) of
1,288✔
858
                ok ->
859
                    NewPrefs = prefs_el(Default, Always, Never, NS),
1,288✔
860
                    xmpp:make_iq_result(IQ, NewPrefs);
1,288✔
861
                _Err ->
862
                    Txt = ?T("Database failure"),
×
863
                    xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
864
            end;
865
        deny ->
866
            Txt = ?T("MAM preference modification denied by service policy"),
×
867
            xmpp:make_error(IQ, xmpp:err_forbidden(Txt, Lang))
×
868
    end;
869
process_iq(#iq{from = #jid{luser = LUser, lserver = LServer},
870
               to = #jid{lserver = LServer}, lang = Lang,
871
               type = get, sub_els = [#mam_prefs{xmlns = NS}]} = IQ) ->
872
    case get_prefs(LUser, LServer) of
768✔
873
        {ok, Prefs} ->
874
            PrefsEl = prefs_el(Prefs#archive_prefs.default,
768✔
875
                               Prefs#archive_prefs.always,
876
                               Prefs#archive_prefs.never,
877
                               NS),
878
            xmpp:make_iq_result(IQ, PrefsEl);
768✔
879
        {error, _} ->
880
            Txt = ?T("Database failure"),
×
881
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
882
    end;
883
process_iq(IQ) ->
884
    xmpp:make_error(IQ, xmpp:err_not_allowed()).
×
885

886
process_iq(LServer, #iq{from = #jid{luser = LUser}, lang = Lang,
887
                        sub_els = [SubEl]} = IQ, MsgType) ->
888
    Ret = case MsgType of
1,536✔
889
              chat ->
890
                  maybe_activate_mam(LUser, LServer);
1,496✔
891
              _ ->
892
                  ok
40✔
893
          end,
894
    case Ret of
1,536✔
895
        ok ->
896
            case SubEl of
1,536✔
897
                #mam_query{rsm = #rsm_set{index = I}} when is_integer(I) ->
898
                    Txt = ?T("Unsupported <index/> element"),
×
899
                    xmpp:make_error(IQ, xmpp:err_feature_not_implemented(Txt, Lang));
×
900
                #mam_query{rsm = RSM, flippage = FlipPage, xmlns = NS} ->
901
                    case parse_query(SubEl, Lang) of
1,536✔
902
                        {ok, Query} ->
903
                            NewRSM = limit_max(RSM, NS),
1,536✔
904
                            select_and_send(LServer, Query, NewRSM, FlipPage, IQ, MsgType);
1,536✔
905
                        {error, Err} ->
906
                            xmpp:make_error(IQ, Err)
×
907
                    end
908
            end;
909
        {error, _} ->
910
             Txt = ?T("Database failure"),
×
911
            xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang))
×
912
    end.
913

914
-spec should_archive(message(), binary()) -> boolean().
915
should_archive(#message{type = error}, _LServer) ->
916
    false;
8,081✔
917
should_archive(#message{type = groupchat, meta = #{is_muc_subscriber := true}} = Msg, LServer) ->
918
        case mod_mam_opt:archive_muc_as_mucsub(LServer) of
×
919
                true ->
920
                    should_archive(Msg#message{type = chat}, LServer);
×
921
                false ->
922
                        false
×
923
        end;
924
should_archive(#message{type = groupchat}, _LServer) ->
925
    false;
6,948✔
926
should_archive(#message{meta = #{from_offline := true}}, _LServer) ->
927
    false;
2,512✔
928
should_archive(#message{body = Body, subject = Subject,
929
                        type = Type} = Pkt, LServer) ->
930
    case is_archived(Pkt, LServer) of
23,490✔
931
        true ->
932
            false;
×
933
        false ->
934
            case check_store_hint(Pkt) of
23,490✔
935
                store ->
936
                    true;
1,920✔
937
                no_store ->
938
                    false;
2,296✔
939
                none when Type == headline ->
940
                    false;
4,013✔
941
                none ->
942
                    case xmpp:get_text(Body) /= <<>> orelse
15,261✔
943
                         xmpp:get_text(Subject) /= <<>> of
11,589✔
944
                        true ->
945
                            true;
4,952✔
946
                        _ ->
947
                            case misc:unwrap_mucsub_message(Pkt) of
10,309✔
948
                                #message{type = groupchat} = Msg ->
949
                                    should_archive(Msg#message{type = chat}, LServer);
144✔
950
                                #message{} = Msg ->
951
                                    should_archive(Msg, LServer);
×
952
                                _ ->
953
                                    misc:is_mucsub_message(Pkt)
10,165✔
954
                            end
955
                    end
956
            end
957
    end;
958
should_archive(_, _LServer) ->
959
    false.
×
960

961
-spec strip_my_stanza_id(stanza(), binary()) -> stanza().
962
strip_my_stanza_id(Pkt, LServer) ->
963
    Els = xmpp:get_els(Pkt),
53,743✔
964
    NewEls = lists:filter(
53,743✔
965
               fun(El) ->
966
                       Name = xmpp:get_name(El),
57,476✔
967
                       NS = xmpp:get_ns(El),
57,476✔
968
                       if (Name == <<"archived">> andalso NS == ?NS_MAM_TMP);
57,476✔
969
                          (Name == <<"stanza-id">> andalso NS == ?NS_SID_0) ->
970
                               try xmpp:decode(El) of
592✔
971
                                   #mam_archived{by = By} ->
972
                                       By#jid.lserver /= LServer;
296✔
973
                                   #stanza_id{by = By} ->
974
                                       By#jid.lserver /= LServer
296✔
975
                               catch _:{xmpp_codec, _} ->
976
                                       false
×
977
                               end;
978
                          true ->
979
                               true
56,884✔
980
                       end
981
               end, Els),
982
    xmpp:set_els(Pkt, NewEls).
53,743✔
983

984
-spec strip_x_jid_tags(stanza()) -> stanza().
985
strip_x_jid_tags(Pkt) ->
986
    Els = xmpp:get_els(Pkt),
184✔
987
    NewEls = lists:filter(
184✔
988
               fun(El) ->
989
                       case xmpp:get_name(El) of
192✔
990
                           <<"x">> ->
991
                               NS = xmpp:get_ns(El),
×
992
                               Items = if NS == ?NS_MUC_USER;
×
993
                                          NS == ?NS_MUC_ADMIN;
994
                                          NS == ?NS_MUC_OWNER ->
995
                                               try xmpp:decode(El) of
×
996
                                                   #muc_user{items = Is} -> Is;
×
997
                                                   #muc_admin{items = Is} -> Is;
×
998
                                                   #muc_owner{items = Is} -> Is
×
999
                                               catch _:{xmpp_codec, _} ->
1000
                                                       []
×
1001
                                               end;
1002
                                          true ->
1003
                                               []
×
1004
                                       end,
1005
                               not lists:any(
×
1006
                                     fun(#muc_item{jid = JID}) ->
1007
                                             JID /= undefined
×
1008
                                     end, Items);
1009
                           _ ->
1010
                               true
192✔
1011
                       end
1012
               end, Els),
1013
    xmpp:set_els(Pkt, NewEls).
184✔
1014

1015
-spec should_archive_peer(binary(), binary(),
1016
                          #archive_prefs{}, jid()) -> boolean().
1017
should_archive_peer(LUser, LServer,
1018
                    #archive_prefs{default = Default,
1019
                                   always = Always,
1020
                                   never = Never},
1021
                    Peer) ->
1022
    LPeer = jid:remove_resource(jid:tolower(Peer)),
6,872✔
1023
    case lists:member(LPeer, Always) of
6,872✔
1024
        true ->
1025
            true;
×
1026
        false ->
1027
            case lists:member(LPeer, Never) of
6,872✔
1028
                true ->
1029
                    false;
×
1030
                false ->
1031
                    case Default of
6,872✔
1032
                        always -> true;
2,112✔
1033
                        never -> false;
4,760✔
1034
                        roster ->
1035
                            {Sub, _, _} = ejabberd_hooks:run_fold(
×
1036
                                            roster_get_jid_info,
1037
                                            LServer, {none, none, []},
1038
                                            [LUser, LServer, Peer]),
1039
                            Sub == both orelse Sub == from orelse Sub == to
×
1040
                    end
1041
            end
1042
    end.
1043

1044
-spec should_archive_muc(message()) -> boolean().
1045
should_archive_muc(#message{type = groupchat,
1046
                            body = Body, subject = Subj} = Pkt) ->
1047
    case check_store_hint(Pkt) of
184✔
1048
        store ->
1049
            true;
×
1050
        no_store ->
1051
            false;
8✔
1052
        none ->
1053
            case xmpp:get_text(Body) of
176✔
1054
                <<"">> ->
1055
                    case xmpp:get_text(Subj) of
×
1056
                        <<"">> ->
1057
                            false;
×
1058
                        _ ->
1059
                            true
×
1060
                    end;
1061
                _ ->
1062
                    true
176✔
1063
            end
1064
    end;
1065
should_archive_muc(_) ->
1066
    false.
×
1067

1068
-spec check_store_hint(message()) -> store | no_store | none.
1069
check_store_hint(Pkt) ->
1070
    case has_store_hint(Pkt) of
23,674✔
1071
        true ->
1072
            store;
1,920✔
1073
        false ->
1074
            case has_no_store_hint(Pkt) of
21,754✔
1075
                true ->
1076
                    no_store;
2,304✔
1077
                false ->
1078
                    none
19,450✔
1079
            end
1080
    end.
1081

1082
-spec has_store_hint(message()) -> boolean().
1083
has_store_hint(Message) ->
1084
    xmpp:has_subtag(Message, #hint{type = 'store'}).
23,674✔
1085

1086
-spec has_no_store_hint(message()) -> boolean().
1087
has_no_store_hint(Message) ->
1088
    xmpp:has_subtag(Message, #hint{type = 'no-store'}) orelse
21,754✔
1089
    xmpp:has_subtag(Message, #hint{type = 'no-storage'}) orelse
19,450✔
1090
    xmpp:has_subtag(Message, #hint{type = 'no-permanent-store'}) orelse
19,450✔
1091
    xmpp:has_subtag(Message, #hint{type = 'no-permanent-storage'}).
19,450✔
1092

1093
-spec is_archived(message(), binary()) -> boolean().
1094
is_archived(Pkt, LServer) ->
1095
    case xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}) of
23,490✔
1096
        #stanza_id{by = #jid{lserver = LServer}} ->
1097
            true;
×
1098
        _ ->
1099
            false
23,490✔
1100
    end.
1101

1102
-spec may_enter_room(jid(), mod_muc_room:state()) -> boolean().
1103
may_enter_room(From,
1104
               #state{config = #config{members_only = false}} = MUCState) ->
1105
    mod_muc_room:get_affiliation(From, MUCState) /= outcast;
40✔
1106
may_enter_room(From, MUCState) ->
1107
    mod_muc_room:is_occupant_or_admin(From, MUCState).
×
1108

1109
-spec store_msg(message(), binary(), binary(), jid(), send | recv)
1110
      -> ok | pass | any().
1111
store_msg(#message{type = groupchat, from = From, to = To, meta = #{is_muc_subscriber := true}} = Pkt, LUser, LServer, _Peer, Dir) ->
1112
        BarePeer = jid:remove_resource(From),
×
1113
        StanzaId = xmpp:get_subtag(Pkt, #stanza_id{by = #jid{}}),
×
1114
    Id = case StanzaId of
×
1115
             #stanza_id{id = Id2} ->
1116
                 Id2;
×
1117
             _ ->
1118
                 p1_rand:get_string()
×
1119
         end,
1120
    Pkt2 = #message{
×
1121
             to = To,
1122
             from = BarePeer,
1123
             id = Id,
1124
             sub_els = [#ps_event{
1125
                          items = #ps_items{
1126
                                    node = ?NS_MUCSUB_NODES_MESSAGES,
1127
                                    items = [#ps_item{
1128
                                               id = Id,
1129
                                               sub_els = [Pkt]
1130
                                              }]
1131
                                   }
1132
                         }]
1133
            },
1134
        Pkt3 = xmpp:put_meta(Pkt2, stanza_id, binary_to_integer(Id)),
×
1135
        store_msg(Pkt3, LUser, LServer, BarePeer, Dir);
×
1136
store_msg(Pkt, LUser, LServer, Peer, Dir) ->
1137
    case get_prefs(LUser, LServer) of
6,872✔
1138
        {ok, Prefs} ->
1139
            UseMucArchive = mod_mam_opt:user_mucsub_from_muc_archive(LServer),
6,872✔
1140
            StoredInMucMam = UseMucArchive andalso xmpp:get_meta(Pkt, in_muc_mam, false),
6,872✔
1141
            case {should_archive_peer(LUser, LServer, Prefs, Peer), Pkt, StoredInMucMam} of
6,872✔
1142
                {true, #message{meta = #{sm_copy := true}}, _} ->
1143
                    ok; % Already stored.
×
1144
                {true, _, true} ->
1145
                    ok; % Stored in muc archive.
96✔
1146
                {true, _, _} ->
1147
                    case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
2,016✔
1148
                                                 [LUser, LServer, Peer, <<"">>, chat, Dir]) of
1149
                        #message{} -> ok;
2,016✔
1150
                        _ -> pass
×
1151
                    end;
1152
                {false, _, _} ->
1153
                    pass
4,760✔
1154
            end;
1155
        {error, _} ->
1156
            pass
×
1157
    end.
1158

1159
-spec store_muc(mod_muc_room:state(), message(), jid(), jid(), binary())
1160
      -> ok | pass | any().
1161
store_muc(MUCState, Pkt, RoomJID, Peer, Nick) ->
1162
    case should_archive_muc(Pkt) of
184✔
1163
        true ->
1164
            {U, S, _} = jid:tolower(RoomJID),
176✔
1165
            LServer = MUCState#state.server_host,
176✔
1166
            case ejabberd_hooks:run_fold(store_mam_message, LServer, Pkt,
176✔
1167
                                         [U, S, Peer, Nick, groupchat, recv]) of
1168
                #message{} -> ok;
176✔
1169
                _ -> pass
×
1170
            end;
1171
        false ->
1172
            pass
8✔
1173
    end.
1174

1175
store_mam_message(Pkt, U, S, Peer, Nick, Type, Dir) ->
1176
    LServer = ejabberd_router:host_of_route(S),
2,192✔
1177
    US = {U, S},
2,192✔
1178
    ID = get_stanza_id(Pkt),
2,192✔
1179
    OriginID = get_origin_id(Pkt),
2,192✔
1180
    Retract = case xmpp:get_subtag(Pkt, #message_retract{}) of
2,192✔
1181
                  #message_retract{id = RID} when RID /= <<"">> ->
1182
                      {true, RID};
88✔
1183
                  _ ->
1184
                      false
2,104✔
1185
              end,
1186
    El = xmpp:encode(Pkt),
2,192✔
1187
    Mod = gen_mod:db_mod(LServer, ?MODULE),
2,192✔
1188
    Mod:store(El, LServer, US, Type, Peer, Nick, Dir, ID, OriginID, Retract),
2,192✔
1189
    Pkt.
2,192✔
1190

1191
write_prefs(LUser, LServer, Host, Default, Always, Never) ->
1192
    Prefs = #archive_prefs{us = {LUser, LServer},
1,288✔
1193
                           default = Default,
1194
                           always = Always,
1195
                           never = Never},
1196
    Mod = gen_mod:db_mod(Host, ?MODULE),
1,288✔
1197
    case Mod:write_prefs(LUser, LServer, Prefs, Host) of
1,288✔
1198
        ok ->
1199
            case use_cache(Mod, LServer) of
1,288✔
1200
                true ->
1201
                    ets_cache:delete(archive_prefs_cache, {LUser, LServer},
1,288✔
1202
                                     cache_nodes(Mod, LServer));
1203
                false ->
1204
                    ok
×
1205
            end;
1206
        _Err ->
1207
            {error, db_failure}
×
1208
    end.
1209

1210
get_prefs(LUser, LServer) ->
1211
    Mod = gen_mod:db_mod(LServer, ?MODULE),
7,640✔
1212
    Res = case use_cache(Mod, LServer) of
7,640✔
1213
              true ->
1214
                  ets_cache:lookup(archive_prefs_cache, {LUser, LServer},
7,640✔
1215
                                   fun() -> Mod:get_prefs(LUser, LServer) end);
963✔
1216
              false ->
1217
                  Mod:get_prefs(LUser, LServer)
×
1218
          end,
1219
    case Res of
7,640✔
1220
        {ok, Prefs} ->
1221
            {ok, Prefs};
2,880✔
1222
        {error, _} ->
1223
            {error, db_failure};
×
1224
        error ->
1225
            ActivateOpt = mod_mam_opt:request_activates_archiving(LServer),
4,760✔
1226
            case ActivateOpt of
4,760✔
1227
                true ->
1228
                    {ok, #archive_prefs{us = {LUser, LServer}, default = never}};
×
1229
                false ->
1230
                    Default = mod_mam_opt:default(LServer),
4,760✔
1231
                    {ok, #archive_prefs{us = {LUser, LServer}, default = Default}}
4,760✔
1232
            end
1233
    end.
1234

1235
prefs_el(Default, Always, Never, NS) ->
1236
    #mam_prefs{default = Default,
2,056✔
1237
               always = [jid:make(LJ) || LJ <- Always],
768✔
1238
               never = [jid:make(LJ) || LJ <- Never],
768✔
1239
               xmlns = NS}.
1240

1241
maybe_activate_mam(LUser, LServer) ->
1242
    ActivateOpt = mod_mam_opt:request_activates_archiving(LServer),
1,496✔
1243
    case ActivateOpt of
1,496✔
1244
        true ->
1245
            Mod = gen_mod:db_mod(LServer, ?MODULE),
×
1246
            Res = case use_cache(Mod, LServer) of
×
1247
                      true ->
1248
                          ets_cache:lookup(archive_prefs_cache,
×
1249
                                           {LUser, LServer},
1250
                                           fun() ->
1251
                                                   Mod:get_prefs(LUser, LServer)
×
1252
                                           end);
1253
                      false ->
1254
                          Mod:get_prefs(LUser, LServer)
×
1255
                  end,
1256
            case Res of
×
1257
                {ok, _Prefs} ->
1258
                    ok;
×
1259
                {error, _} ->
1260
                    {error, db_failure};
×
1261
                error ->
1262
                    Default = mod_mam_opt:default(LServer),
×
1263
                    write_prefs(LUser, LServer, LServer, Default, [], [])
×
1264
            end;
1265
        false ->
1266
            ok
1,496✔
1267
    end.
1268

1269
select_and_send(LServer, Query, RSM, FlipPage, #iq{from = From, to = To} = IQ, MsgType) ->
1270
    Ret = case MsgType of
1,536✔
1271
              chat ->
1272
                  select(LServer, From, From, Query, RSM, MsgType);
1,496✔
1273
              _ ->
1274
                  select(LServer, From, To, Query, RSM, MsgType)
40✔
1275
          end,
1276
    case Ret of
1,536✔
1277
        {Msgs, IsComplete, Count} ->
1278
            SortedMsgs = lists:keysort(2, Msgs),
1,536✔
1279
            SortedMsgs2 = case FlipPage of
1,536✔
1280
                               true -> lists:reverse(SortedMsgs);
×
1281
                               false -> SortedMsgs
1,536✔
1282
                          end,
1283
            send(SortedMsgs2, Count, IsComplete, IQ);
1,536✔
1284
        {error, _} ->
1285
            Txt = ?T("Database failure"),
×
1286
            Err = xmpp:err_internal_server_error(Txt, IQ#iq.lang),
×
1287
            xmpp:make_error(IQ, Err)
×
1288
    end.
1289

1290
select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType) ->
1291
    select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, all).
1,542✔
1292

1293
select(_LServer, JidRequestor, JidArchive, Query, RSM,
1294
       {groupchat, _Role, #state{config = #config{mam = false},
1295
                                 history = History}} = MsgType, _Flags) ->
1296
    Start = proplists:get_value(start, Query),
8✔
1297
    End = proplists:get_value('end', Query),
8✔
1298
    #lqueue{queue = Q} = History,
8✔
1299
    L = p1_queue:len(Q),
8✔
1300
    Msgs =
8✔
1301
        lists:flatmap(
1302
          fun({Nick, Pkt, _HaveSubject, Now, _Size}) ->
1303
                  TS = misc:now_to_usec(Now),
160✔
1304
                  case match_interval(Now, Start, End) and
160✔
1305
                      match_rsm(Now, RSM) of
1306
                      true ->
1307
                          case msg_to_el(#archive_msg{
160✔
1308
                                            id = integer_to_binary(TS),
1309
                                            type = groupchat,
1310
                                            timestamp = Now,
1311
                                            peer = undefined,
1312
                                            nick = Nick,
1313
                                            packet = Pkt},
1314
                                         MsgType, JidRequestor, JidArchive) of
1315
                              {ok, Msg} ->
1316
                                  [{integer_to_binary(TS), TS, Msg}];
160✔
1317
                              {error, _} ->
1318
                                  []
×
1319
                          end;
1320
                      false ->
1321
                          []
×
1322
                  end
1323
          end, p1_queue:to_list(Q)),
1324
    case RSM of
8✔
1325
        #rsm_set{max = Max, before = Before} when is_binary(Before) ->
1326
            {NewMsgs, IsComplete} = filter_by_max(lists:reverse(Msgs), Max),
×
1327
            {NewMsgs, IsComplete, L};
×
1328
        #rsm_set{max = Max} ->
1329
            {NewMsgs, IsComplete} = filter_by_max(Msgs, Max),
8✔
1330
            {NewMsgs, IsComplete, L};
8✔
1331
        _ ->
1332
            {Msgs, true, L}
×
1333
    end;
1334
select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) ->
1335
    case might_expose_jid(Query, MsgType) of
1,598✔
1336
        true ->
1337
            {[], true, 0};
×
1338
        false ->
1339
            case {MsgType, mod_mam_opt:user_mucsub_from_muc_archive(LServer)} of
1,598✔
1340
                {chat, true} ->
1341
                    select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags);
24✔
1342
                _ ->
1343
                    db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags)
1,574✔
1344
            end
1345
    end.
1346

1347
select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags) ->
1348
    MucHosts = mod_muc_admin:find_hosts(LServer),
24✔
1349
    Mod = gen_mod:db_mod(LServer, ?MODULE),
24✔
1350
    case proplists:get_value(with, Query) of
24✔
1351
        #jid{lserver = WithLServer} = MucJid ->
1352
            case lists:member(WithLServer, MucHosts) of
×
1353
                true ->
1354
                    select(LServer, JidRequestor, MucJid, Query, RSM,
×
1355
                           {groupchat, member, #state{config = #config{mam = true}}});
1356
                _ ->
1357
                    db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags)
×
1358
            end;
1359
        _ ->
1360
            case erlang:function_exported(Mod, select_with_mucsub, 6) of
24✔
1361
                true ->
1362
                    Mod:select_with_mucsub(LServer, JidRequestor, JidArchive, Query, RSM, Flags);
18✔
1363
                false ->
1364
                    select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags)
6✔
1365
            end
1366
    end.
1367

1368
select_with_mucsub_fallback(LServer, JidRequestor, JidArchive, Query, RSM, Flags) ->
1369
    case db_select(LServer, JidRequestor, JidArchive, Query, RSM, chat, Flags) of
6✔
1370
        {error, _} = Err ->
1371
            Err;
×
1372
        {Entries, All, Count} ->
1373
            {Dir, Max} = case RSM of
6✔
1374
                             #rsm_set{max = M, before = V} when is_binary(V) ->
1375
                                 {desc, M};
2✔
1376
                             #rsm_set{max = M} ->
1377
                                 {asc, M};
4✔
1378
                             _ ->
1379
                                 {asc, undefined}
×
1380
                         end,
1381
            SubRooms = case mod_muc_admin:find_hosts(LServer) of
6✔
1382
                           [First|_] ->
1383
                               case mod_muc:get_subscribed_rooms(First, JidRequestor) of
6✔
1384
                                   {ok, L} -> L;
6✔
1385
                                   {error, _} -> []
×
1386
                               end;
1387
                           _ ->
1388
                               []
×
1389
                       end,
1390
            SubRoomJids = [Jid || {Jid, _, _} <- SubRooms],
6✔
1391
            {E2, A2, C2} =
6✔
1392
                lists:foldl(
1393
                  fun(MucJid, {E0, A0, C0}) ->
1394
                          case select(LServer, JidRequestor, MucJid, Query, RSM,
6✔
1395
                                      {groupchat, member, #state{config = #config{mam = true}}}) of
1396
                              {error, _} ->
1397
                                  {E0, A0, C0};
×
1398
                              {E, A, C} ->
1399
                                  {lists:keymerge(2, E0, wrap_as_mucsub(E, JidRequestor)),
6✔
1400
                                   A0 andalso A, C0 + C}
6✔
1401
                          end
1402
                  end, {Entries, All, Count}, SubRoomJids),
1403
            case {Dir, Max} of
6✔
1404
                {_, undefined} ->
1405
                    {E2, A2, C2};
2✔
1406
                {desc, _} ->
1407
                    Start = case length(E2) of
×
1408
                                Len when Len < Max -> 1;
×
1409
                                Len -> Len - Max + 1
×
1410
                            end,
1411
                    Sub = lists:sublist(E2, Start, Max),
×
1412
                    {Sub, if Sub == E2 -> A2; true -> false end, C2};
×
1413
                _ ->
1414
                    Sub = lists:sublist(E2, 1, Max),
4✔
1415
                    {Sub, if Sub == E2 -> A2; true -> false end, C2}
4✔
1416
            end
1417
    end.
1418

1419
db_select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags) ->
1420
    Mod = gen_mod:db_mod(LServer, ?MODULE),
1,580✔
1421
    case erlang:function_exported(Mod, select, 7) of
1,580✔
1422
        true ->
1423
            Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType, Flags);
1,176✔
1424
        _ ->
1425
        Mod:select(LServer, JidRequestor, JidArchive, Query, RSM, MsgType)
404✔
1426
    end.
1427

1428
wrap_as_mucsub(Messages, #jid{lserver = LServer} = Requester) ->
1429
    ReqBare = jid:remove_resource(Requester),
72✔
1430
    ReqServer = jid:make(<<>>, LServer, <<>>),
72✔
1431
    [{T1, T2, wrap_as_mucsub(M, ReqBare, ReqServer)} || {T1, T2, M} <- Messages].
72✔
1432

1433
wrap_as_mucsub(Message, Requester, ReqServer) ->
1434
    case Message of
88✔
1435
        #forwarded{delay = #delay{stamp = Stamp, desc = Desc},
1436
                   sub_els = [#message{from = From, sub_els = SubEls, subject = Subject} = Msg]} ->
1437
            {L1, SubEls2} = case lists:keytake(mam_archived, 1, SubEls) of
88✔
1438
                                {value, Arch, Rest} ->
1439
                                    {[Arch#mam_archived{by = Requester}], Rest};
88✔
1440
                                _ ->
1441
                                    {[], SubEls}
×
1442
                            end,
1443
            {Sid, L2, SubEls3} = case lists:keytake(stanza_id, 1, SubEls2) of
88✔
1444
                                {value, #stanza_id{id = Sid0} = SID, Rest2} ->
1445
                                    {Sid0, [SID#stanza_id{by = Requester} | L1], Rest2};
88✔
1446
                                _ ->
1447
                                    {p1_rand:get_string(), L1, SubEls2}
×
1448
                            end,
1449
            Msg2 = Msg#message{to = Requester, sub_els = SubEls3},
88✔
1450
            Node = case Subject of
88✔
1451
                       [] ->
1452
                           ?NS_MUCSUB_NODES_MESSAGES;
88✔
1453
                       _ ->
1454
                           ?NS_MUCSUB_NODES_SUBJECT
×
1455
                   end,
1456
            #forwarded{delay = #delay{stamp = Stamp, desc = Desc, from = ReqServer},
88✔
1457
                       sub_els = [
1458
                           #message{from = jid:remove_resource(From), to = Requester,
1459
                                    id = Sid,
1460
                                    sub_els = [#ps_event{
1461
                                        items = #ps_items{
1462
                                            node = Node,
1463
                                            items = [#ps_item{
1464
                                                id = Sid,
1465
                                                sub_els = [Msg2]
1466
                                            }]}} | L2]}]};
1467
        _ ->
1468
            Message
×
1469
    end.
1470

1471

1472
msg_to_el(#archive_msg{timestamp = TS, packet = El, nick = Nick,
1473
                       peer = Peer, id = ID},
1474
          MsgType, JidRequestor, #jid{lserver = LServer} = JidArchive) ->
1475
    CodecOpts = ejabberd_config:codec_options(),
6,104✔
1476
    try xmpp:decode(El, ?NS_CLIENT, CodecOpts) of
6,104✔
1477
        Pkt1 ->
1478
            Pkt2 = case MsgType of
6,104✔
1479
                       chat -> set_stanza_id(Pkt1, JidArchive, ID);
5,696✔
1480
                       {groupchat, _, _} -> set_stanza_id(Pkt1, JidArchive, ID);
408✔
1481
                       _ -> Pkt1
×
1482
                   end,
1483
            Pkt3 = maybe_update_from_to(
6,104✔
1484
                     Pkt2, JidRequestor, JidArchive, Peer, MsgType, Nick),
1485
            Pkt4 = xmpp:put_meta(Pkt3, archive_nick, Nick),
6,104✔
1486
            Delay = #delay{stamp = TS, from = jid:make(LServer)},
6,104✔
1487
            {ok, #forwarded{sub_els = [Pkt4], delay = Delay}}
6,104✔
1488
    catch _:{xmpp_codec, Why} ->
1489
            ?ERROR_MSG("Failed to decode raw element ~p from message "
×
1490
                       "archive of user ~ts: ~ts",
1491
                       [El, jid:encode(JidArchive), xmpp:format_error(Why)]),
×
1492
            {error, invalid_xml}
×
1493
    end.
1494

1495
maybe_update_from_to(#message{sub_els = Els} = Pkt, JidRequestor, JidArchive,
1496
                     Peer, {groupchat, Role,
1497
                            #state{config = #config{anonymous = Anon}}},
1498
                     Nick) ->
1499
    ExposeJID = case {Peer, JidRequestor} of
408✔
1500
                    {undefined, _JidRequestor} ->
1501
                        false;
160✔
1502
                    {{U, S, _R}, #jid{luser = U, lserver = S}} ->
1503
                        true;
160✔
1504
                    {_Peer, _JidRequestor} when not Anon; Role == moderator ->
1505
                        true;
×
1506
                    {_Peer, _JidRequestor} ->
1507
                        false
88✔
1508
                end,
1509
    Items = case ExposeJID of
408✔
1510
                true ->
1511
                    [#muc_user{items = [#muc_item{jid = Peer}]}];
160✔
1512
                false ->
1513
                    []
248✔
1514
            end,
1515
    Pkt#message{from = jid:replace_resource(JidArchive, Nick),
408✔
1516
                to = undefined,
1517
                sub_els = Items ++ Els};
1518
maybe_update_from_to(Pkt, _JidRequestor, _JidArchive, _Peer, _MsgType, _Nick) ->
1519
    Pkt.
5,696✔
1520

1521
-spec send([{binary(), integer(), xmlel()}],
1522
           count(), boolean(), iq()) -> iq() | ignore.
1523
send(Msgs, Count, IsComplete,
1524
     #iq{from = From, to = To,
1525
         sub_els = [#mam_query{id = QID, xmlns = NS}]} = IQ) ->
1526
    Hint = #hint{type = 'no-store'},
1,536✔
1527
    Els = lists:map(
1,536✔
1528
            fun({ID, _IDInt, El}) ->
1529
                    #message{from = To,
4,664✔
1530
                             to = From,
1531
                             sub_els = [#mam_result{xmlns = NS,
1532
                                                    id = ID,
1533
                                                    queryid = QID,
1534
                                                    sub_els = [El]}]}
1535
            end, Msgs),
1536
    RSMOut = make_rsm_out(Msgs, Count),
1,536✔
1537
    Result = if NS == ?NS_MAM_TMP ->
1,536✔
1538
                     #mam_query{xmlns = NS, id = QID, rsm = RSMOut};
368✔
1539
                NS == ?NS_MAM_0 ->
1540
                     #mam_fin{xmlns = NS, id = QID, rsm = RSMOut,
368✔
1541
                              complete = IsComplete};
1542
                true ->
1543
                     #mam_fin{xmlns = NS, rsm = RSMOut, complete = IsComplete}
800✔
1544
             end,
1545
    if NS /= ?NS_MAM_0 ->
1,536✔
1546
            lists:foreach(
1,168✔
1547
              fun(El) ->
1548
                      ejabberd_router:route(El)
3,608✔
1549
              end, Els),
1550
            xmpp:make_iq_result(IQ, Result);
1,168✔
1551
       true ->
1552
            ejabberd_router:route(xmpp:make_iq_result(IQ)),
368✔
1553
            lists:foreach(
368✔
1554
              fun(El) ->
1555
                      ejabberd_router:route(El)
1,056✔
1556
              end, Els),
1557
            ejabberd_router:route(
368✔
1558
              #message{from = To, to = From, sub_els = [Result, Hint]}),
1559
            ignore
368✔
1560
    end.
1561

1562
-spec make_rsm_out([{binary(), integer(), xmlel()}], count()) -> rsm_set().
1563
make_rsm_out([], Count) ->
1564
    #rsm_set{count = Count};
192✔
1565
make_rsm_out([{FirstID, _, _}|_] = Msgs, Count) ->
1566
    {LastID, _, _} = lists:last(Msgs),
1,344✔
1567
    #rsm_set{first = #rsm_first{data = FirstID}, last = LastID, count = Count}.
1,344✔
1568

1569
filter_by_max(Msgs, undefined) ->
1570
    {Msgs, true};
×
1571
filter_by_max(Msgs, Len) when is_integer(Len), Len >= 0 ->
1572
    {lists:sublist(Msgs, Len), length(Msgs) =< Len};
8✔
1573
filter_by_max(_Msgs, _Junk) ->
1574
    {[], true}.
×
1575

1576
-spec limit_max(rsm_set() | undefined, binary()) -> rsm_set() | undefined.
1577
limit_max(RSM, ?NS_MAM_TMP) ->
1578
    RSM; % XEP-0313 v0.2 doesn't require clients to support RSM.
368✔
1579
limit_max(undefined, _NS) ->
1580
    #rsm_set{max = ?DEF_PAGE_SIZE};
208✔
1581
limit_max(#rsm_set{max = Max} = RSM, _NS) when not is_integer(Max) ->
1582
    RSM#rsm_set{max = ?DEF_PAGE_SIZE};
576✔
1583
limit_max(#rsm_set{max = Max} = RSM, _NS) when Max > ?MAX_PAGE_SIZE ->
1584
    RSM#rsm_set{max = ?MAX_PAGE_SIZE};
×
1585
limit_max(RSM, _NS) ->
1586
    RSM.
384✔
1587

1588
match_interval(Now, Start, undefined) ->
1589
    Now >= Start;
160✔
1590
match_interval(Now, Start, End) ->
1591
    (Now >= Start) and (Now =< End).
×
1592

1593
match_rsm(Now, #rsm_set{'after' = ID}) when is_binary(ID), ID /= <<"">> ->
1594
    Now1 = (catch misc:usec_to_now(binary_to_integer(ID))),
×
1595
    Now > Now1;
×
1596
match_rsm(Now, #rsm_set{before = ID}) when is_binary(ID), ID /= <<"">> ->
1597
    Now1 = (catch misc:usec_to_now(binary_to_integer(ID))),
×
1598
    Now < Now1;
×
1599
match_rsm(_Now, _) ->
1600
    true.
160✔
1601

1602
might_expose_jid(Query,
1603
                 {groupchat, Role, #state{config = #config{anonymous = true}}})
1604
  when Role /= moderator ->
1605
    proplists:is_defined(with, Query);
6✔
1606
might_expose_jid(_Query, _MsgType) ->
1607
    false.
1,592✔
1608

1609
get_jids(undefined) ->
1610
    [];
1,040✔
1611
get_jids(Js) ->
1612
    [jid:tolower(jid:remove_resource(J)) || J <- Js].
1,536✔
1613

1614
is_archiving_enabled(LUser, LServer) ->
1615
    case gen_mod:is_loaded(LServer, mod_mam) of
×
1616
        true ->
1617
            case get_prefs(LUser, LServer) of
×
1618
                {ok, #archive_prefs{default = Default}} when Default /= never ->
1619
                    true;
×
1620
                _ ->
1621
                    false
×
1622
            end;
1623
        false ->
1624
            false
×
1625
    end.
1626

1627
get_commands_spec() ->
1628
    [
1629
     #ejabberd_commands{name = get_mam_count, tags = [mam],
218✔
1630
                        desc = "Get number of MAM messages in a local user archive",
1631
                        module = ?MODULE, function = get_mam_count,
1632
                        note = "added in 24.10",
1633
                        policy = user,
1634
                        args = [],
1635
                        result_example = 5,
1636
                        result_desc = "Number",
1637
                        result = {value, integer}},
1638
     #ejabberd_commands{name = get_mam_messages,
1639
                        tags = [internal, mam],
1640
                        desc = "Get the mam messages",
1641
                        policy = user,
1642
                        module = mod_mam, function = get_mam_messages,
1643
                        args = [],
1644
                        result = {archive, {list, {messages, {tuple, [{time, string},
1645
                                                                    {from, string},
1646
                                                                    {to, string},
1647
                                                                    {packet, string}
1648
                                                                   ]}}}}},
1649

1650
     #ejabberd_commands{name = delete_old_mam_messages, tags = [mam, purge],
1651
                        desc = "Delete MAM messages older than DAYS",
1652
                        longdesc = "Valid message TYPEs: "
1653
                                   "`chat`, `groupchat`, `all`.",
1654
                        module = ?MODULE, function = delete_old_messages,
1655
                        args_desc = ["Type of messages to delete (`chat`, `groupchat`, `all`)",
1656
                                     "Days to keep messages"],
1657
                        args_example = [<<"all">>, 31],
1658
                        args = [{type, binary}, {days, integer}],
1659
                        result = {res, rescode}},
1660
     #ejabberd_commands{name = delete_old_mam_messages_batch, tags = [mam, purge],
1661
                        desc = "Delete MAM messages older than DAYS",
1662
                        note = "added in 22.05",
1663
                        longdesc = "Valid message TYPEs: "
1664
                                   "`chat`, `groupchat`, `all`.",
1665
                        module = ?MODULE, function = delete_old_messages_batch,
1666
                        args_desc = ["Name of host where messages should be deleted",
1667
                                     "Type of messages to delete (`chat`, `groupchat`, `all`)",
1668
                                     "Days to keep messages",
1669
                                     "Number of messages to delete per batch",
1670
                                     "Desired rate of messages to delete per minute"],
1671
                        args_example = [<<"localhost">>, <<"all">>, 31, 1000, 10000],
1672
                        args = [{host, binary}, {type, binary}, {days, integer}, {batch_size, integer}, {rate, integer}],
1673
                        result = {res, restuple},
1674
                        result_desc = "Result tuple",
1675
                        result_example = {ok, <<"Removal of 5000 messages in progress">>}},
1676
     #ejabberd_commands{name = delete_old_mam_messages_status, tags = [mam, purge],
1677
                        desc = "Status of delete old MAM messages operation",
1678
                        note = "added in 22.05",
1679
                        module = ?MODULE, function = delete_old_messages_status,
1680
                        args_desc = ["Name of host where messages should be deleted"],
1681
                        args_example = [<<"localhost">>],
1682
                        args = [{host, binary}],
1683
                        result = {status, string},
1684
                        result_desc = "Status test",
1685
                        result_example = "Operation in progress, delete 5000 messages"},
1686
     #ejabberd_commands{name = abort_delete_old_mam_messages, tags = [mam, purge],
1687
                        desc = "Abort currently running delete old MAM messages operation",
1688
                        note = "added in 22.05",
1689
                        module = ?MODULE, function = delete_old_messages_abort,
1690
                        args_desc = ["Name of host where operation should be aborted"],
1691
                        args_example = [<<"localhost">>],
1692
                        args = [{host, binary}],
1693
                        result = {status, string},
1694
                        result_desc = "Status text",
1695
                        result_example = "Operation aborted"},
1696
     #ejabberd_commands{name = remove_mam_for_user, tags = [mam],
1697
                        desc = "Remove mam archive for user",
1698
                        module = ?MODULE, function = remove_mam_for_user,
1699
                        args = [{user, binary}, {host, binary}],
1700
                        args_rename = [{server, host}],
1701
                        args_desc = ["Username", "Server"],
1702
                        args_example = [<<"bob">>, <<"example.com">>],
1703
                        result = {res, restuple},
1704
                        result_desc = "Result tuple",
1705
                        result_example = {ok, <<"MAM archive removed">>}},
1706
     #ejabberd_commands{name = remove_mam_for_user_with_peer, tags = [mam],
1707
                        desc = "Remove mam archive for user with peer",
1708
                        module = ?MODULE, function = remove_mam_for_user_with_peer,
1709
                        args = [{user, binary}, {host, binary}, {with, binary}],
1710
                        args_rename = [{server, host}],
1711
                        args_desc = ["Username", "Server", "Peer"],
1712
                        args_example = [<<"bob">>, <<"example.com">>, <<"anne@example.com">>],
1713
                        result = {res, restuple},
1714
                        result_desc = "Result tuple",
1715
                        result_example = {ok, <<"MAM archive removed">>}}
1716
        ].
1717

1718

1719
%%%
1720
%%% WebAdmin
1721
%%%
1722

1723
webadmin_menu_hostuser(Acc, _Host, _Username, _Lang) ->
1724
    Acc ++ [{<<"mam">>, <<"MAM">>},
24✔
1725
    {<<"mam-archive">>, <<"MAM Archive">>}].
1726

1727
webadmin_page_hostuser(_, Host, U,
1728
              #request{us = _US, path = [<<"mam">>]} = R) ->
1729
    Res = ?H1GL(<<"MAM">>, <<"modules/#mod_mam">>, <<"mod_mam">>)
×
1730
          ++ [make_command(get_mam_count,
1731
                           R,
1732
                           [{<<"user">>, U}, {<<"host">>, Host}],
1733
                           [{result_links,
1734
                             [{value, arg_host, 5, <<"user/", U/binary, "/mam-archive/">>}]}]),
1735
              make_command(remove_mam_for_user,
1736
                           R,
1737
                           [{<<"user">>, U}, {<<"host">>, Host}],
1738
                           [{style, danger}]),
1739
              make_command(remove_mam_for_user_with_peer,
1740
                           R,
1741
                           [{<<"user">>, U}, {<<"host">>, Host}],
1742
                           [{style, danger}])],
1743
    {stop, Res};
×
1744
webadmin_page_hostuser(_, Host, U,
1745
              #request{us = _US, path = [<<"mam-archive">> | RPath],
1746
                       lang = Lang} = R) ->
1747
    PageTitle =
×
1748
        str:translate_and_format(Lang, ?T("~ts's MAM Archive"), [jid:encode({U, Host, <<"">>})]),
1749
    Head = ?H1GL(PageTitle, <<"modules/#mod_mam">>, <<"mod_mam">>),
×
1750
    Res = make_command(get_mam_messages, R, [{<<"user">>, U},
×
1751
                                                     {<<"host">>, Host}],
1752
                        [{table_options, {10, RPath}},
1753
                         {result_links, [{packet, paragraph, 1, <<"">>}]}]),
1754
    {stop, Head ++ [Res]};
×
1755
webadmin_page_hostuser(Acc, _, _, _) -> Acc.
×
1756

1757
%%%
1758
%%% Documentation
1759
%%%
1760

1761
mod_opt_type(compress_xml) ->
1762
    econf:bool();
129✔
1763
mod_opt_type(assume_mam_usage) ->
1764
    econf:bool();
129✔
1765
mod_opt_type(default) ->
1766
    econf:enum([always, never, roster]);
129✔
1767
mod_opt_type(request_activates_archiving) ->
1768
    econf:bool();
129✔
1769
mod_opt_type(clear_archive_on_room_destroy) ->
1770
    econf:bool();
129✔
1771
mod_opt_type(user_mucsub_from_muc_archive) ->
1772
    econf:bool();
129✔
1773
mod_opt_type(archive_muc_as_mucsub) ->
1774
    econf:bool();
129✔
1775
mod_opt_type(access_preferences) ->
1776
    econf:acl();
129✔
1777
mod_opt_type(db_type) ->
1778
    econf:db_type(?MODULE);
129✔
1779
mod_opt_type(use_cache) ->
1780
    econf:bool();
129✔
1781
mod_opt_type(cache_size) ->
1782
    econf:pos_int(infinity);
129✔
1783
mod_opt_type(cache_missed) ->
1784
    econf:bool();
129✔
1785
mod_opt_type(cache_life_time) ->
1786
    econf:timeout(second, infinity).
129✔
1787

1788
mod_options(Host) ->
1789
    [{assume_mam_usage, false},
129✔
1790
     {default, never},
1791
     {request_activates_archiving, false},
1792
     {compress_xml, false},
1793
     {clear_archive_on_room_destroy, true},
1794
     {access_preferences, all},
1795
     {user_mucsub_from_muc_archive, false},
1796
     {archive_muc_as_mucsub, false},
1797
     {db_type, ejabberd_config:default_db(Host, ?MODULE)},
1798
     {use_cache, ejabberd_option:use_cache(Host)},
1799
     {cache_size, ejabberd_option:cache_size(Host)},
1800
     {cache_missed, ejabberd_option:cache_missed(Host)},
1801
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
1802

1803
mod_doc() ->
1804
    #{desc =>
×
1805
          [?T("This module implements "
1806
             "https://xmpp.org/extensions/xep-0313.html"
1807
             "[XEP-0313: Message Archive Management] and "
1808
             "https://xmpp.org/extensions/xep-0441.html"
1809
             "[XEP-0441: Message Archive Management Preferences]. "
1810
             "Compatible XMPP clients can use it to store their "
1811
             "chat history on the server."), "",
1812
           ?T("NOTE: Mnesia backend for mod_mam is not recommended: it's limited "
1813
             "to 2GB and often gets corrupted when reaching this limit. "
1814
             "SQL backend is recommended. Namely, for small servers SQLite "
1815
             "is a preferred choice because it's very easy to configure.")],
1816
      opts =>
1817
          [{access_preferences,
1818
            #{value => ?T("AccessName"),
1819
              desc =>
1820
                  ?T("This access rule defines who is allowed to modify the "
1821
                     "MAM preferences. The default value is 'all'.")}},
1822
           {assume_mam_usage,
1823
            #{value => "true | false",
1824
              desc =>
1825
                  ?T("This option determines how ejabberd's "
1826
                     "stream management code (see _`mod_stream_mgmt`_) "
1827
                     "handles unacknowledged messages when the "
1828
                     "connection is lost. Usually, such messages are "
1829
                     "either bounced or resent. However, neither is "
1830
                     "done for messages that were stored in the user's "
1831
                     "MAM archive if this option is set to 'true'. In "
1832
                     "this case, ejabberd assumes those messages will "
1833
                     "be retrieved from the archive. "
1834
                     "The default value is 'false'.")}},
1835
           {default,
1836
            #{value => "always | never | roster",
1837
              desc =>
1838
                  ?T("The option defines default policy for chat history. "
1839
                     "When 'always' is set every chat message is stored. "
1840
                     "With 'roster' only chat history with contacts from "
1841
                     "user's roster is stored. And 'never' fully disables "
1842
                     "chat history. Note that a client can change its "
1843
                     "policy via protocol commands. "
1844
                     "The default value is 'never'.")}},
1845
           {request_activates_archiving,
1846
            #{value => "true | false",
1847
              desc =>
1848
                  ?T("If the value is 'true', no messages are stored "
1849
                     "for a user until their client issue a MAM request, "
1850
                     "regardless of the value of the 'default' option. "
1851
                     "Once the server received a request, that user's "
1852
                     "messages are archived as usual. "
1853
                     "The default value is 'false'.")}},
1854
           {compress_xml,
1855
            #{value => "true | false",
1856
              desc =>
1857
                  ?T("When enabled, new messages added to archives are "
1858
                     "compressed using a custom compression algorithm. "
1859
                     "This feature works only with SQL backends. "
1860
                     "The default value is 'false'.")}},
1861
           {clear_archive_on_room_destroy,
1862
            #{value => "true | false",
1863
              desc =>
1864
                  ?T("Whether to destroy message archive of a room "
1865
                     "(see _`mod_muc`_) when it gets destroyed. "
1866
                     "The default value is 'true'.")}},
1867
           {db_type,
1868
            #{value => "mnesia | sql",
1869
              desc =>
1870
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
1871
           {use_cache,
1872
            #{value => "true | false",
1873
              desc =>
1874
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
1875
           {cache_size,
1876
            #{value => "pos_integer() | infinity",
1877
              desc =>
1878
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
1879
           {cache_missed,
1880
            #{value => "true | false",
1881
              desc =>
1882
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
1883
           {cache_life_time,
1884
            #{value => "timeout()",
1885
              desc =>
1886
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}},
1887
           {user_mucsub_from_muc_archive,
1888
            #{value => "true | false",
1889
              desc =>
1890
                  ?T("When this option is disabled, for each individual "
1891
                     "subscriber a separate mucsub message is stored. With this "
1892
                     "option enabled, when a user fetches archive virtual "
1893
                     "mucsub, messages are generated from muc archives. "
1894
                     "The default value is 'false'.")
1895
             }},
1896
           {archive_muc_as_mucsub,
1897
            #{value => "true | false",
1898
              note => "added in 25.10",
1899
              desc =>
1900
                  ?T("When this option is enabled incoming groupchat messages "
1901
                     "for users that have mucsub subscription to a room from which "
1902
                     "message originated will have those messages archived after being "
1903
                     "converted to mucsub event messages."
1904
                     "The default value is 'false'.")
1905
             }}]
1906
     }.
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