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

processone / ejabberd / 1173

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

push

github

badlop
CHANGELOG.md: Update to 25.10

15513 of 45940 relevant lines covered (33.77%)

1078.01 hits per line

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

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

25
-module(mod_offline_sql).
26

27

28
-behaviour(mod_offline).
29

30
-export([init/2, store_message/1, pop_messages/2, remove_expired_messages/1,
31
         remove_old_messages/2, remove_user/2, read_message_headers/2,
32
         read_message/3, remove_message/3, read_all_messages/2,
33
         remove_all_messages/2, count_messages/2, import/1, export/1, remove_old_messages_batch/3]).
34
-export([sql_schemas/0]).
35

36
-include_lib("xmpp/include/xmpp.hrl").
37
-include("mod_offline.hrl").
38
-include("logger.hrl").
39
-include("ejabberd_sql_pt.hrl").
40

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

48
sql_schemas() ->
49
    [#sql_schema{
6✔
50
        version = 1,
51
        tables =
52
            [#sql_table{
53
                name = <<"spool">>,
54
                columns =
55
                    [#sql_column{name = <<"username">>, type = text},
56
                     #sql_column{name = <<"server_host">>, type = text},
57
                     #sql_column{name = <<"xml">>, type = {text, big}},
58
                     #sql_column{name = <<"seq">>, type = bigserial},
59
                     #sql_column{name = <<"created_at">>, type = timestamp,
60
                                 default = true}],
61
                indices = [#sql_index{
62
                              columns = [<<"server_host">>, <<"username">>]},
63
                           #sql_index{
64
                              columns = [<<"created_at">>]}]}]}].
65

