diff options
author | Will Thompson <will.thompson@collabora.co.uk> | 2011-11-21 13:14:04 +0000 |
---|---|---|
committer | Will Thompson <will.thompson@collabora.co.uk> | 2011-11-21 13:14:09 +0000 |
commit | 357aef5d882f461a05037255f3f0dee844d18d6f (patch) | |
tree | 00cbf9ed66b37b7d174c6fea880074d7d39efc2d /plugins | |
parent | cbe05e97476160fab38ba491717f4d5a42c17887 (diff) | |
parent | ac3ed729bed7d8c4fb62a11ade54f54b6139c0d0 (diff) |
Merge branch 'xmpp-console'
Fixes: <https://bugs.freedesktop.org/show_bug.cgi?id=xmpp-console>
Reviewed-by: Guillaume Desmottes <guillaume.desmottes@collabora.co.uk>
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/Makefile.am | 11 | ||||
-rw-r--r-- | plugins/console.c | 668 | ||||
-rw-r--r-- | plugins/console.h | 88 | ||||
-rwxr-xr-x | plugins/telepathy-gabble-xmpp-console | 423 | ||||
-rw-r--r-- | plugins/test.c | 2 |
5 files changed, 1192 insertions, 0 deletions
diff --git a/plugins/Makefile.am b/plugins/Makefile.am index 97f87a4c5..0330cec53 100644 --- a/plugins/Makefile.am +++ b/plugins/Makefile.am @@ -1,6 +1,7 @@ plugindir = $(libdir)/telepathy/gabble-0 installable_plugins = \ + console.la \ gateways.la test_only_plugins = \ @@ -29,10 +30,16 @@ endif if ENABLE_PLUGINS plugin_LTLIBRARIES = $(installable_plugins) + +dist_bin_SCRIPTS = \ + telepathy-gabble-xmpp-console else # we still compile the plugin (just to make sure it compiles!) but we don't # install it noinst_LTLIBRARIES += $(installable_plugins) + +EXTRA_DIST = \ + telepathy-gabble-xmpp-console endif AM_LDFLAGS = -module -avoid-version -shared @@ -45,6 +52,10 @@ gateways_la_SOURCES = \ gateways.c \ gateways.h +console_la_SOURCES = \ + console.c \ + console.h + AM_CFLAGS = $(ERROR_CFLAGS) \ -I $(top_srcdir) -I $(top_builddir) \ @DBUS_CFLAGS@ @GLIB_CFLAGS@ @WOCKY_CFLAGS@ @TP_GLIB_CFLAGS@ \ diff --git a/plugins/console.c b/plugins/console.c new file mode 100644 index 000000000..13ca0eccf --- /dev/null +++ b/plugins/console.c @@ -0,0 +1,668 @@ +/* XML console plugin + * + * Copyright © 2011 Collabora Ltd. <http://www.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 "console.h" + +#include "config.h" + +#include <string.h> + +#include <telepathy-glib/telepathy-glib.h> + +#include <wocky/wocky-xmpp-error.h> +#include <wocky/wocky-utils.h> +#include <wocky/wocky-xmpp-reader.h> +#include <wocky/wocky-xmpp-writer.h> +#include <wocky/wocky-namespaces.h> + +#include "extensions/extensions.h" + +#include <gabble/gabble.h> + +/************************* + * Plugin implementation * + *************************/ + +static guint debug = 0; + +#define DEBUG(format, ...) \ +G_STMT_START { \ + if (debug != 0) \ + g_debug ("%s: " format, G_STRFUNC, ## __VA_ARGS__); \ +} G_STMT_END + +static const GDebugKey debug_keys[] = { + { "console", 1 }, + { NULL, 0 } +}; + +static void plugin_iface_init ( + gpointer g_iface, + gpointer data); + +static const gchar * const sidecar_interfaces[] = { + GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, + NULL +}; + +G_DEFINE_TYPE_WITH_CODE (GabbleConsolePlugin, gabble_console_plugin, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_PLUGIN, plugin_iface_init); + ) + +static void +gabble_console_plugin_init (GabbleConsolePlugin *self) +{ +} + +static void +gabble_console_plugin_class_init (GabbleConsolePluginClass *klass) +{ +} + +static void +gabble_console_plugin_create_sidecar ( + GabblePlugin *plugin, + const gchar *sidecar_interface, + GabbleConnection *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 gabble_plugin_create_sidecar_finish() so we + * need to use the same source tag. + */ + gabble_plugin_create_sidecar); + GabbleSidecar *sidecar = NULL; + + if (!tp_strdiff (sidecar_interface, GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE)) + { + sidecar = g_object_new (GABBLE_TYPE_CONSOLE_SIDECAR, + "connection", connection, + "session", session, + NULL); + } + else + { + g_simple_async_result_set_error (result, TP_ERRORS, + TP_ERROR_NOT_IMPLEMENTED, "'%s' not implemented", sidecar_interface); + } + + if (sidecar != NULL) + g_simple_async_result_set_op_res_gpointer (result, sidecar, + g_object_unref); + + g_simple_async_result_complete_in_idle (result); + g_object_unref (result); +} + +static void +plugin_iface_init ( + gpointer g_iface, + gpointer data G_GNUC_UNUSED) +{ + GabblePluginInterface *iface = g_iface; + + iface->name = "XMPP console"; + iface->version = PACKAGE_VERSION; + iface->sidecar_interfaces = sidecar_interfaces; + iface->create_sidecar = gabble_console_plugin_create_sidecar; +} + +GabblePlugin * +gabble_plugin_create (void) +{ + debug = g_parse_debug_string (g_getenv ("GABBLE_DEBUG"), debug_keys, + G_N_ELEMENTS (debug_keys) - 1); + DEBUG ("loaded"); + + return g_object_new (GABBLE_TYPE_CONSOLE_PLUGIN, + NULL); +} + +/************************** + * Sidecar implementation * + **************************/ + +enum { + PROP_0, + PROP_CONNECTION, + PROP_SESSION, + PROP_SPEW +}; + +struct _GabbleConsoleSidecarPrivate +{ + WockySession *session; + TpBaseConnection *connection; + WockyXmppReader *reader; + WockyXmppWriter *writer; + + /* %TRUE if we should emit signals when sending or receiving stanzas */ + gboolean spew; + /* 0 if spew is FALSE; or a WockyPorter handler id for all incoming stanzas + * if spew is TRUE. */ + guint incoming_handler; + /* 0 if spew is FALSE; a GLib signal handler id for WockyPorter::sending if + * spew is TRUE. + */ + gulong sending_id; +}; + +static void sidecar_iface_init ( + gpointer g_iface, + gpointer data); +static void console_iface_init ( + gpointer g_iface, + gpointer data); +static void gabble_console_sidecar_set_spew ( + GabbleConsoleSidecar *self, + gboolean spew); + +G_DEFINE_TYPE_WITH_CODE (GabbleConsoleSidecar, gabble_console_sidecar, + G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SIDECAR, sidecar_iface_init); + G_IMPLEMENT_INTERFACE (GABBLE_TYPE_SVC_GABBLE_PLUGIN_CONSOLE, + console_iface_init); + G_IMPLEMENT_INTERFACE (TP_TYPE_SVC_DBUS_PROPERTIES, + tp_dbus_properties_mixin_iface_init); + ) + +static void +gabble_console_sidecar_init (GabbleConsoleSidecar *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GABBLE_TYPE_CONSOLE_SIDECAR, + GabbleConsoleSidecarPrivate); + self->priv->reader = wocky_xmpp_reader_new_no_stream (); + self->priv->writer = wocky_xmpp_writer_new_no_stream (); +} + +static void +gabble_console_sidecar_get_property ( + GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); + + switch (property_id) + { + case PROP_SPEW: + g_value_set_boolean (value, self->priv->spew); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +gabble_console_sidecar_set_property ( + GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); + + switch (property_id) + { + case PROP_CONNECTION: + g_assert (self->priv->connection == NULL); /* construct-only */ + self->priv->connection = g_value_dup_object (value); + break; + + case PROP_SESSION: + g_assert (self->priv->session == NULL); /* construct-only */ + self->priv->session = g_value_dup_object (value); + break; + + case PROP_SPEW: + gabble_console_sidecar_set_spew (self, g_value_get_boolean (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + } +} + +static void +gabble_console_sidecar_dispose (GObject *object) +{ + void (*chain_up) (GObject *) = + G_OBJECT_CLASS (gabble_console_sidecar_parent_class)->dispose; + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (object); + + gabble_console_sidecar_set_spew (self, FALSE); + + tp_clear_object (&self->priv->connection); + tp_clear_object (&self->priv->reader); + tp_clear_object (&self->priv->writer); + tp_clear_object (&self->priv->session); + + if (chain_up != NULL) + chain_up (object); +} + +static void +gabble_console_sidecar_class_init (GabbleConsoleSidecarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + static TpDBusPropertiesMixinPropImpl console_props[] = { + { "SpewStanzas", "spew-stanzas", "spew-stanzas" }, + { NULL }, + }; + static TpDBusPropertiesMixinIfaceImpl interfaces[] = { + { GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, + tp_dbus_properties_mixin_getter_gobject_properties, + /* FIXME: if we were feeling clever, we'd override the setter so that + * we can monitor the bus name of any application which sets + * SpewStanzas to TRUE and flip it back to false when that application + * dies. + * + * Alternatively, we could just replace this sidecar with a channel. + */ + tp_dbus_properties_mixin_setter_gobject_properties, + console_props + }, + { NULL }, + }; + + object_class->get_property = gabble_console_sidecar_get_property; + object_class->set_property = gabble_console_sidecar_set_property; + object_class->dispose = gabble_console_sidecar_dispose; + + g_type_class_add_private (klass, sizeof (GabbleConsoleSidecarPrivate)); + + g_object_class_install_property (object_class, PROP_CONNECTION, + g_param_spec_object ("connection", "Connection", + "Gabble connection", + GABBLE_TYPE_CONNECTION, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_SESSION, + g_param_spec_object ("session", "Session", + "Wocky session", + WOCKY_TYPE_SESSION, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (object_class, PROP_SPEW, + g_param_spec_boolean ("spew-stanzas", "SpewStanzas", + "If %TRUE, someone wants us to spit out a tonne of stanzas", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + + klass->props_class.interfaces = interfaces; + tp_dbus_properties_mixin_class_init (object_class, + G_STRUCT_OFFSET (GabbleConsoleSidecarClass, props_class)); +} + +static void sidecar_iface_init ( + gpointer g_iface, + gpointer data) +{ + GabbleSidecarInterface *iface = g_iface; + + iface->interface = GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE; + iface->get_immutable_properties = NULL; +} + +static gboolean +incoming_cb ( + WockyPorter *porter, + WockyStanza *stanza, + gpointer user_data) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (user_data); + const guint8 *body; + gsize length; + + wocky_xmpp_writer_write_stanza (self->priv->writer, stanza, &body, &length); + gabble_svc_gabble_plugin_console_emit_stanza_received (self, + (const gchar *) body); + return FALSE; +} + +static void +sending_cb ( + WockyPorter *porter, + WockyStanza *stanza, + gpointer user_data) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (user_data); + + if (stanza != NULL) + { + const guint8 *body; + gsize length; + + wocky_xmpp_writer_write_stanza (self->priv->writer, stanza, &body, + &length); + gabble_svc_gabble_plugin_console_emit_stanza_sent (self, + (const gchar *) body); + } +} + +static void +gabble_console_sidecar_set_spew ( + GabbleConsoleSidecar *self, + gboolean spew) +{ + GabbleConsoleSidecarPrivate *priv = self->priv; + + if (!spew != !priv->spew) + { + WockyPorter *porter = wocky_session_get_porter (self->priv->session); + const gchar *props[] = { "SpewStanzas", NULL }; + + priv->spew = spew; + tp_dbus_properties_mixin_emit_properties_changed (G_OBJECT (self), + GABBLE_IFACE_GABBLE_PLUGIN_CONSOLE, props); + + if (spew) + { + g_return_if_fail (priv->incoming_handler == 0); + priv->incoming_handler = wocky_porter_register_handler_from_anyone ( + porter, WOCKY_STANZA_TYPE_NONE, WOCKY_STANZA_SUB_TYPE_NONE, + WOCKY_PORTER_HANDLER_PRIORITY_MAX, incoming_cb, self, NULL); + + g_return_if_fail (priv->sending_id == 0); + priv->sending_id = g_signal_connect (porter, "sending", + (GCallback) sending_cb, self); + } + else + { + g_return_if_fail (priv->incoming_handler != 0); + wocky_porter_unregister_handler (porter, priv->incoming_handler); + priv->incoming_handler = 0; + + g_return_if_fail (priv->sending_id != 0); + g_signal_handler_disconnect (porter, priv->sending_id); + priv->sending_id = 0; + } + } +} + +static void +return_from_send_iq ( + GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (source); + DBusGMethodInvocation *context = user_data; + GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (result); + GError *error = NULL; + + if (g_simple_async_result_propagate_error (simple, &error)) + { + dbus_g_method_return_error (context, error); + g_error_free (error); + } + else + { + WockyStanza *reply = g_simple_async_result_get_op_res_gpointer (simple); + WockyStanzaSubType sub_type; + const guint8 *body; + gsize length; + + wocky_stanza_get_type_info (reply, NULL, &sub_type); + wocky_xmpp_writer_write_stanza (self->priv->writer, reply, &body, &length); + + /* woop woop */ + gabble_svc_gabble_plugin_console_return_from_send_iq (context, + sub_type == WOCKY_STANZA_SUB_TYPE_RESULT ? "result" : "error", + (const gchar *) body); + } +} + +static void +console_iq_reply_cb ( + GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + WockyPorter *porter = WOCKY_PORTER (source); + GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (user_data); + GError *error = NULL; + WockyStanza *reply = wocky_porter_send_iq_finish (porter, result, &error); + + if (reply != NULL) + { + g_simple_async_result_set_op_res_gpointer (simple, reply, g_object_unref); + } + else + { + g_simple_async_result_set_from_error (simple, error); + g_error_free (error); + } + + g_simple_async_result_complete (simple); + g_object_unref (simple); +} + +static gboolean +get_iq_type (const gchar *type_str, + WockyStanzaSubType *sub_type_out, + GError **error) +{ + if (!wocky_strdiff (type_str, "get")) + { + *sub_type_out = WOCKY_STANZA_SUB_TYPE_GET; + return TRUE; + } + + if (!wocky_strdiff (type_str, "set")) + { + *sub_type_out = WOCKY_STANZA_SUB_TYPE_SET; + return TRUE; + } + + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "Type must be 'get' or 'set', not '%s'", type_str); + return FALSE; +} + +static gboolean +validate_jid (const gchar **to, + GError **error) +{ + if (tp_str_empty (*to)) + { + *to = NULL; + return TRUE; + } + + if (wocky_decode_jid (*to, NULL, NULL, NULL)) + return TRUE; + + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "'%s' is not a valid (or empty) JID", *to); + return FALSE; +} + +/* + * @xml: doesn't actually have to be a top-level stanza. It can be the body of + * an IQ or whatever. + */ +static gboolean +parse_me_a_stanza ( + GabbleConsoleSidecar *self, + const gchar *xml, + WockyStanza **stanza_out, + GError **error) +{ + GabbleConsoleSidecarPrivate *priv = self->priv; + WockyStanza *stanza; + + wocky_xmpp_reader_reset (priv->reader); + wocky_xmpp_reader_push (priv->reader, (const guint8 *) xml, strlen (xml)); + + *error = wocky_xmpp_reader_get_error (priv->reader); + + if (*error != NULL) + return FALSE; + + stanza = wocky_xmpp_reader_pop_stanza (priv->reader); + + if (stanza == NULL) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "Incomplete stanza! Bad person."); + return FALSE; + } + + *stanza_out = stanza; + return TRUE; +} + +static void +console_send_iq ( + GabbleSvcGabblePluginConsole *sidecar, + const gchar *type_str, + const gchar *to, + const gchar *body, + DBusGMethodInvocation *context) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (sidecar); + WockyPorter *porter = wocky_session_get_porter (self->priv->session); + WockyStanzaSubType sub_type; + WockyStanza *fragment; + GError *error = NULL; + + if (get_iq_type (type_str, &sub_type, &error) && + validate_jid (&to, &error) && + parse_me_a_stanza (self, body, &fragment, &error)) + { + GSimpleAsyncResult *simple = g_simple_async_result_new (G_OBJECT (self), + return_from_send_iq, context, console_send_iq); + WockyStanza *stanza = wocky_stanza_build (WOCKY_STANZA_TYPE_IQ, sub_type, + NULL, to, NULL); + + wocky_node_add_node_tree (wocky_stanza_get_top_node (stanza), + WOCKY_NODE_TREE (fragment)); + wocky_porter_send_iq_async (porter, stanza, NULL, console_iq_reply_cb, simple); + g_object_unref (fragment); + } + else + { + DEBUG ("%s", error->message); + dbus_g_method_return_error (context, error); + g_error_free (error); + } +} + +static void +console_stanza_sent_cb ( + GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + WockyPorter *porter = WOCKY_PORTER (source); + DBusGMethodInvocation *context = user_data; + GError *error = NULL; + + if (wocky_porter_send_finish (porter, result, &error)) + { + gabble_svc_gabble_plugin_console_return_from_send_stanza (context); + } + else + { + dbus_g_method_return_error (context, error); + g_clear_error (&error); + } +} + +static gboolean +stanza_looks_coherent ( + WockyStanza *stanza, + GError **error) +{ + WockyNode *top_node = wocky_stanza_get_top_node (stanza); + WockyStanzaType t; + WockyStanzaSubType st; + + wocky_stanza_get_type_info (stanza, &t, &st); + + if (t == WOCKY_STANZA_TYPE_UNKNOWN) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "I don't know what a <%s/> is", top_node->name); + return FALSE; + } + else if (st == WOCKY_STANZA_SUB_TYPE_UNKNOWN) + { + g_set_error (error, TP_ERRORS, TP_ERROR_INVALID_ARGUMENT, + "I don't know what type='%s' means", + wocky_node_get_attribute (top_node, "type")); + return FALSE; + } + else + { + if (top_node->ns == g_quark_from_static_string ("")) + { + /* So... Wocky puts an empty string in as the namespace. Greaaat. */ + top_node->ns = g_quark_from_static_string (WOCKY_XMPP_NS_JABBER_CLIENT); + } + + return TRUE; + } +} + +static void +console_send_stanza ( + GabbleSvcGabblePluginConsole *sidecar, + const gchar *xml, + DBusGMethodInvocation *context) +{ + GabbleConsoleSidecar *self = GABBLE_CONSOLE_SIDECAR (sidecar); + WockyPorter *porter = wocky_session_get_porter (self->priv->session); + WockyStanza *stanza = NULL; + GError *error = NULL; + + if (parse_me_a_stanza (self, xml, &stanza, &error) && + stanza_looks_coherent (stanza, &error)) + { + wocky_porter_send_async (porter, stanza, NULL, console_stanza_sent_cb, + context); + } + else + { + DEBUG ("%s", error->message); + dbus_g_method_return_error (context, error); + g_error_free (error); + } + + tp_clear_object (&stanza); +} + +static void +console_iface_init ( + gpointer klass, + gpointer data G_GNUC_UNUSED) +{ +#define IMPLEMENT(x) gabble_svc_gabble_plugin_console_implement_##x (\ + klass, console_##x) + IMPLEMENT (send_iq); + IMPLEMENT (send_stanza); +#undef IMPLEMENT +} diff --git a/plugins/console.h b/plugins/console.h new file mode 100644 index 000000000..a91b95337 --- /dev/null +++ b/plugins/console.h @@ -0,0 +1,88 @@ +/* XML console plugin + * + * Copyright © 2011 Collabora Ltd. <http://www.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 <glib-object.h> + +#include <gio/gio.h> +#include <wocky/wocky-session.h> +#include <telepathy-glib/dbus-properties-mixin.h> + +typedef struct _GabbleConsolePlugin GabbleConsolePlugin; +typedef struct _GabbleConsolePluginClass GabbleConsolePluginClass; +typedef struct _GabbleConsolePluginPrivate GabbleConsolePluginPrivate; + +struct _GabbleConsolePlugin { + GObject parent; + GabbleConsolePluginPrivate *priv; +}; + +struct _GabbleConsolePluginClass { + GObjectClass parent; +}; + +GType gabble_console_plugin_get_type (void); + +#define GABBLE_TYPE_CONSOLE_PLUGIN \ + (gabble_console_plugin_get_type ()) +#define GABBLE_CONSOLE_PLUGIN(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), GABBLE_TYPE_CONSOLE_PLUGIN, \ + GabbleConsolePlugin)) +#define GABBLE_CONSOLE_PLUGIN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), GABBLE_TYPE_CONSOLE_PLUGIN, \ + GabbleConsolePluginClass)) +#define GABBLE_IS_CONSOLE_PLUGIN(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GABBLE_TYPE_CONSOLE_PLUGIN)) +#define GABBLE_IS_CONSOLE_PLUGIN_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GABBLE_TYPE_CONSOLE_PLUGIN)) +#define GABBLE_CONSOLE_PLUGIN_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GABBLE_TYPE_CONSOLE_PLUGIN, \ + GabbleConsolePluginClass)) + +typedef struct _GabbleConsoleSidecar GabbleConsoleSidecar; +typedef struct _GabbleConsoleSidecarClass GabbleConsoleSidecarClass; +typedef struct _GabbleConsoleSidecarPrivate GabbleConsoleSidecarPrivate; + +struct _GabbleConsoleSidecar { + GObject parent; + GabbleConsoleSidecarPrivate *priv; +}; + +struct _GabbleConsoleSidecarClass { + GObjectClass parent; + + TpDBusPropertiesMixinClass props_class; +}; + +GType gabble_console_sidecar_get_type (void); + +#define GABBLE_TYPE_CONSOLE_SIDECAR \ + (gabble_console_sidecar_get_type ()) +#define GABBLE_CONSOLE_SIDECAR(obj) \ + (G_TYPE_CHECK_INSTANCE_CAST ((obj), GABBLE_TYPE_CONSOLE_SIDECAR, \ + GabbleConsoleSidecar)) +#define GABBLE_CONSOLE_SIDECAR_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST ((klass), GABBLE_TYPE_CONSOLE_SIDECAR, \ + GabbleConsoleSidecarClass)) +#define GABBLE_IS_CONSOLE_SIDECAR(obj) \ + (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GABBLE_TYPE_CONSOLE_SIDECAR)) +#define GABBLE_IS_CONSOLE_SIDECAR_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_TYPE ((klass), GABBLE_TYPE_CONSOLE_SIDECAR)) +#define GABBLE_CONSOLE_SIDECAR_GET_CLASS(obj) \ + (G_TYPE_INSTANCE_GET_CLASS ((obj), GABBLE_TYPE_CONSOLE_SIDECAR, \ + GabbleConsoleSidecarClass)) diff --git a/plugins/telepathy-gabble-xmpp-console b/plugins/telepathy-gabble-xmpp-console new file mode 100755 index 000000000..4ab6ecbea --- /dev/null +++ b/plugins/telepathy-gabble-xmpp-console @@ -0,0 +1,423 @@ +#!/usr/bin/env python +# vim: set fileencoding=utf-8 sts=4 sw=4 et : +""" +The world's worst XMPP console user interface. + +Pass it the bus name of a Gabble connection; type some words; get minimalistic +error reporting. + +Copyright © 2011 Collabora Ltd. <http://www.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 +""" + +import sys +import re +from xml.dom import minidom + +import pygtk +pygtk.require('2.0') + +from gi.repository import Gtk +from gi.repository import GLib +from gi.repository import Gio +from gi.repository import GtkSource + +PADDING = 6 + +def pathify(name): + return '/' + name.replace('.', '/') + +def nameify(path): + return (path[1:]).replace('/', '.') + +CONN_FUTURE_IFACE = "org.freedesktop.Telepathy.Connection.FUTURE" +CONSOLE_IFACE = "org.freedesktop.Telepathy.Gabble.Plugin.Console" + +class StanzaViewer(Gtk.ScrolledWindow): + def __init__(self): + Gtk.ScrolledWindow.__init__(self) + + self.b = GtkSource.Buffer() + self.view = GtkSource.View.new_with_buffer(self.b) + self.b.set_language( + GtkSource.LanguageManager.get_default().get_language('xml')) + self.b.set_highlight_matching_brackets(False) + self.view.set_editable(False) + self.view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR) + self.view.set_property('expand', True) + + self.add(self.view) + + def clear(self): + self.b.set_text("") + + def append_stanza(self, xml): + pretty = minidom.parseString(xml).toprettyxml() + pretty = pretty.replace('<?xml version="1.0" ?>\n', '') + i = self.b.get_end_iter() + self.b.insert(i, pretty + '\n') + + def append_comment(self, text): + i = self.b.get_end_iter() + self.b.insert(i, '<!-- %s -->\n' % text) + + def tell_me_everything(self): + return self.b.get_property('text') + +class SpinWrapper(Gtk.Notebook): + PRIMARY_PAGE = 0 + SPINNER_PAGE = 1 + + def __init__(self, main_widget): + Gtk.Notebook.__init__(self) + self.set_show_tabs(False) + self.set_show_border(False) + self.insert_page(main_widget, None, self.PRIMARY_PAGE) + + self.spinner = Gtk.Spinner() + self.spinner.set_property('halign', Gtk.Align.CENTER) + self.spinner.set_property('valign', Gtk.Align.CENTER) + self.spinner.set_property('width-request', 32) + self.spinner.set_property('height-request', 32) + self.insert_page(self.spinner, None, self.SPINNER_PAGE) + + def start_spinning(self): + self.set_current_page(self.SPINNER_PAGE) + self.spinner.start() + + def stop_spinning(self): + self.spinner.stop() + self.set_current_page(self.PRIMARY_PAGE) + +class Page(Gtk.Grid): + def __init__(self, console_proxy): + Gtk.Grid.__init__(self) + + self.console_proxy = console_proxy + + self.set_column_spacing(PADDING) + self.set_row_spacing(PADDING) + + def add_title(self, title, below=None): + label = Gtk.Label() + label.set_markup("<b>%s</b>" % title) + label.set_property('xalign', 0) + + if below is None: + self.attach(label, 0, 0, 2, 1) + else: + self.attach_next_to(label, below, Gtk.PositionType.BOTTOM, 2, 1) + + return label + + def add_label(self, title, below=None): + label = Gtk.Label(title) + label.set_property('margin-left', PADDING) + label.set_property('xalign', 0) + + if below is None: + self.attach(label, 0, 0, 1, 1) + else: + self.attach_next_to(label, below, Gtk.PositionType.BOTTOM, 1, 1) + + return label + +class IQPage(Page): + def __init__(self, console_proxy): + Page.__init__(self, console_proxy) + + request_label = self.add_title("Request") + + recipient_label, recipient_entry = self.add_label_entry_pair( + 'To:', below=request_label) + self.recipient_entry = recipient_entry + + type_label = self.add_label('IQ Type:', below=recipient_label) + self.get_button = Gtk.RadioButton.new_with_label([], "get") + self.get_button.set_active(True) + self.set_button = Gtk.RadioButton.new_with_label_from_widget( + self.get_button, "set") + + box = Gtk.ButtonBox.new(Gtk.Orientation.HORIZONTAL) + box.set_layout(Gtk.ButtonBoxStyle.START) + box.add(self.get_button) + box.add(self.set_button) + self.attach_next_to(box, type_label, + Gtk.PositionType.RIGHT, 1, 1) + + body_label, body_entry = self.add_label_entry_pair( + 'Body:', below=type_label) + body_entry.set_text( + "<query xmlns='http://jabber.org/protocol/disco#info'/>") + body_entry.set_icon_from_stock( + Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_GO_FORWARD) + body_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, "Send this IQ") + self.body_entry = body_entry + + reply_label = self.add_title("Reply", below=body_label) + + self.stanza_viewer = StanzaViewer() + self.stanza_viewer.append_comment("send a request to see the reply here") + + self.result_nb = SpinWrapper(self.stanza_viewer) + + self.attach_next_to(self.result_nb, reply_label, Gtk.PositionType.BOTTOM, 2, 1) + + body_entry.connect('activate', self.send_iq) + body_entry.connect('icon-release', self.send_iq) + + def add_label_entry_pair(self, title, below): + label = self.add_label(title, below) + + entry = Gtk.Entry() + entry.set_property('margin-right', PADDING) + entry.set_property('hexpand', True) + + self.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1) + + return label, entry + + def send_iq(self, *misc): + type = 'get' if self.get_button.get_active() else 'set' + to = self.recipient_entry.get_text() + body = self.body_entry.get_text() + + self.console_proxy.SendIQ('(sss)', type, to, body, + result_handler=self.send_iq_cb) + self.result_nb.start_spinning() + + def send_iq_cb(self, proxy, result, user_data): + self.stanza_viewer.clear() + + if isinstance(result, Exception): + self.stanza_viewer.append_comment("error:\n%s" % result) + else: + reply_type, reply = result + self.stanza_viewer.append_stanza(reply) + + self.result_nb.stop_spinning() + +class StanzaPage(Page): + def __init__(self, console_proxy): + Page.__init__(self, console_proxy) + + title = self.add_title("Enter a complete stanza:") + + self.sv = StanzaViewer() + self.sv.view.set_editable(True) + self.sv.append_stanza("<message to='t-pain@test.collabora.co.uk' type='chat'><body>Been on any nice boats recently?</body></message>") + + self.spin_wrapper = SpinWrapper(self.sv) + self.attach_next_to(self.spin_wrapper, title, Gtk.PositionType.BOTTOM, + 2, 1) + + self.result_label = self.add_label('', self.spin_wrapper) + self.result_label.set_property('hexpand', True) + self.result_label.set_line_wrap(True) + + b = Gtk.Button.new_with_mnemonic("_Send") + b.connect('clicked', self.__send_stanza) + b.set_property('hexpand', False) + self.attach_next_to(b, self.result_label, Gtk.PositionType.RIGHT, 1, 1) + + def __send_stanza(self, button): + self.console_proxy.SendStanza('(s)', self.sv.tell_me_everything(), + result_handler=self.__send_stanza_cb) + self.spin_wrapper.start_spinning() + + def __send_stanza_cb(self, proxy, result, user_data): + if isinstance(result, Exception): + # FIXME: this sucks. You can't just get the free text bit without + # the D-Bus error bit. + t = result.message + else: + t = "yes sir, captain tightpants" + + self.result_label.set_text(t) + self.spin_wrapper.stop_spinning() + +class SnoopyPage(Page): + def __init__(self, console_proxy): + Page.__init__(self, console_proxy) + + label = self.add_label("Stanza monitor:") + label.set_property('hexpand', True) + + switch = Gtk.Switch() + self.attach_next_to(switch, label, Gtk.PositionType.RIGHT, 1, 1) + + self.stanza_viewer = StanzaViewer() + self.attach_next_to(self.stanza_viewer, label, Gtk.PositionType.BOTTOM, 2, 1) + + switch.set_active(self.get_remote_active()) + switch.connect('notify::active', self.__switch_switched_cb) + + self.console_proxy.connect('g-signal', self.__g_signal_cb) + + def teardown(self): + """Turn off the monitor when we quit.""" + self.__set_spew(False) + + def __set_spew(self, spew): + args = GLib.Variant("(ssv)", (CONSOLE_IFACE, "SpewStanzas", + GLib.Variant.new_boolean(spew))) + self.console_proxy.call_sync( + "org.freedesktop.DBus.Properties.Set", + args, + 0, -1, None) + + def get_remote_active(self): + return self.console_proxy.get_cached_property('SpewStanzas').get_boolean() + + def __switch_switched_cb(self, switch, pspec): + remote = self.get_remote_active() + new_local = switch.get_active() + + if new_local != remote: + self.__set_spew(new_local) + self.stanza_viewer.append_comment( + 'started monitoring' if new_local else 'stopped monitoring') + + def __g_signal_cb(self, console_proxy, sender_name, signal_name, parameters): + if signal_name in ['StanzaSent', 'StanzaReceived']: + outgoing = (signal_name == 'StanzaSent') + xml, = parameters + + self.stanza_viewer.append_comment('sent' if outgoing else 'received') + self.stanza_viewer.append_stanza(xml) + +class Window(Gtk.Window): + IQ_PAGE = 0 + STANZA_PAGE = 1 + SNOOPY_PAGE = 2 + + def __init__(self, bus, connection_bus_name): + Gtk.Window.__init__(self) + + self.set_title('XMPP Console') + self.set_default_size(600, 371) + + conn_future_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + connection_bus_name, pathify(connection_bus_name), + CONN_FUTURE_IFACE, None) + try: + sidecar_path, _ = conn_future_proxy.EnsureSidecar('(s)', CONSOLE_IFACE) + except Exception, e: + print """ +Couldn't connect to the XMPP console interface on '%(connection_bus_name)s': + %(e)s +Check that it's a running Jabber connection, and that you have the console +plugin installed.""" % locals() + + raise SystemExit(2) + + self.console_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + connection_bus_name, sidecar_path, CONSOLE_IFACE, None) + + # Build up the UI + self.nb = Gtk.Notebook() + self.add(self.nb) + + self.iq = IQPage(self.console_proxy) + self.nb.insert_page(self.iq, + Gtk.Label.new_with_mnemonic("_IQ console"), + self.IQ_PAGE) + + self.stanza = StanzaPage(self.console_proxy) + self.nb.insert_page(self.stanza, + Gtk.Label.new_with_mnemonic("Send a s_tanza"), + self.STANZA_PAGE) + + self.snoopy = SnoopyPage(self.console_proxy) + self.nb.insert_page(self.snoopy, + Gtk.Label.new_with_mnemonic("_Monitor network traffic"), + self.SNOOPY_PAGE) + + self.connect('destroy', Window.__destroy_cb) + + def __destroy_cb(self): + self.snoopy.teardown() + Gtk.main_quit() + +GABBLE_PREFIX = 'org.freedesktop.Telepathy.Connection.gabble.jabber.' + +AM_BUS_NAME = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_PREFIX = '/org/freedesktop/Telepathy/Account' +ACCOUNT_IFACE = 'org.freedesktop.Telepathy.Account' + +def usage(): + print """ +Usage: + + %(arg0)s gabble/jabber/blahblah + %(arg0)s %(prefix)sblahblah + +List account identifiers using `mc-tool list | grep gabble`. +List connection bus names using `qdbus | grep gabble`. +""" % { 'arg0': sys.argv[0], + 'prefix': GABBLE_PREFIX, + } + raise SystemExit(1) + +if __name__ == '__main__': + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + + if len(sys.argv) != 2: + usage() + + thing = sys.argv[1] + + if re.match('^gabble/jabber/[a-zA-Z0-9_]+$', thing): + # Looks like an account path to me. + account_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + AM_BUS_NAME, '%s/%s' % (ACCOUNT_PREFIX, thing), + ACCOUNT_IFACE, None) + path = account_proxy.get_cached_property('Connection').get_string() + if path == '/': + print "%s is not online" % thing + raise SystemExit(1) + else: + thing = nameify(path) + + if not re.match('^%s[a-zA-Z0-9_]+$' % GABBLE_PREFIX, thing): + usage() + + win = Window(bus, thing) + win.show_all() + + Gtk.main() + +""" + .,,. + ,;;*;;;;, + .-'``;-');;. + /' .-. /*;; + .' \d \;; .;;;, + / o ` \; ,__. ,;*;;;*;, + \__, _.__,' \_.-') __)--.;;;;;*;;;;, + `""`;;;\ /-')_) __) `\' ';;;;;; + ;*;;; -') `)_) |\ | ;;;;*; + ;;;;| `---` O | | ;;*;;; + *;*;\| O / ;;;;;* + ;;;;;/| .-------\ / ;*;;;;; + ;;;*;/ \ | '. (`. ;;;*;;; + ;;;;;'. ; | ) \ | ;;;;;; + ,;*;;;;\/ |. / /` | ';;;*; + ;;;;;;/ |/ / /__/ ';;; + '*jgs/ | / | ;*; + `""""` `""""` ;' +""" diff --git a/plugins/test.c b/plugins/test.c index 9130cd1b3..05d784052 100644 --- a/plugins/test.c +++ b/plugins/test.c @@ -1,4 +1,5 @@ #include "test.h" +#include "config.h" #include <stdio.h> @@ -161,6 +162,7 @@ plugin_iface_init ( GabblePluginInterface *iface = g_iface; iface->name = "Sidecar test plugin"; + iface->version = PACKAGE_VERSION; iface->sidecar_interfaces = sidecar_interfaces; iface->create_sidecar = test_plugin_create_sidecar; iface->create_channel_managers = test_plugin_create_channel_managers; |