diff options
author | Will Thompson <will.thompson@collabora.co.uk> | 2012-12-06 18:35:37 +0000 |
---|---|---|
committer | Will Thompson <will.thompson@collabora.co.uk> | 2012-12-06 18:35:37 +0000 |
commit | 51e301d6366e7e5f0f203f19fd594cef27ae05db (patch) | |
tree | d83fa853c1939ba5f1aee395bb47c205f30b0379 | |
parent | c33a817b5bcf9f6bec7c744e4c5a3f0b96df4118 (diff) | |
parent | 52beb82932c6c31f43e6b4465a22f5543cff1d06 (diff) |
Merge branch 'xep-0184'
https://bugs.freedesktop.org/show_bug.cgi?id=47378
-rw-r--r-- | src/capabilities.c | 2 | ||||
-rw-r--r-- | src/im-channel.c | 127 | ||||
-rw-r--r-- | src/im-channel.h | 5 | ||||
-rw-r--r-- | src/im-factory.c | 50 | ||||
-rw-r--r-- | src/namespaces.h | 1 | ||||
-rw-r--r-- | src/roster.c | 3 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 2 | ||||
-rw-r--r-- | tests/twisted/ns.py | 1 | ||||
-rw-r--r-- | tests/twisted/presence/__init__.py | 0 | ||||
-rw-r--r-- | tests/twisted/text/receipts.py | 197 |
10 files changed, 365 insertions, 23 deletions
diff --git a/src/capabilities.c b/src/capabilities.c index 9d8a97f69..1c4801fd8 100644 --- a/src/capabilities.c +++ b/src/capabilities.c @@ -61,6 +61,8 @@ static const Feature self_advertised_features[] = { FEATURE_FIXED, NS_TUBES }, { FEATURE_FIXED, NS_BYTESTREAMS }, { FEATURE_FIXED, NS_VERSION }, + { FEATURE_FIXED, NS_LAST }, + { FEATURE_FIXED, NS_RECEIPTS }, #ifdef ENABLE_FILE_TRANSFER { FEATURE_OPTIONAL, NS_FILE_TRANSFER }, diff --git a/src/im-channel.c b/src/im-channel.c index 1d1cf2fa5..9cbdbfaab 100644 --- a/src/im-channel.c +++ b/src/im-channel.c @@ -81,6 +81,7 @@ typedef struct { GabbleIMChannel *channel; TpMessage *message; gchar *token; + TpMessageSendingFlags flags; } _GabbleIMSendMessageCtx; static GPtrArray * @@ -144,6 +145,10 @@ gabble_im_channel_constructed (GObject *obj) tp_message_mixin_init (obj, G_STRUCT_OFFSET (GabbleIMChannel, message_mixin), base_conn); + /* We deliberately do not include + * TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_SUCCESSES here, even though we + * support requesting receipts, because XEP-0184 provides no guarantees. + */ tp_message_mixin_implement_sending (obj, _gabble_im_channel_send_message, G_N_ELEMENTS (types), types, 0, TP_DELIVERY_REPORTING_SUPPORT_FLAG_RECEIVE_FAILURES, @@ -237,6 +242,28 @@ chat_states_supported (GabbleIMChannel *self, } } +static gboolean +receipts_conceivably_supported ( + GabbleIMChannel *self) +{ + TpBaseChannel *base = (TpBaseChannel *) self; + GabbleConnection *conn = + GABBLE_CONNECTION (tp_base_channel_get_connection (base)); + GabblePresence *presence; + + presence = gabble_presence_cache_get (conn->presence_cache, + tp_base_channel_get_target_handle (base)); + + /* ...except it's never null because _parse_message_message() in + * presence-cache.c. I hate this exactly as much as I did when I wrote the + * FIXME on that function. */ + if (presence != NULL) + return gabble_presence_has_cap (presence, NS_RECEIPTS); + + /* Otherwise ... who knows. Why not ask for one? */ + return TRUE; +} + static void gabble_im_channel_dispose (GObject *object) { @@ -300,7 +327,7 @@ _gabble_im_channel_message_sent_cb (GObject *source, if (wocky_porter_send_finish (porter, res, &error)) { - tp_message_mixin_sent ((GObject *) chan, message, 0, + tp_message_mixin_sent ((GObject *) chan, message, context->flags, context->token, NULL); } else @@ -345,21 +372,30 @@ _gabble_im_channel_send_message (GObject *object, tp_base_connection_get_self_handle (base_conn), state); } - /* We don't support providing successful delivery reports. */ - flags = 0; - stanza = gabble_message_util_build_stanza (message, gabble_conn, 0, state, priv->peer_jid, priv->send_nick, &id, &error); - if (stanza != NULL) { + if ((flags & TP_MESSAGE_SENDING_FLAG_REPORT_DELIVERY) && + receipts_conceivably_supported (self)) + { + wocky_node_add_child_ns (wocky_stanza_get_top_node (stanza), + "request", NS_RECEIPTS); + flags = TP_MESSAGE_SENDING_FLAG_REPORT_DELIVERY; + } + else + { + flags = 0; + } + porter = gabble_connection_dup_porter (gabble_conn); context = g_slice_new0 (_GabbleIMSendMessageCtx); context->channel = g_object_ref (base); context->message = g_object_ref (message); context->token = id; + context->flags = flags; wocky_porter_send_async (porter, stanza, NULL, _gabble_im_channel_message_sent_cb, context); g_object_unref (porter); @@ -367,7 +403,7 @@ _gabble_im_channel_send_message (GObject *object, } else { - tp_message_mixin_sent (object, message, flags, NULL, error); + tp_message_mixin_sent (object, message, 0, NULL, error); g_error_free (error); } @@ -400,29 +436,61 @@ build_message ( return msg; } +static void +maybe_send_delivery_report ( + GabbleIMChannel *self, + WockyStanza *message, + const gchar *jid, + const gchar *id) +{ + TpBaseChannel *base = TP_BASE_CHANNEL (self); + TpHandle target = tp_base_channel_get_target_handle (base); + TpBaseConnection *base_conn = tp_base_channel_get_connection (base); + GabbleConnection *conn = GABBLE_CONNECTION (base_conn); + WockyStanza *report; + + if (id == NULL) + return; + + if (wocky_node_get_child_ns (wocky_stanza_get_top_node (message), + "request", NS_RECEIPTS) == NULL) + return; + + if (conn->self_presence->status == GABBLE_PRESENCE_HIDDEN || + !gabble_roster_handle_gets_presence_from_us (conn->roster, target)) + return; + + report = wocky_stanza_build ( + WOCKY_STANZA_TYPE_MESSAGE, WOCKY_STANZA_SUB_TYPE_NONE, + NULL, jid, + '(', "received", ':', NS_RECEIPTS, + '@', "id", id, + ')', NULL); + + _gabble_connection_send (conn, report, NULL); + g_object_unref (report); +} + /* * _gabble_im_channel_receive: * @chan: a channel + * @message: the <message> stanza, from which all the following arguments were + * extracted. * @type: the message type * @from: the full JID we received the message from * @timestamp: the time at which the message was sent (not the time it was * received) * @id: the id='' attribute from the <message/> stanza, if any * @text: the plaintext body of the message - * @send_error: the reason why sending @text to @sender failed, or - * GABBLE_TEXT_CHANNEL_SEND_NO_ERROR if this call is not to report - * a failure to send. - * @delivery_status: if @send_error is GABBLE_TEXT_CHANNEL_SEND_NO_ERROR, - * ignored; else the delivery status to attach to the report. * @state: a #TpChannelChatState, or -1 if there was no chat state in the * message. * * Shoves an incoming message into @chan, possibly updating the chat state at - * the same time; or maybe this is a delivery report? Who knows! It's a magical - * adventure. + * the same time. */ void _gabble_im_channel_receive (GabbleIMChannel *chan, + WockyStanza *message, TpChannelTextMessageType type, const char *from, time_t timestamp, @@ -460,6 +528,7 @@ _gabble_im_channel_receive (GabbleIMChannel *chan, tp_message_set_string (msg, 0, "message-token", id); tp_message_mixin_take_received (G_OBJECT (chan), msg); + maybe_send_delivery_report (chan, message, from, id); } void @@ -476,7 +545,7 @@ _gabble_im_channel_report_delivery ( TpBaseChannel *base_chan = (TpBaseChannel *) self; TpBaseConnection *base_conn; TpHandle peer; - TpMessage *msg, *delivery_report; + TpMessage *delivery_report; gchar *tmp; g_return_if_fail (GABBLE_IS_IM_CHANNEL (self)); @@ -495,7 +564,6 @@ _gabble_im_channel_report_delivery ( priv->chat_states_supported = CHAT_STATES_UNKNOWN; } - msg = build_message (self, type, timestamp, text); delivery_report = tp_cm_message_new (base_conn, 1); tp_message_set_uint32 (delivery_report, 0, "message-type", TP_CHANNEL_TEXT_MESSAGE_TYPE_DELIVERY_REPORT); @@ -514,15 +582,20 @@ _gabble_im_channel_report_delivery ( if (id != NULL) tp_message_set_string (delivery_report, 0, "delivery-token", id); - /* This is a delivery report, so the original sender of the echoed message - * must be us! */ - tp_cm_message_set_sender (msg, tp_base_connection_get_self_handle (base_conn)); + if (text != NULL) + { + TpMessage *msg = build_message (self, type, timestamp, text); + /* This is a delivery report, so the original sender of the echoed message + * must be us! */ + tp_cm_message_set_sender (msg, tp_base_connection_get_self_handle (base_conn)); - /* Since this is a delivery report, we can trust the id on the message. */ - if (id != NULL) - tp_message_set_string (msg, 0, "message-token", id); + /* Since this is a delivery report, we can trust the id on the message. */ + if (id != NULL) + tp_message_set_string (msg, 0, "message-token", id); + + tp_cm_message_take_message (delivery_report, 0, "delivery-echo", msg); + } - tp_cm_message_take_message (delivery_report, 0, "delivery-echo", msg); tp_message_mixin_take_received (G_OBJECT (self), delivery_report); } @@ -550,6 +623,16 @@ _gabble_im_channel_state_receive (GabbleIMChannel *chan, tp_base_channel_get_target_handle (base_chan), state); } +void +gabble_im_channel_receive_receipt ( + GabbleIMChannel *self, + const gchar *receipt_id) +{ + _gabble_im_channel_report_delivery (self, + TP_CHANNEL_TEXT_MESSAGE_TYPE_NORMAL, 0, receipt_id, NULL, + GABBLE_TEXT_CHANNEL_SEND_NO_ERROR, TP_DELIVERY_STATUS_DELIVERED); +} + static void gabble_im_channel_close (TpBaseChannel *base_chan) { diff --git a/src/im-channel.h b/src/im-channel.h index 076565ef5..c09408484 100644 --- a/src/im-channel.h +++ b/src/im-channel.h @@ -25,6 +25,7 @@ #include <time.h> #include <telepathy-glib/telepathy-glib.h> +#include <wocky/wocky.h> G_BEGIN_DECLS @@ -63,6 +64,7 @@ GType gabble_im_channel_get_type (void); GabbleIMChannelClass)) void _gabble_im_channel_receive (GabbleIMChannel *chan, + WockyStanza *message, TpChannelTextMessageType type, const char *from, time_t timestamp, @@ -71,6 +73,9 @@ void _gabble_im_channel_receive (GabbleIMChannel *chan, gint state); void _gabble_im_channel_state_receive (GabbleIMChannel *chan, TpChannelChatState state); +void gabble_im_channel_receive_receipt ( + GabbleIMChannel *self, + const gchar *receipt_id); void _gabble_im_channel_report_delivery ( GabbleIMChannel *self, diff --git a/src/im-factory.c b/src/im-factory.c index 661f6fbc1..4170d1012 100644 --- a/src/im-factory.c +++ b/src/im-factory.c @@ -60,6 +60,7 @@ struct _GabbleImFactoryPrivate { GabbleConnection *conn; guint message_cb_id; + guint delivery_report_cb_id; GHashTable *channels; gulong status_changed_id; @@ -256,7 +257,8 @@ im_factory_message_cb ( } else if (body != NULL) { - _gabble_im_channel_receive (chan, msgtype, from, stamp, id, body, state); + _gabble_im_channel_receive (chan, message, msgtype, from, stamp, id, + body, state); } else if (state != -1) { @@ -266,6 +268,42 @@ im_factory_message_cb ( return TRUE; } +/* Signals incoming delivery receipts. http://xmpp.org/extensions/xep-0184.html + */ +static gboolean +im_factory_receipt_cb ( + WockyPorter *porter, + WockyStanza *message, + gpointer user_data) +{ + GabbleImFactory *self = GABBLE_IM_FACTORY (user_data); + WockyNode *received; + const gchar *from, *received_id; + GabbleIMChannel *channel; + + received = wocky_node_get_child_ns (wocky_stanza_get_top_node (message), + "received", NS_RECEIPTS); + g_return_val_if_fail (received != NULL, FALSE); + + received_id = wocky_node_get_attribute (received, "id"); + if (received_id == NULL) + { + STANZA_DEBUG (message, "but *what* did you receive?!"); + return TRUE; + } + + from = wocky_stanza_get_from (message); + channel = get_channel_for_incoming_message (self, from, FALSE); + if (channel == NULL) + { + DEBUG ("no existing channel with '%s'; ignoring receipt", from); + return TRUE; + } + + gabble_im_channel_receive_receipt (channel, received_id); + return TRUE; +} + /** * im_channel_closed_cb: * @@ -433,6 +471,10 @@ gabble_im_factory_close_all (GabbleImFactory *self) wocky_porter_unregister_handler (porter, self->priv->message_cb_id); self->priv->message_cb_id = 0; + + wocky_porter_unregister_handler (porter, self->priv->delivery_report_cb_id); + self->priv->delivery_report_cb_id = 0; + g_object_unref (porter); } } @@ -519,6 +561,12 @@ porter_available_cb ( WOCKY_STANZA_TYPE_MESSAGE, WOCKY_STANZA_SUB_TYPE_NONE, WOCKY_PORTER_HANDLER_PRIORITY_MIN, im_factory_message_cb, self, NULL); + self->priv->delivery_report_cb_id = wocky_porter_register_handler_from_anyone (porter, + WOCKY_STANZA_TYPE_MESSAGE, WOCKY_STANZA_SUB_TYPE_NONE, + WOCKY_PORTER_HANDLER_PRIORITY_MIN, im_factory_receipt_cb, self, + '(', + "received", ':', NS_RECEIPTS, + ')', NULL); g_object_get (conn, "stream-server", &stream_server, NULL); diff --git a/src/namespaces.h b/src/namespaces.h index 0d06d7d54..2a2d8f91b 100644 --- a/src/namespaces.h +++ b/src/namespaces.h @@ -103,6 +103,7 @@ #define NS_PRESENCE_INVISIBLE "presence-invisible" #define NS_PRIVACY "jabber:iq:privacy" #define NS_INVISIBLE "urn:xmpp:invisible:0" +#define NS_RECEIPTS "urn:xmpp:receipts" #define NS_REGISTER "jabber:iq:register" #define NS_ROSTER "jabber:iq:roster" #define NS_SEARCH "jabber:iq:search" diff --git a/src/roster.c b/src/roster.c index 64360cea0..d2cc4dae8 100644 --- a/src/roster.c +++ b/src/roster.c @@ -3601,6 +3601,9 @@ gabble_roster_handle_gets_presence_from_us (GabbleRoster *self, if (item == NULL) return FALSE; + if (item->blocked) + return FALSE; + return (item->subscription == GABBLE_ROSTER_SUBSCRIPTION_FROM || item->subscription == GABBLE_ROSTER_SUBSCRIPTION_BOTH); } diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 8e88892d5..7797d5173 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -113,6 +113,7 @@ TWISTED_TESTS = \ text/facebook-own-message.py \ text/initiate.py \ text/initiate-requestotron.py \ + text/receipts.py \ text/respawn.py \ text/send-error.py \ text/send-to-correct-resource.py \ @@ -268,6 +269,7 @@ TWISTED_OTHER_FILES = \ mucutil.py \ ns.py \ olpc/util.py \ + presence/__init__.py \ presence/invisible_helper.py \ rostertest.py \ sasl/saslutil.py \ diff --git a/tests/twisted/ns.py b/tests/twisted/ns.py index 48b656b30..30f9a8114 100644 --- a/tests/twisted/ns.py +++ b/tests/twisted/ns.py @@ -57,6 +57,7 @@ OLPC_CURRENT_ACTIVITY = "http://laptop.org/xmpp/current-activity" OLPC_CURRENT_ACTIVITY_NOTIFY = "%s+notify" % OLPC_CURRENT_ACTIVITY PUBSUB = "http://jabber.org/protocol/pubsub" PUBSUB_EVENT = "%s#event" % PUBSUB +RECEIPTS = "urn:xmpp:receipts" REGISTER = "jabber:iq:register" ROSTER = "jabber:iq:roster" SEARCH = 'jabber:iq:search' diff --git a/tests/twisted/presence/__init__.py b/tests/twisted/presence/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/tests/twisted/presence/__init__.py diff --git a/tests/twisted/text/receipts.py b/tests/twisted/text/receipts.py new file mode 100644 index 000000000..45c7d9f89 --- /dev/null +++ b/tests/twisted/text/receipts.py @@ -0,0 +1,197 @@ +# coding=utf-8 +""" +Test XEP-0184 receipts. +""" + +from servicetest import ( + EventPattern, assertEquals, assertLength, sync_dbus, wrap_channel, +) +from gabbletest import exec_test, elem, sync_stream, acknowledge_iq +import constants as cs +import ns + +import caps_helper +import rostertest +from presence.invisible_helper import Xep0186Stream + +GUYBRUSH = 'guybrush@mi.lit' +GUYBRUSH_FULL_JID = GUYBRUSH + '/Sea Cucumber' + +def send_received_report(stream, jid, received_id): + stream.send( + elem('message', from_=jid)( + elem(ns.RECEIPTS, 'received', id=received_id) + )) + +def report_received_on_open_channel(q, bus, conn, stream, chan): + received_id = 'fine-leather-jackets' + + send_received_report(stream, GUYBRUSH, received_id) + e = q.expect('dbus-signal', signal='MessageReceived', path=chan.object_path) + message, = e.args + header, = message + + assertEquals(cs.MT_DELIVERY_REPORT, header['message-type']) + assertEquals(cs.DELIVERY_STATUS_DELIVERED, header['delivery-status']) + assertEquals(received_id, header['delivery-token']) + +def report_ignored_without_channel(q, bus, conn, stream): + q.forbid_events([EventPattern('dbus-signal', signal='MessageReceived')]) + send_received_report(stream, 'marley@mi.gov', 'only-one-candidate') + sync_dbus(bus, q, conn) + q.unforbid_all() + +def not_sending_request_to_contact(q, bus, conn, stream, chan): + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Mancomb Seepgood?', + }] + chan.Messages.SendMessage(message, 0) + + e = q.expect('stream-message', to=GUYBRUSH) + assertLength(0, list(e.stanza.elements(uri=ns.RECEIPTS, name='request'))) + +def sending_request_to_presenceless_contact(q, bus, conn, stream, chan): + """ + Initially we know nothing of Guybrush's presence, so should just try our + level best if asked to. + """ + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Thriftweed?', + }] + chan.Messages.SendMessage(message, cs.MSG_SENDING_FLAGS_REPORT_DELIVERY) + + e = q.expect('stream-message', to=GUYBRUSH) + assertLength(1, list(e.stanza.elements(uri=ns.RECEIPTS, name='request'))) + +def sending_request_to_cappy_contact(q, bus, conn, stream, chan): + """ + Test that Gabble requests a receipt from a contact whom we know supports + this extension, but only if asked. + """ + + # Convince Gabble that Guybrush supports this extension + caps = { 'node': 'http://whatever', + 'ver': caps_helper.compute_caps_hash([], [ns.RECEIPTS], {}), + 'hash': 'sha-1', + } + caps_helper.presence_and_disco(q, conn, stream, GUYBRUSH_FULL_JID, + disco=True, + client=caps['node'], + caps=caps, + features=[ns.RECEIPTS]) + sync_stream(q, stream) + + # Don't ask, don't tell — even if we know Guybrush does support this. + not_sending_request_to_contact(q, bus, conn, stream, chan) + + # Ask, tell. + message = [ + { 'message-type': cs.MT_NORMAL, + }, + { 'content-type': 'text/plain', + 'content': 'Ulysses?', + }] + chan.Messages.SendMessage(message, cs.MSG_SENDING_FLAGS_REPORT_DELIVERY) + + e = q.expect('stream-message', to=GUYBRUSH) + assertLength(1, list(e.stanza.elements(uri=ns.RECEIPTS, name='request'))) + +def replying_to_requests(q, bus, conn, stream): + jid = 'lechuck@lucasarts.lit' + + # We shouldn't send receipts to people who aren't on our roster. + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat', id='alpha')( + elem('body')( + u"You didn't kill me, you moron!" + ), + elem(ns.RECEIPTS, 'request') + )) + + q.expect('dbus-signal', signal='MessageReceived') + sync_stream(q, stream) + q.unforbid_all() + + # We should send receipts to people on our roster, seeing as we're not + # invisible. + rostertest.send_roster_push(stream, jid, subscription='from') + + stream.send( + elem('message', from_=jid, type='chat', id='beta')( + elem('body')( + u"You've just destroyed my spiritual essences." + ), + elem(ns.RECEIPTS, 'request') + )) + q.expect('dbus-signal', signal='MessageReceived') + e = q.expect('stream-message', to=jid) + receipt = e.stanza.elements(uri=ns.RECEIPTS, name='received').next() + assertEquals('beta', receipt['id']) + + # We would like requests in messages without id=''s not to crash Gabble, + # and also for it not to send a reply. + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat')( # NB. no id='' attribute + elem('body')( + u"A favor that I shall now return!" + ), + elem(ns.RECEIPTS, 'request') + )) + q.expect('dbus-signal', signal='MessageReceived') + sync_stream(q, stream) + q.unforbid_all() + + # If we're invisible, LeChuck shouldn't get receipts. + conn.SimplePresence.SetPresence("hidden", "") + event = q.expect('stream-iq', query_name='invisible') + acknowledge_iq(stream, event.stanza) + + q.forbid_events([EventPattern('stream-message', to=jid)]) + stream.send( + elem('message', from_=jid, type='chat', id='epsilon')( + elem('body')( + u"… but where am I going to find a duck wearing burlap chaps?" + ), + elem(ns.RECEIPTS, 'request') + )) + q.expect('dbus-signal', signal='MessageReceived') + sync_stream(q, stream) + q.unforbid_all() + +def test(q, bus, conn, stream): + path = conn.Requests.CreateChannel( + { cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_ID: GUYBRUSH, + })[0] + chan = wrap_channel(bus.get_object(conn.bus_name, path), 'Text', + ['Messages']) + + # Let's start out with an empty roster, eh? + e = q.expect('stream-iq', iq_type='get', query_ns=ns.ROSTER) + e.stanza['type'] = 'result' + stream.send(e.stanza) + + report_received_on_open_channel(q, bus, conn, stream, chan) + report_ignored_without_channel(q, bus, conn, stream) + not_sending_request_to_contact(q, bus, conn, stream, chan) + + # FIXME: This test is disabled because of stupidity in the presence cache. + # See the comment in receipts_conceivably_supported(). + #sending_request_to_presenceless_contact(q, bus, conn, stream, chan) + + sending_request_to_cappy_contact(q, bus, conn, stream, chan) + + replying_to_requests(q, bus, conn, stream) + + +if __name__ == '__main__': + exec_test(test, protocol=Xep0186Stream) |