25
1
mirror of https://github.com/processone/ejabberd.git synced 2024-11-10 15:47:30 +01:00

Support XEP-0352: Client State Indication

This commit is contained in:
Holger Weiss 2014-09-11 17:44:29 +02:00
parent f723c00762
commit b8c98232b8
5 changed files with 222 additions and 8 deletions

View File

@ -66,6 +66,7 @@
\newcommand{\module}[1]{\texttt{#1}} \newcommand{\module}[1]{\texttt{#1}}
\newcommand{\modadhoc}{\module{mod\_adhoc}} \newcommand{\modadhoc}{\module{mod\_adhoc}}
\newcommand{\modannounce}{\module{mod\_announce}} \newcommand{\modannounce}{\module{mod\_announce}}
\newcommand{\modclientstate}{\module{mod\_client\_state}}
\newcommand{\modblocking}{\module{mod\_blocking}} \newcommand{\modblocking}{\module{mod\_blocking}}
\newcommand{\modcaps}{\module{mod\_caps}} \newcommand{\modcaps}{\module{mod\_caps}}
\newcommand{\modcarboncopy}{\module{mod\_carboncopy}} \newcommand{\modcarboncopy}{\module{mod\_carboncopy}}
@ -2781,6 +2782,7 @@ The following table lists all modules included in \ejabberd{}.
\hline \modblocking{} & Simple Communications Blocking (\xepref{0191}) & \modprivacy{} \\ \hline \modblocking{} & Simple Communications Blocking (\xepref{0191}) & \modprivacy{} \\
\hline \modcaps{} & Entity Capabilities (\xepref{0115}) & \\ \hline \modcaps{} & Entity Capabilities (\xepref{0115}) & \\
\hline \modcarboncopy{} & Message Carbons (\xepref{0280}) & \\ \hline \modcarboncopy{} & Message Carbons (\xepref{0280}) & \\
\hline \ahrefloc{modclientstate}{\modclientstate{}} & Filter stanzas for inactive clients & \\
\hline \modconfigure{} & Server configuration using Ad-Hoc & \modadhoc{} \\ \hline \modconfigure{} & Server configuration using Ad-Hoc & \modadhoc{} \\
\hline \ahrefloc{moddisco}{\moddisco{}} & Service Discovery (\xepref{0030}) & \\ \hline \ahrefloc{moddisco}{\moddisco{}} & Service Discovery (\xepref{0030}) & \\
\hline \ahrefloc{modecho}{\modecho{}} & Echoes XMPP stanzas & \\ \hline \ahrefloc{modecho}{\modecho{}} & Echoes XMPP stanzas & \\
@ -3001,6 +3003,38 @@ Note that \modannounce{} can be resource intensive on large
deployments as it can broadcast lot of messages. This module should be deployments as it can broadcast lot of messages. This module should be
disabled for instances of \ejabberd{} with hundreds of thousands users. disabled for instances of \ejabberd{} with hundreds of thousands users.
\makesubsection{modclientstate}{\modclientstate{}}
\ind{modules!\modclientstate{}}\ind{Client State Indication}
\ind{protocols!XEP-0352: Client State Indication}
This module allows for queueing or dropping certain types of stanzas
when a client indicates that the user is not actively using the client
at the moment (see \xepref{0352}). This can save bandwidth and
resources.
Options:
\begin{description}
\titem{drop\_chat\_states: true|false} \ind{options!drop\_chat\_states}
Drop most "standalone" Chat State Notifications (as defined in
\xepref{0085}) while a client indicates inactivity. The default value
is \term{false}.
\titem{queue\_presence: true|false} \ind{options!queue\_presence}
While a client is inactive, queue presence stanzas that indicate
(un)availability. The latest queued stanza of each contact is
delivered as soon as the client becomes active again. The default
value is \term{false}.
\end{description}
Example:
\begin{verbatim}
modules:
...
mod_client_state:
drop_chat_states: true
queue_presence: true
...
\end{verbatim}
\makesubsection{moddisco}{\moddisco{}} \makesubsection{moddisco}{\moddisco{}}
\ind{modules!\moddisco{}} \ind{modules!\moddisco{}}
\ind{protocols!XEP-0030: Service Discovery} \ind{protocols!XEP-0030: Service Discovery}

View File

@ -558,6 +558,9 @@ modules:
mod_blocking: {} # requires mod_privacy mod_blocking: {} # requires mod_privacy
mod_caps: {} mod_caps: {}
mod_carboncopy: {} mod_carboncopy: {}
mod_client_state:
drop_chat_states: true
queue_presence: false
mod_configure: {} # requires mod_adhoc mod_configure: {} # requires mod_adhoc
mod_disco: {} mod_disco: {}
## mod_echo: {} ## mod_echo: {}

View File

@ -147,5 +147,6 @@
-define(NS_CARBONS_2, <<"urn:xmpp:carbons:2">>). -define(NS_CARBONS_2, <<"urn:xmpp:carbons:2">>).
-define(NS_CARBONS_1, <<"urn:xmpp:carbons:1">>). -define(NS_CARBONS_1, <<"urn:xmpp:carbons:1">>).
-define(NS_FORWARD, <<"urn:xmpp:forward:0">>). -define(NS_FORWARD, <<"urn:xmpp:forward:0">>).
-define(NS_CLIENT_STATE, <<"urn:xmpp:csi:0">>).
-define(NS_STREAM_MGMT_2, <<"urn:xmpp:sm:2">>). -define(NS_STREAM_MGMT_2, <<"urn:xmpp:sm:2">>).
-define(NS_STREAM_MGMT_3, <<"urn:xmpp:sm:3">>). -define(NS_STREAM_MGMT_3, <<"urn:xmpp:sm:3">>).

View File

@ -108,6 +108,8 @@
auth_module = unknown, auth_module = unknown,
ip, ip,
aux_fields = [], aux_fields = [],
csi_state = active,
csi_queue = [],
mgmt_state, mgmt_state,
mgmt_xmlns, mgmt_xmlns,
mgmt_queue, mgmt_queue,
@ -475,6 +477,10 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
false -> false ->
[] []
end, end,
ClientStateFeature =
[#xmlel{name = <<"csi">>,
attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}],
children = []}],
StreamFeatures = [#xmlel{name = <<"bind">>, StreamFeatures = [#xmlel{name = <<"bind">>,
attrs = [{<<"xmlns">>, ?NS_BIND}], attrs = [{<<"xmlns">>, ?NS_BIND}],
children = []}, children = []},
@ -484,6 +490,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
++ ++
RosterVersioningFeature ++ RosterVersioningFeature ++
StreamManagementFeature ++ StreamManagementFeature ++
ClientStateFeature ++
ejabberd_hooks:run_fold(c2s_stream_features, ejabberd_hooks:run_fold(c2s_stream_features,
Server, [], [Server]), Server, [], [Server]),
send_element(StateData, send_element(StateData,
@ -1165,6 +1172,17 @@ wait_for_session(closed, StateData) ->
session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData) session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData)
when ?IS_STREAM_MGMT_TAG(Name) -> when ?IS_STREAM_MGMT_TAG(Name) ->
fsm_next_state(session_established, dispatch_stream_mgmt(El, StateData)); fsm_next_state(session_established, dispatch_stream_mgmt(El, StateData));
session_established({xmlstreamelement,
#xmlel{name = <<"active">>,
attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}},
StateData) ->
NewStateData = csi_queue_flush(StateData),
fsm_next_state(session_established, NewStateData#state{csi_state = active});
session_established({xmlstreamelement,
#xmlel{name = <<"inactive">>,
attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}]}},
StateData) ->
fsm_next_state(session_established, StateData#state{csi_state = inactive});
session_established({xmlstreamelement, El}, session_established({xmlstreamelement, El},
StateData) -> StateData) ->
FromJID = StateData#state.jid, FromJID = StateData#state.jid,
@ -1855,6 +1873,8 @@ send_element(StateData, El) when StateData#state.xml_socket ->
send_element(StateData, El) -> send_element(StateData, El) ->
send_text(StateData, xml:element_to_binary(El)). send_text(StateData, xml:element_to_binary(El)).
send_stanza(StateData, Stanza) when StateData#state.csi_state == inactive ->
csi_filter_stanza(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending -> send_stanza(StateData, Stanza) when StateData#state.mgmt_state == pending ->
mgmt_queue_add(StateData, Stanza); mgmt_queue_add(StateData, Stanza);
send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active -> send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
@ -1869,18 +1889,14 @@ send_stanza(StateData, Stanza) ->
send_element(StateData, Stanza), send_element(StateData, Stanza),
StateData. StateData.
send_packet(StateData, Packet) when StateData#state.mgmt_state == active; send_packet(StateData, Packet) ->
StateData#state.mgmt_state == pending ->
case is_stanza(Packet) of case is_stanza(Packet) of
true -> true ->
send_stanza(StateData, Packet); send_stanza(StateData, Packet);
false -> false ->
send_element(StateData, Packet), send_element(StateData, Packet),
StateData StateData
end; end.
send_packet(StateData, Stanza) ->
send_element(StateData, Stanza),
StateData.
send_header(StateData, Server, Version, Lang) send_header(StateData, Server, Version, Lang)
when StateData#state.xml_socket -> when StateData#state.xml_socket ->
@ -2762,9 +2778,11 @@ handle_resume(StateData, Attrs) ->
#xmlel{name = <<"r">>, #xmlel{name = <<"r">>,
attrs = [{<<"xmlns">>, AttrXmlns}], attrs = [{<<"xmlns">>, AttrXmlns}],
children = []}), children = []}),
FlushedState = csi_queue_flush(NewState),
NewStateData = FlushedState#state{csi_state = active},
?INFO_MSG("Resumed session for ~s", ?INFO_MSG("Resumed session for ~s",
[jlib:jid_to_string(NewState#state.jid)]), [jlib:jid_to_string(NewStateData#state.jid)]),
{ok, NewState}; {ok, NewStateData};
{error, El, Msg} -> {error, El, Msg} ->
send_element(StateData, El), send_element(StateData, El),
?INFO_MSG("Cannot resume session for ~s@~s: ~s", ?INFO_MSG("Cannot resume session for ~s@~s: ~s",
@ -2953,6 +2971,8 @@ inherit_session_state(#state{user = U, server = S} = StateData, ResumeID) ->
pres_invis = OldStateData#state.pres_invis, pres_invis = OldStateData#state.pres_invis,
privacy_list = OldStateData#state.privacy_list, privacy_list = OldStateData#state.privacy_list,
aux_fields = OldStateData#state.aux_fields, aux_fields = OldStateData#state.aux_fields,
csi_state = OldStateData#state.csi_state,
csi_queue = OldStateData#state.csi_queue,
mgmt_xmlns = OldStateData#state.mgmt_xmlns, mgmt_xmlns = OldStateData#state.mgmt_xmlns,
mgmt_queue = OldStateData#state.mgmt_queue, mgmt_queue = OldStateData#state.mgmt_queue,
mgmt_timeout = OldStateData#state.mgmt_timeout, mgmt_timeout = OldStateData#state.mgmt_timeout,
@ -2976,6 +2996,71 @@ make_resume_id(StateData) ->
{Time, _} = StateData#state.sid, {Time, _} = StateData#state.sid,
jlib:term_to_base64({StateData#state.resource, Time}). jlib:term_to_base64({StateData#state.resource, Time}).
%%%----------------------------------------------------------------------
%%% XEP-0352
%%%----------------------------------------------------------------------
csi_filter_stanza(#state{csi_state = CsiState, jid = JID} = StateData,
Stanza) ->
Action = ejabberd_hooks:run_fold(csi_filter_stanza,
StateData#state.server,
send, [Stanza]),
?DEBUG("Going to ~p stanza for inactive client ~p",
[Action, jlib:jid_to_string(JID)]),
case Action of
queue -> csi_queue_add(StateData, Stanza);
drop -> StateData;
send ->
From = xml:get_tag_attr_s(<<"from">>, Stanza),
StateData1 = csi_queue_send(StateData, From),
StateData2 = send_stanza(StateData1#state{csi_state = active},
Stanza),
StateData2#state{csi_state = CsiState}
end.
csi_queue_add(#state{csi_queue = Queue, server = Host} = StateData,
#xmlel{children = Els} = Stanza) ->
From = xml:get_tag_attr_s(<<"from">>, Stanza),
Time = calendar:now_to_universal_time(os:timestamp()),
DelayTag = [jlib:timestamp_to_xml(Time, utc,
jlib:make_jid(<<"">>, Host, <<"">>),
<<"Client Inactive">>)],
NewStanza = Stanza#xmlel{children = Els ++ DelayTag},
case length(StateData#state.csi_queue) >= csi_max_queue(StateData) of
true -> csi_queue_add(csi_queue_flush(StateData), NewStanza);
false ->
NewQueue = lists:keystore(From, 1, Queue, {From, NewStanza}),
StateData#state{csi_queue = NewQueue}
end.
csi_queue_send(#state{csi_queue = Queue, csi_state = CsiState} = StateData,
From) ->
case lists:keytake(From, 1, Queue) of
{value, {From, Stanza}, NewQueue} ->
NewStateData = send_stanza(StateData#state{csi_state = active},
Stanza),
NewStateData#state{csi_queue = NewQueue, csi_state = CsiState};
false -> StateData
end.
csi_queue_flush(#state{csi_queue = Queue, csi_state = CsiState, jid = JID} =
StateData) ->
?DEBUG("Flushing CSI queue for ~s", [jlib:jid_to_string(JID)]),
NewStateData =
lists:foldl(fun({_From, Stanza}, AccState) ->
send_stanza(AccState, Stanza)
end, StateData#state{csi_state = active}, Queue),
NewStateData#state{csi_queue = [], csi_state = CsiState}.
%% Make sure we won't push too many messages to the XEP-0198 queue when the
%% client becomes 'active' again. Otherwise, the client might not manage to
%% acknowledge the message flood in time. Also, don't let the queue grow to
%% more than 100 stanzas.
csi_max_queue(#state{mgmt_max_queue = infinity}) -> 100;
csi_max_queue(#state{mgmt_max_queue = Max}) when Max > 200 -> 100;
csi_max_queue(#state{mgmt_max_queue = Max}) when Max < 2 -> 1;
csi_max_queue(#state{mgmt_max_queue = Max}) -> Max div 2.
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------
%%% JID Set memory footprint reduction code %%% JID Set memory footprint reduction code
%%%---------------------------------------------------------------------- %%%----------------------------------------------------------------------

91
src/mod_client_state.erl Normal file
View File

@ -0,0 +1,91 @@
%%%----------------------------------------------------------------------
%%% File : mod_client_state.erl
%%% Author : Holger Weiss
%%% Purpose : Filter stanzas sent to inactive clients (XEP-0352)
%%% Created : 11 Sep 2014 by Holger Weiss
%%%
%%%
%%% ejabberd, Copyright (C) 2014 ProcessOne
%%%
%%% This program is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU General Public License as
%%% published by the Free Software Foundation; either version 2 of the
%%% License, or (at your option) any later version.
%%%
%%% This program is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License along
%%% with this program; if not, write to the Free Software Foundation, Inc.,
%%% 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
%%%
%%%----------------------------------------------------------------------
-module(mod_client_state).
-author('holger@zedat.fu-berlin.de').
-behavior(gen_mod).
-export([start/2, stop/1, filter_presence/2, filter_chat_states/2]).
-include("ejabberd.hrl").
-include("logger.hrl").
-include("jlib.hrl").
start(Host, Opts) ->
QueuePresence = gen_mod:get_opt(queue_presence, Opts,
fun(true) -> true end, false),
DropChatStates = gen_mod:get_opt(drop_chat_states, Opts,
fun(true) -> true end, false),
if QueuePresence ->
ejabberd_hooks:add(csi_filter_stanza, Host, ?MODULE,
filter_presence, 50);
true -> ok
end,
if DropChatStates ->
ejabberd_hooks:add(csi_filter_stanza, Host, ?MODULE,
filter_chat_states, 50);
true -> ok
end,
ok.
stop(Host) ->
ejabberd_hooks:delete(csi_filter_stanza, Host, ?MODULE,
filter_presence, 50),
ejabberd_hooks:delete(csi_filter_stanza, Host, ?MODULE,
filter_chat_states, 50),
ok.
filter_presence(_Action, #xmlel{name = <<"presence">>, attrs = Attrs}) ->
case xml:get_attr(<<"type">>, Attrs) of
{value, Type} when Type /= <<"unavailable">> ->
?DEBUG("Got important presence stanza", []),
{stop, send};
_ ->
?DEBUG("Got availability presence stanza", []),
{stop, queue}
end;
filter_presence(Action, _Stanza) -> Action.
filter_chat_states(_Action, #xmlel{name = <<"message">>} = Stanza) ->
%% All XEP-0085 chat states except for <gone/>:
ChatStates = [<<"active">>, <<"inactive">>, <<"composing">>, <<"paused">>],
Stripped =
lists:foldl(fun(ChatState, AccStanza) ->
xml:remove_subtags(AccStanza, ChatState,
{<<"xmlns">>, ?NS_CHATSTATES})
end, Stanza, ChatStates),
case Stripped of
#xmlel{children = [#xmlel{name = <<"thread">>}]} ->
?DEBUG("Got standalone chat state notification", []),
{stop, drop};
#xmlel{children = []} ->
?DEBUG("Got standalone chat state notification", []),
{stop, drop};
_ ->
?DEBUG("Got message with chat state notification", []),
{stop, send}
end;
filter_chat_states(Action, _Stanza) -> Action.