• 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

46.79
/src/mod_last.erl
1
%%%----------------------------------------------------------------------
2
%%% File    : mod_last.erl
3
%%% Author  : Alexey Shchepin <alexey@process-one.net>
4
%%% Purpose : jabber:iq:last support (XEP-0012)
5
%%% Created : 24 Oct 2003 by Alexey Shchepin <alexey@process-one.net>
6
%%%
7
%%%
8
%%% ejabberd, Copyright (C) 2002-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_last).
27

28
-author('alexey@process-one.net').
29

30
-protocol({xep, 12, '2.0', '0.5.0', "complete", ""}).
31

32
-behaviour(gen_mod).
33

34
-export([start/2, stop/1, reload/3, process_local_iq/1, export/1,
35
         process_sm_iq/1, on_presence_update/4, import_info/0,
36
         import/5, import_start/2, store_last_info/4, get_last_info/2,
37
         remove_user/2, mod_opt_type/1, mod_options/1, mod_doc/0,
38
         register_user/2, depends/2, privacy_check_packet/4]).
39

40
-include("logger.hrl").
41
-include_lib("xmpp/include/xmpp.hrl").
42
-include("mod_privacy.hrl").
43
-include("mod_last.hrl").
44
-include("translate.hrl").
45

46
-define(LAST_CACHE, last_activity_cache).
47

48
-type c2s_state() :: ejabberd_c2s:state().
49

50
-callback init(binary(), gen_mod:opts()) -> any().
51
-callback import(binary(), #last_activity{}) -> ok | pass.
52
-callback get_last(binary(), binary()) ->
53
    {ok, {non_neg_integer(), binary()}} | error | {error, any()}.
54
-callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}.
55
-callback remove_user(binary(), binary()) -> any().
56
-callback use_cache(binary()) -> boolean().
57
-callback cache_nodes(binary()) -> [node()].
58

59
-optional_callbacks([use_cache/1, cache_nodes/1]).
60

61
start(Host, Opts) ->
62
    Mod = gen_mod:db_mod(Opts, ?MODULE),
98✔
63
    Mod:init(Host, Opts),
98✔
64
    init_cache(Mod, Host, Opts),
98✔
65
    {ok, [{iq_handler, ejabberd_local, ?NS_LAST, process_local_iq},
98✔
66
          {iq_handler, ejabberd_sm, ?NS_LAST, process_sm_iq},
67
          {hook, privacy_check_packet, privacy_check_packet, 30},
68
          {hook, register_user, register_user, 50},
69
          {hook, remove_user, remove_user, 50},
70
          {hook, unset_presence_hook, on_presence_update, 50}]}.
71

72
stop(_Host) ->
73
    ok.
98✔
74

75
reload(Host, NewOpts, OldOpts) ->
76
    NewMod = gen_mod:db_mod(NewOpts, ?MODULE),
×
77
    OldMod = gen_mod:db_mod(OldOpts, ?MODULE),
×
78
    if NewMod /= OldMod ->
×
79
            NewMod:init(Host, NewOpts);
×
80
       true ->
81
            ok
×
82
    end,
83
    init_cache(NewMod, Host, NewOpts).
×
84

85
%%%
86
%%% Uptime of ejabberd node
87
%%%
88

89
-spec process_local_iq(iq()) -> iq().
90
process_local_iq(#iq{type = set, lang = Lang} = IQ) ->
91
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
92
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
93
process_local_iq(#iq{type = get} = IQ) ->
94
    xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}).
8✔
95

96
-spec get_node_uptime() -> non_neg_integer().
97
%% @doc Get the uptime of the ejabberd node, expressed in seconds.
98
%% When ejabberd is starting, ejabberd_config:start/0 stores the datetime.
99
get_node_uptime() ->
100
    NodeStart = ejabberd_config:get_node_start(),
8✔
101
    erlang:monotonic_time(second) - NodeStart.
8✔
102

103
%%%
104
%%% Serve queries about user last online
105
%%%
106

107
-spec process_sm_iq(iq()) -> iq().
108
process_sm_iq(#iq{type = set, lang = Lang} = IQ) ->
109
    Txt = ?T("Value 'set' of 'type' attribute is not allowed"),
×
110
    xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang));
×
111
process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) ->
112
    User = To#jid.luser,
×
113
    Server = To#jid.lserver,
×
114
    {Subscription, _Ask, _Groups} =
×
115
        ejabberd_hooks:run_fold(roster_get_jid_info, Server,
116
                                {none, none, []}, [User, Server, From]),
117
    if (Subscription == both) or (Subscription == from) or
