27
28
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
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
114
115
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
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
158
159
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
209
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}]).