]> granicus.if.org Git - ejabberd/commitdiff
Support XEP-0352: Client State Indication
authorHolger Weiss <holger@zedat.fu-berlin.de>
Thu, 11 Sep 2014 15:44:29 +0000 (17:44 +0200)
committerHolger Weiss <holger@zedat.fu-berlin.de>
Thu, 11 Sep 2014 15:44:29 +0000 (17:44 +0200)
doc/guide.tex
ejabberd.yml.example
include/ns.hrl
src/ejabberd_c2s.erl
src/mod_client_state.erl [new file with mode: 0644]

index 8e2b91049cba06cd5307987490c7fd93752b1e6d..27fcfc18128bfaef0df7c2329d2b07bdabba169e 100644 (file)
@@ -66,6 +66,7 @@
 \newcommand{\module}[1]{\texttt{#1}}
 \newcommand{\modadhoc}{\module{mod\_adhoc}}
 \newcommand{\modannounce}{\module{mod\_announce}}
+\newcommand{\modclientstate}{\module{mod\_client\_state}}
 \newcommand{\modblocking}{\module{mod\_blocking}}
 \newcommand{\modcaps}{\module{mod\_caps}}
 \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 \modcaps{} &  Entity Capabilities (\xepref{0115}) & \\
     \hline \modcarboncopy{} & Message Carbons (\xepref{0280}) & \\
+    \hline \ahrefloc{modclientstate}{\modclientstate{}} & Filter stanzas for inactive clients &  \\
     \hline \modconfigure{} & Server configuration using Ad-Hoc & \modadhoc{} \\
     \hline \ahrefloc{moddisco}{\moddisco{}} & Service Discovery (\xepref{0030}) &  \\
     \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
 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{}}
 \ind{modules!\moddisco{}}
 \ind{protocols!XEP-0030: Service Discovery}
index 8f108bf52edb5ff828bd4d6b8d81713f958d529a..4755a7d4498c02db4b9b289bce902481e843f172 100644 (file)
@@ -558,6 +558,9 @@ modules:
   mod_blocking: {} # requires mod_privacy
   mod_caps: {}
   mod_carboncopy: {}
+  mod_client_state:
+    drop_chat_states: true
+    queue_presence: false
   mod_configure: {} # requires mod_adhoc
   mod_disco: {}
   ## mod_echo: {}
index 6eb54fc3d70a574a49751cc5be91672d28ebe468..3ec19aca8922016dceef705e89220fd667defdac 100644 (file)
 -define(NS_CARBONS_2, <<"urn:xmpp:carbons:2">>).
 -define(NS_CARBONS_1, <<"urn:xmpp:carbons:1">>).
 -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_3,  <<"urn:xmpp:sm:3">>).
index 87f1bbdfbfe3a19c75e80d79bc7cd4b3e6125495..cb6f9e6d83059f024c9be56939458da83b467dd1 100644 (file)
                auth_module = unknown,
                ip,
                aux_fields = [],
+               csi_state = active,
+               csi_queue = [],
                mgmt_state,
                mgmt_xmlns,
                mgmt_queue,
@@ -475,6 +477,10 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
                                              false ->
                                                  []
                                            end,
+                                       ClientStateFeature =
+                                           [#xmlel{name = <<"csi">>,
+                                                   attrs = [{<<"xmlns">>, ?NS_CLIENT_STATE}],
+                                                   children = []}],
                                        StreamFeatures = [#xmlel{name = <<"bind">>,
                                                                attrs = [{<<"xmlns">>, ?NS_BIND}],
                                                                children = []},
@@ -484,6 +490,7 @@ wait_for_stream({xmlstreamstart, _Name, Attrs}, StateData) ->
                                                            ++
                                                            RosterVersioningFeature ++
                                                            StreamManagementFeature ++
+                                                           ClientStateFeature ++
                                                            ejabberd_hooks:run_fold(c2s_stream_features,
                                                                Server, [], [Server]),
                                        send_element(StateData,
@@ -1165,6 +1172,17 @@ wait_for_session(closed, StateData) ->
 session_established({xmlstreamelement, #xmlel{name = Name} = El}, StateData)
     when ?IS_STREAM_MGMT_TAG(Name) ->
     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},
                    StateData) ->
     FromJID = StateData#state.jid,
@@ -1855,6 +1873,8 @@ send_element(StateData, El) when StateData#state.xml_socket ->
 send_element(StateData, 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 ->
     mgmt_queue_add(StateData, Stanza);
 send_stanza(StateData, Stanza) when StateData#state.mgmt_state == active ->
@@ -1869,18 +1889,14 @@ send_stanza(StateData, Stanza) ->
     send_element(StateData, Stanza),
     StateData.
 
-send_packet(StateData, Packet) when StateData#state.mgmt_state == active;
-                                   StateData#state.mgmt_state == pending ->
+send_packet(StateData, Packet) ->
     case is_stanza(Packet) of
       true ->
          send_stanza(StateData, Packet);
       false ->
          send_element(StateData, Packet),
          StateData
-    end;
-send_packet(StateData, Stanza) ->
-    send_element(StateData, Stanza),
-    StateData.
+    end.
 
 send_header(StateData, Server, Version, Lang)
     when StateData#state.xml_socket ->
@@ -2762,9 +2778,11 @@ handle_resume(StateData, Attrs) ->
                       #xmlel{name = <<"r">>,
                              attrs = [{<<"xmlns">>, AttrXmlns}],
                              children = []}),
+         FlushedState = csi_queue_flush(NewState),
+         NewStateData = FlushedState#state{csi_state = active},
          ?INFO_MSG("Resumed session for ~s",
-                   [jlib:jid_to_string(NewState#state.jid)]),
-         {ok, NewState};
+                   [jlib:jid_to_string(NewStateData#state.jid)]),
+         {ok, NewStateData};
       {error, El, Msg} ->
          send_element(StateData, El),
          ?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,
                                           privacy_list = OldStateData#state.privacy_list,
                                           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_queue = OldStateData#state.mgmt_queue,
                                           mgmt_timeout = OldStateData#state.mgmt_timeout,
@@ -2976,6 +2996,71 @@ make_resume_id(StateData) ->
     {Time, _} = StateData#state.sid,
     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
 %%%----------------------------------------------------------------------
diff --git a/src/mod_client_state.erl b/src/mod_client_state.erl
new file mode 100644 (file)
index 0000000..8336316
--- /dev/null
@@ -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.