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-2012   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
21 %%% along with this program; if not, write to the Free Software
22 %%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
23 %%% 02111-1307 USA
24 %%%
25 %%%----------------------------------------------------------------------
26 
27 
28 %%% Database schema (version / storage / table)
29 %%%
30 %%% 2.1.x / mnesia / last_activity
31 %%%  us = {Username::string(), Host::string()}
32 %%%  timestamp = now()
33 %%%  status = string()
34 %%%
35 %%% 2.1.x / odbc / last
36 %%%  username = varchar250
37 %%%  seconds = text
38 %%%  state = text
39 %%%
40 %%% 3.0.0-prealpha / mnesia / last_activity
41 %%%  us = {Username::binary(), Host::binary()}
42 %%%  timestamp = now()
43 %%%  status = binary()
44 %%%
45 %%% 3.0.0-prealpha / odbc / last
46 %%%  Same as 2.1.x
47 %%%
48 %%% 3.0.0-alpha / mnesia / last_activity
49 %%%  user_host = {Username::binary(), Host::binary()}
50 %%%  timestamp = now()
51 %%%  status = binary()
52 %%%
53 %%% 3.0.0-alpha / odbc / last_activity
54 %%%  user = varchar150
55 %%%  host = varchar150
56 %%%  timestamp = bigint
57 %%%  status = text
58 
59 
60 -module(mod_last).
61 
62 -author('alexey@process-one.net').
63 
64 -behaviour(gen_mod).
65 
66 -export([start/2, stop/1, process_local_iq/3, process_sm_iq/3, on_presence_update/4,
67 	 store_last_info/4, get_last_info/2, remove_user/2]).
68 
69 -include_lib("exmpp/include/exmpp.hrl").
70 
71 -include("ejabberd.hrl").
72 
73 -include("mod_privacy.hrl").
74 
75 -record(last_activity, {user_host, timestamp, status}).
76 
77 start(Host, Opts) when is_list(Host) -> start(list_to_binary(Host), Opts);
78 start(HostB, Opts) ->
79     IQDisc = gen_mod:get_opt(iqdisc, Opts, one_queue),
80     Backend = gen_mod:get_opt(backend, Opts, mnesia),
81     gen_storage:create_table(Backend, HostB, last_activity,
82 			     [{disc_copies, [node()]}, {odbc_host, HostB},
83 			      {attributes, record_info(fields, last_activity)},
84 			      {types, [{user_host, {text, text}}, {timestamp, bigint}]}]),
85     update_table(HostB, Backend),
86     gen_iq_handler:add_iq_handler(ejabberd_local, HostB, ?NS_LAST_ACTIVITY, ?MODULE,
87 				  process_local_iq, IQDisc),
88     gen_iq_handler:add_iq_handler(ejabberd_sm, HostB, ?NS_LAST_ACTIVITY, ?MODULE,
89 				  process_sm_iq, IQDisc),
90     ejabberd_hooks:add(remove_user, HostB, ?MODULE, remove_user, 50),
91     ejabberd_hooks:add(unset_presence_hook, HostB, ?MODULE, on_presence_update, 50).
92 
93 stop(Host) when is_list(Host) -> stop(list_to_binary(Host));
94 stop(HostB) ->
95     ejabberd_hooks:delete(remove_user, HostB, ?MODULE, remove_user, 50),
96     ejabberd_hooks:delete(unset_presence_hook, HostB, ?MODULE, on_presence_update, 50),
97     gen_iq_handler:remove_iq_handler(ejabberd_local, HostB, ?NS_LAST_ACTIVITY),
98     gen_iq_handler:remove_iq_handler(ejabberd_sm, HostB, ?NS_LAST_ACTIVITY).
99 
100 %%%
101 %%% Uptime of ejabberd node
102 %%%
103 
104 
105 process_local_iq(_From, _To, #iq{type = get} = IQ_Rec) ->
106     Sec = get_node_uptime(),
107     Response = #xmlel{ns = ?NS_LAST_ACTIVITY, name = 'query',
108 		      attrs = [?XMLATTR(<<"seconds">>, Sec)]},
109     exmpp_iq:result(IQ_Rec, Response);
110 process_local_iq(_From, _To, #iq{type = set} = IQ_Rec) ->
111     exmpp_iq:error(IQ_Rec, 'not-allowed').
112 
113 %% @spec () -> integer()
114 %% @doc Get the uptime of the ejabberd node, expressed in seconds.
115 %% When ejabberd is starting, ejabberd_config:start/0 stores the datetime.
116 get_node_uptime() ->
117     case ejabberd_config:get_local_option(node_start) of
118       {_, _, _} = StartNow -> now_to_seconds(now()) - now_to_seconds(StartNow);
119       _undefined -> trunc(element(1, erlang:statistics(wall_clock)) / 1000)
120     end.
121 
122 now_to_seconds({MegaSecs, Secs, _MicroSecs}) -> MegaSecs * 1000000 + Secs.
123 
124 %%%
125 %%% Serve queries about user last online
126 %%%
127 
128 
129 process_sm_iq(From, To, #iq{type = get} = IQ_Rec) ->
130     {Subscription, _Groups} = ejabberd_hooks:run_fold(roster_get_jid_info,
131 						      exmpp_jid:prep_domain(To),
132 						      {none, []},
133 						      [exmpp_jid:prep_node(To),
134 						       exmpp_jid:prep_domain(To), From]),
135     SameUser = exmpp_jid:bare_compare(From, To),
136     if (Subscription == both) or (Subscription == from) or SameUser ->
137 	   UserListRecord = ejabberd_hooks:run_fold(privacy_get_user_list,
138 						    exmpp_jid:prep_domain(To),
139 						    #userlist{},
140 						    [exmpp_jid:prep_node(To),
141 						     exmpp_jid:prep_domain(To)]),
142 	   case ejabberd_hooks:run_fold(privacy_check_packet, exmpp_jid:prep_domain(To),
143 					allow,
144 					[exmpp_jid:prep_node(To),
145 					 exmpp_jid:prep_domain(To), UserListRecord,
146 					 {To, From, exmpp_presence:available()}, out])
147 	       of
148 	     allow ->
149 		 get_last_iq(IQ_Rec, exmpp_jid:prep_node(To), exmpp_jid:prep_domain(To));
150 	     deny -> exmpp_iq:error(IQ_Rec, forbidden)
151 	   end;
152        true -> exmpp_iq:error(IQ_Rec, forbidden)
153     end;
154 process_sm_iq(_From, _To, #iq{type = set} = IQ_Rec) ->
155     exmpp_iq:error(IQ_Rec, 'not-allowed').
156 
157 %% TODO: This function could use get_last_info/2
158 %% @spec (LUser::string(), LServer::string()) ->
159 %%      {ok, TimeStamp::integer(), Status::string()} | not_found | {error, Reason}
160 get_last(LUser, LServer) ->
161     case catch gen_storage:dirty_read(LServer, last_activity, {LUser, LServer}) of
162       {'EXIT', Reason} -> {error, Reason};
163       [] -> not_found;
164       [#last_activity{timestamp = TimeStamp, status = Status}] -> {ok, TimeStamp, Status}
165     end.
166 
167 get_last_iq(IQ_Rec, LUser, LServer) ->
168     case ejabberd_sm:get_user_resources(LUser, LServer) of
169       [] -> get_last_iq_disconnected(IQ_Rec, LUser, LServer);
170       _ ->
171 	  Sec = 0,
172 	  #xmlel{ns = ?NS_LAST_ACTIVITY, name = 'query',
173 		 attrs = [?XMLATTR(<<"seconds">>, Sec)]}
174     end.
175 
176 get_last_iq_disconnected(IQ_Rec, LUser, LServer) ->
177     case get_last(LUser, LServer) of
178       {error, _Reason} -> exmpp_iq:error(IQ_Rec, 'internal-server-error');
179       not_found -> exmpp_iq:error(IQ_Rec, 'service-unavailable');
180       {ok, TimeStamp, Status} ->
181 	  TimeStamp2 = now_to_seconds(now()),
182 	  Sec = TimeStamp2 - TimeStamp,
183 	  Response = #xmlel{ns = ?NS_LAST_ACTIVITY, name = 'query',
184 			    attrs = [?XMLATTR(<<"seconds">>, Sec)],
185 			    children = [#xmlcdata{cdata = Status}]},
186 	  exmpp_iq:result(IQ_Rec, Response)
187     end.
188 
189 on_presence_update(User, Server, _Resource, Status) ->
190     {MegaSecs, Secs, _MicroSecs} = now(),
191     TimeStamp = MegaSecs * 1000000 + Secs,
192     store_last_info(User, Server, TimeStamp, Status).
193 
194 store_last_info(User, Server, TimeStamp, Status)
195     when is_binary(User), is_binary(Server) ->
196     try US = {User, Server},
197 	F = fun () ->
198 		    gen_storage:write(Server,
199 				      #last_activity{user_host = US,
200 						     timestamp = TimeStamp,
201 						     status = Status})
202 	    end,
203 	gen_storage:transaction(Server, last_activity, F)
204     catch
205       _ -> ok
206     end.
207 
208 %% @spec (LUser::string(), LServer::string()) ->
209 %%      {ok, Timestamp::integer(), Status::string()} | not_found
210 get_last_info(LUser, LServer) when is_list(LUser), is_list(LServer) ->
211     get_last_info(list_to_binary(LUser), list_to_binary(LServer));
212 get_last_info(LUser, LServer) when is_binary(LUser), is_binary(LServer) ->
213     case get_last(LUser, LServer) of
214       {error, _Reason} -> not_found;
215       Res -> Res
216     end.
217 
218 remove_user(User, Server) when is_binary(User), is_binary(Server) ->
219     try LUser = exmpp_stringprep:nodeprep(User),
220 	LServer = exmpp_stringprep:nameprep(Server),
221 	US = {LUser, LServer},
222 	F = fun () -> gen_storage:delete(LServer, {last_activity, US}) end,
223 	gen_storage:transaction(LServer, last_activity, F)
224     catch
225       _ -> ok
226     end.
227 
228 update_table(global, Storage) ->
229     [update_table(HostB, Storage) || HostB <- ejabberd_hosts:get_hosts(ejabberd)];
230 update_table(HostB, mnesia) ->
231     gen_storage_migration:migrate_mnesia(HostB, last_activity,
232 					 [{last_activity, [us, timestamp, status],
233 					   fun ({last_activity, {U, S}, Timestamp,
234 						 Status}) ->
235 						   U1 = case U of
236 							  "" -> undefined;
237 							  V -> V
238 							end,
239 						   #last_activity{user_host =
240 								      {list_to_binary(U1),
241 								       list_to_binary(S)},
242 								  timestamp = Timestamp,
243 								  status =
244 								      list_to_binary(Status)}
245 					   end}]);
246 update_table(HostB, odbc) ->
247     gen_storage_migration:migrate_odbc(HostB, [last_activity],
248 				       [{"last", ["username", "seconds", "state"],
249 					 fun (_, Username, STimeStamp, Status) ->
250 						 case catch list_to_integer(STimeStamp) of
251 						   TimeStamp when is_integer(TimeStamp) ->
252 						       [#last_activity{user_host =
253 									   {Username,
254 									    HostB},
255 								       timestamp =
256 									   TimeStamp,
257 								       status = Status}];
258 						   _ ->
259 						       ?WARNING_MSG("Omitting last_activity migration item with timestamp=~p",
260 								    [STimeStamp])
261 						 end
262 					 end}]).