diff options
-rw-r--r-- | src/im-channel.c | 57 | ||||
-rw-r--r-- | src/im-channel.h | 15 | ||||
-rw-r--r-- | src/im-factory.c | 8 | ||||
-rw-r--r-- | tests/twisted/text/test-chat-state.py | 171 |
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) |