summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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)