[telepathy-gabble/master] Negotiate chat state support for capsless contacts

Will Thompson will.thompson at collabora.co.uk
Sat Nov 7 06:19:14 PST 2009


XEP-0085 §5.1 defines how to negotiate support for chat states when you
don't know a contact's caps. Roughly:

• If you don't know whether someone supports them, don't send
  stand-alone notifications, but include <active/> in messages you send;
• If you receive a chat state, mark the contact as supporting chat
  states;
• If you receive a message without a chat state, mark the contact as not
  supporting chat states.

This is complicated slightly by multiple resources, but basically we
follow the above rules, resetting whenever we change which resource
we're sending to.
---
 src/im-channel.c                      |   58 ++++++++++--
 src/im-channel.h                      |   15 ++-
 src/im-factory.c                      |    8 +-
 tests/twisted/text/test-chat-state.py |  171 +++++++++++++++++++++++++++++----
 4 files changed, 218 insertions(+), 34 deletions(-)

diff --git a/src/im-channel.c b/src/im-channel.c
index 0c077b4..f4edc54 100644
--- a/src/im-channel.c
+++ b/src/im-channel.c
@@ -95,6 +95,12 @@ enum
 
 /* private structure */
 
+typedef enum {
+  CHAT_STATES_UNKNOWN,
+  CHAT_STATES_SUPPORTED,
+  CHAT_STATES_NOT_SUPPORTED
+} ChatStateSupport;
+
 struct _GabbleIMChannelPrivate
 {
   GabbleConnection *conn;
@@ -104,6 +110,7 @@ struct _GabbleIMChannelPrivate
 
   gchar *peer_jid;
   gboolean send_nick;
+  ChatStateSupport chat_states_supported;
 
   /* FALSE unless at least one chat state notification has been sent; <gone/>
    * will only be sent when the channel closes if this is TRUE. This prevents
@@ -172,6 +179,8 @@ gabble_im_channel_constructor (GType type, guint n_props,
   else
     priv->send_nick = TRUE;
 
+  priv->chat_states_supported = CHAT_STATES_UNKNOWN;
+
   tp_message_mixin_init (obj, G_STRUCT_OFFSET (GabbleIMChannel, message_mixin),
       conn);
 
@@ -395,7 +404,8 @@ gabble_im_channel_class_init (GabbleIMChannelClass *gabble_im_channel_class)
 }
 
 static gboolean
-chat_states_supported (GabbleIMChannel *self)
+chat_states_supported (GabbleIMChannel *self,
+                       gboolean include_unknown)
 {
   GabbleIMChannelPrivate *priv = self->priv;
   GabblePresence *presence;
@@ -403,8 +413,21 @@ chat_states_supported (GabbleIMChannel *self)
   presence = gabble_presence_cache_get (priv->conn->presence_cache,
       priv->handle);
 
-  return (presence != NULL &&
-      gabble_presence_has_cap (presence, NS_CHAT_STATES));
+  if (presence != NULL && gabble_presence_has_cap (presence, NS_CHAT_STATES))
+    return TRUE;
+
+  switch (priv->chat_states_supported)
+    {
+      case CHAT_STATES_UNKNOWN:
+        return include_unknown;
+      case CHAT_STATES_SUPPORTED:
+        return TRUE;
+      case CHAT_STATES_NOT_SUPPORTED:
+        return FALSE;
+      default:
+        g_assert_not_reached ();
+        return FALSE;
+    }
 }
 
 static void
@@ -414,7 +437,7 @@ emit_closed_and_send_gone (GabbleIMChannel *self)
 
   if (priv->send_gone)
     {
-      if (chat_states_supported (self))
+      if (chat_states_supported (self, FALSE))
         gabble_message_util_send_chat_state (G_OBJECT (self), priv->conn,
             LM_MESSAGE_SUB_TYPE_CHAT, TP_CHANNEL_CHAT_STATE_GONE,
             priv->peer_jid, NULL);
@@ -501,7 +524,7 @@ _gabble_im_channel_send_message (GObject *object,
   g_assert (GABBLE_IS_IM_CHANNEL (self));
   priv = self->priv;
 
-  if (chat_states_supported (self))
+  if (chat_states_supported (self, TRUE))
     {
       state = TP_CHANNEL_CHAT_STATE_ACTIVE;
       priv->send_gone = TRUE;
@@ -517,7 +540,6 @@ _gabble_im_channel_send_message (GObject *object,
     priv->send_nick = FALSE;
 }
 
-
 /**
  * _gabble_im_channel_receive
  *
@@ -531,7 +553,8 @@ _gabble_im_channel_receive (GabbleIMChannel *chan,
                             const gchar *id,
                             const char *text,
                             TpChannelTextSendError send_error,
-                            TpDeliveryStatus delivery_status)
+                            TpDeliveryStatus delivery_status,
+                            gint state)
 {
   GabbleIMChannelPrivate *priv;
   TpBaseConnection *base_conn;
@@ -550,6 +573,19 @@ _gabble_im_channel_receive (GabbleIMChannel *chan,
           g_free (priv->peer_jid);
           priv->peer_jid = g_strdup (from);
         }
+
+      if (state == -1)
+        {
+          priv->chat_states_supported = CHAT_STATES_NOT_SUPPORTED;
+        }
+      else
+        {
+          priv->chat_states_supported = CHAT_STATES_SUPPORTED;
+
+          tp_svc_channel_interface_chat_state_emit_chat_state_changed (
+              (TpSvcChannelInterfaceChatState *) chan,
+              priv->handle, (TpChannelChatState) state);
+        }
     }
   else
     {
@@ -558,6 +594,8 @@ _gabble_im_channel_receive (GabbleIMChannel *chan,
 
       if (slash != NULL)
         *slash = '\0';
+
+      priv->chat_states_supported = CHAT_STATES_UNKNOWN;
     }
 
   msg = tp_message_new (base_conn, 2, 2);
@@ -634,7 +672,7 @@ _gabble_im_channel_receive (GabbleIMChannel *chan,
 
 void
 _gabble_im_channel_state_receive (GabbleIMChannel *chan,
-                                  guint state)
+                                  TpChannelChatState state)
 {
   GabbleIMChannelPrivate *priv;
 
@@ -642,6 +680,8 @@ _gabble_im_channel_state_receive (GabbleIMChannel *chan,
   g_assert (GABBLE_IS_IM_CHANNEL (chan));
   priv = chan->priv;
 
+  priv->chat_states_supported = CHAT_STATES_SUPPORTED;
+
   tp_svc_channel_interface_chat_state_emit_chat_state_changed (
       (TpSvcChannelInterfaceChatState *) chan,
       priv->handle, state);
@@ -813,7 +853,7 @@ gabble_im_channel_set_chat_state (TpSvcChannelInterfaceChatState *iface,
   /* Only send anything to the peer if we actually know they support chat
    * states.
    */
-  else if (chat_states_supported (self))
+  else if (chat_states_supported (self, FALSE))
     {
       if (gabble_message_util_send_chat_state (G_OBJECT (self), priv->conn,
               LM_MESSAGE_SUB_TYPE_CHAT, state, priv->peer_jid, &error))
diff --git a/src/im-channel.h b/src/im-channel.h
index 7d7d96a..2a2a2dc 100644
--- a/src/im-channel.h
+++ b/src/im-channel.h
@@ -66,10 +66,17 @@ GType gabble_im_channel_get_type (void);
                               GabbleIMChannelClass))
 
 void _gabble_im_channel_receive (GabbleIMChannel *chan,
-    TpChannelTextMessageType type, TpHandle sender, const char *from,
-    time_t timestamp, const char *id, const char *text,
-    TpChannelTextSendError send_error, TpDeliveryStatus delivery_status);
-void _gabble_im_channel_state_receive (GabbleIMChannel *chan, guint state);
+    TpChannelTextMessageType type,
+    TpHandle sender,
+    const char *from,
+    time_t timestamp,
+    const char *id,
+    const char *text,
+    TpChannelTextSendError send_error,
+    TpDeliveryStatus delivery_status,
+    gint state);
+void _gabble_im_channel_state_receive (GabbleIMChannel *chan,
+    TpChannelChatState state);
 
 G_END_DECLS
 
