summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJonny Lamb <jonny.lamb@collabora.co.uk>2011-09-30 10:40:28 +0100
committerJonny Lamb <jonny.lamb@collabora.co.uk>2011-09-30 10:40:28 +0100
commit862aee50a0b80054ff2b7d3d4030f37d3646fff3 (patch)
tree03b9f84888d52b796bcc7ffb6f45fe0306576590
parent2163dc3ca3569401b865fc97930cda4e8d378cc1 (diff)
parent512576d656e37c88cd4f9fc9927ed266a8816202 (diff)
Merge branch 'gabble'
-rw-r--r--.gitignore3
-rw-r--r--Makefile.am2
-rw-r--r--configure.ac22
-rw-r--r--gabble/Makefile.am31
-rw-r--r--gabble/message-channel.c895
-rw-r--r--gabble/message-channel.h82
-rw-r--r--gabble/status.c786
-rw-r--r--gabble/status.h70
-rw-r--r--mission-control/mcp-account-manager-ytstenut.c13
-rw-r--r--plugin-base/Makefile.am9
-rw-r--r--plugin-base/caps-manager.c (renamed from salut/caps-manager.c)58
-rw-r--r--plugin-base/caps-manager.h (renamed from salut/caps-manager.h)0
-rw-r--r--plugin-base/channel-manager.c (renamed from salut/channel-manager.c)136
-rw-r--r--plugin-base/channel-manager.h (renamed from salut/channel-manager.h)4
-rw-r--r--plugin-base/utils.c (renamed from salut/utils.c)0
-rw-r--r--plugin-base/utils.h (renamed from salut/utils.h)1
-rw-r--r--plugin-base/ytstenut.c (renamed from salut/ytstenut.c)54
-rw-r--r--plugin-base/ytstenut.h (renamed from salut/ytstenut.h)0
-rw-r--r--salut/Makefile.am18
-rw-r--r--salut/status.c2
-rw-r--r--tests/twisted/Makefile.am21
-rwxr-xr-xtests/twisted/gabble/hct.py185
-rw-r--r--tests/twisted/gabble/message.py390
-rw-r--r--tests/twisted/gabble/service.py307
-rwxr-xr-xtests/twisted/gabble/sidecar.py35
-rw-r--r--tests/twisted/gabble/slow-service.py137
-rw-r--r--tests/twisted/gabble/status.py286
-rw-r--r--tests/twisted/gabblecaps_helper.py356
-rw-r--r--tests/twisted/gabbleconstants.py458
-rw-r--r--tests/twisted/gabbleservicetest.py640
-rw-r--r--tests/twisted/gabbletest.py824
-rw-r--r--tests/twisted/salut/slow-service.py202
-rw-r--r--tests/twisted/saluttest.py2
-rw-r--r--tests/twisted/tools/Makefile.am10
-rw-r--r--tests/twisted/tools/gabble-exec-with-log.sh.in40
-rw-r--r--tests/twisted/tools/org.freedesktop.Telepathy.ConnectionManager.gabble.service.in3
-rw-r--r--tests/twisted/yconstants.py1
37 files changed, 6031 insertions, 52 deletions
diff --git a/.gitignore b/.gitignore
index 8371169..3d5763b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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'