./ct_report/coverage/mod_last.COVER.html

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-2014 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 -xep([{xep, 12}, {version, "2.0"}]).
31
32 -behaviour(gen_mod).
33 -behaviour(mongoose_module_metrics).
34
35 %% Gen_mod callbacks
36 -export([start/2,
37 stop/1,
38 hooks/1,
39 config_spec/0,
40 supported_features/0]).
41
42 %% IQ and hook handlers
43 -export([user_receive_iq/3,
44 process_local_iq/5,
45 process_sm_iq/5,
46 remove_user/3,
47 on_presence_update/3,
48 session_cleanup/3,
49 remove_domain/3]).
50
51 %% API
52 -export([store_last_info/5,
53 get_last_info/3,
54 count_active_users/3]).
55
56 -export([config_metrics/1]).
57
58 -include("mongoose.hrl").
59 -include("mongoose_config_spec.hrl").
60
61 -include("jlib.hrl").
62
63 %% ------------------------------------------------------------------
64 %% Backend callbacks
65
66 -export_type([timestamp/0, status/0]).
67
68 -type timestamp() :: non_neg_integer().
69 -type status() :: binary().
70
71 -spec start(mongooseim:host_type(), gen_mod:module_opts()) -> ok.
72 start(HostType, #{iqdisc := IQDisc} = Opts) ->
73 12 mod_last_backend:init(HostType, Opts),
74 12 [gen_iq_handler:add_iq_handler_for_domain(HostType, ?NS_LAST, Component, Fn, #{}, IQDisc) ||
75 12 {Component, Fn} <- iq_handlers()],
76 12 ok.
77
78 -spec stop(mongooseim:host_type()) -> ok.
79 stop(HostType) ->
80 12 [gen_iq_handler:remove_iq_handler_for_domain(HostType, ?NS_LAST, Component) ||
81 12 {Component, _Fn} <- iq_handlers()],
82 12 gen_hook:delete_handlers(hooks(HostType)).
83
84 iq_handlers() ->
85 24 [{ejabberd_local, fun ?MODULE:process_local_iq/5},
86 {ejabberd_sm, fun ?MODULE:process_sm_iq/5}].
87
88 -spec hooks(mongooseim:host_type()) -> gen_hook:hook_list().
89 hooks(HostType) ->
90 36 [{remove_user, HostType, fun ?MODULE:remove_user/3, #{}, 50},
91 {anonymous_purge_hook, HostType, fun ?MODULE:remove_user/3, #{}, 50},
92 {unset_presence_hook, HostType, fun ?MODULE:on_presence_update/3, #{}, 50},
93 {session_cleanup, HostType, fun ?MODULE:session_cleanup/3, #{}, 50},
94 {remove_domain, HostType, fun ?MODULE:remove_domain/3, #{}, 50}
95 | c2s_hooks(HostType) ].
96
97 -spec c2s_hooks(mongooseim:host_type()) -> gen_hook:hook_list(mongoose_c2s_hooks:fn()).
98 c2s_hooks(HostType) ->
99 36 [
100 {user_receive_iq, HostType, fun ?MODULE:user_receive_iq/3, #{}, 50}
101 ].
102
103 %%%
104 %%% config_spec
105 %%%
106
107 -spec config_spec() -> mongoose_config_spec:config_section().
108 config_spec() ->
109 206 #section{
110 items = #{<<"iqdisc">> => mongoose_config_spec:iqdisc(),
111 <<"backend">> => #option{type = atom,
112 validate = {module, mod_last}}
113 },
114 defaults = #{<<"iqdisc">> => one_queue,
115 <<"backend">> => mnesia
116 }
117 }.
118
119
:-(
supported_features() -> [dynamic_domains].
120
121 %%%
122 %%% Uptime of ejabberd node
123 %%%
124 -spec process_local_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map())
125 -> {mongoose_acc:t(), jlib:iq()}.
126 process_local_iq(Acc, _From, _To, #iq{type = Type, sub_el = SubEl} = IQ, _Extra) ->
127 1 case Type of
128 set ->
129
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
130 get ->
131 1 Sec = get_node_uptime(),
132 1 {Acc, IQ#iq{type = result,
133 sub_el =
134 [#xmlel{name = <<"query">>,
135 attrs =
136 [{<<"xmlns">>, ?NS_LAST},
137 {<<"seconds">>,
138 integer_to_binary(Sec)}],
139 children = []}]}}
140 end.
141
142 -spec get_node_uptime() -> non_neg_integer().
143 get_node_uptime() ->
144 1 case mongoose_config:lookup_opt(node_start) of
145 {ok, {node_start, Seconds}} ->
146
:-(
erlang:system_time(second) - Seconds;
147 {error, not_found} ->
148 1 trunc(element(1, erlang:statistics(wall_clock))/1000)
149 end.
150
151 %%%
152 %%% Serve queries about user last online
153 %%%
154 -spec process_sm_iq(mongoose_acc:t(), jid:jid(), jid:jid(), jlib:iq(), map()) ->
155 {mongoose_acc:t(), jlib:iq()}.
156 process_sm_iq(Acc, _From, _To, #iq{type = set, sub_el = SubEl} = IQ, _Extra) ->
157
:-(
{Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:not_allowed()]}};
158 process_sm_iq(Acc, From, To, #iq{type = get, sub_el = SubEl} = IQ, _Extra) ->
159 5 HostType = mongoose_acc:host_type(Acc),
160 5 case can_respond(HostType, From, To) of
161 true ->
162 3 UserListRecord = mongoose_hooks:privacy_get_user_list(HostType, To),
163 3 {Res, Acc1} = mongoose_privacy:privacy_check_packet(Acc, To, UserListRecord, To, From, out),
164 3 {Acc1, make_response(HostType, IQ, SubEl, To, Res)};
165 false ->
166 2 {Acc, IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:forbidden()]}}
167 end.
168
169 can_respond(HostType, From, To) ->
170 7 {Subscription, _Groups} = mongoose_hooks:roster_get_jid_info(HostType, To, From),
171 7 MutualSubscription = Subscription =:= both,
172 7 RequesterSubscribedToTarget = Subscription =:= from,
173 7 QueryingSameUsersLast = jid:are_bare_equal(From, To),
174 7 MutualSubscription or RequesterSubscribedToTarget or QueryingSameUsersLast.
175
176 -spec make_response(mongooseim:host_type(), jlib:iq(), SubEl :: 'undefined' | [exml:element()],
177 jid:jid(), allow | deny) -> jlib:iq().
178 make_response(_HostType, IQ, SubEl, _, deny) ->
179
:-(
IQ#iq{type = error, sub_el = [SubEl, mongoose_xmpp_errors:forbidden()]};
180 make_response(HostType, IQ, SubEl, JID, allow) ->
181 3 #jid{luser = LUser, lserver = LServer} = JID,
182 3 case ejabberd_sm:get_user_resources(JID) of
183 [] ->
184 2 case get_last(HostType, LUser, LServer) of
185 {error, _Reason} ->
186
:-(
IQ#iq{type = error,
187 sub_el = [SubEl, mongoose_xmpp_errors:internal_server_error()]};
188 not_found ->
189
:-(
IQ#iq{type = error,
190 sub_el = [SubEl, mongoose_xmpp_errors:service_unavailable()]};
191 {ok, TimeStamp, Status} ->
192 2 TimeStamp2 = erlang:system_time(second),
193 2 Sec = TimeStamp2 - TimeStamp,
194 2 IQ#iq{type = result,
195 sub_el =
196 [#xmlel{name = <<"query">>,
197 attrs =
198 [{<<"xmlns">>, ?NS_LAST},
199 {<<"seconds">>,
200 integer_to_binary(Sec)}],
201 children = [{xmlcdata, Status}]}]}
202 end;
203 _ ->
204 1 IQ#iq{type = result,
205 sub_el =
206 [#xmlel{name = <<"query">>,
207 attrs =
208 [{<<"xmlns">>, ?NS_LAST},
209 {<<"seconds">>, <<"0">>}],
210 children = []}]}
211 end.
212
213 -spec get_last_info(mongooseim:host_type(), jid:luser(), jid:lserver())
214 -> 'not_found' | {'ok', timestamp(), status()}.
215 get_last_info(HostType, LUser, LServer) ->
216 36 case get_last(HostType, LUser, LServer) of
217
:-(
{error, _Reason} -> not_found;
218 36 Res -> Res
219 end.
220
221 %%%
222 %%% Hook handlers
223 %%%
224
225 -spec remove_user(Acc, Params, Extra) -> {ok, Acc} when
226 Acc :: mongoose_acc:t(),
227 Params :: #{jid := jid:jid()},
228 Extra :: gen_hook:extra().
229 remove_user(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, #{host_type := HostType}) ->
230 77 R = mod_last_backend:remove_user(HostType, LUser, LServer),
231 77 mongoose_lib:log_if_backend_error(R, ?MODULE, ?LINE, {Acc, LUser, LServer}),
232 77 {ok, Acc}.
233
234 -spec remove_domain(Acc, Params, Extra) -> {ok, Acc} when
235 Acc :: mongoose_hooks:simple_acc(),
236 Params :: #{domain := jid:lserver()},
237 Extra :: gen_hook:extra().
238 remove_domain(Acc, #{domain := Domain}, #{host_type := HostType}) ->
239 1 mod_last_backend:remove_domain(HostType, Domain),
240 1 {ok, Acc}.
241
242 -spec user_receive_iq(mongoose_acc:t(), mongoose_c2s_hooks:params(), gen_hook:extra()) ->
243 mongoose_c2s_hooks:result().
244 user_receive_iq(Acc, _Params, _Extra) ->
245 64 case mongoose_iq:info(Acc) of
246 {#iq{type = get, xmlns = ?NS_LAST}, Acc1} ->
247 2 maybe_forward_last(Acc1);
248 {_, Acc1} ->
249 62 {ok, Acc1}
250 end.
251
252 -spec maybe_forward_last(mongoose_acc:t()) -> mongoose_c2s_hooks:result().
253 maybe_forward_last(Acc) ->
254 2 HostType = mongoose_acc:host_type(Acc),
255 2 {From, To, _} = mongoose_acc:packet(Acc),
256 2 case can_respond(HostType, From, To) of
257 true ->
258 1 {ok, Acc};
259 false ->
260 1 {Acc1, Err} = jlib:make_error_reply(Acc, mongoose_xmpp_errors:forbidden()),
261 1 ejabberd_router:route(To, From, Acc1, Err),
262 1 {stop, Acc}
263 end.
264
265 -spec on_presence_update(Acc, Params, Extra) -> {ok, Acc} when
266 Acc :: mongoose_acc:t(),
267 Params :: #{jid := jid:jid(), status := status()},
268 Extra :: gen_hook:extra().
269 on_presence_update(Acc, #{jid := #jid{luser = LUser, lserver = LServer}, status := Status}, _) ->
270 40 {ok, store_last_info(Acc, LUser, LServer, Status)}.
271
272 -spec session_cleanup(Acc, Params, Extra) -> {ok, Acc} when
273 Acc :: mongoose_acc:t(),
274 Params :: #{jid := jid:jid()},
275 Extra :: gen_hook:extra().
276 session_cleanup(Acc, #{jid := #jid{luser = LUser, lserver = LServer}}, _) ->
277
:-(
{ok, store_last_info(Acc, LUser, LServer, <<>>)}.
278
279 -spec store_last_info(mongoose_acc:t(), jid:luser(), jid:lserver(), status()) -> mongoose_acc:t().
280 store_last_info(Acc, LUser, LServer, Status) ->
281 40 HostType = mongoose_acc:host_type(Acc),
282 40 TimeStamp = erlang:system_time(second),
283 40 store_last_info(HostType, LUser, LServer, TimeStamp, Status),
284 40 Acc.
285
286 -spec store_last_info(mongooseim:host_type(), jid:luser(), jid:lserver(),
287 timestamp(), status()) -> ok.
288 store_last_info(HostType, LUser, LServer, TimeStamp, Status) ->
289 89 case mod_last_backend:set_last_info(HostType, LUser, LServer, TimeStamp, Status) of
290 {error, Reason} ->
291
:-(
?LOG_ERROR(#{what => set_last_info_failed,
292 text => <<"Unexpected error while storing mod_last information">>,
293 user => LUser, server => LServer,
294 timestamp => TimeStamp, status => Status,
295
:-(
reason => Reason});
296 ok ->
297 89 ok
298 end.
299
300 -spec get_last(mongooseim:host_type(), jid:luser(), jid:lserver()) ->
301 {ok, timestamp(), status()} | {error, term()} | not_found.
302 get_last(HostType, LUser, LServer) ->
303 38 mod_last_backend:get_last(HostType, LUser, LServer).
304
305 -spec count_active_users(mongooseim:host_type(), jid:lserver(), timestamp()) -> non_neg_integer().
306 count_active_users(HostType, LServer, Timestamp) ->
307 9 mod_last_backend:count_active_users(HostType, LServer, Timestamp).
308
309 -spec config_metrics(mongooseim:host_type()) -> [{gen_mod:opt_key(), gen_mod:opt_value()}].
310 config_metrics(HostType) ->
311 24 mongoose_module_metrics:opts_for_module(HostType, ?MODULE, [backend]).
Line Hits Source