summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWill Thompson <will.thompson@collabora.co.uk>2009-11-07 14:19:14 +0000
committerWill Thompson <will.thompson@collabora.co.uk>2010-01-11 10:46:10 +0000
commit4ae769daa29650deac6b9717a1d320111aaf36dc (patch)
tree99a92e67e076155f02e559b63411852555b42036
parent5347d19ec0fd6289af83ea97e3ec67ef1b98f9ef (diff)
Negotiate chat state support for capsless contacts
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.
-rw-r--r--src/im-channel.c57
-rw-r--r--src/im-channel.h15
-rw-r--r--src/im-factory.c8
-rw-r--r--tests/twisted/text/test-chat-state.py171
4 files changed, 218 insertions, 33 deletions
diff --git a/src/im-channel.c b/src/im-channel.c
index f19f244d0..cd464c969 100644
--- a/src/im-channel.c
+++ b/src/im-channel.c
@@ -94,6 +94,12 @@ enum
/* private structure */
+typedef enum {
+ CHAT_STATES_UNKNOWN,
+ CHAT_STATES_SUPPORTED,
+ CHAT_STATES_NOT_SUPPORTED
+} ChatStateSupport;
+
struct _GabbleIMChannelPrivate
{
GabbleConnection *conn;
@@ -103,6 +109,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
@@ -171,6 +178,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);
@@ -394,7 +403,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;
@@ -402,7 +412,21 @@ chat_states_supported (GabbleIMChannel *self)
presence = gabble_presence_cache_get (priv->conn->presence_cache,
priv->handle);
- return (presence != NULL && (presence->caps & PRESENCE_CAP_CHAT_STATES));
+ if (presence != NULL && (presence->caps & PRESENCE_CAP_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
@@ -412,7 +436,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);
@@ -499,7 +523,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;
@@ -515,7 +539,6 @@ _gabble_im_channel_send_message (GObject *object,
priv->send_nick = FALSE;
}
-
/**
* _gabble_im_channel_receive
*
@@ -529,7 +552,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;
@@ -548,6 +572,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
{
@@ -556,6 +593,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);
@@ -632,7 +671,7 @@ _gabble_im_channel_receive (GabbleIMChannel *chan,
void
_gabble_im_channel_state_receive (GabbleIMChannel *chan,
- guint state)
+ TpChannelChatState state)
{
GabbleIMChannelPrivate *priv;
@@ -640,6 +679,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);
@@ -811,7 +852,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 7d7d96a14..2a2a2dc1e 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 0f09a884e..b176f7f02 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 9b3efed9b..289f69226 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@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@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@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)