diff options
Diffstat (limited to 'src/mod_last.erl')
-rw-r--r-- | src/mod_last.erl | 344 |
1 files changed, 205 insertions, 139 deletions
diff --git a/src/mod_last.erl b/src/mod_last.erl index ce9148841..28b66be08 100644 --- a/src/mod_last.erl +++ b/src/mod_last.erl @@ -5,7 +5,7 @@ %%% Created : 24 Oct 2003 by Alexey Shchepin <alexey@process-one.net> %%% %%% -%%% ejabberd, Copyright (C) 2002-2016 ProcessOne +%%% ejabberd, Copyright (C) 2002-2019 ProcessOne %%% %%% This program is free software; you can redistribute it and/or %%% modify it under the terms of the GNU General Public License as @@ -25,45 +25,49 @@ -module(mod_last). --behaviour(ejabberd_config). - -author('alexey@process-one.net'). -protocol({xep, 12, '2.0'}). -behaviour(gen_mod). --export([start/2, stop/1, process_local_iq/3, export/1, - process_sm_iq/3, on_presence_update/4, import/1, - import/3, store_last_info/4, get_last_info/2, - remove_user/2, transform_options/1, mod_opt_type/1, - opt_type/1, register_user/2, depends/2]). +-export([start/2, stop/1, reload/3, process_local_iq/1, export/1, + process_sm_iq/1, on_presence_update/4, import_info/0, + import/5, import_start/2, store_last_info/4, get_last_info/2, + remove_user/2, mod_opt_type/1, mod_options/1, + register_user/2, depends/2, privacy_check_packet/4]). --include("ejabberd.hrl"). -include("logger.hrl"). - --include("jlib.hrl"). - +-include("xmpp.hrl"). -include("mod_privacy.hrl"). -include("mod_last.hrl"). +-include("translate.hrl"). + +-define(LAST_CACHE, last_activity_cache). + +-type c2s_state() :: ejabberd_c2s:state(). -callback init(binary(), gen_mod:opts()) -> any(). -callback import(binary(), #last_activity{}) -> ok | pass. -callback get_last(binary(), binary()) -> - {ok, non_neg_integer(), binary()} | not_found | {error, any()}. --callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> - {atomic, any()}. --callback remove_user(binary(), binary()) -> {atomic, any()}. + {ok, {non_neg_integer(), binary()}} | error | {error, any()}. +-callback store_last_info(binary(), binary(), non_neg_integer(), binary()) -> ok | {error, any()}. +-callback remove_user(binary(), binary()) -> any(). +-callback use_cache(binary()) -> boolean(). +-callback cache_nodes(binary()) -> [node()]. + +-optional_callbacks([use_cache/1, cache_nodes/1]). start(Host, Opts) -> - IQDisc = gen_mod:get_opt(iqdisc, Opts, fun gen_iq_handler:check_type/1, - one_queue), - Mod = gen_mod:db_mod(Host, Opts, ?MODULE), + Mod = gen_mod:db_mod(Opts, ?MODULE), Mod:init(Host, Opts), + init_cache(Mod, Host, Opts), gen_iq_handler:add_iq_handler(ejabberd_local, Host, - ?NS_LAST, ?MODULE, process_local_iq, IQDisc), + ?NS_LAST, ?MODULE, process_local_iq), gen_iq_handler:add_iq_handler(ejabberd_sm, Host, - ?NS_LAST, ?MODULE, process_sm_iq, IQDisc), + ?NS_LAST, ?MODULE, process_sm_iq), + ejabberd_hooks:add(privacy_check_packet, Host, ?MODULE, + privacy_check_packet, 30), ejabberd_hooks:add(register_user, Host, ?MODULE, register_user, 50), ejabberd_hooks:add(remove_user, Host, ?MODULE, @@ -78,130 +82,138 @@ stop(Host) -> remove_user, 50), ejabberd_hooks:delete(unset_presence_hook, Host, ?MODULE, on_presence_update, 50), + ejabberd_hooks:delete(privacy_check_packet, Host, ?MODULE, + privacy_check_packet, 30), gen_iq_handler:remove_iq_handler(ejabberd_local, Host, ?NS_LAST), gen_iq_handler:remove_iq_handler(ejabberd_sm, Host, ?NS_LAST). +reload(Host, NewOpts, OldOpts) -> + NewMod = gen_mod:db_mod(NewOpts, ?MODULE), + OldMod = gen_mod:db_mod(OldOpts, ?MODULE), + if NewMod /= OldMod -> + NewMod:init(Host, NewOpts); + true -> + ok + end, + init_cache(NewMod, Host, NewOpts). + %%% %%% Uptime of ejabberd node %%% -process_local_iq(_From, _To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; - get -> - Sec = get_node_uptime(), - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, - iolist_to_binary(integer_to_list(Sec))}], - children = []}]} - end. +-spec process_local_iq(iq()) -> iq(). +process_local_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_local_iq(#iq{type = get} = IQ) -> + xmpp:make_iq_result(IQ, #last{seconds = get_node_uptime()}). -%% @spec () -> integer() +-spec get_node_uptime() -> non_neg_integer(). %% @doc Get the uptime of the ejabberd node, expressed in seconds. %% When ejabberd is starting, ejabberd_config:start/0 stores the datetime. get_node_uptime() -> - case ejabberd_config:get_option( - node_start, - fun(S) when is_integer(S), S >= 0 -> S end) of - undefined -> - trunc(element(1, erlang:statistics(wall_clock)) / 1000); - Now -> - p1_time_compat:system_time(seconds) - Now - end. - -now_to_seconds({MegaSecs, Secs, _MicroSecs}) -> - MegaSecs * 1000000 + Secs. + NodeStart = ejabberd_config:get_node_start(), + erlang:monotonic_time(second) - NodeStart. %%% %%% Serve queries about user last online %%% -process_sm_iq(From, To, - #iq{type = Type, lang = Lang, sub_el = SubEl} = IQ) -> - case Type of - set -> - Txt = <<"Value 'set' of 'type' attribute is not allowed">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_NOT_ALLOWED(Lang, Txt)]}; - get -> - User = To#jid.luser, - Server = To#jid.lserver, - {Subscription, _Groups} = - ejabberd_hooks:run_fold(roster_get_jid_info, Server, - {none, []}, [User, Server, From]), - if (Subscription == both) or (Subscription == from) or - (From#jid.luser == To#jid.luser) and - (From#jid.lserver == To#jid.lserver) -> - UserListRecord = - ejabberd_hooks:run_fold(privacy_get_user_list, Server, - #userlist{}, [User, Server]), - case ejabberd_hooks:run_fold(privacy_check_packet, - Server, allow, - [User, Server, UserListRecord, - {To, From, - #xmlel{name = <<"presence">>, - attrs = [], - children = []}}, - out]) - of - allow -> get_last_iq(IQ, SubEl, User, Server); - deny -> - IQ#iq{type = error, sub_el = [SubEl, ?ERR_FORBIDDEN]} - end; - true -> - Txt = <<"Not subscribed">>, - IQ#iq{type = error, sub_el = [SubEl, ?ERRT_FORBIDDEN(Lang, Txt)]} - end +-spec process_sm_iq(iq()) -> iq(). +process_sm_iq(#iq{type = set, lang = Lang} = IQ) -> + Txt = ?T("Value 'set' of 'type' attribute is not allowed"), + xmpp:make_error(IQ, xmpp:err_not_allowed(Txt, Lang)); +process_sm_iq(#iq{from = From, to = To, lang = Lang} = IQ) -> + User = To#jid.luser, + Server = To#jid.lserver, + {Subscription, _Ask, _Groups} = + ejabberd_hooks:run_fold(roster_get_jid_info, Server, + {none, none, []}, [User, Server, From]), + if (Subscription == both) or (Subscription == from) or + (From#jid.luser == To#jid.luser) and + (From#jid.lserver == To#jid.lserver) -> + Pres = xmpp:set_from_to(#presence{}, To, From), + case ejabberd_hooks:run_fold(privacy_check_packet, + Server, allow, + [To, Pres, out]) of + allow -> get_last_iq(IQ, User, Server); + deny -> xmpp:make_error(IQ, xmpp:err_forbidden()) + end; + true -> + Txt = ?T("Not subscribed"), + xmpp:make_error(IQ, xmpp:err_subscription_required(Txt, Lang)) end. -%% @spec (LUser::string(), LServer::string()) -> -%% {ok, TimeStamp::integer(), Status::string()} | not_found | {error, Reason} +-spec privacy_check_packet(allow | deny, c2s_state(), stanza(), in | out) -> allow | deny | {stop, deny}. +privacy_check_packet(allow, C2SState, + #iq{from = From, to = To, type = T} = IQ, in) + when T == get; T == set -> + case xmpp:has_subtag(IQ, #last{}) of + true -> + #jid{luser = LUser, lserver = LServer} = To, + {Sub, _, _} = ejabberd_hooks:run_fold( + roster_get_jid_info, LServer, + {none, none, []}, [LUser, LServer, From]), + if Sub == from; Sub == both -> + Pres = #presence{from = To, to = From}, + case ejabberd_hooks:run_fold( + privacy_check_packet, allow, + [C2SState, Pres, out]) of + allow -> + allow; + deny -> + {stop, deny} + end; + true -> + {stop, deny} + end; + false -> + allow + end; +privacy_check_packet(Acc, _, _, _) -> + Acc. + +-spec get_last(binary(), binary()) -> {ok, non_neg_integer(), binary()} | + not_found | {error, any()}. get_last(LUser, LServer) -> Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:get_last(LUser, LServer). + Res = case use_cache(Mod, LServer) of + true -> + ets_cache:lookup( + ?LAST_CACHE, {LUser, LServer}, + fun() -> Mod:get_last(LUser, LServer) end); + false -> + Mod:get_last(LUser, LServer) + end, + case Res of + {ok, {TimeStamp, Status}} -> {ok, TimeStamp, Status}; + error -> not_found; + Err -> Err + end. -get_last_iq(#iq{lang = Lang} = IQ, SubEl, LUser, LServer) -> +-spec get_last_iq(iq(), binary(), binary()) -> iq(). +get_last_iq(#iq{lang = Lang} = IQ, LUser, LServer) -> case ejabberd_sm:get_user_resources(LUser, LServer) of [] -> case get_last(LUser, LServer) of {error, _Reason} -> - Txt = <<"Database failure">>, - IQ#iq{type = error, - sub_el = [SubEl, ?ERRT_INTERNAL_SERVER_ERROR(Lang, Txt)]}; + Txt = ?T("Database failure"), + xmpp:make_error(IQ, xmpp:err_internal_server_error(Txt, Lang)); not_found -> - Txt = <<"No info about last activity found">>, - IQ#iq{type = error, - sub_el = [SubEl, ?ERRT_SERVICE_UNAVAILABLE(Lang, Txt)]}; + Txt = ?T("No info about last activity found"), + xmpp:make_error(IQ, xmpp:err_service_unavailable(Txt, Lang)); {ok, TimeStamp, Status} -> - TimeStamp2 = p1_time_compat:system_time(seconds), + TimeStamp2 = erlang:system_time(second), Sec = TimeStamp2 - TimeStamp, - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, - iolist_to_binary(integer_to_list(Sec))}], - children = [{xmlcdata, Status}]}]} + xmpp:make_iq_result(IQ, #last{seconds = Sec, status = Status}) end; _ -> - IQ#iq{type = result, - sub_el = - [#xmlel{name = <<"query">>, - attrs = - [{<<"xmlns">>, ?NS_LAST}, - {<<"seconds">>, <<"0">>}], - children = []}]} + xmpp:make_iq_result(IQ, #last{seconds = 0}) end. +-spec register_user(binary(), binary()) -> any(). register_user(User, Server) -> on_presence_update( User, @@ -209,59 +221,113 @@ register_user(User, Server) -> <<"RegisterResource">>, <<"Registered but didn't login">>). +-spec on_presence_update(binary(), binary(), binary(), binary()) -> any(). on_presence_update(User, Server, _Resource, Status) -> - TimeStamp = p1_time_compat:system_time(seconds), + TimeStamp = erlang:system_time(second), store_last_info(User, Server, TimeStamp, Status). +-spec store_last_info(binary(), binary(), non_neg_integer(), binary()) -> any(). store_last_info(User, Server, TimeStamp, Status) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:store_last_info(LUser, LServer, TimeStamp, Status). + case use_cache(Mod, LServer) of + true -> + ets_cache:update( + ?LAST_CACHE, {LUser, LServer}, {ok, {TimeStamp, Status}}, + fun() -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) + end, cache_nodes(Mod, LServer)); + false -> + Mod:store_last_info(LUser, LServer, TimeStamp, Status) + end. -%% @spec (LUser::string(), LServer::string()) -> -%% {ok, TimeStamp::integer(), Status::string()} | not_found +-spec get_last_info(binary(), binary()) -> {ok, non_neg_integer(), binary()} | + not_found. get_last_info(LUser, LServer) -> case get_last(LUser, LServer) of {error, _Reason} -> not_found; Res -> Res end. +-spec remove_user(binary(), binary()) -> any(). remove_user(User, Server) -> LUser = jid:nodeprep(User), LServer = jid:nameprep(Server), Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:remove_user(LUser, LServer). + Mod:remove_user(LUser, LServer), + ets_cache:delete(?LAST_CACHE, {LUser, LServer}, cache_nodes(Mod, LServer)). + +-spec init_cache(module(), binary(), gen_mod:opts()) -> ok. +init_cache(Mod, Host, Opts) -> + case use_cache(Mod, Host) of + true -> + CacheOpts = cache_opts(Opts), + ets_cache:new(?LAST_CACHE, CacheOpts); + false -> + ets_cache:delete(?LAST_CACHE) + end. -export(LServer) -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:export(LServer). +-spec cache_opts(gen_mod:opts()) -> [proplists:property()]. +cache_opts(Opts) -> + MaxSize = mod_last_opt:cache_size(Opts), + CacheMissed = mod_last_opt:cache_missed(Opts), + LifeTime = mod_last_opt:cache_life_time(Opts), + [{max_size, MaxSize}, {cache_missed, CacheMissed}, {life_time, LifeTime}]. + +-spec use_cache(module(), binary()) -> boolean(). +use_cache(Mod, Host) -> + case erlang:function_exported(Mod, use_cache, 1) of + true -> Mod:use_cache(Host); + false -> mod_last_opt:use_cache(Host) + end. -import(LServer) -> - Mod = gen_mod:db_mod(LServer, ?MODULE), - Mod:import(LServer). +-spec cache_nodes(module(), binary()) -> [node()]. +cache_nodes(Mod, Host) -> + case erlang:function_exported(Mod, cache_nodes, 1) of + true -> Mod:cache_nodes(Host); + false -> ejabberd_cluster:get_nodes() + end. + +import_info() -> + [{<<"last">>, 3}]. -import(LServer, DBType, LA) -> +import_start(LServer, DBType) -> + Mod = gen_mod:db_mod(DBType, ?MODULE), + Mod:init(LServer, []). + +import(LServer, {sql, _}, DBType, <<"last">>, [LUser, TimeStamp, State]) -> + TS = case TimeStamp of + <<"">> -> 0; + _ -> binary_to_integer(TimeStamp) + end, + LA = #last_activity{us = {LUser, LServer}, + timestamp = TS, + status = State}, Mod = gen_mod:db_mod(DBType, ?MODULE), Mod:import(LServer, LA). -transform_options(Opts) -> - lists:foldl(fun transform_options/2, [], Opts). - -transform_options({node_start, {_, _, _} = Now}, Opts) -> - ?WARNING_MSG("Old 'node_start' format detected. This is still supported " - "but it is better to fix your config.", []), - [{node_start, now_to_seconds(Now)}|Opts]; -transform_options(Opt, Opts) -> - [Opt|Opts]. +export(LServer) -> + Mod = gen_mod:db_mod(LServer, ?MODULE), + Mod:export(LServer). depends(_Host, _Opts) -> []. -mod_opt_type(db_type) -> fun(T) -> ejabberd_config:v_db(?MODULE, T) end; -mod_opt_type(iqdisc) -> fun gen_iq_handler:check_type/1; -mod_opt_type(_) -> [db_type, iqdisc]. - -opt_type(node_start) -> - fun (S) when is_integer(S), S >= 0 -> S end; -opt_type(_) -> [node_start]. +mod_opt_type(db_type) -> + econf:db_type(?MODULE); +mod_opt_type(use_cache) -> + econf:bool(); +mod_opt_type(cache_size) -> + econf:pos_int(infinity); +mod_opt_type(cache_missed) -> + econf:bool(); +mod_opt_type(cache_life_time) -> + econf:timeout(second, infinity). + +mod_options(Host) -> + [{db_type, ejabberd_config:default_db(Host, ?MODULE)}, + {use_cache, ejabberd_option:use_cache(Host)}, + {cache_size, ejabberd_option:cache_size(Host)}, + {cache_missed, ejabberd_option:cache_missed(Host)}, + {cache_life_time, ejabberd_option:cache_life_time(Host)}]. |