66
store_message(#offline_msg{us = {LUser, LServer}} = M) ->
67
    From = M#offline_msg.from,
774✔
68
    To = M#offline_msg.to,
774✔
69
    Packet = xmpp:set_from_to(M#offline_msg.packet, From, To),
774✔
70
    NewPacket = misc:add_delay_info(
774✔
71
                  Packet, jid:make(LServer),
72
                  M#offline_msg.timestamp,
73
                  <<"Offline Storage">>),
74
    XML = fxml:element_to_binary(
774✔
75
            xmpp:encode(NewPacket)),
76
    case ejabberd_sql:sql_query(
774✔
77
           LServer,
78
           ?SQL_INSERT(
774✔
79
              "spool",
80
              ["username=%(LUser)s",
81
               "server_host=%(LServer)s",
82
               "xml=%(XML)s"])) of
83
        {updated, _} ->
84
            ok;
774✔
85
        _ ->
86
            {error, db_failure}
×
87
    end.
88

89
pop_messages(LUser, LServer) ->
90
    case get_and_del_spool_msg_t(LServer, LUser) of
168✔
91
        {atomic, {selected, Rs}} ->
92
            {ok, lists:flatmap(
168✔
93
                   fun({_, XML}) ->
94
                           case xml_to_offline_msg(XML) of
744✔
95
                               {ok, Msg} ->
96
                                   [Msg];
744✔
97
                               _Err ->
98
                                   []
×
99
                           end
100
                   end, Rs)};
101
        Err ->
102
            {error, Err}
×
103
    end.
104

105
remove_expired_messages(_LServer) ->
106
    %% TODO
107
    {atomic, ok}.
×
108

109
remove_old_messages(Days, LServer) ->
110
    case ejabberd_sql:sql_query(
×
111
           LServer,
112
           fun(pgsql, _) ->
113
                   ejabberd_sql:sql_query_t(
×
114
                     ?SQL("DELETE FROM spool"
×
115
                          " WHERE created_at <"
116
                          " NOW() - %(Days)d * INTERVAL '1 DAY'"));
117
              (sqlite, _) ->
118
                  ejabberd_sql:sql_query_t(
×
119
                      ?SQL("DELETE FROM spool"
×
120
                           " WHERE created_at <"
121
                           " DATETIME('now', '-%(Days)d days')"));
122
              (_, _) ->
123
                   ejabberd_sql:sql_query_t(
×
124
                     ?SQL("DELETE FROM spool"
×
125
                          " WHERE created_at < NOW() - INTERVAL %(Days)d DAY"))
126
              end)
127
        of
128
        {updated, N} ->
129
            ?INFO_MSG("~p message(s) deleted from offline spool", [N]);
×
130
        Error ->
131
            ?ERROR_MSG("Cannot delete message in offline spool: ~p", [Error])
×
132
    end,
133
    {atomic, ok}.
×
134

135
remove_old_messages_batch(LServer, Days, Batch) ->
136
    case ejabberd_sql:sql_query(
×
137
        LServer,
138
        fun(pgsql, _) ->
139
            ejabberd_sql:sql_query_t(
×
140
                ?SQL("DELETE FROM spool"
×
141
                     " WHERE created_at <"
142
                     " NOW() - %(Days)d * INTERVAL '1 DAY' LIMIT %(Batch)d"));
143
           (sqlite, _) ->
144
               ejabberd_sql:sql_query_t(
×
145
                   ?SQL("DELETE FROM spool"
×
146
                        " WHERE created_at <"
147
                        " DATETIME('now', '-%(Days)d days') LIMIT %(Batch)d"));
148
           (_, _) ->
149
               ejabberd_sql:sql_query_t(
×
150
                   ?SQL("DELETE FROM spool"
×
151
                        " WHERE created_at < NOW() - INTERVAL %(Days)d DAY LIMIT %(Batch)d"))
152
        end)
153
    of
154
        {updated, N} ->
155
            {ok, N};
×
156
        Error ->
157
            {error, Error}
×
158
    end.
159

160
remove_user(LUser, LServer) ->
161
    ejabberd_sql:sql_query(
36✔
162
      LServer,
163
      ?SQL("delete from spool where username=%(LUser)s and %(LServer)H")).
36✔
164

165
read_message_headers(LUser, LServer) ->
166
    case ejabberd_sql:sql_query(
24✔
167
           LServer,
168
           ?SQL("select @(xml)s, @(seq)d from spool"
24✔
169
                " where username=%(LUser)s and %(LServer)H order by seq")) of
170
        {selected, Rows} ->
171
            lists:flatmap(
24✔
172
              fun({XML, Seq}) ->
173
                      case xml_to_offline_msg(XML) of
60✔
174
                          {ok, #offline_msg{from = From,
175
                                            to = To,
176
                                            timestamp = TS,
177
                                            packet = El}} ->
178
                              [{Seq, From, To, TS, El}];
60✔
179
                          _ ->
180
                              []
×
181
                      end
182
              end, Rows);
183
        _Err ->
184
            error
×
185
    end.
186

187
read_message(LUser, LServer, Seq) ->
188
    case ejabberd_sql:sql_query(
36✔
189
           LServer,
190
           ?SQL("select @(xml)s from spool where username=%(LUser)s"
36✔
191
                " and %(LServer)H"
192
                " and seq=%(Seq)d")) of
193
        {selected, [{RawXML}|_]} ->
194
            case xml_to_offline_msg(RawXML) of
30✔
195
                {ok, Msg} ->
196
                    {ok, Msg};
30✔
197
                _ ->
198
                    error
×
199
            end;
200
        _ ->
201
            error
6✔
202
    end.
203

204
remove_message(LUser, LServer, Seq) ->
205
    ejabberd_sql:sql_query(
18✔
206
      LServer,
207
      ?SQL("delete from spool where username=%(LUser)s and %(LServer)H"
18✔
208
           " and seq=%(Seq)d")),
209
    ok.
18✔
210

211
read_all_messages(LUser, LServer) ->
212
    case ejabberd_sql:sql_query(
×
213
           LServer,
214
           ?SQL("select @(xml)s from spool where "
×
215
                "username=%(LUser)s and %(LServer)H order by seq")) of
216
        {selected, Rs} ->
217
            lists:flatmap(
×
218
              fun({XML}) ->
219
                      case xml_to_offline_msg(XML) of
×
220
                          {ok, Msg} -> [Msg];
×
221
                          _ -> []
×
222
                      end
223
              end, Rs);
224
        _ ->
225
            []
×
226
    end.
227

228
remove_all_messages(LUser, LServer) ->
229
    remove_user(LUser, LServer),
6✔
230
    {atomic, ok}.
6✔
231

232
count_messages(LUser, LServer) ->
233
    case catch ejabberd_sql:sql_query(
42✔
234
                 LServer,
235
                 ?SQL("select @(count(*))d from spool "
42✔
236
                      "where username=%(LUser)s and %(LServer)H")) of
237
        {selected, [{Res}]} ->
238
            {cache, Res};
42✔
239
        {selected, []} ->
240
            {cache, 0};
×
241
        _ ->
242
            {nocache, 0}
×
243
    end.
244

245
export(_Server) ->
246
    [{offline_msg,
×
247
      fun(Host, #offline_msg{us = {LUser, LServer}})
248
            when LServer == Host ->
249
                      [?SQL("delete from spool where username=%(LUser)s"
×
250
                            " and %(LServer)H;")];
251
         (_Host, _R) ->
252
              []
×
253
      end},
254
     {offline_msg,
255
      fun(Host, #offline_msg{us = {LUser, LServer},
256
                             timestamp = TimeStamp, from = From, to = To,
257
                             packet = El})
258
            when LServer == Host ->
259
              try xmpp:decode(El, ?NS_CLIENT, [ignore_els]) of
×
260
                  Packet ->
261
                      Packet1 = xmpp:set_from_to(Packet, From, To),
×
262
                      Packet2 = misc:add_delay_info(
×
263
                                  Packet1, jid:make(LServer),
264
                                  TimeStamp, <<"Offline Storage">>),
265
                      XML = fxml:element_to_binary(xmpp:encode(Packet2)),
×
266
                      [?SQL_INSERT(
×
267
                          "spool",
268
                          ["username=%(LUser)s",
269
                           "server_host=%(LServer)s",
270
                           "xml=%(XML)s"])]
271
              catch _:{xmpp_codec, Why} ->
272
                      ?ERROR_MSG("Failed to decode packet ~p of user ~ts@~ts: ~ts",
×
273
                                 [El, LUser, LServer, xmpp:format_error(Why)]),
×
274
                      []
×
275
              end;
276
         (_Host, _R) ->
277
              []
×
278
      end}].
279

280
import(_) ->
281
    ok.
×
282

283
%%%===================================================================
284
%%% Internal functions
285
%%%===================================================================
286
xml_to_offline_msg(XML) ->
287
    case fxml_stream:parse_element(XML) of
834✔
288
        #xmlel{} = El ->
289
            el_to_offline_msg(El);
834✔
290
        Err ->
291
            ?ERROR_MSG("Got ~p when parsing XML packet ~ts",
×
292
                       [Err, XML]),
×
293
            Err
×
294
    end.
295

296
el_to_offline_msg(El) ->
297
    To_s = fxml:get_tag_attr_s(<<"to">>, El),
834✔
298
    From_s = fxml:get_tag_attr_s(<<"from">>, El),
834✔
299
    try
834✔
300
        To = jid:decode(To_s),
834✔
301
        From = jid:decode(From_s),
834✔
302
        {ok, #offline_msg{us = {To#jid.luser, To#jid.lserver},
834✔
303
                          from = From,
304
                          to = To,
305
                          packet = El}}
