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 | |
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>
-rw-r--r-- | extensions/Gabble_Plugin_Console.xml | 117 | ||||
-rw-r--r-- | extensions/Makefile.am | 3 | ||||
-rw-r--r-- | extensions/all.xml | 1 | ||||
m--------- | lib/ext/wocky | 0 | ||||
-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 | ||||
-rw-r--r-- | tests/twisted/Makefile.am | 1 | ||||
-rw-r--r-- | tests/twisted/console.py | 86 | ||||
-rw-r--r-- | tools/c-constants-gen.py | 57 | ||||
-rw-r--r-- | tools/glib-ginterface-gen.py | 191 | ||||
-rw-r--r-- | tools/glib-gtypes-generator.py | 97 | ||||
-rw-r--r-- | tools/glib-interfaces-gen.py | 84 |
15 files changed, 1684 insertions, 145 deletions
diff --git a/extensions/Gabble_Plugin_Console.xml b/extensions/Gabble_Plugin_Console.xml new file mode 100644 index 000000000..1e3b52385 --- /dev/null +++ b/extensions/Gabble_Plugin_Console.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" ?> +<node name="/Gabble_Plugin_Console" + xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0"> + <tp:copyright>Copyright © 2011 Collabora Ltd.</tp:copyright> + <tp:license xmlns="http://www.w3.org/1999/xhtml"> + <p>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.</p> + + <p>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.</p> + + <p>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 Street, Fifth Floor, Boston, MA 02110-1301, + USA.</p> + </tp:license> + + <interface name="org.freedesktop.Telepathy.Gabble.Plugin.Console"> + <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" + value="true"/> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + <p>A sidecar interface providing an XMPP console.</p> + </tp:docstring> + + <method name="SendIQ" tp:name-for-bindings="Send_IQ"> + <arg direction="in" name="Type" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + The type of IQ request to send; either <code>"get"</code> or + <code>"set"</code>. + </tp:docstring> + </arg> + <arg direction="in" name="To" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + The recipient for the IQ; or the empty string to send an IQ to the + server (with no recipient specified). + </tp:docstring> + </arg> + <arg direction="in" name="Body" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + An XML fragment for the body of the IQ, which MUST have exactly one + top-level element. + </tp:docstring> + </arg> + <arg direction="out" name="Reply_Type" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + The type of IQ reply; either <code>"result"</code> or + <code>"error"</code>. + </tp:docstring> + </arg> + <arg direction="out" name="Body" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + An XML fragment for the body of the response, which MUST have between + zero and two top-level elements (the query element, an error element, + both, or neither). + </tp:docstring> + </arg> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + Hai. + </tp:docstring> + </method> + + <method name="SendStanza" tp:name-for-bindings="Send_Stanza"> + <arg direction="in" name="Stanza" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + A complete stanza. + </tp:docstring> + </arg> + + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + Sends a stanza, yo. + </tp:docstring> + </method> + + <signal name="StanzaSent" tp:name-for-bindings="Stanza_Sent"> + <arg name="Stanza" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + The stanza, in glorious XML. + </tp:docstring> + </arg> + + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + Emitted whenever a stanza is sent and + <tp:member-ref>SpewStanzas</tp:member-ref> is + <code>True</code>. + </tp:docstring> + </signal> + + <signal name="StanzaReceived" tp:name-for-bindings="Stanza_Received"> + <arg name="Stanza" type="s"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + The stanza, in glorious XML. + </tp:docstring> + </arg> + + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + Emitted whenever a stanza is received and + <tp:member-ref>SpewStanzas</tp:member-ref> is + <code>True</code>. + </tp:docstring> + </signal> + + <property name="SpewStanzas" type="b" access="readwrite" + tp:name-for-bindings="Spew_Stanzas"> + <tp:docstring xmlns="http://www.w3.org/1999/xhtml"> + If <code>True</code>, <tp:member-ref>StanzaReceived</tp:member-ref> and + <tp:member-ref>StanzaSent</tp:member-ref> will be emitted, drowning + your session bus in a sea of XML. + </tp:docstring> + </property> + + </interface> +</node> +<!-- vim:set sw=2 sts=2 et ft=xml: --> diff --git a/extensions/Makefile.am b/extensions/Makefile.am index feee4e790..628eee5c4 100644 --- a/extensions/Makefile.am +++ b/extensions/Makefile.am @@ -5,6 +5,7 @@ EXTRA_DIST = \ Channel_Type_FileTransfer_Future.xml \ Connection_Future.xml \ Connection_Interface_Gabble_Decloak.xml \ + Gabble_Plugin_Console.xml \ Gabble_Plugin_Gateways.xml \ Gabble_Plugin_Test.xml \ OLPC_Activity_Properties.xml \ @@ -79,7 +80,7 @@ _gen/signals-marshal.c: _gen/signals-marshal.list Makefile.am _gen/enums.h: _gen/all.xml $(tools_dir)/c-constants-gen.py \ Makefile.am - $(AM_V_GEN)$(PYTHON) $(tools_dir)/c-constants-gen.py Gabble $< > $@ + $(AM_V_GEN)$(PYTHON) $(tools_dir)/c-constants-gen.py Gabble $< _gen/enums _gen/interfaces.h _gen/interfaces-body.h: _gen/all.xml \ $(tools_dir)/glib-interfaces-gen.py \ diff --git a/extensions/all.xml b/extensions/all.xml index cb368ef9e..90f52a89b 100644 --- a/extensions/all.xml +++ b/extensions/all.xml @@ -40,6 +40,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA</p> <xi:include href="Connection_Interface_Gabble_Decloak.xml"/> <xi:include href="Connection_Future.xml"/> +<xi:include href="Gabble_Plugin_Console.xml"/> <xi:include href="Gabble_Plugin_Gateways.xml"/> <xi:include href="Gabble_Plugin_Test.xml"/> diff --git a/lib/ext/wocky b/lib/ext/wocky -Subproject a0e74a1cd04ab5e35922536f047664a5ce69c11 +Subproject 79008a4927792ddda8720d70d09948951e1b842 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; diff --git a/tests/twisted/Makefile.am b/tests/twisted/Makefile.am index 5d7c5abc9..5f52cc7a6 100644 --- a/tests/twisted/Makefile.am +++ b/tests/twisted/Makefile.am @@ -27,6 +27,7 @@ TWISTED_TESTS = \ connect/test-nonblocking-tls.py \ connect/test-success.py \ connect/test-twice.py \ + console.py \ dataforms.py \ gateways.py \ last-activity.py \ diff --git a/tests/twisted/console.py b/tests/twisted/console.py new file mode 100644 index 000000000..f6a6336c9 --- /dev/null +++ b/tests/twisted/console.py @@ -0,0 +1,86 @@ +""" +A smoketest for the XMPP console API. +""" + +from servicetest import ( + ProxyWrapper, EventPattern, + call_async, assertEquals, assertContains, assertNotEquals, sync_dbus, +) +from gabbletest import exec_test, acknowledge_iq, elem, elem_iq +from config import PLUGINS_ENABLED +from twisted.words.xish import domish + +CONSOLE_PLUGIN_IFACE = "org.freedesktop.Telepathy.Gabble.Plugin.Console" +STACY = 'stacy@pilgrim.lit' + +if not PLUGINS_ENABLED: + print "NOTE: built without --enable-plugins, not testing XMPP console" + raise SystemExit(77) + +def send_unrecognised_get(q, stream): + stream.send( + elem_iq(stream, 'get')( + elem('urn:unimaginative', 'dont-handle-me-bro') + )) + + return q.expect('stream-iq', iq_type='error') + +def test(q, bus, conn, stream): + path, _ = conn.Future.EnsureSidecar(CONSOLE_PLUGIN_IFACE) + console = ProxyWrapper(bus.get_object(conn.bus_name, path), + CONSOLE_PLUGIN_IFACE) + + assert not console.Properties.Get(CONSOLE_PLUGIN_IFACE, 'SpewStanzas') + es = [ + EventPattern('dbus-signal', signal='StanzaReceived'), + EventPattern('dbus-signal', signal='StanzaSent'), + ] + q.forbid_events(es) + + call_async(q, console, 'SendIQ', 'get', STACY, + '<coffee xmlns="urn:unimaginative"/>') + e = q.expect('stream-iq', iq_type='get', query_ns='urn:unimaginative', + query_name='coffee') + acknowledge_iq(stream, e.stanza) + e = q.expect('dbus-return', method='SendIQ') + type_, body = e.value + assertEquals('result', type_) + # We just assume the body works. + + # Turn on signalling incoming and outgoing stanzas + console.Properties.Set(CONSOLE_PLUGIN_IFACE, 'SpewStanzas', True) + sync_dbus(bus, q, conn) + q.unforbid_events(es) + + send_unrecognised_get(q, stream) + + e = q.expect('dbus-signal', signal='StanzaReceived') + xml, = e.args + assertContains('<iq', xml) + assertContains('<dont-handle-me-bro', xml) + + signal = q.expect('dbus-signal', signal='StanzaSent') + assertContains('service-unavailable', signal.args[0]) + + # Turn off spewing out stanzas; check it works. + console.Properties.Set(CONSOLE_PLUGIN_IFACE, 'SpewStanzas', False) + q.forbid_events(es) + send_unrecognised_get(q, stream) + sync_dbus(bus, q, conn) + + # Try sending just any old stanza + console.SendStanza(''' + <message to='%(stacy)s' type='headline'> + <body> + Hi sis. + </body> + </message>''' % { 'stacy': STACY }) + + e = q.expect('stream-message', to=STACY, message_type='headline') + # Wocky fills in xmlns='' for us if we don't specify a namespace... great. + # So this means <message/> gets sent as <message xmlns=''/> and the server + # kicks us off. + assertNotEquals('', e.stanza.uri) + +if __name__ == '__main__': + exec_test(test) diff --git a/tools/c-constants-gen.py b/tools/c-constants-gen.py index 8969ffdca..ff2a24d47 100644 --- a/tools/c-constants-gen.py +++ b/tools/c-constants-gen.py @@ -7,17 +7,23 @@ from libglibcodegen import NS_TP, get_docstring, \ get_descendant_text, get_by_path class Generator(object): - def __init__(self, prefix, dom): + def __init__(self, prefix, dom, output_base): self.prefix = prefix + '_' self.spec = get_by_path(dom, "spec")[0] + self.__header = open(output_base + '.h', 'w') + self.__docs = open(output_base + '-gtk-doc.h', 'w') + def __call__(self): self.do_header() self.do_body() self.do_footer() def write(self, code): - stdout.write(code.encode('utf-8')) + self.__header.write(code.encode('utf-8')) + + def d(self, code): + self.__docs.write(code.encode('utf-8')) # Header def do_header(self): @@ -54,25 +60,27 @@ extern "C" { value_prefix = flags.getAttribute('singular') or \ flags.getAttribute('value-prefix') or \ flags.getAttribute('name') - self.write("""\ + self.d("""\ /** * %s: """ % (self.prefix + name).replace('_', '')) for flag in get_by_path(flags, 'flag'): self.do_gtkdoc(flag, value_prefix) - self.write(' *\n') + self.d(' *\n') docstrings = get_by_path(flags, 'docstring') if docstrings: - self.write("""\ + self.d("""\ * <![CDATA[%s]]> * """ % get_descendant_text(docstrings).replace('\n', ' ')) - self.write("""\ + self.d("""\ * Bitfield/set of flags generated from the Telepathy specification. */ -typedef enum { """) + + self.write("typedef enum /*< flags >*/ {\n") + for flag in get_by_path(flags, 'flag'): self.do_val(flag, value_prefix) self.write("""\ @@ -87,7 +95,7 @@ typedef enum { enum.getAttribute('name') name_plural = enum.getAttribute('plural') or \ enum.getAttribute('name') + 's' - self.write("""\ + self.d("""\ /** * %s: @@ -95,28 +103,35 @@ typedef enum { vals = get_by_path(enum, 'enumvalue') for val in vals: self.do_gtkdoc(val, value_prefix) - self.write(' *\n') + self.d(' *\n') docstrings = get_by_path(enum, 'docstring') if docstrings: - self.write("""\ + self.d("""\ * <![CDATA[%s]]> * """ % get_descendant_text(docstrings).replace('\n', ' ')) - self.write("""\ + self.d("""\ * Bitfield/set of flags generated from the Telepathy specification. */ -typedef enum { """) + + self.write("typedef enum {\n") + for val in vals: self.do_val(val, value_prefix) - self.write("""\ -} %(mixed-name)s; + self.write("} %s;\n" % (self.prefix + name).replace('_', '')) + self.d("""\ /** - * NUM_%(upper-plural)s: + * NUM_%(upper-plural)s: (skip) * * 1 higher than the highest valid value of #%(mixed-name)s. */ +""" % {'mixed-name' : (self.prefix + name).replace('_', ''), + 'upper-plural' : (self.prefix + name_plural).upper(), + 'last-val' : vals[-1].getAttribute('value')}) + + self.write("""\ #define NUM_%(upper-plural)s (%(last-val)s+1) """ % {'mixed-name' : (self.prefix + name).replace('_', ''), @@ -133,13 +148,13 @@ typedef enum { self.write(' %s = %s,\n' % (use_name, val.getAttribute('value'))) def do_gtkdoc(self, node, value_prefix): - self.write(' * @') - self.write((self.prefix + value_prefix + '_' + + self.d(' * @') + self.d((self.prefix + value_prefix + '_' + node.getAttribute('suffix')).upper()) - self.write(': <![CDATA[') + self.d(': <![CDATA[') docstring = get_by_path(node, 'docstring') - self.write(get_descendant_text(docstring).replace('\n', ' ')) - self.write(']]>\n') + self.d(get_descendant_text(docstring).replace('\n', ' ')) + self.d(']]>\n') # Footer def do_footer(self): @@ -151,4 +166,4 @@ typedef enum { if __name__ == '__main__': argv = argv[1:] - Generator(argv[0], xml.dom.minidom.parse(argv[1]))() + Generator(argv[0], xml.dom.minidom.parse(argv[1]), argv[2])() diff --git a/tools/glib-ginterface-gen.py b/tools/glib-ginterface-gen.py index 95c827c7a..e277b91f4 100644 --- a/tools/glib-ginterface-gen.py +++ b/tools/glib-ginterface-gen.py @@ -33,6 +33,16 @@ from libglibcodegen import Signature, type_to_gtype, cmp_by_name, \ NS_TP = "http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0" +def get_emits_changed(node): + try: + return [ + annotation.getAttribute('value') + for annotation in node.getElementsByTagName('annotation') + if annotation.getAttribute('name') == 'org.freedesktop.DBus.Property.EmitsChangedSignal' + ][0] + except IndexError: + return None + class Generator(object): def __init__(self, dom, prefix, basename, signal_marshal_prefix, @@ -41,6 +51,7 @@ class Generator(object): self.dom = dom self.__header = [] self.__body = [] + self.__docs = [] assert prefix.endswith('_') assert not signal_marshal_prefix.endswith('_') @@ -74,11 +85,20 @@ class Generator(object): self.allow_havoc = allow_havoc def h(self, s): + if isinstance(s, unicode): + s = s.encode('utf-8') self.__header.append(s) def b(self, s): + if isinstance(s, unicode): + s = s.encode('utf-8') self.__body.append(s) + def d(self, s): + if isinstance(s, unicode): + s = s.encode('utf-8') + self.__docs.append(s) + def do_node(self, node): node_name = node.getAttribute('name').replace('/', '') node_name_mixed = self.node_name_mixed = node_name.replace('_', '') @@ -98,6 +118,8 @@ class Generator(object): if tmp and not self.allow_havoc: raise AssertionError('%s is %s' % (self.iface_name, tmp)) + iface_emits_changed = get_emits_changed(interface) + self.b('static const DBusGObjectInfo _%s%s_object_info;' % (self.prefix_, node_name_lc)) self.b('') @@ -158,54 +180,54 @@ class Generator(object): self.b('}') self.b('') - self.h('/**') - self.h(' * %s%s:' % (self.Prefix, node_name_mixed)) - self.h(' *') - self.h(' * Dummy typedef representing any implementation of this ' + self.d('/**') + self.d(' * %s%s:' % (self.Prefix, node_name_mixed)) + self.d(' *') + self.d(' * Dummy typedef representing any implementation of this ' 'interface.') - self.h(' */') + self.d(' */') + self.h('typedef struct _%s%s %s%s;' % (self.Prefix, node_name_mixed, self.Prefix, node_name_mixed)) self.h('') - self.h('/**') - self.h(' * %s%sClass:' % (self.Prefix, node_name_mixed)) - self.h(' *') - self.h(' * The class of %s%s.' % (self.Prefix, node_name_mixed)) + + self.d('/**') + self.d(' * %s%sClass:' % (self.Prefix, node_name_mixed)) + self.d(' *') + self.d(' * The class of %s%s.' % (self.Prefix, node_name_mixed)) if methods: - self.h(' *') - self.h(' * In a full implementation of this interface (i.e. all') - self.h(' * methods implemented), the interface initialization') - self.h(' * function used in G_IMPLEMENT_INTERFACE() would') - self.h(' * typically look like this:') - self.h(' *') - self.h(' * <programlisting>') - self.h(' * static void') - self.h(' * implement_%s (gpointer klass,' % self.node_name_lc) - self.h(' * gpointer unused G_GNUC_UNUSED)') - self.h(' * {') - # "#" is special to gtkdoc under some circumstances; it appears - # that escaping "##" as "#<!---->#" or "##" doesn't work, - # but adding an extra hash symbol does. Thanks, gtkdoc :-( - self.h(' * #define IMPLEMENT(x) %s%s_implement_###x (\\' + self.d(' *') + self.d(' * In a full implementation of this interface (i.e. all') + self.d(' * methods implemented), the interface initialization') + self.d(' * function used in G_IMPLEMENT_INTERFACE() would') + self.d(' * typically look like this:') + self.d(' *') + self.d(' * <programlisting>') + self.d(' * static void') + self.d(' * implement_%s (gpointer klass,' % self.node_name_lc) + self.d(' * gpointer unused G_GNUC_UNUSED)') + self.d(' * {') + self.d(' * #define IMPLEMENT(x) %s%s_implement_##x (\\' % (self.prefix_, self.node_name_lc)) - self.h(' * klass, my_object_###x)') + self.d(' * klass, my_object_##x)') for method in methods: class_member_name = method.getAttribute('tp:name-for-bindings') class_member_name = class_member_name.lower() - self.h(' * IMPLEMENT (%s);' % class_member_name) + self.d(' * IMPLEMENT (%s);' % class_member_name) - self.h(' * #undef IMPLEMENT') - self.h(' * }') - self.h(' * </programlisting>') + self.d(' * #undef IMPLEMENT') + self.d(' * }') + self.d(' * </programlisting>') else: - self.h(' * This interface has no D-Bus methods, so an') - self.h(' * implementation can typically pass %NULL to') - self.h(' * G_IMPLEMENT_INTERFACE() as the interface') - self.h(' * initialization function.') + self.d(' * This interface has no D-Bus methods, so an') + self.d(' * implementation can typically pass %NULL to') + self.d(' * G_IMPLEMENT_INTERFACE() as the interface') + self.d(' * initialization function.') - self.h(' */') + self.d(' */') + self.d('') self.h('typedef struct _%s%sClass %s%sClass;' % (self.Prefix, node_name_mixed, self.Prefix, node_name_mixed)) @@ -260,6 +282,16 @@ class Generator(object): flags = ('TP_DBUS_PROPERTIES_MIXIN_FLAG_READ | ' 'TP_DBUS_PROPERTIES_MIXIN_FLAG_WRITE') + prop_emits_changed = get_emits_changed(m) + + if prop_emits_changed is None: + prop_emits_changed = iface_emits_changed + + if prop_emits_changed == 'true': + flags += ' | TP_DBUS_PROPERTIES_MIXIN_FLAG_EMITS_CHANGED' + elif prop_emits_changed == 'invalidates': + flags += ' | TP_DBUS_PROPERTIES_MIXIN_FLAG_EMITS_INVALIDATED' + self.b(' { 0, %s, "%s", 0, NULL, NULL }, /* %s */' % (flags, m.getAttribute('type'), m.getAttribute('name'))) @@ -477,18 +509,19 @@ class Generator(object): else: out_args.append(struct) - # Implementation type declaration (in header, docs in body) - self.b('/**') - self.b(' * %s:' % impl_name) - self.b(' * @self: The object implementing this interface') + # Implementation type declaration (in header, docs separated) + self.d('/**') + self.d(' * %s:' % impl_name) + self.d(' * @self: The object implementing this interface') for (ctype, name) in in_args: - self.b(' * @%s: %s (FIXME, generate documentation)' + self.d(' * @%s: %s (FIXME, generate documentation)' % (name, ctype)) - self.b(' * @context: Used to return values or throw an error') - self.b(' *') - self.b(' * The signature of an implementation of the D-Bus method') - self.b(' * %s on interface %s.' % (dbus_method_name, self.iface_name)) - self.b(' */') + self.d(' * @context: Used to return values or throw an error') + self.d(' *') + self.d(' * The signature of an implementation of the D-Bus method') + self.d(' * %s on interface %s.' % (dbus_method_name, self.iface_name)) + self.d(' */') + self.h('typedef void (*%s) (%s%s *self,' % (impl_name, self.Prefix, self.node_name_mixed)) for (ctype, name) in in_args: @@ -533,18 +566,19 @@ class Generator(object): % (self.prefix_, self.node_name_lc, class_member_name, self.Prefix, self.node_name_mixed, impl_name)) - self.b('/**') - self.b(' * %s%s_implement_%s:' + self.d('/**') + self.d(' * %s%s_implement_%s:' % (self.prefix_, self.node_name_lc, class_member_name)) - self.b(' * @klass: A class whose instances implement this interface') - self.b(' * @impl: A callback used to implement the %s D-Bus method' + self.d(' * @klass: A class whose instances implement this interface') + self.d(' * @impl: A callback used to implement the %s D-Bus method' % dbus_method_name) - self.b(' *') - self.b(' * Register an implementation for the %s method in the vtable' + self.d(' *') + self.d(' * Register an implementation for the %s method in the vtable' % dbus_method_name) - self.b(' * of an implementation of this interface. To be called from') - self.b(' * the interface init function.') - self.b(' */') + self.d(' * of an implementation of this interface. To be called from') + self.d(' * the interface init function.') + self.d(' */') + self.b('void') self.b('%s%s_implement_%s (%s%sClass *klass, %s impl)' % (self.prefix_, self.node_name_lc, class_member_name, @@ -555,16 +589,18 @@ class Generator(object): self.b('') # Return convenience function (static inline, in header) - self.h('/**') - self.h(' * %s:' % ret_name) - self.h(' * @context: The D-Bus method invocation context') + self.d('/**') + self.d(' * %s:' % ret_name) + self.d(' * @context: The D-Bus method invocation context') for (ctype, name) in out_args: - self.h(' * @%s: %s (FIXME, generate documentation)' + self.d(' * @%s: %s (FIXME, generate documentation)' % (name, ctype)) - self.h(' *') - self.h(' * Return successfully by calling dbus_g_method_return().') - self.h(' * This inline function exists only to provide type-safety.') - self.h(' */') + self.d(' *') + self.d(' * Return successfully by calling dbus_g_method_return().') + self.d(' * This inline function exists only to provide type-safety.') + self.d(' */') + self.d('') + tmp = (['DBusGMethodInvocation *context'] + [ctype + name for (ctype, name) in out_args]) self.h('static inline') @@ -634,17 +670,17 @@ class Generator(object): # FIXME: emit docs - self.b('/**') - self.b(' * %s:' % stub_name) - self.b(' * @instance: The object implementing this interface') + self.d('/**') + self.d(' * %s:' % stub_name) + self.d(' * @instance: The object implementing this interface') for (ctype, name, gtype) in args: - self.b(' * @%s: %s (FIXME, generate documentation)' + self.d(' * @%s: %s (FIXME, generate documentation)' % (name, ctype)) - self.b(' *') - self.b(' * Type-safe wrapper around g_signal_emit to emit the') - self.b(' * %s signal on interface %s.' + self.d(' *') + self.d(' * Type-safe wrapper around g_signal_emit to emit the') + self.d(' * %s signal on interface %s.' % (dbus_name, self.iface_name)) - self.b(' */') + self.d(' */') self.b('void') self.b(('%s (' % stub_name) + (',\n '.join(tmp)) + ')') @@ -660,16 +696,20 @@ class Generator(object): signal_name = dbus_gutils_wincaps_to_uscore(dbus_name).replace('_', '-') - in_base_init.append(' /**') - in_base_init.append(' * %s%s::%s:' + + self.d('/**') + self.d(' * %s%s::%s:' % (self.Prefix, self.node_name_mixed, signal_name)) + self.d(' * @self: an object') for (ctype, name, gtype) in args: - in_base_init.append(' * @%s: %s (FIXME, generate documentation)' + self.d(' * @%s: %s (FIXME, generate documentation)' % (name, ctype)) - in_base_init.append(' *') - in_base_init.append(' * The %s D-Bus signal is emitted whenever ' + self.d(' *') + self.d(' * The %s D-Bus signal is emitted whenever ' 'this GObject signal is.' % dbus_name) - in_base_init.append(' */') + self.d(' */') + self.d('') + in_base_init.append(' %s_signals[%s] =' % (self.node_name_lc, const_name)) in_base_init.append(' g_signal_new ("%s",' % signal_name) @@ -727,6 +767,7 @@ class Generator(object): self.b('') open(self.basename + '.h', 'w').write('\n'.join(self.__header)) open(self.basename + '.c', 'w').write('\n'.join(self.__body)) + open(self.basename + '-gtk-doc.h', 'w').write('\n'.join(self.__docs)) def cmdline_error(): diff --git a/tools/glib-gtypes-generator.py b/tools/glib-gtypes-generator.py index ebc2ad4c9..a49c36e7f 100644 --- a/tools/glib-gtypes-generator.py +++ b/tools/glib-gtypes-generator.py @@ -44,8 +44,9 @@ class GTypesGenerator(object): self.header = open(output + '.h', 'w') self.body = open(output + '-body.h', 'w') + self.docs = open(output + '-gtk-doc.h', 'w') - for f in (self.header, self.body): + for f in (self.header, self.body, self.docs): f.write('/* Auto-generated, do not edit.\n *\n' ' * This file may be distributed under the same terms\n' ' * as the specification from which it was generated.\n' @@ -70,6 +71,9 @@ class GTypesGenerator(object): def c(self, code): self.body.write(code.encode("utf-8")) + def d(self, code): + self.docs.write(code.encode('utf-8')) + def do_mapping_header(self, mapping): members = mapping.getElementsByTagNameNS(NS_TP, 'member') assert len(members) == 2 @@ -85,41 +89,41 @@ class GTypesGenerator(object): docstring = get_docstring(mapping) or '(Undocumented)' - self.h('/**\n * %s:\n *\n' % name) - self.h(' * %s\n' % xml_escape(docstring)) - self.h(' *\n') - self.h(' * This macro expands to a call to a function\n') - self.h(' * that returns the #GType of a #GHashTable\n') - self.h(' * appropriate for representing a D-Bus\n') - self.h(' * dictionary of signature\n') - self.h(' * <literal>a{%s}</literal>.\n' % impl_sig) - self.h(' *\n') + self.d('/**\n * %s:\n *\n' % name) + self.d(' * %s\n' % xml_escape(docstring)) + self.d(' *\n') + self.d(' * This macro expands to a call to a function\n') + self.d(' * that returns the #GType of a #GHashTable\n') + self.d(' * appropriate for representing a D-Bus\n') + self.d(' * dictionary of signature\n') + self.d(' * <literal>a{%s}</literal>.\n' % impl_sig) + self.d(' *\n') key, value = members - self.h(' * Keys (D-Bus type <literal>%s</literal>,\n' + self.d(' * Keys (D-Bus type <literal>%s</literal>,\n' % key.getAttribute('type')) tp_type = key.getAttributeNS(NS_TP, 'type') if tp_type: - self.h(' * type <literal>%s</literal>,\n' % tp_type) - self.h(' * named <literal>%s</literal>):\n' + self.d(' * type <literal>%s</literal>,\n' % tp_type) + self.d(' * named <literal>%s</literal>):\n' % key.getAttribute('name')) docstring = get_docstring(key) or '(Undocumented)' - self.h(' * %s\n' % xml_escape(docstring)) - self.h(' *\n') + self.d(' * %s\n' % xml_escape(docstring)) + self.d(' *\n') - self.h(' * Values (D-Bus type <literal>%s</literal>,\n' + self.d(' * Values (D-Bus type <literal>%s</literal>,\n' % value.getAttribute('type')) tp_type = value.getAttributeNS(NS_TP, 'type') if tp_type: - self.h(' * type <literal>%s</literal>,\n' % tp_type) - self.h(' * named <literal>%s</literal>):\n' + self.d(' * type <literal>%s</literal>,\n' % tp_type) + self.d(' * named <literal>%s</literal>):\n' % value.getAttribute('name')) docstring = get_docstring(value) or '(Undocumented)' - self.h(' * %s\n' % xml_escape(docstring)) - self.h(' *\n') + self.d(' * %s\n' % xml_escape(docstring)) + self.d(' *\n') - self.h(' */\n') + self.d(' */\n') self.h('#define %s (%s ())\n\n' % (name, impl)) self.need_mappings[impl_sig] = esc_impl_sig @@ -130,11 +134,12 @@ class GTypesGenerator(object): contents_sig = 'a{' + impl_sig + '}' esc_contents_sig = escape_as_identifier(contents_sig) impl = self.prefix_ + 'type_dbus_array_of_' + esc_contents_sig - self.h('/**\n * %s:\n\n' % gtype_name) - self.h(' * Expands to a call to a function\n') - self.h(' * that returns the #GType of a #GPtrArray\n') - self.h(' * of #%s.\n' % name) - self.h(' */\n') + self.d('/**\n * %s:\n\n' % gtype_name) + self.d(' * Expands to a call to a function\n') + self.d(' * that returns the #GType of a #GPtrArray\n') + self.d(' * of #%s.\n' % name) + self.d(' */\n\n') + self.h('#define %s (%s ())\n\n' % (gtype_name, impl)) self.need_other_arrays[contents_sig] = esc_contents_sig @@ -157,41 +162,43 @@ class GTypesGenerator(object): docstring = '(Undocumented)' else: docstring = '(Undocumented)' - self.h('/**\n * %s:\n\n' % name) - self.h(' * %s\n' % xml_escape(docstring)) - self.h(' *\n') - self.h(' * This macro expands to a call to a function\n') - self.h(' * that returns the #GType of a #GValueArray\n') - self.h(' * appropriate for representing a D-Bus struct\n') - self.h(' * with signature <literal>(%s)</literal>.\n' + self.d('/**\n * %s:\n\n' % name) + self.d(' * %s\n' % xml_escape(docstring)) + self.d(' *\n') + self.d(' * This macro expands to a call to a function\n') + self.d(' * that returns the #GType of a #GValueArray\n') + self.d(' * appropriate for representing a D-Bus struct\n') + self.d(' * with signature <literal>(%s)</literal>.\n' % impl_sig) - self.h(' *\n') + self.d(' *\n') for i, member in enumerate(members): - self.h(' * Member %d (D-Bus type ' + self.d(' * Member %d (D-Bus type ' '<literal>%s</literal>,\n' % (i, member.getAttribute('type'))) tp_type = member.getAttributeNS(NS_TP, 'type') if tp_type: - self.h(' * type <literal>%s</literal>,\n' % tp_type) - self.h(' * named <literal>%s</literal>):\n' + self.d(' * type <literal>%s</literal>,\n' % tp_type) + self.d(' * named <literal>%s</literal>):\n' % member.getAttribute('name')) docstring = get_docstring(member) or '(Undocumented)' - self.h(' * %s\n' % xml_escape(docstring)) - self.h(' *\n') + self.d(' * %s\n' % xml_escape(docstring)) + self.d(' *\n') + + self.d(' */\n\n') - self.h(' */\n') self.h('#define %s (%s ())\n\n' % (name, impl)) array_name = struct.getAttribute('array-name') if array_name != '': array_name = (self.PREFIX_ + 'ARRAY_TYPE_' + array_name.upper()) impl = self.prefix_ + 'type_dbus_array_' + esc_impl_sig - self.h('/**\n * %s:\n\n' % array_name) - self.h(' * Expands to a call to a function\n') - self.h(' * that returns the #GType of a #GPtrArray\n') - self.h(' * of #%s.\n' % name) - self.h(' */\n') + self.d('/**\n * %s:\n\n' % array_name) + self.d(' * Expands to a call to a function\n') + self.d(' * that returns the #GType of a #GPtrArray\n') + self.d(' * of #%s.\n' % name) + self.d(' */\n\n') + self.h('#define %s (%s ())\n\n' % (array_name, impl)) self.need_struct_arrays[impl_sig] = esc_impl_sig diff --git a/tools/glib-interfaces-gen.py b/tools/glib-interfaces-gen.py index 95439687e..69c721be3 100644 --- a/tools/glib-interfaces-gen.py +++ b/tools/glib-interfaces-gen.py @@ -9,8 +9,13 @@ from libglibcodegen import NS_TP, get_docstring, \ class Generator(object): def __init__(self, prefix, implfile, declfile, dom): self.prefix = prefix + '_' + + assert declfile.endswith('.h') + docfile = declfile[:-2] + '-gtk-doc.h' + self.impls = open(implfile, 'w') self.decls = open(declfile, 'w') + self.docs = open(docfile, 'w') self.spec = get_by_path(dom, "spec")[0] def h(self, code): @@ -19,6 +24,9 @@ class Generator(object): def c(self, code): self.impls.write(code.encode('utf-8')) + def d(self, code): + self.docs.write(code.encode('utf-8')) + def __call__(self): for f in self.h, self.c: self.do_header(f) @@ -50,25 +58,37 @@ class Generator(object): def do_iface(self, iface): parent_name = get_by_path(iface, '../@name') - self.h("""\ + self.d("""\ /** * %(IFACE_DEFINE)s: * * The interface name "%(name)s" */ +""" % {'IFACE_DEFINE' : (self.prefix + 'IFACE_' + \ + parent_name).upper().replace('/', ''), + 'name' : iface.getAttribute('name')}) + + self.h(""" #define %(IFACE_DEFINE)s \\ "%(name)s" """ % {'IFACE_DEFINE' : (self.prefix + 'IFACE_' + \ parent_name).upper().replace('/', ''), 'name' : iface.getAttribute('name')}) - self.h(""" + self.d(""" /** * %(IFACE_QUARK_DEFINE)s: * * Expands to a call to a function that returns a quark for the interface \ name "%(name)s" */ +""" % {'IFACE_QUARK_DEFINE' : (self.prefix + 'IFACE_QUARK_' + \ + parent_name).upper().replace('/', ''), + 'iface_quark_func' : (self.prefix + 'iface_quark_' + \ + parent_name).lower().replace('/', ''), + 'name' : iface.getAttribute('name')}) + + self.h(""" #define %(IFACE_QUARK_DEFINE)s \\ (%(iface_quark_func)s ()) @@ -99,12 +119,20 @@ GQuark 'name' : iface.getAttribute('name')}) for prop in iface.getElementsByTagNameNS(None, 'property'): - self.decls.write(""" + self.d(""" /** * %(IFACE_PREFIX)s_%(PROP_UC)s: * * The fully-qualified property name "%(name)s.%(prop)s" */ +""" % {'IFACE_PREFIX' : (self.prefix + 'PROP_' + \ + parent_name).upper().replace('/', ''), + 'PROP_UC': prop.getAttributeNS(NS_TP, "name-for-bindings").upper(), + 'name' : iface.getAttribute('name'), + 'prop' : prop.getAttribute('name'), + }) + + self.h(""" #define %(IFACE_PREFIX)s_%(PROP_UC)s \\ "%(name)s.%(prop)s" """ % {'IFACE_PREFIX' : (self.prefix + 'PROP_' + \ @@ -114,6 +142,56 @@ GQuark 'prop' : prop.getAttribute('name'), }) + + for prop in iface.getElementsByTagNameNS(NS_TP, 'contact-attribute'): + self.d(""" +/** + * %(TOKEN_PREFIX)s_%(TOKEN_UC)s: + * + * The fully-qualified contact attribute token name "%(name)s/%(prop)s" + */ +""" % {'TOKEN_PREFIX' : (self.prefix + 'TOKEN_' + \ + parent_name).upper().replace('/', ''), + 'TOKEN_UC': prop.getAttributeNS(None, "name").upper().replace("-", "_").replace(".", "_"), + 'name' : iface.getAttribute('name'), + 'prop' : prop.getAttribute('name'), + }) + + self.h(""" +#define %(TOKEN_PREFIX)s_%(TOKEN_UC)s \\ +"%(name)s/%(prop)s" +""" % {'TOKEN_PREFIX' : (self.prefix + 'TOKEN_' + \ + parent_name).upper().replace('/', ''), + 'TOKEN_UC': prop.getAttributeNS(None, "name").upper().replace("-", "_").replace(".", "_"), + 'name' : iface.getAttribute('name'), + 'prop' : prop.getAttribute('name'), + }) + + for prop in iface.getElementsByTagNameNS(NS_TP, 'hct'): + if (prop.getAttribute('is-family') != "yes"): + self.d(""" +/** + * %(TOKEN_PREFIX)s_%(TOKEN_UC)s: + * + * The fully-qualified capability token name "%(name)s/%(prop)s" + */ +""" % {'TOKEN_PREFIX' : (self.prefix + 'TOKEN_' + \ + parent_name).upper().replace('/', ''), + 'TOKEN_UC': prop.getAttributeNS(None, "name").upper().replace("-", "_").replace(".", "_"), + 'name' : iface.getAttribute('name'), + 'prop' : prop.getAttribute('name'), + }) + + self.h(""" +#define %(TOKEN_PREFIX)s_%(TOKEN_UC)s \\ +"%(name)s/%(prop)s" +""" % {'TOKEN_PREFIX' : (self.prefix + 'TOKEN_' + \ + parent_name).upper().replace('/', ''), + 'TOKEN_UC': prop.getAttributeNS(None, "name").upper().replace("-", "_").replace(".", "_"), + 'name' : iface.getAttribute('name'), + 'prop' : prop.getAttribute('name'), + }) + if __name__ == '__main__': argv = argv[1:] Generator(argv[0], argv[1], argv[2], xml.dom.minidom.parse(argv[3]))() |