diff --git a/src/im-factory.c b/src/im-factory.c
index 8f5cccc..d2b12a1 100644
--- a/src/im-factory.c
+++ b/src/im-factory.c
@@ -283,12 +283,12 @@ im_factory_message_cb (LmMessageHandler *handler,
          from, handle, msgtype, body);
     }
 
-  if (state != -1 && send_error == GABBLE_TEXT_CHANNEL_SEND_NO_ERROR)
-    _gabble_im_channel_state_receive (chan, state);
-
   if (body != NULL)
     _gabble_im_channel_receive (chan, msgtype, handle, from, stamp, id, body,
-        send_error, delivery_status);
+        send_error, delivery_status, state);
+  else if (state != -1 && send_error == GABBLE_TEXT_CHANNEL_SEND_NO_ERROR)
+    _gabble_im_channel_state_receive (chan, (TpChannelChatState) state);
+
 
   return LM_HANDLER_RESULT_REMOVE_MESSAGE;
 }
diff --git a/tests/twisted/text/test-chat-state.py b/tests/twisted/text/test-chat-state.py
index 9b3efed..289f692 100644
--- a/tests/twisted/text/test-chat-state.py
+++ b/tests/twisted/text/test-chat-state.py
@@ -1,4 +1,4 @@
-
+# coding=utf-8
 """
 Test that chat state notifications are correctly sent and received on text
 channels.
@@ -6,21 +6,21 @@ channels.
 
 from twisted.words.xish import domish
 
-from servicetest import assertEquals, wrap_channel, EventPattern
+from servicetest import assertEquals, assertLength, wrap_channel, EventPattern
 from gabbletest import exec_test, make_result_iq, sync_stream, make_presence
 import constants as cs
 import ns
 
-def check_state_notification(elem, name):
+def check_state_notification(elem, name, allow_body=False):
     assertEquals('message', elem.name)
     assertEquals('chat', elem['type'])
 
     children = list(elem.elements())
-    assert len(children) == 1, elem.toXml()
-    notification = children[0]
-
+    notification = [x for x in children if x.uri == ns.CHAT_STATES][0]
     assert notification.name == name, notification.toXml()
-    assert notification.uri == ns.CHAT_STATES, notification.toXml()
+
+    if not allow_body:
+        assert len(children) == 1, elem.toXml()
 
 def make_message(jid, body=None, state=None):
     m = domish.Element((None, 'message'))
@@ -107,22 +107,15 @@ def test(q, bus, conn, stream):
     elem = stream_message.stanza
     assertEquals('chat', elem['type'])
 
+    check_state_notification(elem, 'active', allow_body=True)
+
     def is_body(e):
         if e.name == 'body':
             assert e.children[0] == u'hi.', e.toXml()
             return True
         return False
 
-    def is_active(e):
-        if e.uri == ns.CHAT_STATES:
-            assert e.name == 'active', e.toXml()
-            return True
-        return False
-
-    children = list(elem.elements())
-
-    assert len(filter(is_body,   children)) == 1, elem.toXml()
-    assert len(filter(is_active, children)) == 1, elem.toXml()
+    assert len([x for x in elem.elements() if is_body(x)]) == 1, elem.toXml()
 
     # Close the channel without acking the received message. The peer should
     # get a <gone/> notification, and the channel should respawn.
@@ -162,5 +155,149 @@ def test(q, bus, conn, stream):
     sync_stream(q, stream)
     q.unforbid_events(es)
 
+    # XEP-0085 §5.1 defines how to negotiate support for chat states with a
+    # contact in the absence of capabilities. This is useful when talking to
+    # invisible contacts, for example.
+
+    # First, if we receive a message from a contact, containing an <active/>
+    # notification, they support chat states, so we should send them.
+
+    jid = 'i at example.com'
+    full_jid = jid + '/GTalk'
+
+    path = conn.Requests.CreateChannel(
+            { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+              cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+              cs.TARGET_ID: jid,
+              })[0]
+    chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+        ['ChatState'])
+
+    stream.send(make_message(full_jid, body='i am invisible', state='active'))
+
+    changed = q.expect('dbus-signal', signal='ChatStateChanged')
+    assertEquals(cs.CHAT_STATE_ACTIVE, changed.args[1])
+
+    # We've seen them send a chat state notification, so we should send them
+    # notifications when the UI tells us to.
+    chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING)
+    stream_message = q.expect('stream-message', to=full_jid)
+    check_state_notification(stream_message.stanza, 'composing')
+
+    chan.Text.Send(0, 'very convincing')
+    stream_message = q.expect('stream-message', to=full_jid)
+    check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+    # Now, test the case where we start the negotiation, and the contact
+    # turns out to support chat state notifications.
+
+    jid = 'c at example.com'
+    full_jid = jid + '/GTalk'
+    path = conn.Requests.CreateChannel(
+            { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+              cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+              cs.TARGET_ID: jid,
+              })[0]
+    chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+        ['ChatState'])
+
+    # We shouldn't send any notifications until we actually send a message.
+    e = EventPattern('stream-message', to=jid)
+    q.forbid_events([e])
+    for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+              cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+        chan.ChatState.SetChatState(i)
+    sync_stream(q, stream)
+    q.unforbid_events([e])
+
+    # When we send a message, say we're active.
+    chan.Text.Send(0, 'is anyone there?')
+    stream_message = q.expect('stream-message', to=jid)
+    check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+    # We get a notification back from our contact.
+    stream.send(make_message(full_jid, state='composing'))
+
+    changed = q.expect('dbus-signal', signal='ChatStateChanged')
+    _, state = changed.args
+    assertEquals(cs.CHAT_STATE_COMPOSING, state)
+
+    # So now we know they support notification, so should send notifications.
+    chan.ChatState.SetChatState(cs.CHAT_STATE_COMPOSING)
+
+    # This doesn't check whether we're sending to the bare jid, or the
+    # jid+resource. In fact, the notification is sent to the bare jid, because
+    # we only update which jid we send to when we actually receive a message,
+    # not when we receive a notification. wjt thinks this is less surprising
+    # than the alternative:
+    #
+    #  • I'm talking to you on my N900, and signed in on my laptop;
+    #  • I enter one character in a tab to you on my laptop, and then delete
+    #    it;
+    #  • Now your messages to me appear on my laptop (until I send you another
+    #    one from my N900)!
+    stream_message = q.expect('stream-message')
+    check_state_notification(stream_message.stanza, 'composing')
+
+    # But! Now they start messaging us from a different client, which *doesn't*
+    # support notifications.
+    other_jid = jid + '/Library'
+    stream.send(make_message(other_jid, body='grr, library computers'))
+    q.expect('dbus-signal', signal='Received')
+
+    # Okay, we should stop sending typing notifications.
+    e = EventPattern('stream-message', to=other_jid)
+    q.forbid_events([e])
+    for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+              cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+        chan.ChatState.SetChatState(i)
+    sync_stream(q, stream)
+    q.unforbid_events([e])
+
+    # Now, test the case where we start the negotiation, and the contact
+    # does not support chat state notifications
+
+    jid = 'twitterbot at example.com'
+    full_jid = jid + '/Nonsense'
+    path = conn.Requests.CreateChannel(
+            { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT,
+              cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT,
+              cs.TARGET_ID: jid,
+              })[0]
+    chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text',
+        ['ChatState'])
+
+    # We shouldn't send any notifications until we actually send a message.
+    e = EventPattern('stream-message', to=jid)
+    q.forbid_events([e])
+    for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+              cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+        chan.ChatState.SetChatState(i)
+    sync_stream(q, stream)
+    q.unforbid_events([e])
+
+    # When we send a message, say we're active.
+    chan.Text.Send(0, '#n900 #maemo #zomg #woo #yay http://bit.ly/n900')
+    stream_message = q.expect('stream-message', to=jid)
+    check_state_notification(stream_message.stanza, 'active', allow_body=True)
+
+    # They reply without a chat state.
+    stream.send(make_message(full_jid, body="posted."))
+    q.expect('dbus-signal', signal='Received')
+
+    # Okay, we shouldn't send any more.
+    e = EventPattern('stream-message', to=other_jid)
+    q.forbid_events([e])
+    for i in [cs.CHAT_STATE_COMPOSING, cs.CHAT_STATE_INACTIVE,
+              cs.CHAT_STATE_PAUSED, cs.CHAT_STATE_ACTIVE]:
+        chan.ChatState.SetChatState(i)
+    sync_stream(q, stream)
+    q.unforbid_events([e])
+
+    chan.Text.Send(0, '@stephenfry simmer down')
+    message = q.expect('stream-message')
+    states = [x for x in message.stanza.elements() if x.uri == ns.CHAT_STATES]
+    assertLength(0, states)
+
 if __name__ == '__main__':
     exec_test(test)
-- 
1.5.6.5




More information about the telepathy-commits mailing list