306
    catch _:{bad_jid, To_s} ->
307
            ?ERROR_MSG("Failed to get 'to' JID from offline XML ~p", [El]),
×
308
            {error, bad_jid_to};
×
309
          _:{bad_jid, From_s} ->
310
            ?ERROR_MSG("Failed to get 'from' JID from offline XML ~p", [El]),
×
311
            {error, bad_jid_from}
×
312
    end.
313

314
get_and_del_spool_msg_t(LServer, LUser) ->
315
    F = fun () ->
168✔
316
                Result =
168✔
317
                    ejabberd_sql:sql_query_t(
318
                      ?SQL("select @(username)s, @(xml)s from spool where "
168✔
319
                           "username=%(LUser)s and %(LServer)H order by seq;")),
320
                DResult =
168✔
321
                    ejabberd_sql:sql_query_t(
322
                      ?SQL("delete from spool where"
168✔
323
                           " username=%(LUser)s and %(LServer)H;")),
324
                case {Result, DResult} of
168✔
325
                    {{selected, Rs}, {updated, DC}} when length(Rs) /= DC ->
326
                        ejabberd_sql:restart(concurent_insert);
×
327
                    _ ->
328
                        Result
168✔
329
                end
330
        end,
331
    ejabberd_sql:sql_transaction(LServer, F).
168✔
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