×
118
       (From#jid.luser == To#jid.luser) and
119
       (From#jid.lserver == To#jid.lserver) ->
120
            Pres = xmpp:set_from_to(#presence{}, To, From),
×
121
            case ejabberd_hooks:run_fold(privacy_check_packet,
×
122
                                         Server, allow,
123
                                         [To, Pres, out]) of
124
                allow -> get_last_iq(IQ, User, Server);
×
125
                deny -> xmpp:make_error(IQ, xmpp:err_forbidden())
×
126
            end;
127
       true ->
128
            Txt = ?T("Not subscribed"),
×
129
            xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang))
×
130
    end.
131

132
-spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}.
133
privacy_check_packet(allow, C2SState,
134
                     #iq{from = From, to = To, type = T} = IQ, in)
135
  when T == get; T == set ->
136
    case xmpp:has_subtag(IQ, #last{}) of
5,329✔
137
        true ->
138
            #jid{luser = LUser, lserver = LServer} = To,
×
139
            {Sub, _, _} = ejabberd_hooks:run_fold(
×
140
                            roster_get_jid_info, LServer,
141
                            {none, none, []}, [LUser, LServer, From]),
142
            if Sub == from; Sub == both ->
×
143
                    Pres = #presence{from = To, to = From},
×
144
                    case ejabberd_hooks:run_fold(
×
145
                           privacy_check_packet, allow,
146
                           [C2SState, Pres, out]) of
147
                        allow ->
148
                            allow;
×
149
                        deny ->
150
                            {stop, deny}
×
151
                    end;
152
               true ->
153
                    {stop, deny}
×
154
            end;
155
        false ->
156
            allow
5,329✔
157
    end;
158
privacy_check_packet(Acc, _, _, _) ->
159
    Acc.
98,313✔
160

161
-spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} |
162
                                      not_found | {error, any()}.
163
get_last(LUser, LServer) ->
164
    Mod = gen_mod:db_mod(LServer, ?MODULE),
72✔
165
    Res = case use_cache(Mod, LServer) of
72✔
166
              true ->
167
                  ets_cache:lookup(
72✔
168
                    ?LAST_CACHE, {LUser, LServer},
169
                    fun() -> Mod:get_last(LUser, LServer) end);
×
170
              false ->
171
                  Mod:get_last(LUser, LServer)
×
172
          end,
173
    case Res of
72✔
174
        {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status};
72✔
175
        error -> not_found;
×
176
        Err -> Err
×
177
    end.
178

179
-spec get_last_iq(iq(), binary(), binary()) -> iq().
180
get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) ->
181
    case ejabberd_sm:get_user_resources(LUser, LServer) of
×
182
      [] ->
183
          case get_last(LUser, LServer) of
×
184
            {error, _Reason} ->
185
                Txt = ?T("Database failure"),
×
186
                xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang));
×
187
            not_found ->
188
                Txt = ?T("No info about last activity found"),
×
189
                xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang));
×
190
            {ok, TimeStamp, Status} ->
191
                TimeStamp2 = erlang:system_time(second),
×
192
                Sec = TimeStamp2 - TimeStamp,
×
193
                xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status})
×
194
          end;
195
      _ ->
196
          xmpp:make_iq_result(IQ, #last{seconds = 0})
×
197
    end.
198

199
-spec register_user(binary(), binary()) -> any().
200
register_user(User, Server) ->
201
    on_presence_update(
43✔
202
       User,
203
       Server,
204
       <<"RegisterResource">>,
205
       <<"Registered but didn't login">>).
206

207
-spec on_presence_update(binary(), binary(), binary(), binary()) -> any().
208
on_presence_update(User, Server, _Resource, Status) ->
209
    TimeStamp = erlang:system_time(second),
283✔
210
    store_last_info(User, Server, TimeStamp, Status).
283✔
211

212
-spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any().
213
store_last_info(User, Server, TimeStamp, Status) ->
214
    LUser = jid:nodeprep(User),
283✔
215
    LServer = jid:nameprep(Server),
283✔
216
    Mod = gen_mod:db_mod(LServer, ?MODULE),
283✔
217
    case use_cache(Mod, LServer) of
283✔
218
        true ->
219
            ets_cache:update(
283✔
220
              ?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}},
221
              fun() ->
222
                      Mod:store_last_info(LUser, LServer, TimeStamp, Status)
207✔
223
              end, cache_nodes(Mod, LServer));
224
        false ->
225
            Mod:store_last_info(LUser, LServer, TimeStamp, Status)
×
226
    end.
227

228
-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} |
229
                                           not_found.
230
get_last_info(LUser, LServer) ->
231
    case get_last(LUser, LServer) of
72✔
232
      {error, _Reason} -> not_found;
×
233
      Res -> Res
72✔
234
    end.
235

236
-spec remove_user(binary(), binary()) -> any().
237
remove_user(User, Server) ->
238
    LUser = jid:nodeprep(User),
17✔
239
    LServer = jid:nameprep(Server),
