diff options
author | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-09-30 10:40:28 +0100 |
---|---|---|
committer | Jonny Lamb <jonny.lamb@collabora.co.uk> | 2011-09-30 10:40:28 +0100 |
commit | 862aee50a0b80054ff2b7d3d4030f37d3646fff3 (patch) | |
tree | 03b9f84888d52b796bcc7ffb6f45fe0306576590 | |
parent | 2163dc3ca3569401b865fc97930cda4e8d378cc1 (diff) | |
parent | 512576d656e37c88cd4f9fc9927ed266a8816202 (diff) |
Merge branch 'gabble'
37 files changed, 6031 insertions, 52 deletions
@@ -32,12 +32,15 @@ stamp-h1 /tests/twisted/tools/exec-with-log.sh /tests/twisted/tools/salut-exec-with-log.sh +/tests/twisted/tools/gabble-exec-with-log.sh /tests/twisted/tools/org.freedesktop.Telepathy.Client.Logger.service /tests/twisted/tools/org.freedesktop.Telepathy.MissionControl5.service /tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.salut.service +/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service /tests/twisted/tools/tmp-session-bus.conf /tests/twisted/tools/missioncontrol*.log /tests/twisted/tools/salut-testing.log +/tests/twisted/tools/gabble-testing.log /tests/twisted/tmp-*/ /tests/twisted/config.py /tests/twisted/with-session-bus* diff --git a/Makefile.am b/Makefile.am index a2f5ee3..9a537d3 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,7 @@ SUBDIRS = \ mission-control \ + plugin-base \ salut \ + gabble \ tests diff --git a/configure.ac b/configure.ac index f2b74d8..aef6cbb 100644 --- a/configure.ac +++ b/configure.ac @@ -88,6 +88,24 @@ AC_SUBST(TEST_PYTHON) AM_CONDITIONAL([WANT_TWISTED_TESTS], test false != "$TEST_PYTHON") # ------------------------------------------------------------------------------ +# GABBLE PLUGIN + +PKG_CHECK_MODULES(GABBLE, telepathy-gabble, HAVE_GABBLE=yes, HAVE_GABBLE=no) +AC_SUBST(GABBLE_LIBS) +AC_SUBST(GABBLE_CFLAGS) +gabbleplugindir=`pkg-config --variable=plugindir telepathy-gabble` +AC_SUBST(gabbleplugindir) + +AC_MSG_CHECKING([telepathy-gabble executable]) +GABBLE_EXECUTABLE=`pkg-config --variable=gabblepath telepathy-gabble` +if test "$GABBLE_EXECUTABLE" = ""; then + AC_MSG_ERROR([could not determine location of telepathy-gabble executable]) +else + AC_MSG_RESULT([$GABBLE_EXECUTABLE]) +fi +AC_SUBST(GABBLE_EXECUTABLE) + +# ------------------------------------------------------------------------------ AC_ARG_ENABLE(debug, AC_HELP_STRING([--enable-debug], @@ -111,14 +129,18 @@ AC_SUBST(CFLAGS) AC_OUTPUT([ Makefile mission-control/Makefile + plugin-base/Makefile salut/Makefile + gabble/Makefile tests/Makefile tests/twisted/Makefile tests/twisted/tools/Makefile tests/twisted/tools/exec-with-log.sh tests/twisted/tools/salut-exec-with-log.sh + tests/twisted/tools/gabble-exec-with-log.sh tests/twisted/tools/tmp-session-bus.conf tests/twisted/tools/org.freedesktop.Telepathy.MissionControl5.service tests/twisted/tools/org.freedesktop.Telepathy.Client.Logger.service tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.salut.service + tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service ]) diff --git a/gabble/Makefile.am b/gabble/Makefile.am new file mode 100644 index 0000000..e0ad1bf --- /dev/null +++ b/gabble/Makefile.am @@ -0,0 +1,31 @@ +AM_CFLAGS = \ + -DG_LOG_DOMAIN=\"ytstenut\" \ + -DGABBLE \ + -I$(top_srcdir)/plugin-base \ + $(GABBLE_CFLAGS) \ + $(TELEPATHY_YTSTENUT_CFLAGS) + +plugindir = $(gabbleplugindir) + +plugin_LTLIBRARIES = ytstenut-gabble.la + +AM_LDFLAGS = -module -avoid-version -shared + +ytstenut_gabble_la_LIBADD = \ + $(GABBLE_LIBS) \ + $(TELEPATHY_YTSTENUT_LIBS) + +ytstenut_gabble_la_SOURCES = \ + $(top_srcdir)/plugin-base/ytstenut.c \ + $(top_srcdir)/plugin-base/ytstenut.h \ + $(top_srcdir)/plugin-base/caps-manager.c \ + $(top_srcdir)/plugin-base/caps-manager.h \ + status.c \ + status.h \ + message-channel.c \ + message-channel.h \ + $(top_srcdir)/plugin-base/channel-manager.c \ + $(top_srcdir)/plugin-base/channel-manager.h \ + $(top_srcdir)/plugin-base/utils.c \ + $(top_srcdir)/plugin-base/utils.h + diff --git a/gabble/message-channel.c b/gabble/message-channel.c new file mode 100644 index 0000000..f72eddc --- /dev/null +++ b/gabble/message-channel.c @@ -0,0 +1,895 @@ +/* + * message-channel.c - Source for YtstMessageChannel + * Copyright (C) 2005-2008, 2010, 2011 Collabora Ltd. + * Copyright (C) 2011 Intel, Corp. + * @author: Sjoerd Simons <sjoerd@luon.net> + * @author: Stef Walter <stefw@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "message-channel.h" + +#include <errno.h> +#include <netdb.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> + +#include <dbus/dbus-glib.h> +#include <telepathy-glib/channel.h> +#include <telepathy-glib/channel-iface.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/exportable-channel.h> +#include <telepathy-glib/gtypes.h> +#include <telepathy-glib/interfaces.h> +#include <telepathy-glib/svc-generic.h> +#include <telepathy-glib/svc-channel.h> +#include <telepathy-glib/util.h> + +#include <telepathy-ytstenut-glib/telepathy-ytstenut-glib.h> + +#include <wocky/wocky-namespaces.h> +#include <wocky/wocky-utils.h> +#include <wocky/wocky-xmpp-reader.h> +#include <wocky/wocky-xmpp-writer.h> +#include <wocky/wocky-xmpp-error-enumtypes.h> +#include <wocky/wocky-session.h> + +#include <gabble/connection.h> + +#define DEBUG(msg, ...) \ + g_debug ("%s: " msg, G_STRFUNC, ##__VA_ARGS__) + +#include "utils.h" + +#define EL_YTSTENUT_MESSAGE "message" + +static void channel_ytstenut_iface_init (gpointer g_iface, + gpointer iface_data); + +G_DEFINE_TYPE_WITH_CODE (YtstMessageChannel, ytst_message_channel, + TP_TYPE_BASE_CHANNEL, + G_IMPLEMENT_INTERFACE (TP_TYPE_YTS_SVC_CHANNEL, + channel_ytstenut_iface_init); +); + +static const gchar *ytst_message_channel_interfaces[] = { + TP_IFACE_CHANNEL, + TP_YTS_IFACE_CHANNEL, + NULL +}; + +/* properties */ +enum +{ + PROP_CONTACT = 1, + PROP_TARGET_SERVICE, + PROP_INITIATOR_SERVICE, + PROP_REQUEST, + PROP_REQUEST_TYPE, + PROP_REQUEST_ATTRIBUTES, + PROP_REQUEST_BODY, + LAST_PROPERTY +}; + +/* private structure */ +struct _YtstMessageChannelPrivate +{ + gboolean dispose_has_run; + gchar *contact; + + GCancellable *cancellable; + + /* TRUE if Request() has been called. */ + gboolean requested; + + /* TRUE when either the other side has replied (and Replied/Failed + * has been fired), or one of Fail()/Reply() has been called + * locally. */ + gboolean replied; + + WockyStanza *request; +}; + +/* ----------------------------------------------------------------------------- + * INTERNAL + */ + +static guint32 +channel_get_message_type (WockyStanza *message) +{ + WockyStanzaSubType sub_type; + + wocky_stanza_get_type_info (message, NULL, &sub_type); + switch (sub_type) + { + case WOCKY_STANZA_SUB_TYPE_GET: + return TP_YTS_REQUEST_TYPE_GET; + case WOCKY_STANZA_SUB_TYPE_SET: + return TP_YTS_REQUEST_TYPE_SET; + case WOCKY_STANZA_SUB_TYPE_RESULT: + return TP_YTS_REPLY_TYPE_RESULT; + case WOCKY_STANZA_SUB_TYPE_ERROR: + return TP_YTS_REPLY_TYPE_ERROR; + default: + return 0; + } +} + +static gchar * +channel_get_message_body (WockyStanza *message) +{ + WockyXmppWriter *writer; + WockyNode *top, *body; + WockyNodeTree *tree; + const guint8 *output; + gsize length; + gchar *result; + + top = wocky_stanza_get_top_node (message); + body = wocky_node_get_first_child (top); + + writer = wocky_xmpp_writer_new_no_stream (); + tree = wocky_node_tree_new_from_node (body); + wocky_xmpp_writer_write_node_tree (writer, tree, &output, &length); + result = g_strndup ((const gchar*) output, length); + g_object_unref (writer); + g_object_unref (tree); + + return result; +} + +static GHashTable * +channel_new_message_attributes (void) +{ + return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); +} + +static gboolean +attribute_to_hashtable (const gchar *key, + const gchar *value, + const gchar *pref, + const gchar *ns, + gpointer user_data) +{ + /* We only expose non namespace attributes in these properties */ + if (ns == NULL && wocky_strdiff (key, "from-service") + && wocky_strdiff (key, "to-service")) + g_hash_table_insert (user_data, g_strdup (key), g_strdup (value)); + return TRUE; +} + +static GHashTable * +channel_get_message_attributes (WockyStanza *message) +{ + WockyNode *top, *body; + GHashTable *attributes; + + top = wocky_stanza_get_top_node (message); + body = wocky_node_get_first_child (top); + + attributes = channel_new_message_attributes (); + wocky_node_each_attribute (body, attribute_to_hashtable, attributes); + return attributes; +} + +static const gchar * +channel_get_message_attribute (WockyStanza *message, + const gchar *key) +{ + WockyNode *top, *body; + + top = wocky_stanza_get_top_node (message); + body = wocky_node_get_first_child (top); + + return wocky_node_get_attribute (body, key); +} + +static void +channel_message_stanza_callback (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + WockyPorter *porter = WOCKY_PORTER (source_object); + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (user_data); + YtstMessageChannelPrivate *priv = self->priv; + WockyStanza *stanza; + WockyXmppErrorType error_type; + GError *core_error = NULL; + WockyNode *specialized_node = NULL; + GHashTable *attributes; + gchar *body; + GError *error = NULL; + + stanza = wocky_porter_send_iq_finish (porter, result, &error); + if (stanza == NULL) + { + DEBUG ("Failed to send IQ: %s", error->message); + g_clear_error (&error); + goto out; + } + + priv->replied = TRUE; + + if (wocky_stanza_extract_errors (stanza, &error_type, &core_error, + NULL, &specialized_node)) + { + g_assert (core_error != NULL); + tp_yts_svc_channel_emit_failed (self, + ytst_message_error_type_from_wocky (error_type), + wocky_enum_to_nick (WOCKY_TYPE_XMPP_ERROR, core_error->code), + specialized_node ? specialized_node->name : "", + core_error->message ? core_error->message : ""); + g_clear_error (&core_error); + } + else + { + attributes = channel_get_message_attributes (stanza); + body = channel_get_message_body (stanza); + tp_yts_svc_channel_emit_replied (self, attributes, body); + g_hash_table_destroy (attributes); + g_free (body); + } + +out: + g_object_unref (self); +} + +static void +set_attributes_on_body (WockyNodeTree *body, + GHashTable *attributes) +{ + GHashTableIter iter; + WockyNode *node; + const gchar *name, *value; + + node = wocky_node_tree_get_top_node (body); + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, (gpointer *) &name, + (gpointer *) &value)) + wocky_node_set_attribute (node, name, value); +} + +static WockyNodeTree * +parse_message_body (const gchar *body, + GError **error) +{ + WockyXmppReader *reader; + WockyNodeTree *tree; + WockyNode *node; + GError *err = NULL; + + if (body == NULL || *body == '\0') + body = "<ytstenut:message xmlns:ytstenut=\"" YTST_MESSAGE_NS "\" />"; + + reader = wocky_xmpp_reader_new_no_stream (); + wocky_xmpp_reader_push (reader, (guint8 *) body, strlen (body)); + tree = WOCKY_NODE_TREE (wocky_xmpp_reader_pop_stanza (reader)); + g_object_unref (reader); + + if (tree == NULL) + { + err = wocky_xmpp_reader_get_error (reader); + g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, + "Invalid XML%s%s", + err && err->message ? ": " : ".", + err && err->message ? err->message : ""); + g_clear_error (&err); + return NULL; + } + + /* Make sure it smells right */ + node = wocky_node_tree_get_top_node (tree); + if (!wocky_node_has_ns (node, YTST_MESSAGE_NS)) + { + g_set_error_literal (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, + "Must be a of the ytstenut namespace"); + node = NULL; + } + if (node != NULL && wocky_strdiff (node->name, EL_YTSTENUT_MESSAGE)) + { + g_set_error_literal (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, + "Must be a <ytstenut:message> element"); + node = NULL; + } + + if (node == NULL) + { + g_object_unref (tree); + tree = NULL; + } + + return tree; +} + +/* ----------------------------------------------------------------------------- + * OBJECT + */ + +static void +ytst_message_channel_close (TpBaseChannel *chan) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (chan); + YtstMessageChannelPrivate *priv = self->priv; + + DEBUG ("called\n"); + + /* Need to send an item-not-found reply */ + if (!tp_base_channel_is_requested (chan) && !priv->replied) + { + TpBaseConnection *conn = tp_base_channel_get_connection ( + TP_BASE_CHANNEL (self)); + WockySession *session = gabble_connection_get_session ( + GABBLE_CONNECTION (conn)); + + wocky_porter_send_iq_error ( + wocky_session_get_porter (session), + priv->request, WOCKY_XMPP_ERROR_ITEM_NOT_FOUND, + "channel closed before reply was sent; possibly " + "no handler found?"); + } + + if (!g_cancellable_is_cancelled (priv->cancellable)) + g_cancellable_cancel (priv->cancellable); + + tp_base_channel_destroyed (chan); +} + +static gchar * +ytst_message_channel_get_path (TpBaseChannel *chan) +{ + return g_strdup_printf ("YtstenutChannel/%p", chan); +} + +static void +ytst_message_channel_fill_immutable_properties ( + TpBaseChannel *chan, + GHashTable *properties) +{ + TpBaseChannelClass *klass = TP_BASE_CHANNEL_CLASS ( + ytst_message_channel_parent_class); + + klass->fill_immutable_properties (chan, properties); + + tp_dbus_properties_mixin_fill_properties_hash ( + G_OBJECT (chan), properties, + TP_YTS_IFACE_CHANNEL, "RequestType", + TP_YTS_IFACE_CHANNEL, "RequestAttributes", + TP_YTS_IFACE_CHANNEL, "RequestBody", + TP_YTS_IFACE_CHANNEL, "TargetService", + TP_YTS_IFACE_CHANNEL, "InitiatorService", + NULL); +} + +static void +ytst_message_channel_init (YtstMessageChannel *self) +{ + YtstMessageChannelPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + YTST_TYPE_MESSAGE_CHANNEL, YtstMessageChannelPrivate); + self->priv = priv; + priv->cancellable = g_cancellable_new (); +} + +static void +ytst_message_channel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (object); + YtstMessageChannelPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_CONTACT: + g_value_set_string (value, priv->contact); + break; + case PROP_TARGET_SERVICE: + g_value_set_string (value, channel_get_message_attribute (priv->request, + "to-service")); + break; + case PROP_INITIATOR_SERVICE: + g_value_set_string (value, channel_get_message_attribute (priv->request, + "from-service")); + break; + case PROP_REQUEST: + g_value_set_object (value, priv->request); + break; + case PROP_REQUEST_TYPE: + g_value_set_uint (value, channel_get_message_type (priv->request)); + break; + case PROP_REQUEST_ATTRIBUTES: + g_value_take_boxed (value, + channel_get_message_attributes (priv->request)); + break; + case PROP_REQUEST_BODY: + g_value_take_string (value, channel_get_message_body (priv->request)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +ytst_message_channel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (object); + YtstMessageChannelPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_CONTACT: + priv->contact = g_value_dup_string (value); + break; + case PROP_REQUEST: + g_assert (priv->request == NULL); + priv->request = g_value_dup_object (value); + g_assert (priv->request != NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +ytst_message_channel_dispose (GObject *object) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (object); + YtstMessageChannelPrivate *priv = self->priv; + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + if (priv->cancellable != NULL) + { + if (!g_cancellable_is_cancelled (priv->cancellable)) + g_cancellable_cancel (priv->cancellable); + g_object_unref (priv->cancellable); + priv->cancellable = NULL; + } + + tp_clear_pointer (&priv->contact, g_free); + + if (priv->request != NULL) + { + g_object_unref (priv->request); + priv->request = NULL; + } + + if (G_OBJECT_CLASS (ytst_message_channel_parent_class)->dispose) + G_OBJECT_CLASS (ytst_message_channel_parent_class)->dispose (object); +} + +static void +ytst_message_channel_class_init (YtstMessageChannelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + TpBaseChannelClass *base_class = TP_BASE_CHANNEL_CLASS (klass); + + GParamSpec *param_spec; + + static TpDBusPropertiesMixinPropImpl ytstenut_props[] = { + { "TargetService", "target-service", NULL }, + { "InitiatorService", "initiator-service", NULL }, + { "RequestType", "request-type", NULL }, + { "RequestBody", "request-body", NULL }, + { "RequestAttributes", "request-attributes", NULL }, + { NULL } + }; + + g_type_class_add_private (klass, sizeof (YtstMessageChannelPrivate)); + + object_class->dispose = ytst_message_channel_dispose; + object_class->get_property = ytst_message_channel_get_property; + object_class->set_property = ytst_message_channel_set_property; + + base_class->channel_type = TP_YTS_IFACE_CHANNEL; + base_class->interfaces = ytst_message_channel_interfaces; + base_class->target_handle_type = TP_HANDLE_TYPE_CONTACT; + base_class->close = ytst_message_channel_close; + base_class->get_object_path_suffix = ytst_message_channel_get_path; + base_class->fill_immutable_properties = + ytst_message_channel_fill_immutable_properties; + + param_spec = g_param_spec_string ( + "contact", + "Contact", + "Contact to which this channel is dedicated", + "", + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONTACT, param_spec); + + param_spec = g_param_spec_object ("request", "Request Stanza", + "The stanza of the request iq", WOCKY_TYPE_STANZA, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUEST, + param_spec); + + param_spec = g_param_spec_string ("target-service", "Target Service", + "Target Ytstenut Service Name", "", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_TARGET_SERVICE, + param_spec); + + param_spec = g_param_spec_string ("initiator-service", "Initiator Service", + "Initiator Ytstenut Service Name", "", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_INITIATOR_SERVICE, + param_spec); + + param_spec = g_param_spec_uint ("request-type", "Request Type", + "The type of the ytstenut request message", TP_YTS_REQUEST_TYPE_GET, + NUM_TP_YTS_REQUEST_TYPES, TP_YTS_REQUEST_TYPE_GET, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUEST_TYPE, param_spec); + + param_spec = g_param_spec_string ("request-body", "Request Body", + "The UTF-8 encoded XML body of request message", "", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUEST_BODY, param_spec); + + param_spec = g_param_spec_boxed ("request-attributes", "Request Attributes", + "The attributes of the ytstenut request message", + TP_HASH_TYPE_STRING_STRING_MAP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_REQUEST_ATTRIBUTES, + param_spec); + + tp_dbus_properties_mixin_implement_interface (object_class, + TP_YTS_IFACE_QUARK_CHANNEL, + tp_dbus_properties_mixin_getter_gobject_properties, NULL, + ytstenut_props); + + wocky_xmpp_error_register_domain (ytst_message_error_get_domain ()); +} + +static void +ytst_message_channel_request (TpYtsSvcChannel *channel, + DBusGMethodInvocation *context) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (channel); + YtstMessageChannelPrivate *priv = self->priv; + WockySession *session; + GError *error = NULL; + + /* Can't call this method from this side */ + if (!tp_base_channel_is_requested (TP_BASE_CHANNEL (channel))) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Request() may not be called on the reply side of a channel"); + goto done; + } + + if (priv->requested) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Request() has already been called"); + dbus_g_method_return_error (context, error); + g_clear_error (&error); + return; + } + + session = gabble_connection_get_session (GABBLE_CONNECTION ( + tp_base_channel_get_connection (TP_BASE_CHANNEL (self)))); + + wocky_porter_send_iq_async (wocky_session_get_porter (session), + priv->request, priv->cancellable, + channel_message_stanza_callback, g_object_ref (self)); + priv->requested = TRUE; + +done: + if (error != NULL) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + } + else + { + tp_yts_svc_channel_return_from_request (context); + } +} + +static void +ytst_message_channel_reply (TpYtsSvcChannel *channel, + GHashTable *attributes, + const gchar *body, + DBusGMethodInvocation *context) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (channel); + YtstMessageChannelPrivate *priv = self->priv; + GabbleConnection *conn = GABBLE_CONNECTION (tp_base_channel_get_connection ( + TP_BASE_CHANNEL (self))); + WockySession *session = gabble_connection_get_session (conn); + WockyNodeTree *body_tree = NULL; + WockyNode *msg_node; + WockyStanza *reply; + GError *error = NULL; + + /* Can't call this method from this side */ + if (tp_base_channel_is_requested (TP_BASE_CHANNEL (channel))) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Reply() may not be called on the request side of a channel"); + goto done; + } + + /* Can't call this after a successful call */ + if (priv->replied) + { + g_set_error_literal (&error, TP_ERROR, TP_ERROR_NOT_AVAILABLE, + "Fail() or Reply() has already been successfully called"); + goto done; + } + + body_tree = parse_message_body (body, &error); + if (body_tree == NULL) + goto done; + + /* All attributes override anything in the body */ + set_attributes_on_body (body_tree, attributes); + + /* Add the from and to service properties as well */ + msg_node = wocky_node_tree_get_top_node (body_tree); + wocky_node_set_attribute (msg_node, "to-service", + channel_get_message_attribute (priv->request, "from-service")); + wocky_node_set_attribute (msg_node, "from-service", + channel_get_message_attribute (priv->request, "to-service")); + + reply = wocky_stanza_build_iq_result (priv->request, NULL); + + /* Now append the message node */ + wocky_node_add_node_tree (wocky_stanza_get_top_node (reply), body_tree); + g_object_unref (body_tree); + + wocky_porter_send (wocky_session_get_porter (session), reply); + g_object_unref (reply); + +done: + if (error != NULL) + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + } + else + { + priv->replied = TRUE; + tp_yts_svc_channel_return_from_reply (context); + } +} + +static void +ytst_message_channel_fail (TpYtsSvcChannel *channel, + guint error_type, + const gchar *stanza_error_name, + const gchar *ytstenut_error_name, + const gchar *text, + DBusGMethodInvocation *context) +{ + YtstMessageChannel *self = YTST_MESSAGE_CHANNEL (channel); + YtstMessageChannelPrivate *priv = self->priv; + const gchar *type; + GError *error = NULL; + GabbleConnection *conn = GABBLE_CONNECTION (tp_base_channel_get_connection ( + TP_BASE_CHANNEL (self))); + WockySession *session = gabble_connection_get_session (conn); + WockyStanza *reply; + + /* Can't call this method from this side */ + if (tp_base_channel_is_requested (TP_BASE_CHANNEL (channel))) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Fail() may not be called on the request side of a channel"); + goto done; + } + + /* Can't call this after a successful call */ + if (priv->replied) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Fail() or Reply() has already been called"); + goto done; + } + + /* Must be one of the valid error types */ + type = wocky_enum_to_nick (WOCKY_TYPE_XMPP_ERROR_TYPE, + ytst_message_error_type_to_wocky (error_type)); + if (type == NULL) + { + g_set_error_literal (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "ErrorType is set to an invalid value."); + goto done; + } + + reply = wocky_stanza_build_iq_error (priv->request, + '(', "error", + '@', "type", type, + '(', stanza_error_name, ':', WOCKY_XMPP_NS_STANZAS, ')', + '(', ytstenut_error_name, ':', YTST_MESSAGE_NS, ')', + '(', "text", + ':', WOCKY_XMPP_NS_STANZAS, + '$', text, + ')', + ')', + NULL); + + wocky_porter_send (wocky_session_get_porter (session), reply); + g_object_unref (reply); + +done: + if (error != NULL) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } + else + { + priv->replied = TRUE; + tp_yts_svc_channel_return_from_fail (context); + } +} + +static void +channel_ytstenut_iface_init (gpointer g_iface, + gpointer iface_data) +{ + TpYtsSvcChannelClass *klass = (TpYtsSvcChannelClass *) g_iface; + +#define IMPLEMENT(x) tp_yts_svc_channel_implement_##x (\ + klass, ytst_message_channel_##x) + IMPLEMENT(request); + IMPLEMENT(reply); + IMPLEMENT(fail); +#undef IMPLEMENT +} + +/* ----------------------------------------------------------------------------- + * PUBLIC METHODS + */ + +YtstMessageChannel * +ytst_message_channel_new (GabbleConnection *connection, + const gchar *contact, + WockyStanza *request, + TpHandle handle, + TpHandle initiator, + gboolean requested) +{ + YtstMessageChannel *channel; + + g_return_val_if_fail (GABBLE_IS_CONNECTION (connection), NULL); + g_return_val_if_fail (!tp_str_empty (contact), NULL); + g_return_val_if_fail (WOCKY_IS_STANZA (request), NULL); + + channel = g_object_new (YTST_TYPE_MESSAGE_CHANNEL, + "connection", connection, + "contact", contact, + "request", request, + "handle", handle, + "requested", requested, + "initiator-handle", initiator, + NULL); + + tp_base_channel_register (TP_BASE_CHANNEL (channel)); + + return channel; +} + +WockyStanza * +ytst_message_channel_build_request (GHashTable *request_props, + const gchar *from, + const gchar *to, + GError **error) +{ + WockyStanzaSubType sub_type = WOCKY_STANZA_SUB_TYPE_NONE; + TpYtsRequestType request_type; + WockyStanza *request; + const gchar *body; + WockyNodeTree *tree; + GHashTable *attributes; + const gchar *initiator_service; + const gchar *target_service; + + request_type = tp_asv_get_uint32 (request_props, + TP_YTS_IFACE_CHANNEL ".RequestType", NULL); + switch (request_type) + { + case TP_YTS_REQUEST_TYPE_GET: + sub_type = WOCKY_STANZA_SUB_TYPE_GET; + break; + case TP_YTS_REQUEST_TYPE_SET: + sub_type = WOCKY_STANZA_SUB_TYPE_SET; + break; + default: + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The RequestType property is invalid."); + return NULL; + } + + target_service = tp_asv_get_string (request_props, + TP_YTS_IFACE_CHANNEL ".TargetService"); + if (target_service == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The TargetService property must be set."); + return NULL; + } + else if (!tp_dbus_check_valid_bus_name (target_service, + TP_DBUS_NAME_TYPE_WELL_KNOWN, NULL)) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The TargetService property has an invalid syntax."); + return NULL; + } + + initiator_service = tp_asv_get_string (request_props, + TP_YTS_IFACE_CHANNEL ".InitiatorService"); + if (initiator_service == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The InitiatorService property must be set."); + return NULL; + } + else if (!tp_dbus_check_valid_bus_name (initiator_service, + TP_DBUS_NAME_TYPE_WELL_KNOWN, NULL)) + { + g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, + "The InitiatorService property has an invalid syntax."); + return NULL; + } + + attributes = tp_asv_get_boxed (request_props, + TP_YTS_IFACE_CHANNEL ".RequestAttributes", + TP_HASH_TYPE_STRING_STRING_MAP); + if (!attributes && tp_asv_lookup (request_props, + TP_YTS_IFACE_CHANNEL ".RequestAttributes")) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The RequestAttributes property is invalid."); + return NULL; + } + + body = tp_asv_get_string (request_props, + TP_YTS_IFACE_CHANNEL ".RequestBody"); + tree = parse_message_body (body, error); + if (!tree) + { + g_prefix_error (error, "The RequestBody property is invalid: "); + return NULL; + } + + if (attributes != NULL) + set_attributes_on_body (tree, attributes); + + wocky_node_set_attribute (wocky_node_tree_get_top_node (tree), + "from-service", initiator_service); + wocky_node_set_attribute (wocky_node_tree_get_top_node (tree), + "to-service", target_service); + + request = wocky_stanza_build (WOCKY_STANZA_TYPE_IQ, sub_type, + from, to, NULL); + wocky_node_add_node_tree (wocky_stanza_get_top_node (request), tree); + g_object_unref (tree); + + return request; +} diff --git a/gabble/message-channel.h b/gabble/message-channel.h new file mode 100644 index 0000000..ee8340a --- /dev/null +++ b/gabble/message-channel.h @@ -0,0 +1,82 @@ +/* + * message-channel.h - Header for YtstMessageChannel + * Copyright (C) 2011 Intel, Corp. + * Copyright (C) 2005, 2011 Collabora Ltd. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef __YTST_MESSAGE_CHANNEL_H__ +#define __YTST_MESSAGE_CHANNEL_H__ + +#include <glib-object.h> + +#include <telepathy-glib/base-channel.h> + +#include <wocky/wocky-stanza.h> + +#include <gabble/connection.h> + +G_BEGIN_DECLS + +typedef struct _YtstMessageChannel YtstMessageChannel; +typedef struct _YtstMessageChannelClass YtstMessageChannelClass; +typedef struct _YtstMessageChannelPrivate YtstMessageChannelPrivate; + +struct _YtstMessageChannelClass { + TpBaseChannelClass parent_class; +}; + +struct _YtstMessageChannel { + TpBaseChannel parent; + YtstMessageChannelPrivate *priv; +}; + +GType ytst_message_channel_get_type (void); + +/* TYPE MACROS */ +#define YTST_TYPE_MESSAGE_CHANNEL \ + (ytst_message_channel_get_type ()) +#define YTST_MESSAGE_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), YTST_TYPE_MESSAGE_CHANNEL, YtstMessageChannel)) +#define YTST_MESSAGE_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), YTST_TYPE_MESSAGE_CHANNEL, \ + YtstMessageChannelClass)) +#define YTST_IS_MESSAGE_CHANNEL(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), YTST_TYPE_MESSAGE_CHANNEL)) +#define YTST_IS_MESSAGE_CHANNEL_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), YTST_TYPE_MESSAGE_CHANNEL)) +#define YTST_MESSAGE_CHANNEL_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), YTST_TYPE_MESSAGE_CHANNEL, \ + YtstMessageChannelClass)) + +YtstMessageChannel* ytst_message_channel_new (GabbleConnection *connection, + const gchar *contact, + WockyStanza *request, + TpHandle handle, + TpHandle initiator, + gboolean requested); + +gboolean ytst_message_channel_is_ytstenut_request_with_id ( + WockyStanza *stanza, gchar **id); + +WockyStanza * ytst_message_channel_build_request (GHashTable *request_props, + const gchar *from, + const gchar *to, + GError **error); + +G_END_DECLS + +#endif /* #ifndef __YTST_MESSAGE_CHANNEL_H__*/ diff --git a/gabble/status.c b/gabble/status.c new file mode 100644 index 0000000..0947ecf --- /dev/null +++ b/gabble/status.c @@ -0,0 +1,786 @@ +/* + * status.c - Header for YtstStatus + * + * Copyright (C) 2011 Intel Corp. + * @author: Stef Walter <stefw@collabora.co.uk> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#include "config.h" + +#include "status.h" + +#include <string.h> + +#include <gabble/plugin.h> + +#include <telepathy-glib/svc-generic.h> +#include <telepathy-glib/gtypes.h> + +#include <wocky/wocky-pubsub-helpers.h> +#include <wocky/wocky-xmpp-reader.h> +#include <wocky/wocky-xmpp-writer.h> +#include <wocky/wocky-xep-0115-capabilities.h> +#include <wocky/wocky-data-form.h> + +#include <telepathy-ytstenut-glib/telepathy-ytstenut-glib.h> + +#include "utils.h" + +#define DEBUG(msg, ...) \ + g_debug ("%s: " msg, G_STRFUNC, ##__VA_ARGS__) + +static void sidecar_iface_init (GabbleSidecarInterface *iface); + +static void ytst_status_iface_init (TpYtsSvcStatusClass *iface); + +G_DEFINE_TYPE_WITH_CODE (YtstStatus, ytst_status, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SIDECAR, sidecar_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_YTS_SVC_STATUS, ytst_status_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); +); + +/* properties */ +enum +{ + PROP_SESSION = 1, + PROP_CONNECTION, + PROP_DISCOVERED_STATUSES, + PROP_DISCOVERED_SERVICES, + LAST_PROPERTY +}; + +/* private structure */ +struct _YtstStatusPrivate +{ + WockySession *session; + GabbleConnection *connection; + + guint handler_id; + gulong capabilities_changed_id; + + /* GHashTable<gchar*, + * GHashTable<gchar*, + * GHashTable<gchar*,gchar*>>> + */ + GHashTable *discovered_statuses; + + /* GHashTable<gchar*, + * GHashTable<gchar*, + * GValueArray(gchar*,GHashTable<gchar*,gchar*>,GPtrArray<gchar*>)>> + */ + GHashTable *discovered_services; + + gboolean dispose_has_run; +}; + +/* ----------------------------------------------------------------------------- + * INTERNAL + */ + + +/* ----------------------------------------------------------------------------- + * OBJECT + */ + +static void +ytst_status_init (YtstStatus *self) +{ + YtstStatusPrivate *priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + YTST_TYPE_STATUS, YtstStatusPrivate); + self->priv = priv; +} + +static void +ytst_status_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + YtstStatus *self = YTST_STATUS (object); + YtstStatusPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_SESSION: + g_value_set_object (value, priv->session); + break; + case PROP_CONNECTION: + g_value_set_object (value, priv->connection); + break; + case PROP_DISCOVERED_STATUSES: + g_value_set_boxed (value, priv->discovered_statuses); + break; + case PROP_DISCOVERED_SERVICES: + g_value_set_boxed (value, priv->discovered_services); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +ytst_status_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + YtstStatus *self = YTST_STATUS (object); + YtstStatusPrivate *priv = self->priv; + + switch (property_id) + { + case PROP_SESSION: + priv->session = g_value_dup_object (value); + break; + case PROP_CONNECTION: + priv->connection = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +update_contact_status (YtstStatus *self, + const gchar *from, + const gchar *capability, + const gchar *service_name, + const gchar *status_str) +{ + YtstStatusPrivate *priv = self->priv; + const gchar *old_status; + gboolean emit = FALSE; + + GHashTable *capability_service_map; + GHashTable *service_status_map; + + capability_service_map = g_hash_table_lookup (priv->discovered_statuses, from); + + if (capability_service_map == NULL) + { + capability_service_map = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_hash_table_unref); + g_hash_table_insert (priv->discovered_statuses, g_strdup (from), + capability_service_map); + } + + service_status_map = g_hash_table_lookup (capability_service_map, + capability); + + if (service_status_map == NULL) + { + service_status_map = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + g_hash_table_insert (capability_service_map, + g_strdup (capability), service_status_map); + } + + old_status = g_hash_table_lookup (service_status_map, service_name); + + /* Save this value as old_status will be freed when we call + * g_hash_table_insert next and the spec says we need to update the + * property before emitting the signal. In reality this wouldn't be + * a problem, but let's be nice. */ + emit = tp_strdiff (old_status, status_str); + + if (status_str != NULL) + { + g_hash_table_insert (service_status_map, g_strdup (service_name), + g_strdup (status_str)); + } + else + { + /* remove the service from the service status map */ + g_hash_table_remove (service_status_map, service_name); + + /* now run along up the hash table cleaning up */ + if (g_hash_table_size (service_status_map) == 0) + g_hash_table_remove (capability_service_map, capability); + + if (g_hash_table_size (capability_service_map) == 0) + g_hash_table_remove (priv->discovered_statuses, from); + } + + if (emit) + tp_yts_svc_status_emit_status_changed (self, from, capability, + service_name, status_str); +} + +static gchar * +get_node_body (WockyNode *node) +{ + WockyXmppWriter *writer; + WockyNodeTree *tree; + const guint8 *output; + gsize length; + gchar *result; + + writer = wocky_xmpp_writer_new_no_stream (); + tree = wocky_node_tree_new_from_node (node); + wocky_xmpp_writer_write_node_tree (writer, tree, &output, &length); + result = g_strndup ((const gchar*) output, length); + g_object_unref (writer); + g_object_unref (tree); + + return result; +} + +static gboolean +pep_event_cb (WockyPorter *porter, + WockyStanza *stanza, + gpointer user_data) +{ + YtstStatus *self = user_data; + WockyNode *message, *event, *items, *item, *status; + gchar *status_str = NULL; + const gchar *from, *capability, *service_name; + + message = wocky_stanza_get_top_node (stanza); + + event = wocky_node_get_first_child (message); + + if (event == NULL || tp_strdiff (event->name, "event")) + return FALSE; + + items = wocky_node_get_first_child (event); + if (items == NULL || tp_strdiff (items->name, "items")) + return TRUE; + + item = wocky_node_get_first_child (items); + if (item == NULL || tp_strdiff (item->name, "item")) + return FALSE; + + status = wocky_node_get_first_child (item); + if (status == NULL || tp_strdiff (status->name, "status")) + return FALSE; + + /* looks good */ + + from = wocky_stanza_get_from (stanza); + capability = wocky_node_get_attribute (items, "node"); + service_name = wocky_node_get_attribute (status, "from-service"); + + if (wocky_node_get_attribute (status, "activity") != NULL) + status_str = get_node_body (status); + + update_contact_status (self, from, capability, service_name, status_str); + + g_free (status_str); + + return TRUE; +} + +static GHashTable * +get_name_map_from_strv (const gchar **strv) +{ + GHashTable *out = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, g_free); + const gchar **s; + + for (s = strv; s != NULL && *s != NULL; s++) + { + gchar **parts = g_strsplit (*s, "/", 2); + + g_hash_table_insert (out, + g_strdup (parts[0]), g_strdup(parts[1])); + + g_strfreev (parts); + } + + return out; +} + +static void +contact_capabilities_changed (YtstStatus *self, + gpointer contact, + gboolean do_signal) +{ + YtstStatusPrivate *priv = self->priv; + const GPtrArray *data_forms; + guint i; + GHashTable *old, *new; + GHashTableIter iter; + gpointer key, value; + const gchar *jid; + + data_forms = wocky_xep_0115_capabilities_get_data_forms ( + WOCKY_XEP_0115_CAPABILITIES (contact)); + + jid = gabble_connection_get_jid_for_caps (priv->connection, + WOCKY_XEP_0115_CAPABILITIES (contact)); + + if (jid == NULL) + return; + + old = g_hash_table_lookup (priv->discovered_services, jid); + + new = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_value_array_free); + + for (i = 0; i < data_forms->len; i++) + { + WockyDataForm *form = g_ptr_array_index (data_forms, i); + WockyDataFormField *type, *tmp; + const gchar *form_type; + const gchar *service; + GValueArray *details; + + gchar *yts_service_name; + GHashTable *yts_name_map; + gchar **yts_caps; + + type = g_hash_table_lookup (form->fields, "FORM_TYPE"); + form_type = g_value_get_string (type->default_value); + + if (type == NULL + || !g_str_has_prefix (form_type, SERVICE_PREFIX)) + { + continue; + } + + service = form_type + strlen (SERVICE_PREFIX); + + /* service type */ + tmp = g_hash_table_lookup (form->fields, "type"); + if (tmp == NULL) + continue; + yts_service_name = g_value_dup_string (tmp->default_value); + + /* name map */ + tmp = g_hash_table_lookup (form->fields, "name"); + if (tmp != NULL && tmp->default_value != NULL) + { + yts_name_map = get_name_map_from_strv ( + g_value_get_boxed (tmp->default_value)); + } + else + { + yts_name_map = g_hash_table_new (g_str_hash, g_str_equal); + } + + /* caps */ + tmp = g_hash_table_lookup (form->fields, "capabilities"); + if (tmp != NULL && tmp->default_value != NULL) + { + yts_caps = g_strdupv (tmp->raw_value_contents); + } + else + { + gchar *caps_tmp[] = { NULL }; + yts_caps = g_strdupv (caps_tmp); + } + + /* now build the value array and add it to the new hash table */ + details = tp_value_array_build (3, + G_TYPE_STRING, yts_service_name, + TP_HASH_TYPE_STRING_STRING_MAP, yts_name_map, + G_TYPE_STRV, yts_caps, + G_TYPE_INVALID); + + g_hash_table_insert (new, g_strdup (service), details); + + g_free (yts_service_name); + g_hash_table_unref (yts_name_map); + g_strfreev (yts_caps); + } + + if (do_signal) + { + /* first check for services in old but not in new; they've been + * removed. old can be NULL. */ + if (old != NULL) + { + g_hash_table_iter_init (&iter, old); + while (g_hash_table_iter_next (&iter, &key, NULL)) + { + if (g_hash_table_lookup (new, key) == NULL) + tp_yts_svc_status_emit_service_removed (self, jid, key); + } + } + + /* next check for services in new but not in old; they've been + * added */ + g_hash_table_iter_init (&iter, new); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + if (old == NULL || g_hash_table_lookup (old, key) == NULL) + tp_yts_svc_status_emit_service_added (self, jid, key, value); + } + } + + if (g_hash_table_size (new) > 0) + { + g_hash_table_replace (priv->discovered_services, + g_strdup (jid), new); + } + else + { + g_hash_table_remove (priv->discovered_services, jid); + g_hash_table_unref (new); + } +} + +static gboolean +capabilities_changed_cb (GSignalInvocationHint *ihint, + guint n_param_values, + const GValue *param_values, + gpointer user_data) +{ + YtstStatus *self = YTST_STATUS (user_data); + + contact_capabilities_changed (self, + g_value_get_object (param_values), TRUE); + + return TRUE; +} + +static void +check_contact_capabilities (TpHandleSet *set, + TpHandle handle, + gpointer user_data) +{ + YtstStatus *self = user_data; + YtstStatusPrivate *priv = self->priv; + WockyXep0115Capabilities *caps; + + caps = gabble_connection_get_caps (priv->connection, + handle); + + if (caps != NULL) + contact_capabilities_changed (self, caps, FALSE); +} + +static void +contact_list_state_changed_cb (GabbleConnection *connection, + TpContactListState state, + YtstStatus *self) +{ + TpBaseContactList *contact_list; + TpHandleSet *contacts; + + if (state != TP_CONTACT_LIST_STATE_SUCCESS) + return; + + contact_list = gabble_connection_get_contact_list (connection); + contacts = tp_base_contact_list_dup_contacts (contact_list); + + tp_handle_set_foreach (contacts, + check_contact_capabilities, self); + + tp_handle_set_destroy (contacts); +} + +static gboolean +capabilities_idle_cb (gpointer data) +{ + YtstStatus *self = YTST_STATUS (data); + YtstStatusPrivate *priv = self->priv; + TpBaseContactList *contact_list; + TpContactListState contact_list_state; + + /* connect to all capabilities-changed signals */ + priv->capabilities_changed_id = g_signal_add_emission_hook ( + g_signal_lookup ("capabilities-changed", WOCKY_TYPE_XEP_0115_CAPABILITIES), + 0, capabilities_changed_cb, self, NULL); + + /* and now look through all the contacts that had caps before this + * sidecar was ensured */ + contact_list = gabble_connection_get_contact_list (priv->connection); + contact_list_state = tp_base_contact_list_get_state (contact_list, NULL); + + if (contact_list_state == TP_CONTACT_LIST_STATE_SUCCESS) + { + contact_list_state_changed_cb (priv->connection, contact_list_state, self); + } + else + { + tp_g_signal_connect_object (priv->connection, "contact-list-state-changed", + G_CALLBACK (contact_list_state_changed_cb), self, 0); + } + + return FALSE; +} + +static void +ytst_status_constructed (GObject *object) +{ + YtstStatus *self = YTST_STATUS (object); + YtstStatusPrivate *priv = self->priv; + WockyPorter *porter; + + priv->discovered_statuses = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_hash_table_unref); + + priv->discovered_services = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, (GDestroyNotify) g_hash_table_unref); + + porter = wocky_session_get_porter (priv->session); + priv->handler_id = wocky_porter_register_handler_from_anyone ( + porter, WOCKY_STANZA_TYPE_MESSAGE, WOCKY_STANZA_SUB_TYPE_HEADLINE, + WOCKY_PORTER_HANDLER_PRIORITY_MAX, pep_event_cb, self, + '(', "event", + ':', "http://jabber.org/protocol/pubsub#event", + ')', NULL); + + /* we need an idle for this otherwise the g_signal_lookup fails + * giving this (not entirely sure why): + * + * unable to lookup signal "capabilities-changed" for non + * instantiatable type `WockyXep0115Capabilities' + */ + g_idle_add_full (G_PRIORITY_HIGH_IDLE, capabilities_idle_cb, + self, NULL); +} + +static void +ytst_status_dispose (GObject *object) +{ + YtstStatus *self = YTST_STATUS (object); + YtstStatusPrivate *priv = self->priv; + WockyPorter *porter; + + if (priv->dispose_has_run) + return; + + priv->dispose_has_run = TRUE; + + /* release any references held by the object here */ + + porter = wocky_session_get_porter (priv->session); + wocky_porter_unregister_handler (porter, priv->handler_id); + priv->handler_id = 0; + + if (priv->capabilities_changed_id > 0) + g_signal_remove_emission_hook ( + g_signal_lookup ("capabilities-changed", WOCKY_TYPE_XEP_0115_CAPABILITIES), + priv->capabilities_changed_id); + + tp_clear_pointer (&priv->discovered_statuses, g_hash_table_unref); + tp_clear_pointer (&priv->discovered_services, g_hash_table_unref); + + tp_clear_object (&priv->session); + tp_clear_object (&priv->connection); + + if (G_OBJECT_CLASS (ytst_status_parent_class)->dispose) + G_OBJECT_CLASS (ytst_status_parent_class)->dispose (object); +} + +static void +ytst_status_finalize (GObject *object) +{ +#if 0 + YtstStatus *self = YTST_STATUS (object); + YtstStatusPrivate *priv = self->priv; +#endif + + /* free any data held directly by the object here */ + + G_OBJECT_CLASS (ytst_status_parent_class)->finalize (object); +} + +static void +ytst_status_class_init (YtstStatusClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + GParamSpec *param_spec; + + static TpDBusPropertiesMixinPropImpl ytstenut_props[] = { + { "DiscoveredStatuses", "discovered-statuses", NULL }, + { "DiscoveredServices", "discovered-services", NULL }, + { NULL } + }; + + g_type_class_add_private (klass, sizeof (YtstStatusPrivate)); + + object_class->dispose = ytst_status_dispose; + object_class->finalize = ytst_status_finalize; + object_class->constructed = ytst_status_constructed; + object_class->get_property = ytst_status_get_property; + object_class->set_property = ytst_status_set_property; + + param_spec = g_param_spec_object ( + "session", + "Session object", + "WockySession object", + WOCKY_TYPE_SESSION, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_SESSION, + param_spec); + + param_spec = g_param_spec_object ( + "connection", + "Gabble connection", + "GabbleConnection object", + GABBLE_TYPE_CONNECTION, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_CONNECTION, + param_spec); + + param_spec = g_param_spec_boxed ( + "discovered-statuses", + "Discovered Statuses", + "Discovered Ytstenut statuses", + TP_YTS_HASH_TYPE_CONTACT_CAPABILITY_MAP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_DISCOVERED_STATUSES, + param_spec); + + param_spec = g_param_spec_boxed ( + "discovered-services", + "Discovered Services", + "Discovered Ytstenut services", + TP_YTS_HASH_TYPE_CONTACT_SERVICE_MAP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + g_object_class_install_property (object_class, PROP_DISCOVERED_SERVICES, + param_spec); + + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (YtstStatusClass, dbus_props_class)); + + tp_dbus_properties_mixin_implement_interface (object_class, + TP_YTS_IFACE_QUARK_STATUS, + tp_dbus_properties_mixin_getter_gobject_properties, NULL, + ytstenut_props); +} + +static WockyNodeTree * +parse_status_body (const gchar *body, + GError **error) +{ + WockyXmppReader *reader; + WockyNodeTree *tree; + GError *err = NULL; + + reader = wocky_xmpp_reader_new_no_stream (); + wocky_xmpp_reader_push (reader, (guint8 *) body, strlen (body)); + tree = WOCKY_NODE_TREE (wocky_xmpp_reader_pop_stanza (reader)); + g_object_unref (reader); + + if (tree == NULL) + { + err = wocky_xmpp_reader_get_error (reader); + g_set_error (error, TP_ERROR, TP_ERROR_INVALID_ARGUMENT, + "Invalid XML%s%s", + err != NULL && err->message != NULL ? ": " : ".", + err != NULL && err->message != NULL ? err->message : ""); + g_clear_error (&err); + } + + return tree; +} + +static void +ytst_status_advertise_status (TpYtsSvcStatus *svc, + const gchar *capability, + const gchar *service_name, + const gchar *status, + DBusGMethodInvocation *context) +{ + YtstStatus *self = YTST_STATUS (svc); + YtstStatusPrivate *priv = self->priv; + WockyNodeTree *status_tree = NULL; + GError *error = NULL; + WockyStanza *stanza; + WockyNode *item, *status_node; + + if (tp_str_empty (capability)) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "Capability argument must be set"); + goto out; + } + + if (tp_str_empty (service_name)) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "Service name argument must be set"); + goto out; + } + + if (!tp_str_empty (status)) + { + status_tree = parse_status_body (status, &error); + if (status_tree == NULL) + goto out; + } + else + { + status_tree = wocky_node_tree_new ("status", YTST_STATUS_NS, NULL); + } + + status_node = wocky_node_tree_get_top_node (status_tree); + + wocky_node_set_attribute (status_node, "from-service", + service_name); + wocky_node_set_attribute (status_node, "capability", + capability); + + stanza = wocky_pubsub_make_publish_stanza (NULL, capability, + NULL, NULL, &item); + + wocky_node_add_node_tree (item, status_tree); + g_object_unref (status_tree); + + wocky_porter_send_iq_async (wocky_session_get_porter (priv->session), + stanza, NULL, NULL, NULL); + g_object_unref (stanza); + +out: + if (error == NULL) + { + tp_yts_svc_status_return_from_advertise_status (context); + } + else + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + } +} + +static void +ytst_status_iface_init (TpYtsSvcStatusClass *iface) +{ +#define IMPLEMENT(x) tp_yts_svc_status_implement_##x (\ + iface, ytst_status_##x) + IMPLEMENT(advertise_status); +#undef IMPLEMENT +} + +static void +sidecar_iface_init (GabbleSidecarInterface *iface) +{ + iface->interface = TP_YTS_IFACE_STATUS; + iface->get_immutable_properties = NULL; +} + +/* ----------------------------------------------------------------------------- + * PUBLIC METHODS + */ + +YtstStatus * +ytst_status_new (WockySession *session, + GabbleConnection *connection) +{ + return g_object_new (YTST_TYPE_STATUS, + "session", session, + "connection", connection, + NULL); +} diff --git a/gabble/status.h b/gabble/status.h new file mode 100644 index 0000000..24adbc6 --- /dev/null +++ b/gabble/status.h @@ -0,0 +1,70 @@ +/* + * status.h - Header for YtstStatus + * + * Copyright (C) 2011 Intel Corp. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#ifndef YTST_STATUS_H +#define YTST_STATUS_H + +#include <glib-object.h> + +#include <telepathy-glib/base-channel.h> + +#include <wocky/wocky-session.h> + +#include <gabble/connection.h> + +G_BEGIN_DECLS + +typedef struct _YtstStatus YtstStatus; +typedef struct _YtstStatusClass YtstStatusClass; +typedef struct _YtstStatusPrivate YtstStatusPrivate; + +struct _YtstStatusClass { + GObjectClass parent_class; + + TpDBusPropertiesMixinClass dbus_props_class; +}; + +struct _YtstStatus { + GObject parent; + YtstStatusPrivate *priv; +}; + +GType ytst_status_get_type (void); + +/* TYPE MACROS */ +#define YTST_TYPE_STATUS \ + (ytst_status_get_type ()) +#define YTST_STATUS(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST((obj), YTST_TYPE_STATUS, YtstStatus)) +#define YTST_STATUS_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), YTST_TYPE_STATUS, YtstStatusClass)) +#define YTST_IS_STATUS(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE((obj), YTST_TYPE_STATUS)) +#define YTST_IS_STATUS_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE((klass), YTST_TYPE_STATUS)) +#define YTST_STATUS_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), YTST_TYPE_STATUS, YtstStatusClass)) + +YtstStatus * ytst_status_new (WockySession *session, + GabbleConnection *connection); + +G_END_DECLS + +#endif /* #ifndef YTST_STATUS_H*/ diff --git a/mission-control/mcp-account-manager-ytstenut.c b/mission-control/mcp-account-manager-ytstenut.c index b1e3ce0..eeacd31 100644 --- a/mission-control/mcp-account-manager-ytstenut.c +++ b/mission-control/mcp-account-manager-ytstenut.c @@ -193,7 +193,8 @@ on_account_request_presence_ready (GObject *source, GAsyncResult *res, static void account_manager_set_presence (McpAccountManagerYtstenut *self, - TpConnectionPresenceType presence) + TpConnectionPresenceType presence, + const gchar *presence_name) { McpAccountManagerYtstenutPrivate *priv = self->priv; GError *error = NULL; @@ -210,9 +211,10 @@ account_manager_set_presence (McpAccountManagerYtstenut *self, } } - DEBUG ("Requesting that account presence be changed to: %d", (int)presence); + DEBUG ("Requesting that account presence be changed to: %d (%s)", (int)presence, + presence_name); - tp_account_request_presence_async (priv->account_proxy, presence, "", "", + tp_account_request_presence_async (priv->account_proxy, presence, presence_name, "", on_account_request_presence_ready, g_object_ref (self)); } @@ -227,7 +229,7 @@ on_release_timeout (gpointer user_data) DEBUG ("Release timeout called"); if (g_hash_table_size (priv->hold_requests) == 0) - account_manager_set_presence (self, TP_CONNECTION_PRESENCE_TYPE_OFFLINE); + account_manager_set_presence (self, TP_CONNECTION_PRESENCE_TYPE_OFFLINE, "offline"); /* Remove this source */ return FALSE; @@ -263,7 +265,7 @@ account_manager_hold (McpAccountManagerYtstenut *self, const gchar *client) tp_dbus_daemon_watch_name_owner (priv->dbus_daemon, client, on_name_owner_changed, self, NULL); - account_manager_set_presence (self, TP_CONNECTION_PRESENCE_TYPE_AVAILABLE); + account_manager_set_presence (self, TP_CONNECTION_PRESENCE_TYPE_AVAILABLE, "available"); if (priv->timeout_id != 0) { DEBUG ("Cancelling offline timeout"); @@ -577,6 +579,7 @@ mcp_account_manager_ytstenut_release (TpYtsSvcAccountManager *manager, "calling Release()."); dbus_g_method_return_error (context, error); g_error_free (error); + return; } tp_yts_svc_account_manager_return_from_release (context); diff --git a/plugin-base/Makefile.am b/plugin-base/Makefile.am new file mode 100644 index 0000000..f1e676e --- /dev/null +++ b/plugin-base/Makefile.am @@ -0,0 +1,9 @@ +EXTRA_DIST = \ + caps-manager.c \ + caps-manager.h \ + channel-manager.c \ + channel-manager.h \ + ytstenut.c \ + ytstenut.h \ + utils.c \ + utils.h diff --git a/salut/caps-manager.c b/plugin-base/caps-manager.c index 194bdd1..849c6ff 100644 --- a/salut/caps-manager.c +++ b/plugin-base/caps-manager.c @@ -26,11 +26,19 @@ #include <telepathy-glib/channel-manager.h> #include <telepathy-glib/util.h> +#include <telepathy-glib/dbus.h> +#include <telepathy-glib/interfaces.h> + +#include <telepathy-ytstenut-glib/telepathy-ytstenut-glib.h> #include <wocky/wocky-data-form.h> #include <wocky/wocky-namespaces.h> +#ifdef SALUT #include <salut/caps-channel-manager.h> +#else +#include <gabble/caps-channel-manager.h> +#endif #include "utils.h" @@ -160,6 +168,7 @@ make_new_data_form (const gchar *uid, return out; } +#ifdef SALUT static void add_to_array (gpointer key, gpointer value, @@ -167,6 +176,7 @@ add_to_array (gpointer key, { g_ptr_array_add (user_data, g_object_ref (value)); } +#endif static void ytst_caps_manager_represent_client (GabbleCapsChannelManager *manager, @@ -176,8 +186,10 @@ ytst_caps_manager_represent_client (GabbleCapsChannelManager *manager, GabbleCapabilitySet *cap_set, GPtrArray *data_forms) { +#ifdef SALUT YtstCapsManager *self = YTST_CAPS_MANAGER (manager); YtstCapsManagerPrivate *priv = self->priv; +#endif const gchar * const *t; const gchar *uid = NULL; @@ -185,6 +197,33 @@ ytst_caps_manager_represent_client (GabbleCapsChannelManager *manager, GPtrArray *names = g_ptr_array_new (); GPtrArray *caps = g_ptr_array_new (); +#ifdef GABBLE + guint i; + + for (i = 0; i < filters->len; i++) + { + GHashTable *channel_class = g_ptr_array_index (filters, i); + const gchar *service_name; + gchar *cap; + + if (tp_strdiff (tp_asv_get_string (channel_class, + TP_IFACE_CHANNEL ".ChannelType"), + TP_YTS_IFACE_CHANNEL)) + continue; + + service_name = tp_asv_get_string (channel_class, + TP_YTS_IFACE_CHANNEL ".TargetService"); + + if (service_name == NULL) + continue; + + cap = g_strdup_printf ("%s#%s", + YTST_SERVICE_NS, service_name); + gabble_capability_set_add (cap_set, cap); + g_free (cap); + } +#endif + for (t = cap_tokens; t != NULL && *t != NULL; t++) { const gchar *cap = *t; @@ -215,18 +254,33 @@ ytst_caps_manager_represent_client (GabbleCapsChannelManager *manager, } } + /* So, gabble and salut have different ideas of how to save caps for + * clients. salut is arguably wrong here as it relies on the caps + * channel manager keeping a record of what clients can do. gabble + * does not need this and is simpler, so doesn't need the + * priv->services hash table at all. We should fix salut. */ + if (uid != NULL) { +#ifdef SALUT g_hash_table_insert (priv->services, g_strdup (client_name), make_new_data_form (uid, yts_type, names, caps)); +#else + g_ptr_array_add (data_forms, + make_new_data_form (uid, yts_type, names, caps)); +#endif } else { +#ifdef SALUT g_hash_table_remove (priv->services, client_name); +#endif } +#ifdef SALUT g_hash_table_foreach (priv->services, add_to_array, data_forms); +#endif g_ptr_array_unref (names); g_ptr_array_unref (caps); @@ -237,7 +291,11 @@ caps_channel_manager_iface_init ( gpointer g_iface, gpointer data G_GNUC_UNUSED) { +#ifdef SALUT /* sigh */ GabbleCapsChannelManagerIface *iface = g_iface; +#else + GabbleCapsChannelManagerInterface *iface = g_iface; +#endif iface->represent_client = ytst_caps_manager_represent_client; } diff --git a/salut/caps-manager.h b/plugin-base/caps-manager.h index e25d89b..e25d89b 100644 --- a/salut/caps-manager.h +++ b/plugin-base/caps-manager.h diff --git a/salut/channel-manager.c b/plugin-base/channel-manager.c index c8ba267..0cf6e97 100644 --- a/salut/channel-manager.c +++ b/plugin-base/channel-manager.c @@ -32,7 +32,17 @@ #include <telepathy-glib/interfaces.h> #include <telepathy-glib/util.h> +#include <wocky/wocky-session.h> + +#ifdef SALUT #include <salut/caps-channel-manager.h> +typedef SalutConnection FooConnection; +#define foo_connection_get_session salut_connection_get_session +#else +#include <gabble/caps-channel-manager.h> +typedef GabbleConnection FooConnection; +#define foo_connection_get_session gabble_connection_get_session +#endif #include <telepathy-ytstenut-glib/telepathy-ytstenut-glib.h> @@ -61,7 +71,7 @@ enum /* private structure */ struct _YtstChannelManagerPrivate { - SalutConnection *connection; + FooConnection *connection; GQueue *channels; gulong status_changed_id; guint message_handler_id; @@ -118,7 +128,9 @@ message_stanza_callback (WockyPorter *porter, TP_HANDLE_TYPE_CONTACT); YtstMessageChannel *channel; TpHandle handle; +#ifdef SALUT WockyContact *contact = wocky_stanza_get_from_contact (stanza); +#endif gchar *jid; /* needs to be type get or set */ @@ -132,7 +144,11 @@ message_stanza_callback (WockyPorter *porter, if (wocky_node_get_attribute (top, "id") == NULL) return FALSE; +#ifdef SALUT jid = wocky_contact_dup_jid (WOCKY_CONTACT (contact)); +#else + jid = g_strdup (wocky_stanza_get_from (stanza)); +#endif handle = tp_handle_lookup (handle_repo, jid, NULL, NULL); if (handle == 0) { @@ -141,7 +157,12 @@ message_stanza_callback (WockyPorter *porter, } channel = ytst_message_channel_new (priv->connection, - WOCKY_LL_CONTACT (contact), stanza, handle, handle, FALSE); +#ifdef SALUT + WOCKY_LL_CONTACT (contact), +#else + jid, +#endif + stanza, handle, handle, FALSE); manager_take_ownership_of_channel (self, channel); tp_channel_manager_emit_new_channel (self, TP_EXPORTABLE_CHANNEL (channel), NULL); @@ -173,7 +194,7 @@ manager_close_all (YtstChannelManager *self) } static void -on_connection_status_changed (SalutConnection *conn, +on_connection_status_changed (TpBaseConnection *conn, guint status, guint reason, YtstChannelManager *self) @@ -235,24 +256,47 @@ ytst_channel_manager_set_property (GObject *object, } static void -ytst_channel_manager_constructed (GObject *object) +ytst_channel_manager_porter_available_cb ( + FooConnection *connection, + WockyPorter *porter, + YtstChannelManager *self) { - YtstChannelManager *self = YTST_CHANNEL_MANAGER (object); YtstChannelManagerPrivate *priv = self->priv; - WockySession *session; - priv->channels = g_queue_new (); - - session = salut_connection_get_session (priv->connection); + if (priv->message_handler_id > 0) + return; priv->message_handler_id = wocky_porter_register_handler_from_anyone ( - wocky_session_get_porter (session), + porter, WOCKY_STANZA_TYPE_IQ, WOCKY_STANZA_SUB_TYPE_NONE, WOCKY_PORTER_HANDLER_PRIORITY_NORMAL, message_stanza_callback, self, '(', "message", ':', YTST_MESSAGE_NS, ')', NULL); +} + +static void +ytst_channel_manager_constructed (GObject *object) +{ + YtstChannelManager *self = YTST_CHANNEL_MANAGER (object); + YtstChannelManagerPrivate *priv = self->priv; +#ifdef SALUT + WockySession *session; +#endif + + priv->channels = g_queue_new (); + +#ifdef SALUT + session = salut_connection_get_session (priv->connection); + + ytst_channel_manager_porter_available_cb (priv->connection, + wocky_session_get_porter (session), self); +#else + tp_g_signal_connect_object (priv->connection, "porter-available", + G_CALLBACK (ytst_channel_manager_porter_available_cb), + self, 0); +#endif priv->status_changed_id = g_signal_connect (priv->connection, "status-changed", (GCallback) on_connection_status_changed, self); @@ -273,7 +317,7 @@ ytst_channel_manager_dispose (GObject *object) priv->dispose_has_run = TRUE; - session = salut_connection_get_session (priv->connection); + session = foo_connection_get_session (priv->connection); if (session != NULL) { @@ -304,9 +348,9 @@ ytst_channel_manager_class_init (YtstChannelManagerClass *klass) param_spec = g_param_spec_object ( "connection", - "SalutConnection object", - "Salut connection object that owns this channel factory object.", - SALUT_TYPE_CONNECTION, + "TpBaseConnection object", + "Connection object that owns this channel factory object.", + TP_TYPE_BASE_CONNECTION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); g_object_class_install_property (object_class, PROP_CONNECTION, param_spec); @@ -395,9 +439,15 @@ ytst_channel_manager_create_channel (TpChannelManager *manager, TpHandle handle; GError *error = NULL; const gchar *name; +#ifdef SALUT WockySession *session; WockyContactFactory *factory; WockyLLContact *contact; +#else + gchar *jid, *full_jid; + gchar *service; + const gchar *resource; +#endif WockyStanza *request; GSList *tokens = NULL; YtstMessageChannel *channel; @@ -425,6 +475,7 @@ ytst_channel_manager_create_channel (TpChannelManager *manager, name = tp_handle_inspect (handle_repo, handle); DEBUG ("Requested channel for handle: %u (%s)", handle, name); +#ifdef SALUT session = salut_connection_get_session (priv->connection); factory = wocky_session_get_contact_factory (session); contact = wocky_contact_factory_lookup_ll_contact (factory, name); @@ -434,18 +485,63 @@ ytst_channel_manager_create_channel (TpChannelManager *manager, "%s is not online", name); goto error; } +#else + if (tp_asv_get_string (request_properties, + TP_YTS_IFACE_CHANNEL ".TargetService") == NULL) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "The TargetService property must be set."); + goto error; + } + + service = g_strdup_printf ("%s#%s", YTST_SERVICE_NS, + tp_asv_get_string (request_properties, + TP_YTS_IFACE_CHANNEL ".TargetService")); + + resource = gabble_connection_pick_best_resource_for_caps (priv->connection, + name, gabble_capability_set_predicate_has, service); + g_free (service); + + if (resource == NULL) + { + g_set_error (&error, TP_ERRORS, TP_ERROR_NOT_AVAILABLE, + "Cannot find appropriate resource for contact."); + goto error; + } + + jid = g_strdup_printf ("%s/%s", name, resource); + + full_jid = gabble_connection_get_full_jid (priv->connection); +#endif request = ytst_message_channel_build_request (request_properties, - salut_connection_get_name (priv->connection), contact, &error); +#ifdef SALUT + salut_connection_get_name (priv->connection), contact, +#else + full_jid, jid, +#endif + &error); +#ifdef GABBLE + g_free (full_jid); +#endif if (request == NULL) goto error; - channel = ytst_message_channel_new (priv->connection, contact, request, handle, - base_conn->self_handle, TRUE); + channel = ytst_message_channel_new (priv->connection, +#ifdef SALUT + contact, +#else + jid, +#endif + request, handle, base_conn->self_handle, TRUE); manager_take_ownership_of_channel (self, channel); g_object_unref (request); +#ifdef GABBLE + g_free (jid); +#endif + if (request_token != NULL) tokens = g_slist_prepend (tokens, request_token); tp_channel_manager_emit_new_channel (self, TP_EXPORTABLE_CHANNEL (channel), @@ -484,7 +580,11 @@ static void ytst_caps_channel_manager_iface_init (gpointer g_iface, gpointer iface_data) { +#ifdef SALUT /* sigh */ GabbleCapsChannelManagerIface *iface = g_iface; +#else + GabbleCapsChannelManagerInterface *iface = g_iface; +#endif /* we don't need any of these */ iface->reset_caps = NULL; @@ -494,7 +594,7 @@ ytst_caps_channel_manager_iface_init (gpointer g_iface, /* public functions */ YtstChannelManager * -ytst_channel_manager_new (SalutConnection *connection) +ytst_channel_manager_new (TpBaseConnection *connection) { return g_object_new (YTST_TYPE_CHANNEL_MANAGER, "connection", connection, diff --git a/salut/channel-manager.h b/plugin-base/channel-manager.h index 31e3952..13b27a5 100644 --- a/salut/channel-manager.h +++ b/plugin-base/channel-manager.h @@ -23,7 +23,7 @@ #include <glib-object.h> -#include <salut/connection.h> +#include <telepathy-glib/base-connection.h> G_BEGIN_DECLS @@ -59,6 +59,6 @@ GType ytst_channel_manager_get_type (void); (G_TYPE_INSTANCE_GET_CLASS ((obj), YTST_TYPE_CHANNEL_MANAGER, \ YtstChannelManagerClass)) -YtstChannelManager * ytst_channel_manager_new (SalutConnection *connection); +YtstChannelManager * ytst_channel_manager_new (TpBaseConnection *connection); #endif /* #ifndef __YTST_CHANNEL_MANAGER_H__*/ diff --git a/salut/utils.c b/plugin-base/utils.c index 255f400..255f400 100644 --- a/salut/utils.c +++ b/plugin-base/utils.c diff --git a/salut/utils.h b/plugin-base/utils.h index e73ca8e..48ae2ac 100644 --- a/salut/utils.h +++ b/plugin-base/utils.h @@ -29,6 +29,7 @@ G_BEGIN_DECLS #define YTST_MESSAGE_NS "urn:ytstenut:message" #define YTST_STATUS_NS "urn:ytstenut:status" #define YTST_CAPABILITIES_NS "urn:ytstenut:capabilities" +#define YTST_SERVICE_NS "urn:ytstenut:service" GQuark ytst_message_error_quark (void); #define YTST_MESSAGE_ERROR (ytst_message_error_quark ()) diff --git a/salut/ytstenut.c b/plugin-base/ytstenut.c index edb03fa..e293354 100644 --- a/salut/ytstenut.c +++ b/plugin-base/ytstenut.c @@ -26,8 +26,18 @@ #include "status.h" #include "channel-manager.h" +#ifdef SALUT #include <salut/plugin.h> #include <salut/protocol.h> +typedef SalutPlugin FooPlugin; +typedef SalutConnection FooConnection; +typedef SalutSidecar FooSidecar; +#else +#include <gabble/plugin.h> +typedef GabblePlugin FooPlugin; +typedef GabbleConnection FooConnection; +typedef GabbleSidecar FooSidecar; +#endif #include <telepathy-ytstenut-glib/telepathy-ytstenut-glib.h> @@ -42,7 +52,11 @@ static const gchar * const sidecar_interfaces[] = { static void plugin_iface_init (gpointer g_iface, gpointer data); G_DEFINE_TYPE_WITH_CODE (YtstPlugin, ytst_plugin, G_TYPE_OBJECT, +#ifdef SALUT G_IMPLEMENT_INTERFACE (SALUT_TYPE_PLUGIN, plugin_iface_init); +#else + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_PLUGIN, plugin_iface_init); +#endif ) static void @@ -56,6 +70,7 @@ ytst_plugin_class_init (YtstPluginClass *klass) { } +#ifdef SALUT static void ytstenut_plugin_initialize (SalutPlugin *plugin, TpBaseConnectionManager *connection_manager) @@ -68,27 +83,33 @@ ytstenut_plugin_initialize (SalutPlugin *plugin, "_ytstenut._tcp", "local-ytstenut", "Ytstenut protocol", "im-ytstenut"); tp_base_connection_manager_add_protocol (connection_manager, protocol); } +#endif static void ytstenut_plugin_create_sidecar ( - SalutPlugin *plugin, + FooPlugin *plugin, const gchar *sidecar_interface, - SalutConnection *connection, + FooConnection *connection, WockySession *session, GAsyncReadyCallback callback, gpointer user_data) { GSimpleAsyncResult *result = g_simple_async_result_new (G_OBJECT (plugin), callback, user_data, - /* sic: all plugins share salut_plugin_create_sidecar_finish() so we + /* sic: all plugins share {salut,gabble}_plugin_create_sidecar_finish() so we * need to use the same source tag. */ - salut_plugin_create_sidecar_async); - SalutSidecar *sidecar = NULL; +#ifdef SALUT + salut_plugin_create_sidecar_async +#else + gabble_plugin_create_sidecar +#endif + ); + FooSidecar *sidecar = NULL; if (!tp_strdiff (sidecar_interface, TP_YTS_IFACE_STATUS)) { - sidecar = SALUT_SIDECAR (ytst_status_new (session, connection)); + sidecar = (FooSidecar *) ytst_status_new (session, connection); DEBUG ("created side car for: %s", TP_YTS_IFACE_STATUS); } else @@ -105,7 +126,8 @@ ytstenut_plugin_create_sidecar ( } static GPtrArray * -ytstenut_plugin_create_channel_managers (SalutPlugin *plugin, +ytstenut_plugin_create_channel_managers ( + FooPlugin *plugin, TpBaseConnection *connection) { GPtrArray *ret = g_ptr_array_sized_new (1); @@ -113,8 +135,7 @@ ytstenut_plugin_create_channel_managers (SalutPlugin *plugin, DEBUG ("%p on connection %p", plugin, connection); g_ptr_array_add (ret, g_object_new (YTST_TYPE_CAPS_MANAGER, NULL)); - g_ptr_array_add (ret, ytst_channel_manager_new ( - SALUT_CONNECTION (connection))); + g_ptr_array_add (ret, ytst_channel_manager_new (connection)); return ret; } @@ -123,20 +144,31 @@ static void plugin_iface_init (gpointer g_iface, gpointer data G_GNUC_UNUSED) { +#ifdef SALUT SalutPluginInterface *iface = g_iface; +#else + GabblePluginInterface *iface = g_iface; + #endif +#ifdef SALUT iface->api_version = SALUT_PLUGIN_CURRENT_VERSION; + iface->initialize = ytstenut_plugin_initialize; +#endif + iface->name = "Ytstenut plugin"; iface->version = PACKAGE_VERSION; iface->sidecar_interfaces = sidecar_interfaces; iface->create_sidecar = ytstenut_plugin_create_sidecar; - iface->initialize = ytstenut_plugin_initialize; iface->create_channel_managers = ytstenut_plugin_create_channel_managers; } -SalutPlugin * +FooPlugin * +#ifdef SALUT salut_plugin_create (void) +#else +gabble_plugin_create (void) +#endif { return g_object_new (YTST_TYPE_PLUGIN, NULL); } diff --git a/salut/ytstenut.h b/plugin-base/ytstenut.h index fdd5389..fdd5389 100644 --- a/salut/ytstenut.h +++ b/plugin-base/ytstenut.h diff --git a/salut/Makefile.am b/salut/Makefile.am index c71be85..20ea5ac 100644 --- a/salut/Makefile.am +++ b/salut/Makefile.am @@ -1,5 +1,7 @@ AM_CFLAGS = \ -DG_LOG_DOMAIN=\"ytstenut\" \ + -DSALUT \ + -I$(top_srcdir)/plugin-base \ $(SALUT_CFLAGS) \ $(TELEPATHY_YTSTENUT_CFLAGS) @@ -14,15 +16,15 @@ ytstenut_salut_la_LIBADD = \ $(TELEPATHY_YTSTENUT_LIBS) ytstenut_salut_la_SOURCES = \ - ytstenut.c \ - ytstenut.h \ - caps-manager.c \ - caps-manager.h \ + $(top_srcdir)/plugin-base/ytstenut.c \ + $(top_srcdir)/plugin-base/ytstenut.h \ + $(top_srcdir)/plugin-base/caps-manager.c \ + $(top_srcdir)/plugin-base/caps-manager.h \ status.c \ status.h \ message-channel.c \ message-channel.h \ - channel-manager.c \ - channel-manager.h \ - utils.c \ - utils.h + $(top_srcdir)/plugin-base/channel-manager.c \ + $(top_srcdir)/plugin-base/channel-manager.h \ + $(top_srcdir)/plugin-base/utils.c \ + $(top_srcdir)/plugin-base/utils.h diff --git a/salut/status.c b/salut/status.c index ceca3f3..9a77206 100644 --- a/salut/status.c +++ b/salut/status.c @@ -275,7 +275,7 @@ pep_event_cb (WockyPorter *porter, /* looks good */ from = wocky_stanza_get_from (stanza); - capability = wocky_node_get_ns (items); + capability = wocky_node_get_attribute (items, "node"); service_name = wocky_node_get_attribute (status, "from-service"); if (wocky_node_get_attribute (status, "activity") != NULL) diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 78175e5..1b26802 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -9,7 +9,15 @@ TWISTED_BASIC_TESTS += \ salut/message.py \ salut/status.py \ salut/service.py \ - salut/hct.py + salut/hct.py \ + salut/slow-service.py \ + gabble/sidecar.py \ + gabble/message.py \ + gabble/status.py \ + gabble/service.py \ + gabble/hct.py \ + gabble/slow-service.py + endif config.py: Makefile @@ -46,7 +54,9 @@ BASIC_TESTS_ENVIRONMENT = \ XDG_CACHE_HOME=@abs_top_builddir@/tests/twisted/tmp-$(TMPSUFFIX) \ G_DEBUG=fatal_criticals \ SALUT_PLUGIN_DIR=@abs_top_builddir@/salut/.libs \ - SALUT_TEST_BACKTRACE=1 + SALUT_TEST_BACKTRACE=1 \ + GABBLE_PLUGIN_DIR=@abs_top_builddir@/gabble/.libs \ + GABBLE_TEST_BACKTRACE=1 WITH_SESSION_BUS = \ sh $(srcdir)/tools/with-session-bus.sh \ @@ -63,6 +73,7 @@ check-twisted: rm -f tools/core rm -f tools/missioncontrol-*.log rm -f tools/salut-testing.log + rm -f tools/gabble-testing.log mkdir tmp-$$$$ && { \ $(MAKE) check-combined TMPSUFFIX=$$$$; \ e=$$?; \ @@ -143,7 +154,11 @@ EXTRA_DIST = \ avahitest.py \ xmppstream.py \ ipv6.py \ - caps_helper.py + caps_helper.py \ + gabbleservicetest.py \ + gabbletest.py \ + gabbleconstants.py \ + gabblecaps_helper.py CLEANFILES = \ accounts/accounts.cfg \ diff --git a/tests/twisted/gabble/hct.py b/tests/twisted/gabble/hct.py new file mode 100755 index 0000000..714afcc --- /dev/null +++ b/tests/twisted/gabble/hct.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + assertLength, assertContains, sync_dbus, ProxyWrapper +from gabbletest import exec_test, elem_iq, elem +from gabblecaps_helper import presence_and_disco, receive_presence_and_ask_caps, \ + disco_caps + +import gabbleconstants as cs +import ns +import yconstants as ycs + +client = 'http://telepathy.im/fake' +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def test(q, bus, conn, stream): + bare_jid = "test-hct@example.com" + full_jid = bare_jid + "/LikeLava" + + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', args=[0, 1]) + + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + self_handle = conn.GetSelfHandle() + self_handle_name = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0] + + caps = {'ver': '0.1', 'node': client} + presence_and_disco(q, conn, stream, full_jid, True, client, caps, + features, identity, {}, True, None) + + # now update the caps + conn.ContactCapabilities.UpdateCapabilities([ + ('well.gnome.name', [], + ['com.meego.xpmn.ytstenut.Channel/uid/org.gnome.Banshee', + 'com.meego.xpmn.ytstenut.Channel/type/application', + 'com.meego.xpmn.ytstenut.Channel/name/en_GB/Banshee Media Player', + 'com.meego.xpmn.ytstenut.Channel/name/fr/Banshee Lecteur de Musique', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:capabilities:yts-caps-audio', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:data:jingle:rtp'])]) + + + _, e = q.expect_many(EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('stream-presence')) + + e, _, _ = disco_caps(q, stream, e) + + iq = e.stanza + query = iq.children[0] + + x = None + for child in query.children: + if child.name == 'x' and child.uri == ns.X_DATA: + # we should only have one child + assert x is None + x = child + # don't break here as we can waste time to make sure x + # isn't assigned twice + + assert x is not None + + for field in x.children: + if field['var'] == 'FORM_TYPE': + assertEquals('hidden', field['type']) + assertEquals('urn:ytstenut:capabilities#org.gnome.Banshee', + field.children[0].children[0]) + elif field['var'] == 'type': + assertEquals('application', field.children[0].children[0]) + elif field['var'] == 'name': + names = [a.children[0] for a in field.children] + assertLength(2, names) + assertContains('en_GB/Banshee Media Player', names) + assertContains('fr/Banshee Lecteur de Musique', names) + elif field['var'] == 'capabilities': + caps = [a.children[0] for a in field.children] + assertLength(2, caps) + assertContains('urn:ytstenut:capabilities:yts-caps-audio', caps) + assertContains('urn:ytstenut:data:jingle:rtp', caps) + else: + assert False + + # now add another service + forbidden = [EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('another.nice.gname', [], + ['com.meego.xpmn.ytstenut.Channel/uid/org.gnome.Eog', + 'com.meego.xpmn.ytstenut.Channel/type/application', + 'com.meego.xpmn.ytstenut.Channel/name/en_GB/Eye Of Gnome', + 'com.meego.xpmn.ytstenut.Channel/name/it/Occhio Di uno Gnomo', + 'com.meego.xpmn.ytstenut.Channel/caps/urn:ytstenut:capabilities:yts-picz'])]) + + e = q.expect('dbus-signal', signal='ServiceAdded') + + sync_dbus(bus, q, conn) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': + {'org.gnome.Banshee': ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Eog': ('application', + {'en_GB': 'Eye Of Gnome', + 'it': 'Occhio Di uno Gnomo'}, + ['urn:ytstenut:capabilities:yts-picz'])}}, + discovered) + + q.unforbid_events(forbidden) + + forbidden = [EventPattern('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Eog'])] + q.forbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('well.gnome.name', [], [])]) + + e = q.expect('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Banshee']) + + sync_dbus(bus, q, conn) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': + {'org.gnome.Eog': ('application', + {'en_GB': 'Eye Of Gnome', + 'it': 'Occhio Di uno Gnomo'}, + ['urn:ytstenut:capabilities:yts-picz'])}}, + discovered) + + q.unforbid_events(forbidden) + + conn.ContactCapabilities.UpdateCapabilities([ + ('another.nice.gname', [], [])]) + + e = q.expect('dbus-signal', signal='ServiceRemoved', + args=[self_handle_name, 'org.gnome.Eog']) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/message.py b/tests/twisted/gabble/message.py new file mode 100644 index 0000000..43e941f --- /dev/null +++ b/tests/twisted/gabble/message.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +from gabbleservicetest import call_async, EventPattern, assertEquals, ProxyWrapper +from gabbletest import exec_test, make_result_iq, sync_stream +from gabblecaps_helper import presence_and_disco + +from twisted.words.protocols.jabber.client import IQ +from twisted.words.xish.domish import Element + +import gabbleconstants as cs +import yconstants as ycs +import ns + +client = 'http://telepathy.im/fake' +caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def wrap_channel(bus, conn, path): + return ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.CHANNEL_IFACE, {}) + +def setup_tests(q, bus, conn, stream, announce=False): + bare_jid = "test-yst-message@example.com" + full_jid = bare_jid + "/HotHotResource" + + if announce: + presence_and_disco(q, conn, stream, full_jid, + True, client, caps, + features, identity, {}, + True, None) + + sync_stream(q, stream) + + handle = conn.RequestHandles(cs.HT_CONTACT, [full_jid])[0] + + return handle, bare_jid, full_jid + +def setup_outgoing_tests(q, bus, conn, stream, announce=True): + handle, _, _ = setup_tests(q, bus, conn, stream, announce) + + # okay we got our contact, let's go + request_props = { + cs.CHANNEL_TYPE: ycs.CHANNEL_IFACE, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.TARGET_HANDLE: handle, + ycs.REQUEST_TYPE: ycs.REQUEST_TYPE_GET, + ycs.REQUEST_ATTRIBUTES: {'hi': 'mom'}, + ycs.TARGET_SERVICE: 'the.target.service', + ycs.INITIATOR_SERVICE: 'the.initiator.service' + } + + call_async(q, conn.Requests, 'CreateChannel', request_props) + + e, _ = q.expect_many(EventPattern('dbus-return', method='CreateChannel'), + EventPattern('dbus-signal', signal='NewChannels')) + path, props = e.value + + for k, v in request_props.items(): + assertEquals(v, props[k]) + + # finally we have our channel + chan = wrap_channel(bus, conn, path) + + # let's check we can't call Fail()/Reply() + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + # okay enough, let's move on. + call_async(q, chan, 'Request') + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='Request')) + + assertEquals('get', e.iq_type) + assertEquals('message', e.query_name) + assertEquals('urn:ytstenut:message', e.query_ns) + + # we shouldn't be able to call this again + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + + return path, e.stanza + +def outgoing_reply(q, bus, conn, stream): + path, stanza = setup_outgoing_tests(q, bus, conn, stream) + + # reply with nothing + reply = make_result_iq(stream, stanza) + stream.send(reply) + + e = q.expect('dbus-signal', signal='Replied', path=path) + args, xml = e.args + assertEquals({}, args) + assertEquals('<?xml version="1.0" encoding="UTF-8"?>\n' \ + + '<message xmlns="urn:ytstenut:message"/>\n', xml) + +def outgoing_fail(q, bus, conn, stream): + path, stanza = setup_outgoing_tests(q, bus, conn, stream) + + # construct a nice error reply + reply = IQ(None, 'error') + reply['id'] = stanza['id'] + reply['from'] = stanza['to'] + error = reply.addElement('error') + error['type'] = 'cancel' + error['code'] = '409' + error.addElement((ns.STANZA, 'conflict')) + error.addElement((ycs.MESSAGE_NS, 'yodawg')) + text = error.addElement((ns.STANZA, 'text'), + content='imma let you finish') + + stream.send(reply) + + e = q.expect('dbus-signal', signal='Failed', path=path) + error_type, stanza_error_name, yst_error_name, text = e.args + assertEquals(ycs.ERROR_TYPE_CANCEL, error_type) + assertEquals('conflict', stanza_error_name) + assertEquals('yodawg', yst_error_name) + assertEquals('imma let you finish', text) + +def bad_requests(q, bus, conn, stream): + handle, _, _ = setup_tests(q, bus, conn, stream) + + props = { + cs.CHANNEL_TYPE: ycs.CHANNEL_IFACE, + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + } + + def ensure_error(extra={}): + copy = props.copy() + copy.update(extra) + + call_async(q, conn.Requests, 'CreateChannel', copy) + q.expect('dbus-error', method='CreateChannel') + + # bad handle + ensure_error({cs.TARGET_HANDLE: 42}) + + # offline + ensure_error({cs.TARGET_ID: 'lolbags@dingdong'}) + + props.update({cs.TARGET_HANDLE: handle}) + + # RequestType + ensure_error() + ensure_error({ycs.REQUEST_TYPE: 99}) + props.update({ycs.REQUEST_TYPE: ycs.REQUEST_TYPE_GET}) + + # TargetService + ensure_error() + ensure_error({ycs.TARGET_SERVICE: 'lol/bags/what\'s this?!!!!'}) + props.update({ycs.TARGET_SERVICE: 'the.target.service'}) + + # InitiatorService + ensure_error() + ensure_error({ycs.INITIATOR_SERVICE: 'lol/bags/what\'s this?!!!!'}) + props.update({ycs.INITIATOR_SERVICE: 'the.initiator.service'}) + + # RequestAttributes: a{ss}, not a{si} + ensure_error({ycs.REQUEST_ATTRIBUTES: {'lol': 2}}) + + # RequestBody + ensure_error({ycs.REQUEST_BODY: 'no way is this real XML'}) + +def setup_incoming_tests(q, bus, conn, stream): + handle, bare_jid, full_jid = setup_tests(q, bus, conn, stream) + + self_handle = conn.GetSelfHandle() + self_handle_name = conn.InspectHandles(cs.HT_CONTACT, [self_handle])[0] + + iq = IQ(None, 'get') + iq['id'] = 'le-loldongs' + iq['from'] = full_jid + iq['to'] = self_handle_name + msg = iq.addElement((ycs.MESSAGE_NS, 'message')) + msg['from-service'] = 'the.from.service' + msg['to-service'] = 'the.to.service' + msg['owl-companions'] = 'the pussy cat' + msg['destination'] = 'sea' + msg['seacraft'] = 'beautiful pea green boat' + + lol = msg.addElement((None, 'lol')) + lol['some'] = 'stuff' + lol['to'] = 'fill' + lol['the'] = 'time' + lol.addElement((None, 'look-into-my-eyes'), + content='and tell me how boring writing these tests is') + + stream.send(iq) + + e = q.expect('dbus-signal', signal='NewChannels', predicate=lambda e: + e.args[0][0][1][cs.CHANNEL_TYPE] == ycs.CHANNEL_IFACE) + path, props = e.args[0][0] + + assertEquals(handle, props[cs.INITIATOR_HANDLE]) + assertEquals(bare_jid, props[cs.INITIATOR_ID]) + assertEquals(False, props[cs.REQUESTED]) + assertEquals(handle, props[cs.TARGET_HANDLE]) + assertEquals(cs.HT_CONTACT, props[cs.TARGET_HANDLE_TYPE]) + assertEquals(bare_jid, props[cs.TARGET_ID]) + + assertEquals('the.from.service', props[ycs.INITIATOR_SERVICE]) + assertEquals('the.to.service', props[ycs.TARGET_SERVICE]) + assertEquals(ycs.REQUEST_TYPE_GET, props[ycs.REQUEST_TYPE]) + assertEquals({'destination': 'sea', + 'owl-companions': 'the pussy cat', + 'seacraft': 'beautiful pea green boat'}, + props[ycs.REQUEST_ATTRIBUTES]) + + assertEquals('<?xml version="1.0" encoding="UTF-8"?>\n' \ + '<message seacraft="beautiful pea green boat" ' \ + 'from-service="the.from.service" destination="sea" ' \ + 'owl-companions="the pussy cat" to-service="the.to.service" ' \ + 'xmlns="urn:ytstenut:message">' \ + '<lol to="fill" the="time" some="stuff">' \ + '<look-into-my-eyes>and tell me how boring ' \ + 'writing these tests is</look-into-my-eyes>' \ + '</lol></message>\n', props[ycs.REQUEST_BODY]) + + # finally we have our channel + chan = wrap_channel(bus, conn, path) + + # let's check we can't call Request() + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + + return chan, bare_jid, full_jid, self_handle_name + +def incoming_reply(q, bus, conn, stream): + chan, bare_jid, full_jid, self_handle_name = \ + setup_incoming_tests(q, bus, conn, stream) + + moar = Element((ycs.MESSAGE_NS, 'message')) + moar['ninety-nine-problems'] = 'but a sauvignon blanc aint one' + moar['also'] = 'my mum said hi' + trollface = moar.addElement('trollface', content='problem?') + + call_async(q, chan, 'Reply', + {'ninety-nine-problems': 'but a sauvignon blanc aint one', + 'also': 'my mum said hi'}, + moar.toXml()) + + _, e = q.expect_many(EventPattern('dbus-return', method='Reply'), + EventPattern('stream-message')) + + iq = e.stanza + assertEquals('le-loldongs', iq['id']) + assertEquals('result', iq['type']) + assertEquals(self_handle_name, iq['from']) + assertEquals(full_jid, iq['to']) + assertEquals(1, len(iq.children)) + + message = iq.children[0] + + assertEquals('message', message.name) + assertEquals(ycs.MESSAGE_NS, message.uri) + assertEquals('my mum said hi', message['also']) + assertEquals('but a sauvignon blanc aint one', message['ninety-nine-problems']) + assertEquals('the.from.service', message['to-service']) + assertEquals('the.to.service', message['from-service']) + assertEquals(1, len(message.children)) + + trollface = message.children[0] + + assertEquals('trollface', trollface.name) + assertEquals(1, len(trollface.children)) + + assertEquals('problem?', trollface.children[0]) + + # check we can't call anything any more + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + +def incoming_fail(q, bus, conn, stream): + chan, bare_jid, full_jid, self_handle_name = \ + setup_incoming_tests(q, bus, conn, stream) + + call_async(q, chan, 'Fail', + ycs.ERROR_TYPE_AUTH, 'auth', 'omgwtfbbq', + 'I most certainly dont feel like dancing') + + _, e = q.expect_many(EventPattern('dbus-return', method='Fail'), + EventPattern('stream-message')) + + iq = e.stanza + assertEquals('le-loldongs', iq['id']) + assertEquals('error', iq['type']) + assertEquals(self_handle_name, iq['from']) + assertEquals(full_jid, iq['to']) + assertEquals(2, len(iq.children)) + + def check_message(message): + assertEquals('message', message.name) + assertEquals(ycs.MESSAGE_NS, message.uri) + assertEquals('beautiful pea green boat', message['seacraft']) + assertEquals('sea', message['destination']) + assertEquals('the pussy cat', message['owl-companions']) + assertEquals('the.from.service', message['from-service']) + assertEquals('the.to.service', message['to-service']) + assertEquals(1, len(message.children)) + + lol = message.children[0] + + assertEquals('lol', lol.name) + assertEquals('fill', lol['to']) + assertEquals('time', lol['the']) + assertEquals('stuff', lol['some']) + assertEquals(1, len(lol.children)) + + look = lol.children[0] + + assertEquals('look-into-my-eyes', look.name) + assertEquals(1, len(look.children)) + assertEquals('and tell me how boring writing these tests is', look.children[0]) + + def check_error(error): + assertEquals('error', error.name) + assertEquals('auth', error['type']) + assertEquals(3, len(error.children)) + + for c in error.children: + if c.name == 'auth': + assertEquals(ns.STANZA, c.uri) + elif c.name == 'omgwtfbbq': + assertEquals(ycs.MESSAGE_NS, c.uri) + elif c.name == 'text': + assertEquals(ns.STANZA, c.uri) + assertEquals(1, len(c.children)) + assertEquals('I most certainly dont feel like dancing', + c.children[0]) + else: + raise + + for child in iq.children: + if child.name == 'message': + check_message(child) + elif child.name == 'error': + check_error(child) + else: + raise + + # check we can't call anything any more + call_async(q, chan, 'Fail', ycs.ERROR_TYPE_CANCEL, 'lol', 'whut', 'pear') + q.expect('dbus-error', method='Fail') + + call_async(q, chan, 'Reply', {'lol':'whut'}, '') + q.expect('dbus-error', method='Reply') + + call_async(q, chan, 'Request') + q.expect('dbus-error', method='Request') + +if __name__ == '__main__': + exec_test(outgoing_reply) + exec_test(outgoing_fail) + exec_test(bad_requests) + exec_test(incoming_reply) + exec_test(incoming_fail) diff --git a/tests/twisted/gabble/service.py b/tests/twisted/gabble/service.py new file mode 100644 index 0000000..6ee49f4 --- /dev/null +++ b/tests/twisted/gabble/service.py @@ -0,0 +1,307 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from gabbletest import exec_test, make_result_iq, sync_stream +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CLIENT_NAME = 'il-cliente-del-futuro' + +client = 'http://telepathy.im/fake' +client_caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +# TODO: move more of the common parts of this test into different +# functions to cut out the duplication! + +def test(q, bus, conn, stream): + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + # announce a contact with the right caps + bare_jid = "test-service@example.com" + full_jid = bare_jid + "/NeeNawNeeNawIAmAnAmbulance" + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, banshee, True, None) + + # this will be fired as text channel caps will be fired + _, e = q.expect_many(EventPattern('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]), + EventPattern('dbus-signal', signal='ServiceAdded')) + + contact_id, service_name, details = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])}, + }, discovered) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, tmp, False) + + e = q.expect('dbus-signal', signal='ServiceAdded') + + contact_id, service_name, details = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # remove evince + forbidden = [EventPattern('dbus-signal', signal='stream-iq')] + q.forbid_events(forbidden) + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + e = q.expect('dbus-signal', signal='ServiceRemoved') + + contact_id, service_name = e.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])}, + }, discovered) + + sync_stream(q, stream) + + q.unforbid_events(forbidden) + + # now just evince + client_caps['ver'] = compute_caps_hash(identity, features, evince) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, evince, False) + + sa, sr = q.expect_many(EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')) + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + contact_id, service_name = sr.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # just banshee again + forbidden = [EventPattern('dbus-signal', signal='stream-iq')] + q.forbid_events(forbidden) + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + sr, sa = q.expect_many(EventPattern('dbus-signal', signal='ServiceRemoved'), + EventPattern('dbus-signal', signal='ServiceAdded')) + + contact_id, service_name = sr.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Banshee', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'])} + }, discovered) + + sync_stream(q, stream) + + # both again + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + send_presence(q, conn, stream, full_jid, client_caps, initial=False) + + sa = q.expect('dbus-signal', signal='ServiceAdded') + + contact_id, service_name, details = sa.args + assertEquals(bare_jid, contact_id) + assertEquals('org.gnome.Evince', service_name) + + type, name_map, caps = details + assertEquals('application', type) + assertEquals({'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, name_map) + assertSameSets(['urn:ytstenut:capabilities:pics'], caps) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + sync_stream(q, stream) + + q.unforbid_events(forbidden) + + # and finally, nothing + client_caps['ver'] = compute_caps_hash(identity, features, {}) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, {}, False) + + q.expect_many(EventPattern('dbus-signal', signal='ServiceRemoved', + args=[bare_jid, 'org.gnome.Banshee']), + EventPattern('dbus-signal', signal='ServiceRemoved', + args=[bare_jid, 'org.gnome.Evince'])) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + # super. + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/sidecar.py b/tests/twisted/gabble/sidecar.py new file mode 100755 index 0000000..92e16b7 --- /dev/null +++ b/tests/twisted/gabble/sidecar.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +from gabbleservicetest import call_async, EventPattern, assertEquals +from gabbletest import exec_test +from yconstants import STATUS_IFACE + +def test(q, bus, conn, stream): + call_async(q, conn.Future, 'EnsureSidecar', STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/slow-service.py b/tests/twisted/gabble/slow-service.py new file mode 100644 index 0000000..d51cdd8 --- /dev/null +++ b/tests/twisted/gabble/slow-service.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from gabbletest import exec_test, make_result_iq, sync_stream +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CLIENT_NAME = 'il-cliente-del-futuro' + +client = 'http://telepathy.im/fake' +client_caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +def test(q, bus, conn, stream): + bare_jid = "test-service@example.com" + full_jid = bare_jid + "/NeeNawNeeNawIAmAnAmbulance" + + # we don't want these two signalled, ever. + forbidden = [EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.Connect() + + _, e = q.expect_many(EventPattern('dbus-signal', signal='StatusChanged', + args=[0, 1]), + EventPattern('stream-iq', query_ns=ns.ROSTER, + iq_type='get', query_name='query')) + + e.stanza['type'] = 'result' + + item = e.query.addElement('item') + item['jid'] = bare_jid + item['subscription'] = 'both' + + stream.send(e.stanza) + + q.expect('dbus-signal', signal='ContactListStateChanged', + args=[cs.CONTACT_LIST_STATE_SUCCESS]) + + # announce a contact with the right caps + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + client_caps['ver'] = compute_caps_hash(identity, features, banshee) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, banshee, True, None) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + client_caps['ver'] = compute_caps_hash(identity, features, tmp) + presence_and_disco(q, conn, stream, full_jid, True, client, client_caps, + features, identity, tmp, False) + + sync_stream(q, stream) + + # now finally ensure the sidecar + path, props = conn.Future.EnsureSidecar(ycs.STATUS_IFACE) + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({bare_jid: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # sweet. + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabble/status.py b/tests/twisted/gabble/status.py new file mode 100644 index 0000000..8917575 --- /dev/null +++ b/tests/twisted/gabble/status.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from gabbleservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals +from gabbletest import exec_test, make_result_iq, acknowledge_iq +import gabbleconstants as cs +import yconstants as ycs +from gabblecaps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +CAP_NAME = 'urn:ytstenut:capabilities:h264-over-ants' +CLIENT_NAME = 'fake-client' + +client = 'http://telepathy.im/fake' +caps = {'ver': '0.1', 'node': client} +features = [ + ns.JINGLE_015, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.GOOGLE_P2P, + ycs.SERVICE_NS + '#the.target.service', + CAP_NAME + '+notify' + ] +identity = ['client/pc/en/Lolclient 0.L0L'] + +def check_pep_set(iq): + pubsub = iq.children[0] + publish = pubsub.children[0] + item = publish.children[0] + status_el = item.children[0] + + desc = None + if status_el.children: + desc = status_el.children[0] + + assertEquals('set', iq['type']) + assertEquals(1, len(iq.children)) + + assertEquals('pubsub', pubsub.name) + assertEquals(ns.PUBSUB, pubsub.uri) + assertEquals(1, len(pubsub.children)) + + assertEquals('publish', publish.name) + assertEquals(CAP_NAME, publish['node']) + assertEquals(1, len(publish.children)) + + assertEquals('item', item.name) + assertEquals(1, len(item.children)) + + assertEquals('status', status_el.name) + + if desc: + assertEquals(1, len(status_el.children)) + + assertEquals('description', desc.name) + assertEquals(1, len(desc.children)) + + return status_el, desc + +def send_back_pep_event(stream, status_el): + msg = Element((None, 'message')) + msg['type'] = 'headline' + msg['from'] = 'test@localhost' + msg['to'] = 'test@localhost/Resource' + msg['id'] = 'le-headline' + event = msg.addElement('event') + event['xmlns'] = ns.PUBSUB_EVENT + items = event.addElement('items') + items['node'] = CAP_NAME + item = items.addElement('item') + + # just steal this + item.addChild(status_el) + + # and go + stream.send(msg) + +def test(q, bus, conn, stream): + # we won't be using any data forms, so these two shouldn't ever be + # fired. + q.forbid_events([EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')]) + + call_async(q, conn.Future, 'EnsureSidecar', ycs.STATUS_IFACE) + + conn.Connect() + + # Now we're connected, the call we made earlier should return. + e = q.expect('dbus-return', method='EnsureSidecar') + path, props = e.value + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + # bad capability argument + call_async(q, status, 'AdvertiseStatus', '', 'service.name', '') + q.expect('dbus-error', method='AdvertiseStatus') + + # bad service name + call_async(q, status, 'AdvertiseStatus', CAP_NAME, '', '') + q.expect('dbus-error', method='AdvertiseStatus') + + # we can't test that the message type="headline" stanza is + # actually received because it's thrown into the loopback stream + # immediately. + + # announce a contact with the right caps + bare_jid = "test-status@example.com" + full_jid = bare_jid + "/BIGGESTRESOURCEEVAAAAHHH" + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [bare_jid])[0] + + presence_and_disco(q, conn, stream, full_jid, True, client, caps, + features, identity, {}, True, None) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # okay now we know about the contact's caps, we can go ahead + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + + el = Element(('urn:ytstenut:status', 'status')) + el['activity'] = 'messing-with-your-stuff' + desc = el.addElement('ytstenut:description', content='Yeah sorry about that') + desc['xml:lang'] = 'en-GB' + + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'ants.in.their.pants', el.toXml()) + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('messing-with-your-stuff', status_el['activity']) + assertEquals('ants.in.their.pants', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assertEquals('Yeah sorry about that', desc.children[0]) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('ants.in.their.pants', service_name) + assertNotEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: {'ants.in.their.pants': status_str}}}, + discovered) + + # set another + el = Element(('urn:ytstenut:status', 'status')) + el['activity'] = 'rofling' + desc = el.addElement('ytstenut:description', content='U MAD?') + desc['xml:lang'] = 'en-GB' + + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'bananaman.on.holiday', el.toXml()) + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('rofling', status_el['activity']) + assertEquals('bananaman.on.holiday', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assertEquals('U MAD?', desc.children[0]) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, bananaman_status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('bananaman.on.holiday', service_name) + assertNotEquals('', bananaman_status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: { + 'ants.in.their.pants': status_str, + 'bananaman.on.holiday': bananaman_status_str}}}, + discovered) + + # unset the status from one service + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'ants.in.their.pants', '') + + e, _, = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + status_el, desc = check_pep_set(e.stanza) + assertEquals('ants.in.their.pants', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assert 'activity' not in status_el.attributes + assertEquals([], status_el.children) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('ants.in.their.pants', service_name) + assertEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({'test@localhost': {CAP_NAME: { + 'bananaman.on.holiday': bananaman_status_str}}}, + discovered) + + # unset the status from the other service + call_async(q, status, 'AdvertiseStatus', CAP_NAME, + 'bananaman.on.holiday', '') + + e, _ = q.expect_many(EventPattern('stream-iq'), + EventPattern('dbus-return', method='AdvertiseStatus')) + + # check message + status_el, desc = check_pep_set(e.stanza) + assertEquals('bananaman.on.holiday', status_el['from-service']) + assertEquals(CAP_NAME, status_el['capability']) + assert 'activity' not in status_el.attributes + assertEquals([], status_el.children) + + acknowledge_iq(stream, e.stanza) + send_back_pep_event(stream, status_el) + + sig = q.expect('dbus-signal', signal='StatusChanged', + interface=ycs.STATUS_IFACE) + + # check signal + contact_id, capability, service_name, status_str = sig.args + assertEquals(CAP_NAME, capability) + assertEquals('bananaman.on.holiday', service_name) + assertEquals('', status_str) + + # check property + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredStatuses', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({}, discovered) + +if __name__ == '__main__': + exec_test(test, do_connect=False) diff --git a/tests/twisted/gabblecaps_helper.py b/tests/twisted/gabblecaps_helper.py new file mode 100644 index 0000000..6f80ba1 --- /dev/null +++ b/tests/twisted/gabblecaps_helper.py @@ -0,0 +1,356 @@ +# vim: set fileencoding=utf-8 : +import hashlib +import base64 +import dbus + +from twisted.words.xish import domish, xpath +from gabbletest import make_result_iq, make_presence, elem_iq, elem +from gabbleservicetest import ( + EventPattern, + assertEquals, assertContains, assertDoesNotContain, assertLength, + ) + +import config +import ns +import gabbleconstants as cs + +# The caps we always have, regardless of any clients' caps +FIXED_CAPS = [ + ns.JINGLE, + ns.JINGLE_015, + ns.GOOGLE_FEAT_SESSION, + ns.JINGLE_TRANSPORT_RAWUDP, + ns.NICK, + ns.NICK + '+notify', + ns.CHAT_STATES, + ns.SI, + ns.IBB, + ns.BYTESTREAMS, + ] + +JINGLE_CAPS = [ + # Additional Jingle transports + ns.JINGLE_TRANSPORT_ICEUDP, + ns.GOOGLE_P2P, + # Jingle content types + ns.GOOGLE_FEAT_VOICE, + ns.GOOGLE_FEAT_VIDEO, + ns.JINGLE_015_AUDIO, + ns.JINGLE_015_VIDEO, + ns.JINGLE_RTP, + ns.JINGLE_RTP_AUDIO, + ns.JINGLE_RTP_VIDEO, + ] + +VARIABLE_CAPS = ( + JINGLE_CAPS + + [ + ns.FILE_TRANSFER, + + # FIXME: currently we always advertise these, but in future we should + # only advertise them if >= 1 client supports them: + # ns.TUBES, + + # there is an unlimited set of these; only the ones actually relevant to + # the tests so far are shown here + ns.TUBES + '/stream#x-abiword', + ns.TUBES + '/stream#daap', + ns.TUBES + '/stream#http', + ns.TUBES + '/dbus#com.example.Go', + ns.TUBES + '/dbus#com.example.Xiangqi', + ]) + +def check_caps(namespaces, desired): + """Assert that all the FIXED_CAPS are supported, and of the VARIABLE_CAPS, + every capability in desired is supported, and every other capability is + not. + """ + for c in FIXED_CAPS: + assertContains(c, namespaces) + + for c in VARIABLE_CAPS: + if c in desired: + assertContains(c, namespaces) + else: + assertDoesNotContain(c, namespaces) + +text_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_TEXT + }) +text_allowed_properties = dbus.Array([cs.TARGET_HANDLE]) + +stream_tube_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_STREAM_TUBE + }) +stream_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE, + cs.TARGET_ID, cs.STREAM_TUBE_SERVICE]) + +dbus_tube_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_DBUS_TUBE + }) +dbus_tube_allowed_properties = dbus.Array([cs.TARGET_HANDLE, + cs.TARGET_ID, cs.DBUS_TUBE_SERVICE_NAME]) + +ft_fixed_properties = dbus.Dictionary({ + cs.TARGET_HANDLE_TYPE: cs.HT_CONTACT, + cs.CHANNEL_TYPE: cs.CHANNEL_TYPE_FILE_TRANSFER, + }) +ft_allowed_properties = dbus.Array([ + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType', + cs.TARGET_HANDLE, + cs.TARGET_ID, + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentType', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Filename', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Size', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Description', + cs.CHANNEL_TYPE_FILE_TRANSFER + '.Date', + cs.FT_URI]) + +fake_client_dataforms = { + 'urn:xmpp:dataforms:softwareinfo': + {'software': ['A Fake Client with Twisted'], + 'software_version': ['5.11.2-svn-20080512'], + 'os': ['Debian GNU/Linux unstable (sid) unstable sid'], + 'os_version': ['2.6.24-1-amd64'], + }, +} + +def compute_caps_hash(identities, features, dataforms): + """ + Accepts a list of slash-separated identities, a list of feature namespaces, + and a map from FORM_TYPE to (map from field name to values), returns the + verification string as defined by + <http://xmpp.org/extensions/xep-0115.html#ver>. + """ + components = [] + + for identity in sorted(identities): + if len(identity.split('/')) != 4: + raise ValueError( + "expecting identities of the form " + + "'category/type/lang/client': got " + repr(identity)) + + components.append(identity) + + for feature in sorted(features): + components.append(feature) + + for form_type in sorted(dataforms.keys()): + components.append(form_type) + + for var in sorted(dataforms[form_type].keys()): + components.append(var) + + for value in sorted(dataforms[form_type][var]): + components.append(value) + + components.append('') + + m = hashlib.sha1() + S = u'<'.join(components) + m.update(S.encode('utf-8')) + return base64.b64encode(m.digest()) + +def make_caps_disco_reply(stream, req, identities, features, dataforms={}): + iq = make_result_iq(stream, req) + query = iq.firstChildElement() + + for identity in identities: + category, type_, lang, name = identity.split('/') + el = query.addElement('identity') + el['category'] = category + el['type'] = type_ + el['name'] = name + el['xml:lang'] = lang + + for f in features: + el = domish.Element((None, 'feature')) + el['var'] = f + query.addChild(el) + + for type, fields in dataforms.iteritems(): + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + field = x.addElement('field') + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement('value', content=type) + + for var, values in fields.iteritems(): + field = x.addElement('field') + field['var'] = var + + for value in values: + field.addElement('value', content=value) + + return iq + +def receive_presence_and_ask_caps(q, stream, expect_dbus=True): + # receive presence stanza + if expect_dbus: + presence, event_dbus = q.expect_many( + EventPattern('stream-presence'), + EventPattern('dbus-signal', signal='ContactCapabilitiesChanged') + ) + assertLength(1, event_dbus.args) + signaled_caps = event_dbus.args[0] + else: + presence = q.expect('stream-presence') + signaled_caps = None + + return disco_caps(q, stream, presence) + (signaled_caps,) + +def extract_disco_parts(stanza): + identity_nodes = xpath.queryForNodes('/iq/query/identity', stanza) + assertLength(1, identity_nodes) + identity_node = identity_nodes[0] + + assertEquals('client', identity_node['category']) + assertDoesNotContain('xml:lang', identity_node.attributes) + + identity = 'client/%s//%s' % (identity_node['type'], identity_node['name']) + + features = [] + for feature in xpath.queryForNodes('/iq/query/feature', stanza): + features.append(feature['var']) + + # a quick and ugly data form extractor + x_nodes = xpath.queryForNodes('/iq/query/x', stanza) or [] + dataforms = {} + for form in x_nodes: + name = None + fields = {} + for field in xpath.queryForNodes('/x/field', form): + if field['var'] == 'FORM_TYPE': + name = str(field.firstChildElement()) + else: + values = [str(x) for x in xpath.queryForNodes('/field/value', field)] + + fields[field['var']] = values + + if name is not None: + dataforms[name] = fields + + return ([identity], features, dataforms) + +def disco_caps(q, stream, presence): + c_nodes = xpath.queryForNodes('/presence/c', presence.stanza) + assert c_nodes is not None + assertLength(1, c_nodes) + hash = c_nodes[0].attributes['hash'] + ver = c_nodes[0].attributes['ver'] + node = c_nodes[0].attributes['node'] + assertEquals('sha-1', hash) + + # ask caps + request = \ + elem_iq(stream, 'get', from_='fake_contact@jabber.org/resource')( + elem(ns.DISCO_INFO, 'query', node=(node + '#' + ver)) + ) + stream.send(request) + + # receive caps + event = q.expect('stream-iq', query_ns=ns.DISCO_INFO, iq_id=request['id']) + + # Check that Gabble's announcing the identity we think it should be. + (identities, features, dataforms) = extract_disco_parts(event.stanza) + + # Check if the hash matches the announced capabilities + assertEquals(compute_caps_hash(identities, features, dataforms), ver) + + return (event, features, dataforms) + +def caps_contain(event, cap): + node = xpath.queryForNodes('/iq/query/feature[@var="%s"]' + % cap, + event.stanza) + if node is None: + return False + if len(node) != 1: + return False + var = node[0].attributes['var'] + if var is None: + return False + return var == cap + +def presence_and_disco(q, conn, stream, contact, disco, + client, caps, + features, identities=[], dataforms={}, + initial=True, show=None): + h = send_presence(q, conn, stream, contact, caps, initial=initial, + show=show) + + if disco: + stanza = expect_disco(q, contact, client, caps) + send_disco_reply(stream, stanza, identities, features, dataforms) + + return h + +def send_presence(q, conn, stream, contact, caps, initial=True, show=None): + h = conn.RequestHandles(cs.HT_CONTACT, [contact])[0] + + if initial: + stream.send(make_presence(contact, status='hello')) + + q.expect_many( + EventPattern('dbus-signal', signal='PresenceUpdate', + args=[{h: + (0L, {u'available': {'message': 'hello'}})}]), + EventPattern('dbus-signal', signal='PresencesChanged', + args=[{h: + (2, u'available', 'hello')}])) + + # no special capabilities + assertEquals([(h, cs.CHANNEL_TYPE_TEXT, 3, 0)], + conn.Capabilities.GetCapabilities([h])) + + # send updated presence with caps info + stream.send(make_presence(contact, show=show, status='hello', caps=caps)) + + return h + +def expect_disco(q, contact, client, caps): + # Gabble looks up our capabilities + event = q.expect('stream-iq', to=contact, query_ns=ns.DISCO_INFO) + assertEquals(client + '#' + caps['ver'], event.query['node']) + + return event.stanza + +def send_disco_reply(stream, stanza, identities, features, dataforms={}): + stream.send( + make_caps_disco_reply(stream, stanza, identities, features, dataforms)) + +if __name__ == '__main__': + # example from XEP-0115 + assertEquals('QgayPKawpkPSDYmwT/WM94uAlu0=', + compute_caps_hash(['client/pc//Exodus 0.9.1'], + ["http://jabber.org/protocol/disco#info", + "http://jabber.org/protocol/disco#items", + "http://jabber.org/protocol/muc", + "http://jabber.org/protocol/caps"], + {})) + + # another example from XEP-0115 + identities = [u'client/pc/en/Psi 0.11', u'client/pc/el/Ψ 0.11'] + features = [ + u'http://jabber.org/protocol/caps', + u'http://jabber.org/protocol/disco#info', + u'http://jabber.org/protocol/disco#items', + u'http://jabber.org/protocol/muc', + ] + dataforms = { + u'urn:xmpp:dataforms:softwareinfo': + { u'ip_version': [u'ipv4', u'ipv6'], + u'os': [u'Mac'], + u'os_version': [u'10.5.1'], + u'software': [u'Psi'], + u'software_version': [u'0.11'], + }, + } + assertEquals('q07IKJEyjvHSyhy//CH0CxmKi8w=', + compute_caps_hash(identities, features, dataforms)) diff --git a/tests/twisted/gabbleconstants.py b/tests/twisted/gabbleconstants.py new file mode 100644 index 0000000..aff01d1 --- /dev/null +++ b/tests/twisted/gabbleconstants.py @@ -0,0 +1,458 @@ +""" +Some handy constants for other tests to share and enjoy. +""" + +from dbus import PROPERTIES_IFACE + +CM = "org.freedesktop.Telepathy.ConnectionManager" + +HT_NONE = 0 +HT_CONTACT = 1 +HT_ROOM = 2 +HT_LIST = 3 +HT_GROUP = 4 + +CHANNEL = "org.freedesktop.Telepathy.Channel" + +CHANNEL_IFACE_CALL_STATE = CHANNEL + ".Interface.CallState" +CHANNEL_IFACE_CHAT_STATE = CHANNEL + '.Interface.ChatState' +CHANNEL_IFACE_DESTROYABLE = CHANNEL + ".Interface.Destroyable" +CHANNEL_IFACE_DTMF = CHANNEL + ".Interface.DTMF" +CHANNEL_IFACE_GROUP = CHANNEL + ".Interface.Group" +CHANNEL_IFACE_HOLD = CHANNEL + ".Interface.Hold" +CHANNEL_IFACE_MEDIA_SIGNALLING = CHANNEL + ".Interface.MediaSignalling" +CHANNEL_IFACE_MESSAGES = CHANNEL + ".Interface.Messages" +CHANNEL_IFACE_PASSWORD = CHANNEL + ".Interface.Password" +CHANNEL_IFACE_TUBE = CHANNEL + ".Interface.Tube" +CHANNEL_IFACE_SASL_AUTH = CHANNEL + ".Interface.SASLAuthentication" +CHANNEL_IFACE_CONFERENCE = CHANNEL + '.Interface.Conference' +CHANNEL_IFACE_ROOM = CHANNEL + '.Interface.Room.DRAFT' + +CHANNEL_TYPE_CALL = CHANNEL + ".Type.Call.DRAFT" +CHANNEL_TYPE_CONTACT_LIST = CHANNEL + ".Type.ContactList" +CHANNEL_TYPE_CONTACT_SEARCH = CHANNEL + ".Type.ContactSearch" +CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" +CHANNEL_TYPE_TUBES = CHANNEL + ".Type.Tubes" +CHANNEL_TYPE_STREAM_TUBE = CHANNEL + ".Type.StreamTube" +CHANNEL_TYPE_DBUS_TUBE = CHANNEL + ".Type.DBusTube" +CHANNEL_TYPE_STREAMED_MEDIA = CHANNEL + ".Type.StreamedMedia" +CHANNEL_TYPE_TEXT = CHANNEL + ".Type.Text" +CHANNEL_TYPE_FILE_TRANSFER = CHANNEL + ".Type.FileTransfer" +CHANNEL_TYPE_SERVER_AUTHENTICATION = \ + CHANNEL + ".Type.ServerAuthentication" +CHANNEL_TYPE_SERVER_TLS_CONNECTION = \ + CHANNEL + ".Type.ServerTLSConnection" + +TP_AWKWARD_PROPERTIES = "org.freedesktop.Telepathy.Properties" +PROPERTY_FLAG_READ = 1 +PROPERTY_FLAG_WRITE = 2 +PROPERTY_FLAGS_RW = PROPERTY_FLAG_READ | PROPERTY_FLAG_WRITE + +CHANNEL_TYPE = CHANNEL + '.ChannelType' +TARGET_HANDLE_TYPE = CHANNEL + '.TargetHandleType' +TARGET_HANDLE = CHANNEL + '.TargetHandle' +TARGET_ID = CHANNEL + '.TargetID' +REQUESTED = CHANNEL + '.Requested' +INITIATOR_HANDLE = CHANNEL + '.InitiatorHandle' +INITIATOR_ID = CHANNEL + '.InitiatorID' +INTERFACES = CHANNEL + '.Interfaces' + +INITIAL_AUDIO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialAudio' +INITIAL_VIDEO = CHANNEL_TYPE_STREAMED_MEDIA + '.InitialVideo' +IMMUTABLE_STREAMS = CHANNEL_TYPE_STREAMED_MEDIA + '.ImmutableStreams' + +CALL_INITIAL_AUDIO = CHANNEL_TYPE_CALL + '.InitialAudio' +CALL_INITIAL_AUDIO_NAME = CHANNEL_TYPE_CALL + '.InitialAudioName' +CALL_INITIAL_VIDEO = CHANNEL_TYPE_CALL + '.InitialVideo' +CALL_INITIAL_VIDEO_NAME = CHANNEL_TYPE_CALL + '.InitialVideoName' +CALL_MUTABLE_CONTENTS = CHANNEL_TYPE_CALL + '.MutableContents' + +CALL_CONTENT = 'org.freedesktop.Telepathy.Call.Content.DRAFT' +CALL_CONTENT_IFACE_MEDIA = \ + 'org.freedesktop.Telepathy.Call.Content.Interface.Media.DRAFT' + +CALL_CONTENT_CODECOFFER = \ + 'org.freedesktop.Telepathy.Call.Content.CodecOffer.DRAFT' + +CALL_STREAM = 'org.freedesktop.Telepathy.Call.Stream.DRAFT' +CALL_STREAM_IFACE_MEDIA = \ + 'org.freedesktop.Telepathy.Call.Stream.Interface.Media.DRAFT' + +CALL_STREAM_ENDPOINT = 'org.freedesktop.Telepathy.Call.Stream.Endpoint.DRAFT' + +CALL_MEDIA_TYPE_AUDIO = 0 +CALL_MEDIA_TYPE_VIDEO = 1 + +CALL_CONTENT_PACKETIZATION_RTP = 0 +CALL_CONTENT_PACKETIZATION_RAW = 1 +CALL_CONTENT_PACKETIZATION_MSN_WEBCAM = 2 + +CALL_STREAM_TRANSPORT_RAW_UDP = 1 +CALL_STREAM_TRANSPORT_ICE = 2 +CALL_STREAM_TRANSPORT_GOOGLE = 3 + +CALL_STATE_UNKNOWN = 0 +CALL_STATE_PENDING_INITIATOR = 1 +CALL_STATE_PENDING_RECEIVER = 2 +CALL_STATE_ACCEPTED = 3 +CALL_STATE_ENDED = 4 + +CALL_MEMBER_FLAG_RINGING = 1 +CALL_MEMBER_FLAG_HELD = 2 + +CALL_DISPOSITION_NONE = 0 +CALL_DISPOSITION_INITIAL = 1 + +CALL_SENDING_STATE_NONE = 0 +CALL_SENDING_STATE_PENDING_SEND = 1 +CALL_SENDING_STATE_SENDING = 2 + +SUBSCRIPTION_STATE_UNKNOWN = 0 +SUBSCRIPTION_STATE_NO = 1 +SUBSCRIPTION_STATE_REMOVED_REMOTELY = 2 +SUBSCRIPTION_STATE_ASK = 3 +SUBSCRIPTION_STATE_YES = 4 + +CONTACT_LIST_STATE_NONE = 0 +CONTACT_LIST_STATE_WAITING = 1 +CONTACT_LIST_STATE_FAILURE = 2 +CONTACT_LIST_STATE_SUCCESS = 3 + +CONN = "org.freedesktop.Telepathy.Connection" +CONN_IFACE_AVATARS = CONN + '.Interface.Avatars' +CONN_IFACE_ALIASING = CONN + '.Interface.Aliasing' +CONN_IFACE_CAPS = CONN + '.Interface.Capabilities' +CONN_IFACE_CONTACTS = CONN + '.Interface.Contacts' +CONN_IFACE_CONTACT_CAPS = CONN + '.Interface.ContactCapabilities' +CONN_IFACE_CONTACT_INFO = CONN + ".Interface.ContactInfo" +CONN_IFACE_PRESENCE = CONN + '.Interface.Presence' +CONN_IFACE_SIMPLE_PRESENCE = CONN + '.Interface.SimplePresence' +CONN_IFACE_REQUESTS = CONN + '.Interface.Requests' +CONN_IFACE_LOCATION = CONN + '.Interface.Location' +CONN_IFACE_GABBLE_DECLOAK = CONN + '.Interface.Gabble.Decloak' +CONN_IFACE_MAIL_NOTIFICATION = CONN + '.Interface.MailNotification' +CONN_IFACE_CONTACT_LIST = CONN + '.Interface.ContactList' +CONN_IFACE_CONTACT_GROUPS = CONN + '.Interface.ContactGroups' +CONN_IFACE_CLIENT_TYPES = CONN + '.Interface.ClientTypes' +CONN_IFACE_POWER_SAVING = CONN + '.Interface.PowerSaving' + +ATTR_CONTACT_CAPABILITIES = CONN_IFACE_CONTACT_CAPS + '/capabilities' + +STREAM_HANDLER = 'org.freedesktop.Telepathy.Media.StreamHandler' + +ERROR = 'org.freedesktop.Telepathy.Error' +INVALID_ARGUMENT = ERROR + '.InvalidArgument' +NOT_IMPLEMENTED = ERROR + '.NotImplemented' +NOT_AVAILABLE = ERROR + '.NotAvailable' +PERMISSION_DENIED = ERROR + '.PermissionDenied' +OFFLINE = ERROR + '.Offline' +NOT_CAPABLE = ERROR + '.NotCapable' +CONNECTION_REFUSED = ERROR + '.ConnectionRefused' +CONNECTION_FAILED = ERROR + '.ConnectionFailed' +CONNECTION_LOST = ERROR + '.ConnectionLost' +CANCELLED = ERROR + '.Cancelled' +DISCONNECTED = ERROR + '.Disconnected' +REGISTRATION_EXISTS = ERROR + '.RegistrationExists' +AUTHENTICATION_FAILED = ERROR + '.AuthenticationFailed' +CONNECTION_REPLACED = ERROR + '.ConnectionReplaced' +ALREADY_CONNECTED = ERROR + '.AlreadyConnected' +NETWORK_ERROR = ERROR + '.NetworkError' +NOT_YET = ERROR + '.NotYet' +INVALID_HANDLE = ERROR + '.InvalidHandle' +CERT_UNTRUSTED = ERROR + '.Cert.Untrusted' +SERVICE_BUSY = ERROR + '.ServiceBusy' +SERVICE_CONFUSED = ERROR + '.ServiceConfused' + +BANNED = ERROR + '.Channel.Banned' + +UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod' + +TUBE_PARAMETERS = CHANNEL_IFACE_TUBE + '.Parameters' +TUBE_STATE = CHANNEL_IFACE_TUBE + '.State' +STREAM_TUBE_SERVICE = CHANNEL_TYPE_STREAM_TUBE + '.Service' +DBUS_TUBE_SERVICE_NAME = CHANNEL_TYPE_DBUS_TUBE + '.ServiceName' +DBUS_TUBE_DBUS_NAMES = CHANNEL_TYPE_DBUS_TUBE + '.DBusNames' +DBUS_TUBE_SUPPORTED_ACCESS_CONTROLS = CHANNEL_TYPE_DBUS_TUBE + '.SupportedAccessControls' +STREAM_TUBE_SUPPORTED_SOCKET_TYPES = CHANNEL_TYPE_STREAM_TUBE + '.SupportedSocketTypes' + +CONFERENCE_INITIAL_CHANNELS = CHANNEL_IFACE_CONFERENCE + '.InitialChannels' +CONFERENCE_INITIAL_INVITEE_HANDLES = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeHandles' +CONFERENCE_INITIAL_INVITEE_IDS = CHANNEL_IFACE_CONFERENCE + '.InitialInviteeIDs' + +CONTACT_SEARCH_ASK = CHANNEL_TYPE_CONTACT_SEARCH + '.AvailableSearchKeys' +CONTACT_SEARCH_SERVER = CHANNEL_TYPE_CONTACT_SEARCH + '.Server' +CONTACT_SEARCH_STATE = CHANNEL_TYPE_CONTACT_SEARCH + '.SearchState' + +SEARCH_NOT_STARTED = 0 +SEARCH_IN_PROGRESS = 1 +SEARCH_MORE_AVAILABLE = 2 +SEARCH_COMPLETED = 3 +SEARCH_FAILED = 4 + +TUBE_CHANNEL_STATE_LOCAL_PENDING = 0 +TUBE_CHANNEL_STATE_REMOTE_PENDING = 1 +TUBE_CHANNEL_STATE_OPEN = 2 +TUBE_CHANNEL_STATE_NOT_OFFERED = 3 + +MEDIA_STREAM_TYPE_AUDIO = 0 +MEDIA_STREAM_TYPE_VIDEO = 1 + +MEDIA_STREAM_BASE_PROTO_UDP = 0 +MEDIA_STREAM_BASE_PROTO_TCP = 1 + +MEDIA_STREAM_TRANSPORT_TYPE_LOCAL = 0 +MEDIA_STREAM_TRANSPORT_TYPE_DERIVED = 1 +MEDIA_STREAM_TRANSPORT_TYPE_RELAY = 2 + +SOCKET_ADDRESS_TYPE_UNIX = 0 +SOCKET_ADDRESS_TYPE_ABSTRACT_UNIX = 1 +SOCKET_ADDRESS_TYPE_IPV4 = 2 +SOCKET_ADDRESS_TYPE_IPV6 = 3 + +SOCKET_ACCESS_CONTROL_LOCALHOST = 0 +SOCKET_ACCESS_CONTROL_PORT = 1 +SOCKET_ACCESS_CONTROL_NETMASK = 2 +SOCKET_ACCESS_CONTROL_CREDENTIALS = 3 + +TUBE_STATE_LOCAL_PENDING = 0 +TUBE_STATE_REMOTE_PENDING = 1 +TUBE_STATE_OPEN = 2 +TUBE_STATE_NOT_OFFERED = 3 + +TUBE_TYPE_DBUS = 0 +TUBE_TYPE_STREAM = 1 + +MEDIA_STREAM_DIRECTION_NONE = 0 +MEDIA_STREAM_DIRECTION_SEND = 1 +MEDIA_STREAM_DIRECTION_RECEIVE = 2 +MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3 + +MEDIA_STREAM_PENDING_LOCAL_SEND = 1 +MEDIA_STREAM_PENDING_REMOTE_SEND = 2 + +MEDIA_STREAM_TYPE_AUDIO = 0 +MEDIA_STREAM_TYPE_VIDEO = 1 + +MEDIA_STREAM_STATE_DISCONNECTED = 0 +MEDIA_STREAM_STATE_CONNECTING = 1 +MEDIA_STREAM_STATE_CONNECTED = 2 + +MEDIA_STREAM_DIRECTION_NONE = 0 +MEDIA_STREAM_DIRECTION_SEND = 1 +MEDIA_STREAM_DIRECTION_RECEIVE = 2 +MEDIA_STREAM_DIRECTION_BIDIRECTIONAL = 3 + +FT_STATE_NONE = 0 +FT_STATE_PENDING = 1 +FT_STATE_ACCEPTED = 2 +FT_STATE_OPEN = 3 +FT_STATE_COMPLETED = 4 +FT_STATE_CANCELLED = 5 + +FT_STATE_CHANGE_REASON_NONE = 0 +FT_STATE_CHANGE_REASON_REQUESTED = 1 +FT_STATE_CHANGE_REASON_LOCAL_STOPPED = 2 +FT_STATE_CHANGE_REASON_REMOTE_STOPPED = 3 +FT_STATE_CHANGE_REASON_LOCAL_ERROR = 4 +FT_STATE_CHANGE_REASON_REMOTE_ERROR = 5 + +FILE_HASH_TYPE_NONE = 0 +FILE_HASH_TYPE_MD5 = 1 +FILE_HASH_TYPE_SHA1 = 2 +FILE_HASH_TYPE_SHA256 = 3 + +FT_STATE = CHANNEL_TYPE_FILE_TRANSFER + '.State' +FT_CONTENT_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentType' +FT_FILENAME = CHANNEL_TYPE_FILE_TRANSFER + '.Filename' +FT_SIZE = CHANNEL_TYPE_FILE_TRANSFER + '.Size' +FT_CONTENT_HASH_TYPE = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHashType' +FT_CONTENT_HASH = CHANNEL_TYPE_FILE_TRANSFER + '.ContentHash' +FT_DESCRIPTION = CHANNEL_TYPE_FILE_TRANSFER + '.Description' +FT_DATE = CHANNEL_TYPE_FILE_TRANSFER + '.Date' +FT_AVAILABLE_SOCKET_TYPES = CHANNEL_TYPE_FILE_TRANSFER + '.AvailableSocketTypes' +FT_TRANSFERRED_BYTES = CHANNEL_TYPE_FILE_TRANSFER + '.TransferredBytes' +FT_INITIAL_OFFSET = CHANNEL_TYPE_FILE_TRANSFER + '.InitialOffset' +FT_FILE_COLLECTION = CHANNEL_TYPE_FILE_TRANSFER + '.FUTURE.FileCollection' +FT_URI = CHANNEL_TYPE_FILE_TRANSFER + '.URI' + +GF_CAN_ADD = 1 +GF_CAN_REMOVE = 2 +GF_CAN_RESCIND = 4 +GF_MESSAGE_ADD = 8 +GF_MESSAGE_REMOVE = 16 +GF_MESSAGE_ACCEPT = 32 +GF_MESSAGE_REJECT = 64 +GF_MESSAGE_RESCIND = 128 +GF_CHANNEL_SPECIFIC_HANDLES = 256 +GF_ONLY_ONE_GROUP = 512 +GF_HANDLE_OWNERS_NOT_AVAILABLE = 1024 +GF_PROPERTIES = 2048 +GF_MEMBERS_CHANGED_DETAILED = 4096 + +GC_REASON_NONE = 0 +GC_REASON_OFFLINE = 1 +GC_REASON_KICKED = 2 +GC_REASON_BUSY = 3 +GC_REASON_INVITED = 4 +GC_REASON_BANNED = 5 +GC_REASON_ERROR = 6 +GC_REASON_INVALID_CONTACT = 7 +GC_REASON_NO_ANSWER = 8 +GC_REASON_RENAMED = 9 +GC_REASON_PERMISSION_DENIED = 10 +GC_REASON_SEPARATED = 11 + +HS_UNHELD = 0 +HS_HELD = 1 +HS_PENDING_HOLD = 2 +HS_PENDING_UNHOLD = 3 + +HSR_NONE = 0 +HSR_REQUESTED = 1 +HSR_RESOURCE_NOT_AVAILABLE = 2 + +CALL_STATE_RINGING = 1 +CALL_STATE_QUEUED = 2 +CALL_STATE_HELD = 4 +CALL_STATE_FORWARDED = 8 + +CONN_STATUS_CONNECTED = 0 +CONN_STATUS_CONNECTING = 1 +CONN_STATUS_DISCONNECTED = 2 + +CSR_NONE_SPECIFIED = 0 +CSR_REQUESTED = 1 +CSR_NETWORK_ERROR = 2 +CSR_AUTHENTICATION_FAILED = 3 +CSR_ENCRYPTION_ERROR = 4 +CSR_NAME_IN_USE = 5 +CSR_CERT_NOT_PROVIDED = 6 +CSR_CERT_UNTRUSTED = 7 +CSR_CERT_EXPIRED = 8 +CSR_CERT_NOT_ACTIVATED = 9 +CSR_CERT_HOSTNAME_MISMATCH = 10 +CSR_CERT_FINGERPRINT_MISMATCH = 11 +CSR_CERT_SELF_SIGNED = 12 +CSR_CERT_OTHER_ERROR = 13 + +BUDDY_INFO = 'org.laptop.Telepathy.BuddyInfo' +ACTIVITY_PROPERTIES = 'org.laptop.Telepathy.ActivityProperties' + +CHAT_STATE_GONE = 0 +CHAT_STATE_INACTIVE = 1 +CHAT_STATE_ACTIVE = 2 +CHAT_STATE_PAUSED = 3 +CHAT_STATE_COMPOSING = 4 + +# Channel_Media_Capabilities +MEDIA_CAP_AUDIO = 1 +MEDIA_CAP_VIDEO = 2 +MEDIA_CAP_STUN = 4 +MEDIA_CAP_GTALKP2P = 8 +MEDIA_CAP_ICEUDP = 16 +MEDIA_CAP_IMMUTABLE_STREAMS = 32 + +CLIENT = 'org.freedesktop.Telepathy.Client' + +PRESENCE_OFFLINE = 1 +PRESENCE_AVAILABLE = 2 +PRESENCE_AWAY = 3 +PRESENCE_EXTENDED_AWAY = 4 +PRESENCE_HIDDEN = 5 +PRESENCE_BUSY = 6 +PRESENCE_UNKNOWN = 7 +PRESENCE_ERROR = 8 + +CONTACT_INFO_FLAG_CAN_SET = 1 +CONTACT_INFO_FLAG_PUSH = 2 +CONTACT_INFO_FIELD_FLAG_PARAMETERS_EXACT = 1 +CONTACT_INFO_FIELD_FLAG_OVERWRITTEN_BY_NICKNAME = 2 + +# Channel_Interface_SaslAuthentication +SASL_STATUS_NOT_STARTED = 0 +SASL_STATUS_IN_PROGRESS = 1 +SASL_STATUS_SERVER_SUCCEEDED = 2 +SASL_STATUS_CLIENT_ACCEPTED = 3 +SASL_STATUS_SUCCEEDED = 4 +SASL_STATUS_SERVER_FAILED = 5 +SASL_STATUS_CLIENT_FAILED = 6 + +SASL_ABORT_REASON_INVALID_CHALLENGE = 0 +SASL_ABORT_REASON_USER_ABORT = 1 + +AUTH_METHOD = CHANNEL_TYPE_SERVER_AUTHENTICATION + ".AuthenticationMethod" +SASL_AVAILABLE_MECHANISMS = CHANNEL_IFACE_SASL_AUTH + ".AvailableMechanisms" +SASL_STATUS = CHANNEL_IFACE_SASL_AUTH + ".SASLStatus" +SASL_ERROR = CHANNEL_IFACE_SASL_AUTH + ".SASLError" +SASL_ERROR_DETAILS = CHANNEL_IFACE_SASL_AUTH + ".SASLErrorDetails" +SASL_CONTEXT = CHANNEL_IFACE_SASL_AUTH + ".SASLContext" +SASL_AUTHORIZATION_IDENTITY = CHANNEL_IFACE_SASL_AUTH + ".AuthorizationIdentity" +SASL_DEFAULT_REALM = CHANNEL_IFACE_SASL_AUTH + ".DefaultRealm" +SASL_DEFAULT_USERNAME = CHANNEL_IFACE_SASL_AUTH + ".DefaultUsername" + +# Channel_Type_ServerTLSConnection +TLS_CERT_PATH = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ServerCertificate" +TLS_HOSTNAME = CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".Hostname" +TLS_REFERENCE_IDENTITIES = \ + CHANNEL_TYPE_SERVER_TLS_CONNECTION + ".ReferenceIdentities" + +# Connection.Interface.Location + +LOCATION_FEATURE_CAN_SET = 1 + +# Channel.Type.Text + +MT_NORMAL = 0 +MT_ACTION = 1 +MT_NOTICE = 2 +MT_AUTO_REPLY = 3 +MT_DELIVERY_REPORT = 4 + +PROTOCOL = 'org.freedesktop.Telepathy.Protocol' +PROTOCOL_IFACE_PRESENCES = PROTOCOL + '.Interface.Presence' +PARAM_REQUIRED = 1 +PARAM_REGISTER = 2 +PARAM_HAS_DEFAULT = 4 +PARAM_SECRET = 8 +PARAM_DBUS_PROPERTY = 16 + +AUTHENTICATION = 'org.freedesktop.Telepathy.Authentication' +AUTH_TLS_CERT = AUTHENTICATION + ".TLSCertificate" + +TLS_CERT_STATE_PENDING = 0 +TLS_CERT_STATE_ACCEPTED = 1 +TLS_CERT_STATE_REJECTED = 2 + +TLS_REJECT_REASON_UNKNOWN = 0 +TLS_REJECT_REASON_UNTRUSTED = 1 + +# Channel.Interface.Messages + +MESSAGE_PART_SUPPORT_FLAGS = CHANNEL_IFACE_MESSAGES + '.MessagePartSupportFlags' +DELIVERY_REPORTING_SUPPORT = CHANNEL_IFACE_MESSAGES + '.DeliveryReportingSupport' +SUPPORTED_CONTENT_TYPES = CHANNEL_IFACE_MESSAGES + '.SupportedContentTypes' + +MSG_SENDING_FLAGS_REPORT_DELIVERY = 1 +MSG_SENDING_FLAGS_REPORT_READ = 2 +MSG_SENDING_FLAGS_REPORT_DELETED = 4 + +DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_FAILURES = 1 +DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_SUCCESSES = 2 +DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_READ = 4 +DELIVERY_REPORTING_SUPPORT_FLAGS_RECEIVE_DELETED = 8 + +MEDIA_STREAM_ERROR_UNKNOWN = 0 +MEDIA_STREAM_ERROR_EOS = 1 +MEDIA_STREAM_ERROR_CODEC_NEGOTIATION_FAILED = 2 +MEDIA_STREAM_ERROR_CONNECTION_FAILED = 3 +MEDIA_STREAM_ERROR_NETWORK_ERROR = 4 +MEDIA_STREAM_ERROR_NO_CODECS = 5 +MEDIA_STREAM_ERROR_INVALID_CM_BEHAVIOR = 6 +MEDIA_STREAM_ERROR_MEDIA_ERROR = 7 + +PASSWORD_FLAG_PROVIDE = 8 + +# Channel.Interface.Room +ROOM_ROOM_ID = CHANNEL_IFACE_ROOM + '.RoomID' +ROOM_SERVER = CHANNEL_IFACE_ROOM + '.Server' +ROOM_SUBJECT = CHANNEL_IFACE_ROOM + '.Subject' diff --git a/tests/twisted/gabbleservicetest.py b/tests/twisted/gabbleservicetest.py new file mode 100644 index 0000000..8d217ca --- /dev/null +++ b/tests/twisted/gabbleservicetest.py @@ -0,0 +1,640 @@ + +""" +Infrastructure code for testing connection managers. +""" + +from twisted.internet import glib2reactor +from twisted.internet.protocol import Protocol, Factory, ClientFactory +glib2reactor.install() +import sys +import time + +import pprint +import unittest + +import dbus.glib + +from twisted.internet import reactor + +import gabbleconstants as cs + +tp_name_prefix = 'org.freedesktop.Telepathy' +tp_path_prefix = '/org/freedesktop/Telepathy' + +class DictionarySupersetOf (object): + """Utility class for expecting "a dictionary with at least these keys".""" + def __init__(self, dictionary): + self._dictionary = dictionary + def __repr__(self): + return "DictionarySupersetOf(%s)" % self._dictionary + def __eq__(self, other): + """would like to just do: + return set(other.items()).issuperset(self._dictionary.items()) + but it turns out that this doesn't work if you have another dict + nested in the values of your dicts""" + try: + for k,v in self._dictionary.items(): + if k not in other or other[k] != v: + return False + return True + except TypeError: # other is not iterable + return False + +class Event(object): + def __init__(self, type, **kw): + self.__dict__.update(kw) + self.type = type + (self.subqueue, self.subtype) = type.split ("-", 1) + + def __str__(self): + return '\n'.join([ str(type(self)) ] + format_event(self)) + +def format_event(event): + ret = ['- type %s' % event.type] + + for key in sorted(dir(event)): + if key != 'type' and not key.startswith('_'): + ret.append('- %s: %s' % ( + key, pprint.pformat(getattr(event, key)))) + + if key == 'error': + ret.append('%s' % getattr(event, key)) + + return ret + +class EventPattern: + def __init__(self, type, **properties): + self.type = type + self.predicate = None + if 'predicate' in properties: + self.predicate = properties['predicate'] + del properties['predicate'] + self.properties = properties + (self.subqueue, self.subtype) = type.split ("-", 1) + + def __repr__(self): + properties = dict(self.properties) + + if self.predicate is not None: + properties['predicate'] = self.predicate + + return '%s(%r, **%r)' % ( + self.__class__.__name__, self.type, properties) + + def match(self, event): + if event.type != self.type: + return False + + for key, value in self.properties.iteritems(): + try: + if getattr(event, key) != value: + return False + except AttributeError: + return False + + if self.predicate is None or self.predicate(event): + return True + + return False + + +class TimeoutError(Exception): + pass + +class ForbiddenEventOccurred(Exception): + def __init__(self, event): + Exception.__init__(self) + self.event = event + + def __str__(self): + return '\n' + '\n'.join(format_event(self.event)) + +class BaseEventQueue: + """Abstract event queue base class. + + Implement the wait() method to have something that works. + """ + + def __init__(self, timeout=None): + self.verbose = False + self.forbidden_events = set() + self.event_queues = {} + + if timeout is None: + self.timeout = 5 + else: + self.timeout = timeout + + def log(self, s): + if self.verbose: + print s + + def log_queues(self, queues): + self.log ("Waiting for event on: %s" % ", ".join(queues)) + + def log_event(self, event): + self.log('got event:') + + if self.verbose: + map(self.log, format_event(event)) + + def forbid_events(self, patterns): + """ + Add patterns (an iterable of EventPattern) to the set of forbidden + events. If a forbidden event occurs during an expect or expect_many, + the test will fail. + """ + self.forbidden_events.update(set(patterns)) + + def unforbid_events(self, patterns): + """ + Remove 'patterns' (an iterable of EventPattern) from the set of + forbidden events. These must be the same EventPattern pointers that + were passed to forbid_events. + """ + self.forbidden_events.difference_update(set(patterns)) + + def _check_forbidden(self, event): + for e in self.forbidden_events: + if e.match(event): + raise ForbiddenEventOccurred(event) + + def expect(self, type, **kw): + """ + Waits for an event matching the supplied pattern to occur, and returns + it. For example, to await a D-Bus signal with particular arguments: + + e = q.expect('dbus-signal', signal='Badgers', args=["foo", 42]) + """ + pattern = EventPattern(type, **kw) + t = time.time() + + while True: + event = self.wait([pattern.subqueue]) + self._check_forbidden(event) + + if pattern.match(event): + self.log('handled, took %0.3f ms' + % ((time.time() - t) * 1000.0) ) + self.log('') + return event + + self.log('not handled') + self.log('') + + def expect_many(self, *patterns): + """ + Waits for events matching all of the supplied EventPattern instances to + return, and returns a list of events in the same order as the patterns + they matched. After a pattern is successfully matched, it is not + considered for future events; if more than one unsatisfied pattern + matches an event, the first "wins". + + Note that the expected events may occur in any order. If you're + expecting a series of events in a particular order, use repeated calls + to expect() instead. + + This method is useful when you're awaiting a number of events which may + happen in any order. For instance, in telepathy-gabble, calling a D-Bus + method often causes a value to be returned immediately, as well as a + query to be sent to the server. Since these events may reach the test + in either order, the following is incorrect and will fail if the IQ + happens to reach the test first: + + ret = q.expect('dbus-return', method='Foo') + query = q.expect('stream-iq', query_ns=ns.FOO) + + The following would be correct: + + ret, query = q.expect_many( + EventPattern('dbus-return', method='Foo'), + EventPattern('stream-iq', query_ns=ns.FOO), + ) + """ + ret = [None] * len(patterns) + t = time.time() + + while None in ret: + try: + queues = set() + for i, pattern in enumerate(patterns): + if ret[i] is None: + queues.add(pattern.subqueue) + event = self.wait(queues) + except TimeoutError: + self.log('timeout') + self.log('still expecting:') + for i, pattern in enumerate(patterns): + if ret[i] is None: + self.log(' - %r' % pattern) + raise + self._check_forbidden(event) + + for i, pattern in enumerate(patterns): + if ret[i] is None and pattern.match(event): + self.log('handled, took %0.3f ms' + % ((time.time() - t) * 1000.0) ) + self.log('') + ret[i] = event + break + else: + self.log('not handled') + self.log('') + + return ret + + def demand(self, type, **kw): + pattern = EventPattern(type, **kw) + + event = self.wait([pattern.subqueue]) + + if pattern.match(event): + self.log('handled') + self.log('') + return event + + self.log('not handled') + raise RuntimeError('expected %r, got %r' % (pattern, event)) + + def queues_available(self, queues): + if queues == None: + return self.event_queues.keys() + else: + available = self.event_queues.keys() + return filter(lambda x: x in available, queues) + + + def pop_next(self, queue): + events = self.event_queues[queue] + e = events.pop(0) + if not events: + self.event_queues.pop (queue) + return e + + def append(self, event): + self.log ("Adding to queue") + self.log_event (event) + self.event_queues[event.subqueue] = \ + self.event_queues.get(event.subqueue, []) + [event] + +class IteratingEventQueue(BaseEventQueue): + """Event queue that works by iterating the Twisted reactor.""" + + def __init__(self, timeout=None): + BaseEventQueue.__init__(self, timeout) + + def wait(self, queues=None): + stop = [False] + + def later(): + stop[0] = True + + delayed_call = reactor.callLater(self.timeout, later) + + self.log_queues(queues) + + qa = self.queues_available(queues) + while not qa and (not stop[0]): + reactor.iterate(0.01) + qa = self.queues_available(queues) + + if qa: + delayed_call.cancel() + e = self.pop_next (qa[0]) + self.log_event (e) + return e + else: + raise TimeoutError + +class TestEventQueue(BaseEventQueue): + def __init__(self, events): + BaseEventQueue.__init__(self) + for e in events: + self.append (e) + + def wait(self, queues = None): + qa = self.queues_available(queues) + + if qa: + return self.pop_next (qa[0]) + else: + raise TimeoutError + +class EventQueueTest(unittest.TestCase): + def test_expect(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + assert queue.expect('test-foo').type == 'test-foo' + assert queue.expect('test-bar').type == 'test-bar' + + def test_expect_many(self): + queue = TestEventQueue([Event('test-foo'), + Event('test-bar')]) + bar, foo = queue.expect_many( + EventPattern('test-bar'), + EventPattern('test-foo')) + assert bar.type == 'test-bar' + assert foo.type == 'test-foo' + + def test_expect_many2(self): + # Test that events are only matched against patterns that haven't yet + # been matched. This tests a regression. + queue = TestEventQueue([Event('test-foo', x=1), Event('test-foo', x=2)]) + foo1, foo2 = queue.expect_many( + EventPattern('test-foo'), + EventPattern('test-foo')) + assert foo1.type == 'test-foo' and foo1.x == 1 + assert foo2.type == 'test-foo' and foo2.x == 2 + + def test_expect_queueing(self): + queue = TestEventQueue([Event('foo-test', x=1), + Event('foo-test', x=2)]) + + queue.append(Event('bar-test', x=1)) + queue.append(Event('bar-test', x=2)) + + queue.append(Event('baz-test', x=1)) + queue.append(Event('baz-test', x=2)) + + for x in xrange(1,2): + e = queue.expect ('baz-test') + assertEquals (x, e.x) + + e = queue.expect ('bar-test') + assertEquals (x, e.x) + + e = queue.expect ('foo-test') + assertEquals (x, e.x) + + def test_timeout(self): + queue = TestEventQueue([]) + self.assertRaises(TimeoutError, queue.expect, 'test-foo') + + def test_demand(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + foo = queue.demand('test-foo') + assert foo.type == 'test-foo' + + def test_demand_fail(self): + queue = TestEventQueue([Event('test-foo'), Event('test-bar')]) + self.assertRaises(RuntimeError, queue.demand, 'test-bar') + +def unwrap(x): + """Hack to unwrap D-Bus values, so that they're easier to read when + printed.""" + + if isinstance(x, list): + return map(unwrap, x) + + if isinstance(x, tuple): + return tuple(map(unwrap, x)) + + if isinstance(x, dict): + return dict([(unwrap(k), unwrap(v)) for k, v in x.iteritems()]) + + if isinstance(x, dbus.Boolean): + return bool(x) + + for t in [unicode, str, long, int, float]: + if isinstance(x, t): + return t(x) + + return x + +def call_async(test, proxy, method, *args, **kw): + """Call a D-Bus method asynchronously and generate an event for the + resulting method return/error.""" + + def reply_func(*ret): + test.append(Event('dbus-return', method=method, + value=unwrap(ret))) + + def error_func(err): + test.append(Event('dbus-error', method=method, error=err, + name=err.get_dbus_name(), message=str(err))) + + method_proxy = getattr(proxy, method) + kw.update({'reply_handler': reply_func, 'error_handler': error_func}) + method_proxy(*args, **kw) + +def sync_dbus(bus, q, conn): + # Dummy D-Bus method call. We can't use DBus.Peer.Ping() because libdbus + # replies to that message immediately, rather than handing it up to + # dbus-glib and thence Gabble, which means that Ping()ing Gabble doesn't + # ensure that it's processed all D-Bus messages prior to our ping. + # + # This won't do the right thing unless the proxy has a unique name. + assert conn.object.bus_name.startswith(':') + root_object = bus.get_object(conn.object.bus_name, '/', introspect=False) + call_async(q, + dbus.Interface(root_object, 'org.freedesktop.Telepathy.Tests'), + 'DummySyncDBus') + q.expect('dbus-error', method='DummySyncDBus') + +class ProxyWrapper: + def __init__(self, object, default, others={}): + self.object = object + self.default_interface = dbus.Interface(object, default) + self.Properties = dbus.Interface(object, dbus.PROPERTIES_IFACE) + self.TpProperties = \ + dbus.Interface(object, tp_name_prefix + '.Properties') + self.interfaces = dict([ + (name, dbus.Interface(object, iface)) + for name, iface in others.iteritems()]) + + def __getattr__(self, name): + if name in self.interfaces: + return self.interfaces[name] + + if name in self.object.__dict__: + return getattr(self.object, name) + + return getattr(self.default_interface, name) + +def wrap_connection(conn): + return ProxyWrapper(conn, tp_name_prefix + '.Connection', + dict([ + (name, tp_name_prefix + '.Connection.Interface.' + name) + for name in ['Aliasing', 'Avatars', 'Capabilities', 'Contacts', + 'Presence', 'SimplePresence', 'Requests']] + + [('Peer', 'org.freedesktop.DBus.Peer'), + ('ContactCapabilities', cs.CONN_IFACE_CONTACT_CAPS), + ('ContactInfo', cs.CONN_IFACE_CONTACT_INFO), + ('Location', cs.CONN_IFACE_LOCATION), + ('Future', tp_name_prefix + '.Connection.FUTURE'), + ('MailNotification', cs.CONN_IFACE_MAIL_NOTIFICATION), + ('ContactList', cs.CONN_IFACE_CONTACT_LIST), + ('ContactGroups', cs.CONN_IFACE_CONTACT_GROUPS), + ('PowerSaving', cs.CONN_IFACE_POWER_SAVING), + ])) + +def wrap_channel(chan, type_, extra=None): + interfaces = { + type_: tp_name_prefix + '.Channel.Type.' + type_, + 'Group': tp_name_prefix + '.Channel.Interface.Group', + } + + if extra: + interfaces.update(dict([ + (name, tp_name_prefix + '.Channel.Interface.' + name) + for name in extra])) + + return ProxyWrapper(chan, tp_name_prefix + '.Channel', interfaces) + +def make_connection(bus, event_func, name, proto, params): + cm = bus.get_object( + tp_name_prefix + '.ConnectionManager.%s' % name, + tp_path_prefix + '/ConnectionManager/%s' % name, + introspect=False) + cm_iface = dbus.Interface(cm, tp_name_prefix + '.ConnectionManager') + + connection_name, connection_path = cm_iface.RequestConnection( + proto, dbus.Dictionary(params, signature='sv')) + conn = wrap_connection(bus.get_object(connection_name, connection_path)) + + return conn + +def make_channel_proxy(conn, path, iface): + bus = dbus.SessionBus() + chan = bus.get_object(conn.object.bus_name, path) + chan = dbus.Interface(chan, tp_name_prefix + '.' + iface) + return chan + +# block_reading can be used if the test want to choose when we start to read +# data from the socket. +class EventProtocol(Protocol): + def __init__(self, queue=None, block_reading=False): + self.queue = queue + self.block_reading = block_reading + + def dataReceived(self, data): + if self.queue is not None: + self.queue.append(Event('socket-data', protocol=self, + data=data)) + + def sendData(self, data): + self.transport.write(data) + + def connectionMade(self): + if self.block_reading: + self.transport.stopReading() + + def connectionLost(self, reason=None): + if self.queue is not None: + self.queue.append(Event('socket-disconnected', protocol=self)) + +class EventProtocolFactory(Factory): + def __init__(self, queue, block_reading=False): + self.queue = queue + self.block_reading = block_reading + + def _create_protocol(self): + return EventProtocol(self.queue, self.block_reading) + + def buildProtocol(self, addr): + proto = self._create_protocol() + self.queue.append(Event('socket-connected', protocol=proto)) + return proto + +class EventProtocolClientFactory(EventProtocolFactory, ClientFactory): + pass + +def watch_tube_signals(q, tube): + def got_signal_cb(*args, **kwargs): + q.append(Event('tube-signal', + path=kwargs['path'], + signal=kwargs['member'], + args=map(unwrap, args), + tube=tube)) + + tube.add_signal_receiver(got_signal_cb, + path_keyword='path', member_keyword='member', + byte_arrays=True) + +def pretty(x): + return pprint.pformat(unwrap(x)) + +def assertEquals(expected, value): + if expected != value: + raise AssertionError( + "expected:\n%s\ngot:\n%s" % (pretty(expected), pretty(value))) + +def assertSameSets(expected, value): + exp_set = set(expected) + val_set = set(value) + + if exp_set != val_set: + raise AssertionError( + "expected contents:\n%s\ngot:\n%s" % ( + pretty(exp_set), pretty(val_set))) + +def assertNotEquals(expected, value): + if expected == value: + raise AssertionError( + "expected something other than:\n%s" % pretty(value)) + +def assertContains(element, value): + if element not in value: + raise AssertionError( + "expected:\n%s\nin:\n%s" % (pretty(element), pretty(value))) + +def assertDoesNotContain(element, value): + if element in value: + raise AssertionError( + "expected:\n%s\nnot in:\n%s" % (pretty(element), pretty(value))) + +def assertLength(length, value): + if len(value) != length: + raise AssertionError("expected: length %d, got length %d:\n%s" % ( + length, len(value), pretty(value))) + +def assertFlagsSet(flags, value): + masked = value & flags + if masked != flags: + raise AssertionError( + "expected flags %u, of which only %u are set in %u" % ( + flags, masked, value)) + +def assertFlagsUnset(flags, value): + masked = value & flags + if masked != 0: + raise AssertionError( + "expected none of flags %u, but %u are set in %u" % ( + flags, masked, value)) + +def assertDBusError(name, error): + if error.get_dbus_name() != name: + raise AssertionError( + "expected DBus error named:\n %s\ngot:\n %s\n(with message: %s)" + % (name, error.get_dbus_name(), error.message)) + +def install_colourer(): + def red(s): + return '\x1b[31m%s\x1b[0m' % s + + def green(s): + return '\x1b[32m%s\x1b[0m' % s + + patterns = { + 'handled': green, + 'not handled': red, + } + + class Colourer: + def __init__(self, fh, patterns): + self.fh = fh + self.patterns = patterns + + def write(self, s): + for p, f in self.patterns.items(): + if s.startswith(p): + self.fh.write(f(p) + s[len(p):]) + return + + self.fh.write(s) + + sys.stdout = Colourer(sys.stdout, patterns) + return sys.stdout + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/twisted/gabbletest.py b/tests/twisted/gabbletest.py new file mode 100644 index 0000000..a635d19 --- /dev/null +++ b/tests/twisted/gabbletest.py @@ -0,0 +1,824 @@ + +""" +Infrastructure code for testing Gabble by pretending to be a Jabber server. +""" + +import base64 +import os +import hashlib +import sys +import random +import re +import traceback + +import ns +import gabbleconstants as cs +import gabbleservicetest as servicetest +from gabbleservicetest import ( + assertEquals, assertLength, assertContains, wrap_channel, + EventPattern, call_async, unwrap, Event) +import twisted +from twisted.words.xish import domish, xpath +from twisted.words.protocols.jabber.client import IQ +from twisted.words.protocols.jabber import xmlstream +from twisted.internet import reactor, ssl + +import dbus + +def make_result_iq(stream, iq, add_query_node=True): + result = IQ(stream, "result") + result["id"] = iq["id"] + to = iq.getAttribute('to') + if to is not None: + result["from"] = to + query = iq.firstChildElement() + + if query and add_query_node: + result.addElement((query.uri, query.name)) + + return result + +def acknowledge_iq(stream, iq): + stream.send(make_result_iq(stream, iq)) + +def send_error_reply(stream, iq, error_stanza=None): + result = IQ(stream, "error") + result["id"] = iq["id"] + query = iq.firstChildElement() + to = iq.getAttribute('to') + if to is not None: + result["from"] = to + + if query: + result.addElement((query.uri, query.name)) + + if error_stanza: + result.addChild(error_stanza) + + stream.send(result) + +def request_muc_handle(q, conn, stream, muc_jid): + servicetest.call_async(q, conn, 'RequestHandles', 2, [muc_jid]) + event = q.expect('dbus-return', method='RequestHandles') + return event.value[0][0] + +def make_muc_presence(affiliation, role, muc_jid, alias, jid=None, photo=None): + presence = domish.Element((None, 'presence')) + presence['from'] = '%s/%s' % (muc_jid, alias) + x = presence.addElement((ns.MUC_USER, 'x')) + item = x.addElement('item') + item['affiliation'] = affiliation + item['role'] = role + if jid is not None: + item['jid'] = jid + + if photo is not None: + presence.addChild( + elem(ns.VCARD_TEMP_UPDATE, 'x')( + elem('photo')(unicode(photo)) + )) + + return presence + +def sync_stream(q, stream): + """Used to ensure that Gabble has processed all stanzas sent to it.""" + + iq = IQ(stream, "get") + id = iq['id'] + iq.addElement(('http://jabber.org/protocol/disco#info', 'query')) + stream.send(iq) + q.expect('stream-iq', query_ns='http://jabber.org/protocol/disco#info', + predicate=(lambda event: + event.stanza['id'] == id and event.iq_type == 'result')) + +class GabbleAuthenticator(xmlstream.Authenticator): + def __init__(self, username, password, resource=None): + self.username = username + self.password = password + self.resource = resource + self.bare_jid = None + self.full_jid = None + self._event_func = lambda e: None + xmlstream.Authenticator.__init__(self) + + def set_event_func(self, event_func): + self._event_func = event_func + +class JabberAuthenticator(GabbleAuthenticator): + "Trivial XML stream authenticator that accepts one username/digest pair." + + # Patch in fix from http://twistedmatrix.com/trac/changeset/23418. + # This monkeypatch taken from Gadget source code + from twisted.words.xish.utility import EventDispatcher + + def _addObserver(self, onetime, event, observerfn, priority, *args, + **kwargs): + if self._dispatchDepth > 0: + self._updateQueue.append(lambda: self._addObserver(onetime, event, + observerfn, priority, *args, **kwargs)) + + return self._oldAddObserver(onetime, event, observerfn, priority, + *args, **kwargs) + + EventDispatcher._oldAddObserver = EventDispatcher._addObserver + EventDispatcher._addObserver = _addObserver + + def __init__(self, username, password, resource=None, emit_events=False): + GabbleAuthenticator.__init__(self, username, password, resource) + self.emit_events = emit_events + + def streamStarted(self, root=None): + if root: + self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) + + self.xmlstream.sendHeader() + self.xmlstream.addOnetimeObserver( + "/iq/query[@xmlns='jabber:iq:auth']", self.initialIq) + + def initialIq(self, iq): + if self.emit_events: + self._event_func(Event('auth-initial-iq', authenticator=self, + iq=iq, id=iq["id"])) + else: + self.respondToInitialIq(iq) + + self.xmlstream.addOnetimeObserver('/iq/query/username', self.secondIq) + + def respondToInitialIq(self, iq): + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + query = result.addElement('query') + query["xmlns"] = "jabber:iq:auth" + query.addElement('username', content='test') + query.addElement('password') + query.addElement('digest') + query.addElement('resource') + self.xmlstream.send(result) + + def secondIq(self, iq): + if self.emit_events: + self._event_func(Event('auth-second-iq', authenticator=self, + iq=iq, id=iq["id"])) + else: + self.respondToSecondIq(self, iq) + + def respondToSecondIq(self, iq): + username = xpath.queryForNodes('/iq/query/username', iq) + assert map(str, username) == [self.username] + + digest = xpath.queryForNodes('/iq/query/digest', iq) + expect = hashlib.sha1(self.xmlstream.sid + self.password).hexdigest() + assert map(str, digest) == [expect] + + resource = xpath.queryForNodes('/iq/query/resource', iq) + assertLength(1, resource) + if self.resource is not None: + assertEquals(self.resource, str(resource[0])) + + self.bare_jid = '%s@localhost' % self.username + self.full_jid = '%s/%s' % (self.bare_jid, resource) + + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + self.xmlstream.send(result) + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + +class XmppAuthenticator(GabbleAuthenticator): + def __init__(self, username, password, resource=None): + GabbleAuthenticator.__init__(self, username, password, resource) + self.authenticated = False + + def streamInitialize(self, root): + if root: + self.xmlstream.sid = root.getAttribute('id') + + if self.xmlstream.sid is None: + self.xmlstream.sid = '%x' % random.randint(1, sys.maxint) + + self.xmlstream.sendHeader() + + def streamIQ(self): + features = elem(xmlstream.NS_STREAMS, 'features')( + elem(ns.NS_XMPP_BIND, 'bind'), + elem(ns.NS_XMPP_SESSION, 'session'), + ) + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver( + "/iq/bind[@xmlns='%s']" % ns.NS_XMPP_BIND, self.bindIq) + self.xmlstream.addOnetimeObserver( + "/iq/session[@xmlns='%s']" % ns.NS_XMPP_SESSION, self.sessionIq) + + def streamSASL(self): + features = domish.Element((xmlstream.NS_STREAMS, 'features')) + mechanisms = features.addElement((ns.NS_XMPP_SASL, 'mechanisms')) + mechanism = mechanisms.addElement('mechanism', content='PLAIN') + self.xmlstream.send(features) + + self.xmlstream.addOnetimeObserver("/auth", self.auth) + + def streamStarted(self, root=None): + self.streamInitialize(root) + + if self.authenticated: + # Initiator authenticated itself, and has started a new stream. + self.streamIQ() + else: + self.streamSASL() + + def auth(self, auth): + assert (base64.b64decode(str(auth)) == + '\x00%s\x00%s' % (self.username, self.password)) + + success = domish.Element((ns.NS_XMPP_SASL, 'success')) + self.xmlstream.send(success) + self.xmlstream.reset() + self.authenticated = True + + def bindIq(self, iq): + resource = xpath.queryForString('/iq/bind/resource', iq) + if self.resource is not None: + assertEquals(self.resource, resource) + else: + assert resource is not None + + result = IQ(self.xmlstream, "result") + result["id"] = iq["id"] + bind = result.addElement((ns.NS_XMPP_BIND, 'bind')) + self.bare_jid = '%s@localhost' % self.username + self.full_jid = '%s/%s' % (self.bare_jid, resource) + jid = bind.addElement('jid', content=self.full_jid) + self.xmlstream.send(result) + + self.xmlstream.dispatch(self.xmlstream, xmlstream.STREAM_AUTHD_EVENT) + + def sessionIq(self, iq): + self.xmlstream.send(make_result_iq(self.xmlstream, iq)) + +class StreamEvent(servicetest.Event): + def __init__(self, type_, stanza, stream): + servicetest.Event.__init__(self, type_, stanza=stanza) + self.stream = stream + self.to = stanza.getAttribute("to") + +class IQEvent(StreamEvent): + def __init__(self, stream, iq): + StreamEvent.__init__(self, 'stream-iq', iq, stream) + self.iq_type = iq.getAttribute("type") + self.iq_id = iq.getAttribute("id") + + query = iq.firstChildElement() + + if query: + self.query = query + self.query_ns = query.uri + self.query_name = query.name + + if query.getAttribute("node"): + self.query_node = query.getAttribute("node") + else: + self.query = None + +class PresenceEvent(StreamEvent): + def __init__(self, stream, stanza): + StreamEvent.__init__(self, 'stream-presence', stanza, stream) + self.presence_type = stanza.getAttribute('type') + + statuses = xpath.queryForNodes('/presence/status', stanza) + + if statuses: + self.presence_status = str(statuses[0]) + +class MessageEvent(StreamEvent): + def __init__(self, stream, stanza): + StreamEvent.__init__(self, 'stream-message', stanza, stream) + self.message_type = stanza.getAttribute('type') + +class StreamFactory(twisted.internet.protocol.Factory): + def __init__(self, streams, jids): + self.streams = streams + self.jids = jids + self.presences = {} + self.mappings = dict(map (lambda jid, stream: (jid, stream), + jids, streams)) + + # Make a copy of the streams + self.factory_streams = list(streams) + self.factory_streams.reverse() + + # Do not add observers for single instances because it's unnecessary and + # some unit tests need to respond to the roster request, and we shouldn't + # answer it for them otherwise we break compatibility + if len(streams) > 1: + # We need to have a function here because lambda keeps a reference on + # the stream and jid and in the for loop, there is no context + def addObservers(stream, jid): + stream.addObserver('/iq', lambda x: \ + self.forward_iq(stream, jid, x)) + stream.addObserver('/presence', lambda x: \ + self.got_presence(stream, jid, x)) + + for (jid, stream) in self.mappings.items(): + addObservers(stream, jid) + + def protocol(self, *args): + return self.factory_streams.pop() + + + def got_presence (self, stream, jid, stanza): + stanza.attributes['from'] = jid + self.presences[jid] = stanza + + for dest_jid in self.presences.keys(): + # Dispatch the new presence to other clients + stanza.attributes['to'] = dest_jid + self.mappings[dest_jid].send(stanza) + + # Don't echo the presence twice + if dest_jid != jid: + # Dispatch other client's presence to this stream + presence = self.presences[dest_jid] + presence.attributes['to'] = jid + stream.send(presence) + + def lost_presence(self, stream, jid): + if self.presences.has_key(jid): + del self.presences[jid] + for dest_jid in self.presences.keys(): + presence = domish.Element(('jabber:client', 'presence')) + presence['from'] = jid + presence['to'] = dest_jid + presence['type'] = 'unavailable' + self.mappings[dest_jid].send(presence) + + def forward_iq(self, stream, jid, stanza): + stanza.attributes['from'] = jid + + query = stanza.firstChildElement() + + # Fake other accounts as being part of our roster + if query and query.uri == ns.ROSTER: + roster = make_result_iq(stream, stanza) + query = roster.firstChildElement() + for roster_jid in self.mappings.keys(): + if jid != roster_jid: + item = query.addElement('item') + item['jid'] = roster_jid + item['subscription'] = 'both' + stream.send(roster) + return + + to = stanza.getAttribute('to') + dest = None + if to is not None: + dest = self.mappings.get(to) + + if dest is not None: + dest.send(stanza) + +class BaseXmlStream(xmlstream.XmlStream): + initiating = False + namespace = 'jabber:client' + pep_support = True + disco_features = [] + handle_privacy_lists = True + + def __init__(self, event_func, authenticator): + xmlstream.XmlStream.__init__(self, authenticator) + self.event_func = event_func + self.addObserver('//iq', lambda x: event_func( + IQEvent(self, x))) + self.addObserver('//message', lambda x: event_func( + MessageEvent(self, x))) + self.addObserver('//presence', lambda x: event_func( + PresenceEvent(self, x))) + self.addObserver('//event/stream/authd', self._cb_authd) + if self.handle_privacy_lists: + self.addObserver("/iq/query[@xmlns='%s']" % ns.PRIVACY, + self._cb_priv_list) + + def _cb_priv_list(self, iq): + send_error_reply(self, iq) + + def _cb_authd(self, _): + # called when stream is authenticated + assert self.authenticator.full_jid is not None + assert self.authenticator.bare_jid is not None + + self.addObserver( + "/iq[@to='localhost']/query[@xmlns='http://jabber.org/protocol/disco#info']", + self._cb_disco_iq) + self.addObserver( + "/iq[@to='%s']/query[@xmlns='http://jabber.org/protocol/disco#info']" + % self.authenticator.bare_jid, + self._cb_bare_jid_disco_iq) + self.event_func(servicetest.Event('stream-authenticated')) + + def _cb_disco_iq(self, iq): + nodes = xpath.queryForNodes( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", iq) + query = nodes[0] + + for feature in self.disco_features: + query.addChild(elem('feature', var=feature)) + + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + def _cb_bare_jid_disco_iq(self, iq): + # advertise PEP support + nodes = xpath.queryForNodes( + "/iq/query[@xmlns='http://jabber.org/protocol/disco#info']", + iq) + query = nodes[0] + identity = query.addElement('identity') + identity['category'] = 'pubsub' + identity['type'] = 'pep' + + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + def onDocumentEnd(self): + self.event_func(servicetest.Event('stream-closed')) + # We don't chain up XmlStream.onDocumentEnd() because it will + # disconnect the TCP connection making tests as + # connect/disconnect-timeout.py not working + + def send_stream_error(self, error='system-shutdown'): + # Yes, there are meant to be two different STREAMS namespaces. + go_away = \ + elem(xmlstream.NS_STREAMS, 'error')( + elem(ns.STREAMS, error) + ) + + self.send(go_away) + +class JabberXmlStream(BaseXmlStream): + version = (0, 9) + +class XmppXmlStream(BaseXmlStream): + version = (1, 0) + +class GoogleXmlStream(BaseXmlStream): + version = (1, 0) + + pep_support = False + disco_features = [ns.GOOGLE_ROSTER, + ns.GOOGLE_JINGLE_INFO, + ns.GOOGLE_MAIL_NOTIFY, + ns.GOOGLE_QUEUE, + ] + + def _cb_bare_jid_disco_iq(self, iq): + # Google talk doesn't support PEP :( + iq['type'] = 'result' + iq['from'] = iq['to'] + self.send(iq) + + +def make_connection(bus, event_func, params=None, suffix=''): + # Gabble accepts a resource in 'account', but the value of 'resource' + # overrides it if there is one. + test_name = re.sub('(.*tests/twisted/|\./)', '', sys.argv[0]) + account = 'test%s@localhost/%s' % (suffix, test_name) + + default_params = { + 'account': account, + 'password': 'pass', + 'resource': 'Resource', + 'server': 'localhost', + 'port': dbus.UInt32(4242), + 'fallback-socks5-proxies': dbus.Array([], signature='s'), + 'require-encryption': False, + } + + if params: + default_params.update(params) + + # Allow omitting the 'password' param + if default_params['password'] is None: + del default_params['password'] + + # Allow omitting the 'account' param + if default_params['account'] is None: + del default_params['account'] + + jid = default_params.get('account', None) + conn = servicetest.make_connection(bus, event_func, 'gabble', 'jabber', + default_params) + return (conn, jid) + +def make_stream(event_func, authenticator=None, protocol=None, + resource=None, suffix=''): + # set up Jabber server + if authenticator is None: + authenticator = XmppAuthenticator('test%s' % suffix, 'pass', resource=resource) + + authenticator.set_event_func(event_func) + + if protocol is None: + protocol = XmppXmlStream + + stream = protocol(event_func, authenticator) + return stream + +def disconnect_conn(q, conn, stream, expected_before=[], expected_after=[]): + call_async(q, conn, 'Disconnect') + + tmp = expected_before + [ + EventPattern('dbus-signal', signal='StatusChanged', args=[cs.CONN_STATUS_DISCONNECTED, cs.CSR_REQUESTED]), + EventPattern('stream-closed')] + + before_events = q.expect_many(*tmp) + + stream.sendFooter() + + tmp = expected_after + [EventPattern('dbus-return', method='Disconnect')] + after_events = q.expect_many(*tmp) + + return before_events[:-2], after_events[:-1] + +def exec_test_deferred(fun, params, protocol=None, timeout=None, + authenticator=None, num_instances=1, + do_connect=True): + # hack to ease debugging + domish.Element.__repr__ = domish.Element.toXml + colourer = None + + if sys.stdout.isatty() or 'CHECK_FORCE_COLOR' in os.environ: + colourer = servicetest.install_colourer() + + bus = dbus.SessionBus() + + queue = servicetest.IteratingEventQueue(timeout) + queue.verbose = ( + os.environ.get('CHECK_TWISTED_VERBOSE', '') != '' + or '-v' in sys.argv) + + conns = [] + jids = [] + streams = [] + resource = params.get('resource') if params is not None else None + for i in range(0, num_instances): + if i == 0: + suffix = '' + else: + suffix = str(i) + + try: + (conn, jid) = make_connection(bus, queue.append, params, suffix) + except Exception, e: + # Crap. This is normally because the connection's still kicking + # around on the bus. Let's bin any connections we *did* manage to + # get going and then bail out unceremoniously. + print e + + for conn in conns: + conn.Disconnect() + + os._exit(1) + + conns.append(conn) + jids.append(jid) + streams.append(make_stream(queue.append, protocol=protocol, + authenticator=authenticator, + resource=resource, suffix=suffix)) + + factory = StreamFactory(streams, jids) + port = reactor.listenTCP(4242, factory, interface='localhost') + + def signal_receiver(*args, **kw): + if kw['path'] == '/org/freedesktop/DBus' and \ + kw['member'] == 'NameOwnerChanged': + bus_name, old_name, new_name = args + if new_name == '': + for i, conn in enumerate(conns): + stream = streams[i] + jid = jids[i] + if conn._requested_bus_name == bus_name: + factory.lost_presence(stream, jid) + break + queue.append(Event('dbus-signal', + path=unwrap(kw['path']), + signal=kw['member'], args=map(unwrap, args), + interface=kw['interface'])) + + match_all_signals = bus.add_signal_receiver( + signal_receiver, + None, # signal name + None, # interface + None, + path_keyword='path', + member_keyword='member', + interface_keyword='interface', + byte_arrays=True + ) + + error = None + + try: + if do_connect: + for conn in conns: + conn.Connect() + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTING, cs.CSR_REQUESTED]) + queue.expect('stream-authenticated') + queue.expect('dbus-signal', signal='PresenceUpdate', + args=[{1L: (0L, {u'available': {}})}]) + queue.expect('dbus-signal', signal='StatusChanged', + args=[cs.CONN_STATUS_CONNECTED, cs.CSR_REQUESTED]) + + if len(conns) == 1: + fun(queue, bus, conns[0], streams[0]) + else: + fun(queue, bus, conns, streams) + except Exception, e: + traceback.print_exc() + error = e + queue.verbose = False + + if colourer: + sys.stdout = colourer.fh + + d = port.stopListening() + + # Does the Connection object still exist? + for i, conn in enumerate(conns): + if not bus.name_has_owner(conn.object.bus_name): + # Connection has already been disconnected and destroyed + continue + try: + if conn.GetStatus() == cs.CONN_STATUS_CONNECTED: + # Connection is connected, properly disconnect it + disconnect_conn(queue, conn, streams[i]) + else: + # Connection is not connected, call Disconnect() to destroy it + conn.Disconnect() + except dbus.DBusException, e: + pass + except Exception, e: + traceback.print_exc() + error = e + + try: + conn.Disconnect() + raise AssertionError("Connection didn't disappear; " + "all subsequent tests will probably fail") + except dbus.DBusException, e: + pass + except Exception, e: + traceback.print_exc() + error = e + + match_all_signals.remove() + + if error is None: + d.addBoth((lambda *args: reactor.crash())) + else: + # please ignore the POSIX behind the curtain + d.addBoth((lambda *args: os._exit(1))) + + +def exec_test(fun, params=None, protocol=None, timeout=None, + authenticator=None, num_instances=1, do_connect=True): + reactor.callWhenRunning( + exec_test_deferred, fun, params, protocol, timeout, authenticator, num_instances, + do_connect) + reactor.run() + +# Useful routines for server-side vCard handling +current_vcard = domish.Element(('vcard-temp', 'vCard')) + +def expect_and_handle_get_vcard(q, stream): + get_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='get') + + iq = get_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + # Send back current vCard + result = make_result_iq(stream, iq, add_query_node=False) + result.addChild(current_vcard) + stream.send(result) + +def expect_and_handle_set_vcard(q, stream, check=None): + global current_vcard + set_vcard_event = q.expect('stream-iq', query_ns=ns.VCARD_TEMP, + query_name='vCard', iq_type='set') + iq = set_vcard_event.stanza + vcard = iq.firstChildElement() + assert vcard.name == 'vCard', vcard.toXml() + + if check is not None: + check(vcard) + + # Update current vCard + current_vcard = vcard + + stream.send(make_result_iq(stream, iq)) + +def _elem_add(elem, *children): + for child in children: + if isinstance(child, domish.Element): + elem.addChild(child) + elif isinstance(child, unicode): + elem.addContent(child) + else: + raise ValueError( + 'invalid child object %r (must be element or unicode)', child) + +def elem(a, b=None, attrs={}, **kw): + r""" + >>> elem('foo')().toXml() + u'<foo/>' + >>> elem('foo', x='1')().toXml() + u"<foo x='1'/>" + >>> elem('foo', x='1')(u'hello').toXml() + u"<foo x='1'>hello</foo>" + >>> elem('foo', x='1')(u'hello', + ... elem('http://foo.org', 'bar', y='2')(u'bye')).toXml() + u"<foo x='1'>hello<bar xmlns='http://foo.org' y='2'>bye</bar></foo>" + >>> elem('foo', attrs={'xmlns:bar': 'urn:bar', 'bar:cake': 'yum'})( + ... elem('bar:e')(u'i') + ... ).toXml() + u"<foo xmlns:bar='urn:bar' bar:cake='yum'><bar:e>i</bar:e></foo>" + """ + + class _elem(domish.Element): + def __call__(self, *children): + _elem_add(self, *children) + return self + + if b is not None: + elem = _elem((a, b)) + else: + elem = _elem((None, a)) + + # Can't just update kw into attrs, because that *modifies the parameter's + # default*. Thanks python. + allattrs = {} + allattrs.update(kw) + allattrs.update(attrs) + + # First, let's pull namespaces out + realattrs = {} + for k, v in allattrs.iteritems(): + if k.startswith('xmlns:'): + abbr = k[len('xmlns:'):] + elem.localPrefixes[abbr] = v + else: + realattrs[k] = v + + for k, v in realattrs.iteritems(): + if k == 'from_': + elem['from'] = v + else: + elem[k] = v + + return elem + +def elem_iq(server, type, **kw): + class _iq(IQ): + def __call__(self, *children): + _elem_add(self, *children) + return self + + iq = _iq(server, type) + + for k, v in kw.iteritems(): + if k == 'from_': + iq['from'] = v + else: + iq[k] = v + + return iq + +def make_presence(_from, to='test@localhost', type=None, show=None, + status=None, caps=None, photo=None): + presence = domish.Element((None, 'presence')) + presence['from'] = _from + presence['to'] = to + + if type is not None: + presence['type'] = type + + if show is not None: + presence.addElement('show', content=show) + + if status is not None: + presence.addElement('status', content=status) + + if caps is not None: + cel = presence.addElement(('http://jabber.org/protocol/caps', 'c')) + for key,value in caps.items(): + cel[key] = value + + # <x xmlns="vcard-temp:x:update"><photo>4a1...</photo></x> + if photo is not None: + x = presence.addElement((ns.VCARD_TEMP_UPDATE, 'x')) + x.addElement('photo').addContent(photo) + + return presence diff --git a/tests/twisted/salut/slow-service.py b/tests/twisted/salut/slow-service.py new file mode 100644 index 0000000..ac85424 --- /dev/null +++ b/tests/twisted/salut/slow-service.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# +# Copyright (C) 2011 Intel Corp. +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA +# 02110-1301 USA + +import dbus + +from salutservicetest import call_async, EventPattern, assertEquals, \ + ProxyWrapper, assertNotEquals, assertSameSets +from saluttest import exec_test, wait_for_contact_in_publish, make_result_iq, \ + sync_stream +import salutconstants as cs +import yconstants as ycs +from caps_helper import * + +from twisted.words.xish import xpath +from twisted.words.xish.domish import Element +from twisted.words.xish import domish + +from avahitest import AvahiAnnouncer, AvahiListener +from avahitest import get_host_name +from xmppstream import setup_stream_listener, connect_to_stream + +CLIENT_NAME = 'il-cliente-del-futuro' + +banshee = { + 'urn:ytstenut:capabilities#org.gnome.Banshee': + {'type': ['application'], + 'name': ['en_GB/Banshee Media Player', + 'fr/Banshee Lecteur de Musique'], + 'capabilities': ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp'] + } +} + +evince = { + 'urn:ytstenut:capabilities#org.gnome.Evince': + {'type': ['application'], + 'name': ['en_GB/Evince Picture Viewer', + 'fr/Evince uh, ow do you say'], + 'capabilities': ['urn:ytstenut:capabilities:pics'], + } +} + +def test(q, bus, conn): + forbidden = [EventPattern('dbus-signal', signal='ServiceAdded'), + EventPattern('dbus-signal', signal='ServiceRemoved')] + q.forbid_events(forbidden) + + conn.Connect() + + q.expect('dbus-signal', signal='StatusChanged', args=[0, 0]) + + # announce a contact with the right caps + ver = compute_caps_hash([], [], banshee) + txt_record = { "txtvers": "1", "status": "avail", + "node": CLIENT_NAME, "ver": ver, "hash": "sha-1"} + contact_name = "test-service@" + get_host_name() + listener, port = setup_stream_listener(q, contact_name) + + announcer = AvahiAnnouncer(contact_name, "_presence._tcp", port, txt_record) + + handle = wait_for_contact_in_publish(q, bus, conn, contact_name) + + # this is the first presence, Salut connects to the contact + e = q.expect('incoming-connection', listener=listener) + incoming = e.connection + + # Salut looks up its capabilities + event = q.expect('stream-iq', connection=incoming, + query_ns=ns.DISCO_INFO) + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assertEquals(CLIENT_NAME + '#' + ver, query_node.attributes['node']) + + contact_handle = conn.RequestHandles(cs.HT_CONTACT, [contact_name])[0] + + # send good reply + result = make_result_iq(event.stanza) + query = result.firstChildElement() + query['node'] = CLIENT_NAME + '#' + ver + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + # FORM_TYPE + field = x.addElement((None, 'field')) + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities#org.gnome.Banshee') + + # type + field = x.addElement((None, 'field')) + field['var'] = 'type' + field.addElement((None, 'value'), content='application') + + # name + field = x.addElement((None, 'field')) + field['var'] = 'name' + field.addElement((None, 'value'), content='en_GB/Banshee Media Player') + field.addElement((None, 'value'), content='fr/Banshee Lecteur de Musique') + + # capabilities + field = x.addElement((None, 'field')) + field['var'] = 'capabilities' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities:yts-caps-audio') + field.addElement((None, 'value'), content='urn:ytstenut:data:jingle:rtp') + + incoming.send(result) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # add evince + tmp = banshee.copy() + tmp.update(evince) + ver = compute_caps_hash([], [], tmp) + txt_record['ver'] = ver + announcer.update(txt_record) + + # Salut looks up our capabilities + event = q.expect('stream-iq', connection=incoming, + query_ns='http://jabber.org/protocol/disco#info') + query_node = xpath.queryForNodes('/iq/query', event.stanza)[0] + assert query_node.attributes['node'] == \ + CLIENT_NAME + '#' + txt_record['ver'] + + # send good reply + result['id'] = event.stanza['id'] + query['node'] = CLIENT_NAME + '#' + ver + + x = query.addElement((ns.X_DATA, 'x')) + x['type'] = 'result' + + # FORM_TYPE + field = x.addElement((None, 'field')) + field['var'] = 'FORM_TYPE' + field['type'] = 'hidden' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities#org.gnome.Evince') + + # type + field = x.addElement((None, 'field')) + field['var'] = 'type' + field.addElement((None, 'value'), content='application') + + # name + field = x.addElement((None, 'field')) + field['var'] = 'name' + field.addElement((None, 'value'), content='en_GB/Evince Picture Viewer') + field.addElement((None, 'value'), content='fr/Evince uh, ow do you say') + + # capabilities + field = x.addElement((None, 'field')) + field['var'] = 'capabilities' + field.addElement((None, 'value'), content='urn:ytstenut:capabilities:pics') + + incoming.send(result) + + # this will be fired as text channel caps will be fired + q.expect('dbus-signal', signal='ContactCapabilitiesChanged', + predicate=lambda e: contact_handle in e.args[0]) + + # now finally ensure the sidecar + path, props = conn.Future.EnsureSidecar(ycs.STATUS_IFACE) + assertEquals({}, props) + + status = ProxyWrapper(bus.get_object(conn.bus_name, path), + ycs.STATUS_IFACE, {}) + + discovered = status.Get(ycs.STATUS_IFACE, 'DiscoveredServices', + dbus_interface=dbus.PROPERTIES_IFACE) + assertEquals({contact_name: { + 'org.gnome.Banshee': + ('application', + {'en_GB': 'Banshee Media Player', + 'fr': 'Banshee Lecteur de Musique'}, + ['urn:ytstenut:capabilities:yts-caps-audio', + 'urn:ytstenut:data:jingle:rtp']), + 'org.gnome.Evince': + ('application', + {'en_GB': 'Evince Picture Viewer', + 'fr': 'Evince uh, ow do you say'}, + ['urn:ytstenut:capabilities:pics'])} + }, discovered) + + # sweet. + +if __name__ == '__main__': + exec_test(test) diff --git a/tests/twisted/saluttest.py b/tests/twisted/saluttest.py index 0ab6d3c..c168a77 100644 --- a/tests/twisted/saluttest.py +++ b/tests/twisted/saluttest.py @@ -204,7 +204,7 @@ def wait_for_contact_in_publish(q, bus, conn, contact_name): e = q.expect('dbus-signal', signal='MembersChangedDetailed', path=publish) for h in e.args[0]: - name = e.args[4]['member-ids'][h] + name = e.args[4]['contact-ids'][h] if name == contact_name: handle = h diff --git a/tests/twisted/tools/Makefile.am b/tests/twisted/tools/Makefile.am index 66e9265..fe7b620 100644 --- a/tests/twisted/tools/Makefile.am +++ b/tests/twisted/tools/Makefile.am @@ -2,7 +2,8 @@ service_in_files = \ org.freedesktop.Telepathy.MissionControl5.service.in \ org.freedesktop.Telepathy.Client.Logger.service.in \ - org.freedesktop.Telepathy.ConnectionManager.salut.service.in + org.freedesktop.Telepathy.ConnectionManager.salut.service.in \ + org.freedesktop.Telepathy.ConnectionManager.gabble.service.in service_files = $(service_in_files:.service.in=.service) # D-Bus config file for testing @@ -13,13 +14,15 @@ BUILT_SOURCES = \ $(service_files) \ $(conf_files) \ exec-with-log.sh \ - salut-exec-with-log.sh + salut-exec-with-log.sh \ + gabble-exec-with-log.sh EXTRA_DIST = \ $(service_in_files) \ $(conf_in_files) \ exec-with-log.sh.in \ salut-exec-with-log.sh.in \ + gabble-exec-with-log.sh.in \ fake-startup.sh \ valgrind.supp \ with-session-bus.sh \ @@ -29,4 +32,5 @@ CLEANFILES = \ $(BUILT_SOURCES) \ missioncontrol.log \ missioncontrol-*.log \ - salut-testing.log + salut-testing.log \ + gabble-testing.log diff --git a/tests/twisted/tools/gabble-exec-with-log.sh.in b/tests/twisted/tools/gabble-exec-with-log.sh.in new file mode 100644 index 0000000..48a6a43 --- /dev/null +++ b/tests/twisted/tools/gabble-exec-with-log.sh.in @@ -0,0 +1,40 @@ +#!/bin/sh + +cd "@abs_top_builddir@/tests/twisted/tools" + +export GABBLE_DEBUG=all LM_DEBUG=net GIBBER_DEBUG=all WOCKY_DEBUG=all +export GABBLE_TIMING=1 +export WOCKY_CAPS_CACHE=:memory: WOCKY_CAPS_CACHE_SIZE=50 +ulimit -c unlimited +exec >> gabble-testing.log 2>&1 + +export G_SLICE=debug-blocks + +if test -n "$GABBLE_TEST_VALGRIND"; then + export G_DEBUG=${G_DEBUG:+"${G_DEBUG},"}gc-friendly + export G_SLICE=${G_SLICE},always-malloc + export DBUS_DISABLE_MEM_POOLS=1 + GABBLE_WRAPPER="valgrind --leak-check=full --num-callers=20" + GABBLE_WRAPPER="$GABBLE_WRAPPER --show-reachable=yes" + GABBLE_WRAPPER="$GABBLE_WRAPPER --gen-suppressions=all" + GABBLE_WRAPPER="$GABBLE_WRAPPER --child-silent-after-fork=yes" + GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/tp-glib.supp" + GABBLE_WRAPPER="$GABBLE_WRAPPER --suppressions=@abs_top_srcdir@/tests/suppressions/gabble.supp" +elif test -n "$GABBLE_TEST_REFDBG"; then + if test -z "$REFDBG_OPTIONS" ; then + export REFDBG_OPTIONS="btnum=10" + fi + if test -z "$GABBLE_WRAPPER" ; then + GABBLE_WRAPPER="refdbg" + fi +elif test -n "$GABBLE_TEST_STRACE"; then + GABBLE_WRAPPER="strace -o strace.log" +elif test -n "$GABBLE_TEST_BACKTRACE"; then + GABBLE_WRAPPER="gdb -x run_and_bt.gdb" +fi + +# Prevent libproxy from hitting the network for wpad configuration +export PX_MODULE_BLACKLIST=config_wpad + +export G_DEBUG=fatal-warnings,fatal-criticals" ${G_DEBUG}" +exec @abs_top_builddir@/libtool --mode=execute $GABBLE_WRAPPER @GABBLE_EXECUTABLE@ diff --git a/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in new file mode 100644 index 0000000..3e3c58d --- /dev/null +++ b/tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.freedesktop.Telepathy.ConnectionManager.gabble +Exec=/bin/sh @abs_top_builddir@/tests/twisted/tools/gabble-exec-with-log.sh diff --git a/tests/twisted/yconstants.py b/tests/twisted/yconstants.py index 4f965d9..4a69cb5 100644 --- a/tests/twisted/yconstants.py +++ b/tests/twisted/yconstants.py @@ -23,3 +23,4 @@ ERROR_TYPE_WAIT = 5 MESSAGE_NS = 'urn:ytstenut:message' STATUS_NS = 'urn:ytstenut:status' +SERVICE_NS = 'urn:ytstenut:service' |