17✔
240
    Mod = gen_mod:db_mod(LServer, ?MODULE),
17✔
241
    Mod:remove_user(LUser, LServer),
17✔
242
    ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)).
17✔
243

244
-spec init_cache(module(), binary(), gen_mod:opts()) -> ok.
245
init_cache(Mod, Host, Opts) ->
246
    case use_cache(Mod, Host) of
98✔
247
        true ->
248
            CacheOpts = cache_opts(Opts),
98✔
249
            ets_cache:new(?LAST_CACHE, CacheOpts);
98✔
250
        false ->
251
            ets_cache:delete(?LAST_CACHE)
×
252
    end.
253

254
-spec cache_opts(gen_mod:opts()) -> [proplists:property()].
255
cache_opts(Opts) ->
256
    MaxSize = mod_last_opt:cache_size(Opts),
98✔
257
    CacheMissed = mod_last_opt:cache_missed(Opts),
98✔
258
    LifeTime = mod_last_opt:cache_life_time(Opts),
98✔
259
    [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}].
98✔
260

261
-spec use_cache(module(), binary()) -> boolean().
262
use_cache(Mod, Host) ->
263
    case erlang:function_exported(Mod, use_cache, 1) of
453✔
264
        true -> Mod:use_cache(Host);
189✔
265
        false -> mod_last_opt:use_cache(Host)
264✔
266
    end.
267

268
-spec cache_nodes(module(), binary()) -> [node()].
269
cache_nodes(Mod, Host) ->
270
    case erlang:function_exported(Mod, cache_nodes, 1) of
300✔
271
        true -> Mod:cache_nodes(Host);
×
272
        false -> ejabberd_cluster:get_nodes()
300✔
273
    end.
274

275
import_info() ->
276
    [{<<"last">>, 3}].
×
277

278
import_start(LServer, DBType) ->
279
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
280
    Mod:init(LServer, []).
×
281

282
import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) ->
283
    TS = case TimeStamp of
×
284
             <<"">> -> 0;
×
285
             _ -> binary_to_integer(TimeStamp)
×
286
         end,
287
    LA = #last_activity{us = {LUser, LServer},
×
288
                        timestamp = TS,
289
                        status = State},
290
    Mod = gen_mod:db_mod(DBType, ?MODULE),
×
291
    Mod:import(LServer, LA).
×
292

293
export(LServer) ->
294
    Mod = gen_mod:db_mod(LServer, ?MODULE),
×
295
    Mod:export(LServer).
×
296

297
depends(_Host, _Opts) ->
298
    [].
116✔
299

300
mod_opt_type(db_type) ->
301
    econf:db_type(?MODULE);
116✔
302
mod_opt_type(use_cache) ->
303
    econf:bool();
116✔
304
mod_opt_type(cache_size) ->
305
    econf:pos_int(infinity);
116✔
306
mod_opt_type(cache_missed) ->
307
    econf:bool();
116✔
308
mod_opt_type(cache_life_time) ->
309
    econf:timeout(second, infinity).
116✔
310

311
mod_options(Host) ->
312
    [{db_type, ejabberd_config:default_db(Host, ?MODULE)},
116✔
313
     {use_cache, ejabberd_option:use_cache(Host)},
314
     {cache_size, ejabberd_option:cache_size(Host)},
315
     {cache_missed, ejabberd_option:cache_missed(Host)},
316
     {cache_life_time, ejabberd_option:cache_life_time(Host)}].
317

318
mod_doc() ->
319
    #{desc =>
×
320
          ?T("This module adds support for "
321
             "https://xmpp.org/extensions/xep-0012.html"
322
             "[XEP-0012: Last Activity]. It can be used "
323
             "to discover when a disconnected user last accessed "
324
             "the server, to know when a connected user was last "
325
             "active on the server, or to query the uptime of the ejabberd server."),
326
      opts =>
327
          [{db_type,
328
            #{value => "mnesia | sql",
329
              desc =>
330
                  ?T("Same as top-level _`default_db`_ option, but applied to this module only.")}},
331
           {use_cache,
332
            #{value => "true | false",
333
              desc =>
334
                  ?T("Same as top-level _`use_cache`_ option, but applied to this module only.")}},
335
           {cache_size,
336
            #{value => "pos_integer() | infinity",
337
              desc =>
338
                  ?T("Same as top-level _`cache_size`_ option, but applied to this module only.")}},
339
           {cache_missed,
340
            #{value => "true | false",
341
              desc =>
342
                  ?T("Same as top-level _`cache_missed`_ option, but applied to this module only.")}},
343
           {cache_life_time,
344
            #{value => "timeout()",
345
              desc =>
346
                  ?T("Same as top-level _`cache_life_time`_ option, but applied to this module only.")}}]}.
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