changeset 27052:413006df9828

propagate from branch 'im.pidgin.pidgin' (head 13ac492a493b4d31c8b29905174b43a533304300) to branch 'im.pidgin.cpw.darkrain42.xmpp.disco' (head e73974597bbcc75504a88eac45d6893c182d058e)
author Paul Aurich <paul@darkrain42.org>
date Wed, 29 Apr 2009 23:20:51 +0000
parents 836a36564ff5 (current diff) 464dbfad4474 (diff)
children 58ff88dba33a
files libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h libpurple/protocols/jabber/libxmpp.c libpurple/prpl.h pidgin/gtkblist.c
diffstat 18 files changed, 2176 insertions(+), 90 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/Makefile.am	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/Makefile.am	Wed Apr 29 23:20:51 2009 +0000
@@ -47,6 +47,7 @@
 	core.c \
 	debug.c \
 	desktopitem.c \
+	disco.c \
 	eventloop.c \
 	ft.c \
 	idle.c \
@@ -107,6 +108,7 @@
 	dbus-maybe.h \
 	debug.h \
 	desktopitem.h \
+	disco.h \
 	eventloop.h \
 	ft.h \
 	gaim-compat.h \
@@ -183,7 +185,7 @@
 dbus_headers  = dbus-bindings.h dbus-purple.h dbus-server.h dbus-useful.h dbus-define-api.h dbus-types.h
 
 dbus_exported = dbus-useful.h dbus-define-api.h account.h blist.h buddyicon.h \
-                connection.h conversation.h core.h ft.h log.h notify.h prefs.h roomlist.h \
+                connection.h conversation.h core.h disco.h ft.h log.h notify.h prefs.h roomlist.h \
                 savedstatuses.h smiley.h status.h server.h util.h xmlnode.h prpl.h
 
 purple_build_coreheaders = $(addprefix $(srcdir)/, $(purple_coreheaders)) \
--- a/libpurple/Makefile.mingw	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/Makefile.mingw	Wed Apr 29 23:20:51 2009 +0000
@@ -41,6 +41,7 @@
 			conversation.c \
 			core.c \
 			debug.c \
+			disco.c \
 			dnsquery.c \
 			dnssrv.c \
 			eventloop.c \
--- a/libpurple/dbus-analyze-functions.py	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/dbus-analyze-functions.py	Wed Apr 29 23:20:51 2009 +0000
@@ -31,6 +31,13 @@
     "purple_account_unregister",
     "purple_connection_new_unregister",
 
+    # Similar to all the above:
+    "purple_disco_list_set_service_close_func",
+    "purple_disco_list_set_cancel_func",
+    "purple_disco_list_set_protocol_data",
+    "purple_disco_list_set_expand_func",
+    "purple_disco_list_set_register_func",
+
     # This is excluded because this script treats PurpleLogReadFlags*
     # as pointer to a struct, instead of a pointer to an enum.  This
     # causes a compilation error. Someone should fix this script.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/disco.c	Wed Apr 29 23:20:51 2009 +0000
@@ -0,0 +1,411 @@
+/**
+ * @file disco.c Service Discovery API
+ * @ingroup core
+ */
+
+/* purple
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#include "internal.h"
+#include "debug.h"
+
+#include "disco.h"
+
+/**************************************************************************/
+/* Main structures, members and constants                                 */
+/**************************************************************************/
+
+/**
+ * Represents a list of services for a given connection on a given protocol.
+ */
+struct _PurpleDiscoList {
+	PurpleAccount *account; /**< The account this list belongs to. */
+	GList *services; /**< The list of services. */
+
+	gboolean in_progress;
+
+	gpointer ui_data; /**< UI private data. */
+	gpointer proto_data; /**< Prpl private data. */
+	guint ref; /**< The reference count. */
+
+	struct {
+		PurpleDiscoCancelFunc cancel_cb;
+		PurpleDiscoCloseFunc close_cb; /**< Callback to free the prpl data */
+		PurpleDiscoServiceCloseFunc service_close_cb;
+		PurpleDiscoServiceExpandFunc expand_cb; /**< Expand a service (iteratively
+		                                             look for things that are
+		                                             "sub-elements" in the tree */
+		PurpleDiscoRegisterFunc register_cb;
+	} ops;
+};
+
+/**
+ * Represents a list of services for a given connection on a given protocol.
+ */
+struct _PurpleDiscoService {
+	PurpleDiscoList *list;
+	gchar *name; /**< The name of the service. */
+	gchar *description; /**< The name of the service. */
+
+	gpointer proto_data;
+
+	gchar *gateway_type; /**< The type of the gateway service. */
+	PurpleDiscoServiceType type; /**< The type of service. */
+	PurpleDiscoServiceFlags flags;
+};
+
+static PurpleDiscoUiOps *ops = NULL;
+
+PurpleDiscoList *purple_disco_list_new(PurpleAccount *account)
+{
+	PurpleDiscoList *list;
+
+	g_return_val_if_fail(account != NULL, NULL);
+
+	list = g_new0(PurpleDiscoList, 1);
+	list->account = account;
+	list->ref = 1;
+
+	if (ops && ops->create)
+		ops->create(list);
+
+	return list;
+}
+
+PurpleDiscoList *purple_disco_list_ref(PurpleDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	list->ref++;
+	purple_debug_misc("disco", "reffing list, ref count now %d\n", list->ref);
+
+	return list;
+}
+
+static void purple_disco_list_service_destroy(PurpleDiscoList *list, PurpleDiscoService *r)
+{
+	if (list->ops.service_close_cb)
+		list->ops.service_close_cb(r);
+
+	g_free(r->name);
+	g_free(r->description);
+	g_free(r->gateway_type);
+	g_free(r);
+}
+
+static void purple_disco_list_destroy(PurpleDiscoList *list)
+{
+	GList *l;
+
+	purple_debug_misc("disco", "destroying list %p\n", list);
+
+	if (ops && ops->destroy)
+		ops->destroy(list);
+
+	if (list->ops.close_cb)
+		list->ops.close_cb(list);
+
+	for (l = list->services; l; l = l->next) {
+		PurpleDiscoService *s = l->data;
+		purple_disco_list_service_destroy(list, s);
+	}
+	g_list_free(list->services);
+
+	g_free(list);
+}
+
+void purple_disco_list_unref(PurpleDiscoList *list)
+{
+	g_return_if_fail(list != NULL);
+	g_return_if_fail(list->ref > 0);
+
+	list->ref--;
+
+	purple_debug_misc("disco", "unreffing list, ref count now %d\n", list->ref);
+	if (list->ref == 0)
+		purple_disco_list_destroy(list);
+}
+
+void purple_disco_list_service_add(PurpleDiscoList *list,
+                                   PurpleDiscoService *service,
+                                   PurpleDiscoService *parent)
+{
+	g_return_if_fail(list != NULL);
+	g_return_if_fail(service != NULL);
+
+	list->services = g_list_append(list->services, service);
+	service->list = list;
+
+	if (ops && ops->add_service)
+		ops->add_service(list, service, parent);
+}
+
+PurpleDiscoService *purple_disco_list_service_new(PurpleDiscoServiceType type,
+		const gchar *name, const gchar *description,
+		PurpleDiscoServiceFlags flags, gpointer proto_data)
+{
+	PurpleDiscoService *s;
+
+	g_return_val_if_fail(name != NULL, NULL);
+	g_return_val_if_fail(type != PURPLE_DISCO_SERVICE_TYPE_UNSET, NULL);
+
+	s = g_new0(PurpleDiscoService, 1);
+	s->name = g_strdup(name);
+	s->type = type;
+	s->description = g_strdup(description);
+	s->flags = flags;
+	s->proto_data = proto_data;
+
+	return s;
+}
+
+PurpleDiscoList *purple_disco_get_list(PurpleConnection *pc)
+{
+	PurplePlugin *prpl = NULL;
+	PurplePluginProtocolInfo *prpl_info = NULL;
+
+	g_return_val_if_fail(pc != NULL, NULL);
+
+	prpl = purple_connection_get_prpl(pc);
+	if (prpl != NULL)
+		prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
+
+	if (prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, disco_get_list))
+		return prpl_info->disco_get_list(pc);
+
+	return NULL;
+}
+
+void purple_disco_cancel_get_list(PurpleDiscoList *list)
+{
+	PurpleConnection *pc;
+
+	g_return_if_fail(list != NULL);
+
+	pc = purple_account_get_connection(list->account);
+
+	g_return_if_fail(pc != NULL);
+
+	if (list->ops.cancel_cb)
+		list->ops.cancel_cb(list);
+
+	purple_disco_list_set_in_progress(list, FALSE);
+}
+
+void purple_disco_service_expand(PurpleDiscoService *service)
+{
+	PurpleDiscoList *list;
+	PurpleConnection *pc;
+
+	g_return_if_fail(service != NULL);
+	g_return_if_fail((service->flags & PURPLE_DISCO_BROWSE));
+
+	list = service->list;
+	pc = purple_account_get_connection(list->account);
+	
+	g_return_if_fail(pc != NULL);
+
+	purple_disco_list_set_in_progress(list, TRUE);
+
+	if (list->ops.expand_cb)
+		list->ops.expand_cb(list, service);
+	else
+		purple_debug_warning("disco", "Cannot expand %s for account %s, "
+		                              "protocol did not provide expand op.\n",
+		                     service->name,
+		                     purple_account_get_username(list->account));
+}
+
+void purple_disco_service_register(PurpleDiscoService *service)
+{
+	PurpleDiscoList *list;
+	PurpleConnection *pc;
+
+	g_return_if_fail(service != NULL);
+
+	list = service->list;
+	pc = purple_account_get_connection(list->account);
+
+	g_return_if_fail(pc != NULL);
+
+	if (list->ops.register_cb)
+		list->ops.register_cb(pc, service);
+	else
+		purple_debug_warning("disco", "Cannot register to %s for account %s, "
+		                              "protocol did not provide register op.\n",
+		                     service->name,
+		                     purple_account_get_username(list->account));
+}
+
+const gchar *purple_disco_service_get_name(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, NULL);
+
+	return service->name;
+}
+
+const gchar *purple_disco_service_get_description(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, NULL);
+
+	return service->description;
+}
+
+gpointer
+purple_disco_service_get_protocol_data(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, NULL);
+
+	return service->proto_data;
+}
+
+PurpleDiscoServiceType
+purple_disco_service_get_type(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, PURPLE_DISCO_SERVICE_TYPE_UNSET);
+
+	return service->type;
+}
+
+PurpleDiscoServiceFlags
+purple_disco_service_get_flags(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, PURPLE_DISCO_NONE);
+
+	return service->flags;
+}
+
+void purple_disco_service_set_gateway_type(PurpleDiscoService *service, const gchar *type)
+{
+	g_return_if_fail(service != NULL);
+
+	g_free(service->gateway_type);
+	service->gateway_type = g_strdup(type);
+}
+
+const gchar *purple_disco_service_get_gateway_type(PurpleDiscoService *service)
+{
+	g_return_val_if_fail(service != NULL, NULL);
+
+	return service->gateway_type;
+}
+
+PurpleAccount* purple_disco_list_get_account(PurpleDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	return list->account;
+}
+
+GList* purple_disco_list_get_services(PurpleDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	return list->services;
+}
+
+void purple_disco_list_set_ui_data(PurpleDiscoList *list, gpointer ui_data)
+{
+	g_return_if_fail(list != NULL);
+
+	list->ui_data = ui_data;
+}
+
+gpointer purple_disco_list_get_ui_data(PurpleDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	return list->ui_data;
+}
+
+void purple_disco_list_set_in_progress(PurpleDiscoList *list,
+                                       gboolean in_progress)
+{
+	g_return_if_fail(list != NULL);
+
+	list->in_progress = in_progress;
+
+	if (ops && ops->in_progress)
+		ops->in_progress(list, in_progress);
+}
+
+gboolean purple_disco_list_get_in_progress(PurpleDiscoList *list) 
+{
+	g_return_val_if_fail(list != NULL, FALSE);
+
+	return list->in_progress;
+}
+
+void purple_disco_list_set_protocol_data(PurpleDiscoList *list,
+                                         gpointer proto_data,
+                                         PurpleDiscoCloseFunc cb)
+{
+	g_return_if_fail(list != NULL);
+
+	list->proto_data   = proto_data;
+	list->ops.close_cb = cb;
+}
+
+gpointer purple_disco_list_get_protocol_data(PurpleDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	return list->proto_data;
+}
+
+void purple_disco_list_set_service_close_func(PurpleDiscoList *list,
+                                              PurpleDiscoServiceCloseFunc cb)
+{
+	g_return_if_fail(list != NULL);
+
+	list->ops.service_close_cb = cb;
+}
+
+void purple_disco_list_set_cancel_func(PurpleDiscoList *list, PurpleDiscoCancelFunc cb)
+{
+	g_return_if_fail(list != NULL);
+
+	list->ops.cancel_cb = cb;
+}
+
+void purple_disco_list_set_expand_func(PurpleDiscoList *list, PurpleDiscoServiceExpandFunc cb)
+{
+	g_return_if_fail(list != NULL);
+
+	list->ops.expand_cb = cb;
+}
+
+void purple_disco_list_set_register_func(PurpleDiscoList *list, PurpleDiscoRegisterFunc cb)
+{
+	g_return_if_fail(list != NULL);
+
+	list->ops.register_cb = cb;
+}
+
+void purple_disco_set_ui_ops(PurpleDiscoUiOps *ui_ops)
+{
+	ops = ui_ops;
+}
+
+PurpleDiscoUiOps *purple_disco_get_ui_ops(void)
+{
+	return ops;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/disco.h	Wed Apr 29 23:20:51 2009 +0000
@@ -0,0 +1,414 @@
+/**
+ * @file disco.h Service Discovery API
+ * @ingroup core
+ */
+
+/* purple
+ *
+ * Purple is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#ifndef _PURPLE_DISCO_H_
+#define _PURPLE_DISCO_H_
+
+
+typedef struct _PurpleDiscoList PurpleDiscoList;
+typedef struct _PurpleDiscoService PurpleDiscoService;
+typedef struct _PurpleDiscoUiOps PurpleDiscoUiOps;
+
+#include "account.h"
+#include "connection.h"
+
+/**
+ * A prpl callback called to tell the prpl to cancel creating the list.
+ * The prpl must implement this and must not add services to the
+ * PurpleDiscoList after this is called.
+ *
+ * @param list  The disco list.
+ */
+typedef void (*PurpleDiscoCancelFunc)(PurpleDiscoList *list);
+
+/**
+ * A prpl callback called to tell the prpl to tell it to destroy
+ * prpl-specific data relating to this PurpleDiscoList (which will be
+ * destroyed imminently).
+ *
+ * @param list  The disco list.
+ */
+typedef void (*PurpleDiscoCloseFunc)(PurpleDiscoList *list);
+typedef void (*PurpleDiscoServiceCloseFunc)(PurpleDiscoService *service);
+
+typedef void (*PurpleDiscoServiceExpandFunc)(PurpleDiscoList *list,
+                                             PurpleDiscoService *service);
+
+/**
+ * A prpl callback called to initiate registration with the specificed
+ * service.
+ *
+ * @param pc       The connection.
+ * @param service  The service to which to register.
+ *
+ */
+typedef void (*PurpleDiscoRegisterFunc)(PurpleConnection *pc,
+                                        PurpleDiscoService *service);
+
+/**
+ * The types of services.
+ */
+typedef enum
+{
+	PURPLE_DISCO_SERVICE_TYPE_UNSET,
+	/**
+	 * A registerable gateway to another protocol. An example would be
+	 * XMPP legacy transports.
+	 */
+	PURPLE_DISCO_SERVICE_TYPE_GATEWAY,
+
+	/**
+	 * A directory (e.g. allows the user to search for other users).
+	 */
+	PURPLE_DISCO_SERVICE_TYPE_DIRECTORY,
+
+	/**
+	 * A chat (multi-user conversation).
+	 */
+	PURPLE_DISCO_SERVICE_TYPE_CHAT,
+
+	/**
+	 * Something else. Do we need more categories?
+	 */
+	PURPLE_DISCO_SERVICE_TYPE_OTHER
+} PurpleDiscoServiceType;
+
+/**
+ * The flags of services.
+ */
+typedef enum
+{
+	PURPLE_DISCO_NONE          = 0x0000,
+	PURPLE_DISCO_ADD           = 0x0001, /**< Supports an 'add' operation */
+	PURPLE_DISCO_BROWSE        = 0x0002, /**< Supports browsing */
+	PURPLE_DISCO_REGISTER      = 0x0004  /**< Supports a 'register' operation */
+} PurpleDiscoServiceFlags;
+
+struct _PurpleDiscoUiOps {
+	/** Ask the UI to display a dialog for the specified account.
+	 */
+	void (*dialog_show_with_account)(PurpleAccount* account);
+	void (*create)(PurpleDiscoList *list); /**< Sets UI-specific data on a disco list */
+	void (*destroy)(PurpleDiscoList *list); /**< Free UI-specific data on the disco list */
+	void (*add_service)(PurpleDiscoList *list, PurpleDiscoService *service, PurpleDiscoService *parent); /**< Add service to dialog */
+	void (*in_progress)(PurpleDiscoList *list, gboolean in_progress); /**< Set progress to dialog */
+
+	/* Padding */
+	void (*_purple_reserved1)(void);
+	void (*_purple_reserved2)(void);
+	void (*_purple_reserved3)(void);
+	void (*_purple_reserved4)(void);
+};
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * Returns a newly created service discovery object.
+ *
+ * It has an initial reference count of 1.
+ *
+ * @param account The account that's listing rooms.
+ * @return The new service discovery list handle.
+ */
+PurpleDiscoList *purple_disco_list_new(PurpleAccount *account);
+
+/**
+ * Increases the reference count on the service discovery list.
+ *
+ * @param list The disco list to ref.
+ */
+PurpleDiscoList *purple_disco_list_ref(PurpleDiscoList *list);
+
+/**
+ * Decreases the reference count on the service discovery list.
+ *
+ * The room list will be destroyed when this reaches 0.
+ *
+ * @param list The room list object to unref and possibly
+ *             destroy.
+ */
+void purple_disco_list_unref(PurpleDiscoList *list);
+
+/**
+ * Instructs the prpl to start fetching the list.
+ *
+ */
+PurpleDiscoList *purple_disco_get_list(PurpleConnection *gc);
+
+/**
+ * Tells the prpl to stop fetching the list.
+ * If this is possible and done, the prpl will
+ * call set_in_progress with @c FALSE and possibly
+ * unref the list if it took a reference.
+ *
+ * @param list The service list to cancel a get_list on.
+ */
+void purple_disco_cancel_get_list(PurpleDiscoList *list);
+
+/**
+ * Create new service object
+ */
+PurpleDiscoService *
+purple_disco_list_service_new(PurpleDiscoServiceType type, const gchar *name,
+                              const gchar *description,
+                              PurpleDiscoServiceFlags flags,
+                              gpointer proto_data);
+
+/**
+ * Add service to list
+ */
+void purple_disco_list_service_add(PurpleDiscoList *list, PurpleDiscoService *service, PurpleDiscoService *parent);
+
+/**
+ * Expand a (browsable) service. The UI should call this in order to
+ * iteratively browse the children of this service. The service must
+ * have the PURPLE_DISCO_BROWSE flag set.
+ *
+ * You probably don't want to call this if the service already has children.
+ *
+ * @param service  The browsable disco service.
+ *
+ * @since TODO
+ */
+void purple_disco_service_expand(PurpleDiscoService *service);
+
+/**
+ * Register service
+ * @param service The service that will be registered
+ */
+void purple_disco_service_register(PurpleDiscoService *service);
+
+/**
+ * Returns a service's name.
+ *
+ * @param service The service.
+ * @return The name.
+ *
+ * @since TODO
+ */
+const gchar *purple_disco_service_get_name(PurpleDiscoService *service);
+
+/**
+ * Return a service's description.
+ *
+ * @param service The service.
+ * @return The description.
+ *
+ * @since TODO
+ */
+const gchar* purple_disco_service_get_description(PurpleDiscoService *service);
+
+/**
+ * Returns the service's protocol-specific data.
+ *
+ * This should only be called from the associated prpl.
+ *
+ * @param service The disco service.
+ * @return        The protocol data.
+ *
+ * @since TODO
+ */
+gpointer purple_disco_service_get_protocol_data(PurpleDiscoService *service);
+
+/**
+ * Return a service's type.
+ *
+ * @param service The service.
+ * @return The type.
+ *
+ * @since TODO
+ */
+PurpleDiscoServiceType purple_disco_service_get_type(PurpleDiscoService *service);
+
+/**
+ * Return a service's flags.
+ *
+ * @param service The service.
+ * @return The flags.
+ *
+ * @since TODO
+ */
+PurpleDiscoServiceFlags purple_disco_service_get_flags(PurpleDiscoService *service);
+
+/**
+ * Set the gateway type for a gateway service. The gateway type is a string
+ * that represents a canonical name of the protocol to which this service is
+ * a gateway. For example, for an XMPP legacy transport to AIM, this would
+ * be "aim".
+ *
+ * These strings should conform to the names of the libpurple prpls where
+ * possible (so a UI can easily map types to icons) and, as a backup, the
+ * XMPP registry list of gateways at
+ * http://xmpp.org/registrar/disco-categories.html#gateway. 
+ */
+void purple_disco_service_set_gateway_type(PurpleDiscoService *service,
+                                           const gchar *type);
+
+/**
+ * Get the gateway type for a gateway service.
+ *
+ * @param service The service.
+ * @returns       The gateway type or NULL if none was set or service is not
+ *                a gateway.
+ *
+ * @see purple_disco_service_set_gateway_type().
+ */
+const gchar *purple_disco_service_get_gateway_type(PurpleDiscoService *service);
+
+/**
+ * Get the account associated with a service list.
+ *
+ * @param list  The service list.
+ * @return      The account
+ *
+ * @since TODO
+ */
+PurpleAccount* purple_disco_list_get_account(PurpleDiscoList *list);
+
+/**
+ * Get a list of the services associated with this service list.
+ *
+ * @param dl The serivce list.
+ * @returns    A list of PurpleDiscoService items.
+ *
+ * @since TODO
+ */
+GList* purple_disco_list_get_services(PurpleDiscoList *dl);
+
+/**
+ * Set the service list's UI data.
+ *
+ * @param list  The service list.
+ * @param data  The data.
+ *
+ * @see purple_disco_list_get_ui_data()
+ * @since TODO
+ */
+void purple_disco_list_set_ui_data(PurpleDiscoList *list, gpointer data);
+
+/**
+ * Get the service list's UI data.
+ *
+ * @param list  The service list.
+ * @return      The data.
+ *
+ * @see purple_disco_list_set_ui_data()
+ * @since TODO
+ */
+gpointer purple_disco_list_get_ui_data(PurpleDiscoList *list);
+
+/**
+ * Set the "in progress" state of the Service Discovery.
+ *
+ * The UI is encouraged to somehow hint to the user
+ * whether or not we're busy downloading a service list or not.
+ *
+ * @param list The service list.
+ * @param in_progress We're downloading it, or we're not.
+ *
+ * @see purple_disco_list_get_in_progress()
+ * @since TODO
+ */
+void purple_disco_list_set_in_progress(PurpleDiscoList *list, gboolean in_progress);
+
+/**
+ * Gets the "in progress" state of the Service Discovery.
+ *
+ * The UI is encouraged to somehow hint to the user
+ * whether or not we're busy downloading a service list or not.
+ *
+ * @param list The service list.
+ * @return True if we're downloading it, or false if we're not.
+ *
+ * @see purple_disco_list_set_in_progress()
+ * @since TODO
+ */
+gboolean purple_disco_list_get_in_progress(PurpleDiscoList *list);
+
+/**
+ * Sets the disco list's protocol-specific data.
+ *
+ * This should only be called from the associated prpl.
+ *
+ * @param list     The disco list.
+ * @param data     The protocol data.
+ * @param close_cb The function to be called when destroying the disco list to
+ *                 free the protocol data. May be NULL if the data does not need
+ *                 to be freed.
+ *
+ * @see purple_disco_list_get_protocol_data()
+ * @since TODO
+ */
+void purple_disco_list_set_protocol_data(PurpleDiscoList *list, gpointer data,
+                                         PurpleDiscoCloseFunc cb);
+
+/**
+ * Returns the disco list's protocol-specific data.
+ *
+ * This should only be called from the associated prpl.
+ *
+ * @param list The disco list.
+ * @return     The protocol data.
+ *
+ * @see purple_disco_list_set_protocol_data()
+ * @since TODO
+ */
+gpointer purple_disco_list_get_protocol_data(PurpleDiscoList *list);
+
+void purple_disco_list_set_service_close_func(PurpleDiscoList *list,
+                                              PurpleDiscoServiceCloseFunc cb);
+void purple_disco_list_set_cancel_func(PurpleDiscoList *list,
+                                       PurpleDiscoCancelFunc cb);
+void purple_disco_list_set_expand_func(PurpleDiscoList *list,
+                                       PurpleDiscoServiceExpandFunc cb);
+void purple_disco_list_set_register_func(PurpleDiscoList *list,
+                                         PurpleDiscoRegisterFunc cb);
+
+/**
+ * Sets the UI operations structure to be used in all purple service discovery.
+ *
+ * @param ops The UI operations structure.
+ *
+ * @since TODO
+ */
+void purple_disco_set_ui_ops(PurpleDiscoUiOps *ui_ops);
+
+/**
+ * Returns the service discovery UI operations structure.
+ *
+ * @return A filled-out PurpleDiscoUiOps structure.
+ *
+ * @since TODO
+ */
+PurpleDiscoUiOps *purple_disco_get_ui_ops(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* _PURPLE_DISCO_H_ */
--- a/libpurple/protocols/jabber/disco.c	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/protocols/jabber/disco.c	Wed Apr 29 23:20:51 2009 +0000
@@ -1,5 +1,5 @@
 /*
- * purple - Jabber Protocol Plugin
+ * purple - Jabber Service Discovery
  *
  * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com>
  *
@@ -40,11 +40,28 @@
 	JabberDiscoInfoCallback *callback;
 };
 
+struct _jabber_disco_items_cb_data {
+	gpointer data;
+	JabberDiscoItemsCallback *callback;
+};
+
 #define SUPPORT_FEATURE(x) { \
 	feature = xmlnode_new_child(query, "feature"); \
 	xmlnode_set_attrib(feature, "var", x); \
 }
 
+struct jabber_disco_list_data {
+	JabberStream *js; /* TODO: Needed? */
+	PurpleDiscoList *list;
+	char *server;
+	int fetch_count;
+};
+
+struct jabber_disco_service_data {
+	char *jid;
+	char *node;
+};
+
 static void
 jabber_disco_bytestream_server_cb(JabberStream *js, const char *from,
                                   JabberIqType type, const char *id,
@@ -160,11 +177,11 @@
 #endif
 		} else {
 			xmlnode *error, *inf;
-				
+
 			/* XXX: gross hack, implement jabber_iq_set_type or something */
 			xmlnode_set_attrib(iq->node, "type", "error");
 			iq->type = JABBER_IQ_ERROR;
-			
+
 			error = xmlnode_new_child(query, "error");
 			xmlnode_set_attrib(error, "code", "404");
 			xmlnode_set_attrib(error, "type", "cancel");
@@ -173,7 +190,37 @@
 		}
 		g_free(node_uri);
 		jabber_iq_send(iq);
-	} else if(type == JABBER_IQ_RESULT) {
+	} else if (type == JABBER_IQ_SET) {
+		/* wtf? seriously. wtf‽ */
+		JabberIq *iq = jabber_iq_new(js, JABBER_IQ_ERROR);
+		xmlnode *error, *bad_request;
+
+		/* Free the <query/> */
+		xmlnode_free(xmlnode_get_child(iq->node, "query"));
+		/* Add an error */
+		error = xmlnode_new_child(iq->node, "error");
+		xmlnode_set_attrib(error, "type", "modify");
+		bad_request = xmlnode_new_child(error, "bad-request");
+		xmlnode_set_namespace(bad_request, "urn:ietf:params:xml:ns:xmpp-stanzas");
+
+		jabber_iq_set_id(iq, id);
+		xmlnode_set_attrib(iq->node, "to", from);
+
+		jabber_iq_send(iq);
+	}
+}
+
+static void jabber_disco_info_cb(JabberStream *js, const char *from,
+                                 JabberIqType type, const char *id,
+                                 xmlnode *packet, gpointer data)
+{
+	struct _jabber_disco_info_cb_data *jdicd = data;
+	xmlnode *query;
+
+	query = xmlnode_get_child_with_namespace(packet, "query",
+				"http://jabber.org/protocol/disco#info");
+
+	if (type == JABBER_IQ_RESULT && query) {
 		xmlnode *child;
 		JabberID *jid;
 		JabberBuddy *jb;
@@ -190,7 +237,7 @@
 		if(jbr)
 			capabilities = jbr->capabilities;
 
-		for(child = in_query->child; child; child = child->next) {
+		for(child = query->child; child; child = child->next) {
 			if(child->type != XMLNODE_TYPE_TAG)
 				continue;
 
@@ -242,6 +289,8 @@
 					capabilities |= JABBER_CAP_IQ_REGISTER;
 				else if(!strcmp(var, "urn:xmpp:ping"))
 					capabilities |= JABBER_CAP_PING;
+				else if(!strcmp(var, "http://jabber.org/protocol/disco#items"))
+					capabilities |= JABBER_CAP_ITEMS;
 				else if(!strcmp(var, "http://jabber.org/protocol/commands")) {
 					capabilities |= JABBER_CAP_ADHOC;
 				}
@@ -257,19 +306,12 @@
 		if(jbr)
 			jbr->capabilities = capabilities;
 
-		if((jdicd = g_hash_table_lookup(js->disco_callbacks, from))) {
-			jdicd->callback(js, from, capabilities, jdicd->data);
-			g_hash_table_remove(js->disco_callbacks, from);
-		}
-	} else if(type == JABBER_IQ_ERROR) {
+		jdicd->callback(js, from, capabilities, jdicd->data);
+	} else { /* type == JABBER_IQ_ERROR or query == NULL */
 		JabberID *jid;
 		JabberBuddy *jb;
 		JabberBuddyResource *jbr = NULL;
 		JabberCapabilities capabilities = JABBER_CAP_NONE;
-		struct _jabber_disco_info_cb_data *jdicd;
-
-		if(!(jdicd = g_hash_table_lookup(js->disco_callbacks, from)))
-			return;
 
 		if((jid = jabber_id_new(from))) {
 			if(jid->resource && (jb = jabber_buddy_find(js, from, TRUE)))
@@ -281,7 +323,6 @@
 			capabilities = jbr->capabilities;
 
 		jdicd->callback(js, from, capabilities, jdicd->data);
-		g_hash_table_remove(js->disco_callbacks, from);
 	}
 }
 
@@ -371,6 +412,12 @@
 
 }
 
+struct _disco_data {
+	struct jabber_disco_list_data *list_data;
+	PurpleDiscoService *parent;
+	char *node;
+};
+
 static void
 jabber_disco_server_info_result_cb(JabberStream *js, const char *from,
                                    JabberIqType type, const char *id,
@@ -526,10 +573,541 @@
 	jdicd->data = data;
 	jdicd->callback = callback;
 
-	g_hash_table_insert(js->disco_callbacks, g_strdup(who), jdicd);
-
 	iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info");
 	xmlnode_set_attrib(iq->node, "to", who);
 
+	jabber_iq_set_callback(iq, jabber_disco_info_cb, jdicd);
 	jabber_iq_send(iq);
 }
+
+static void
+jabber_disco_list_data_destroy(struct jabber_disco_list_data *data)
+{
+	g_free(data->server);
+	g_free(data);
+}
+
+static void
+disco_proto_data_destroy_cb(PurpleDiscoList *list)
+{
+	struct jabber_disco_list_data *data;
+	
+	data = purple_disco_list_get_protocol_data(list);
+	jabber_disco_list_data_destroy(data);
+}
+
+static PurpleDiscoServiceType
+jabber_disco_category_from_string(const gchar *str)
+{
+	if (!g_ascii_strcasecmp(str, "gateway"))
+		return PURPLE_DISCO_SERVICE_TYPE_GATEWAY;
+	else if (!g_ascii_strcasecmp(str, "directory"))
+		return PURPLE_DISCO_SERVICE_TYPE_DIRECTORY;
+	else if (!g_ascii_strcasecmp(str, "conference"))
+		return PURPLE_DISCO_SERVICE_TYPE_CHAT;
+
+	return PURPLE_DISCO_SERVICE_TYPE_OTHER;
+}
+
+static const struct {
+	const char *from;
+	const char *to;
+} disco_type_mappings[] = {
+	{ "gadu-gadu", "gg" },
+	{ "sametime",  "meanwhile" },
+	{ "myspaceim", "myspace" },
+	{ "xmpp",      "jabber" },
+	{ NULL,        NULL }
+};
+
+static const gchar *
+jabber_disco_type_from_string(const gchar *str)
+{
+	int i = 0;
+
+	g_return_val_if_fail(str != NULL, "");
+
+	for ( ; disco_type_mappings[i].from; ++i) {
+		if (!strcasecmp(str, disco_type_mappings[i].from))
+			return disco_type_mappings[i].to;
+	}
+
+	/* fallback to the string itself */
+	return str;
+}
+
+static void
+jabber_disco_service_info_cb(JabberStream *js, const char *from,
+                             JabberIqType type, const char *id,
+                             xmlnode *packet, gpointer data);
+
+static void
+jabber_disco_service_items_cb(JabberStream *js, const char *who, const char *node,
+                              GSList *items, gpointer data)
+{
+	GSList *l;
+	struct _disco_data *disco_data;
+	struct jabber_disco_list_data *list_data;
+	PurpleDiscoList *list;	
+	PurpleDiscoService *parent;
+	PurpleDiscoServiceType parent_type;
+	const char *parent_node;
+
+	disco_data = data;
+	list_data = disco_data->list_data;
+	list = list_data->list;
+
+	--list_data->fetch_count;
+
+	if (list_data->list == NULL) {
+		if (list_data->fetch_count == 0)
+			jabber_disco_list_data_destroy(list_data);
+
+		return;
+	}
+
+	if (items == NULL) {
+		if (list_data->fetch_count == 0)
+			purple_disco_list_set_in_progress(list, FALSE);
+
+		purple_disco_list_unref(list);
+		return;
+	}
+
+	parent = disco_data->parent;
+	parent_type = purple_disco_service_get_type(parent);
+	parent_node = disco_data->node;
+
+	for (l = items; l; l = l->next) {
+		JabberDiscoItem *item = l->data;
+
+		if (parent_type & PURPLE_DISCO_SERVICE_TYPE_CHAT) {
+			/* A chat server's items are chats. I promise. */
+			PurpleDiscoService *service;
+			struct jabber_disco_service_data *service_data;
+			JabberID *jid = jabber_id_new(item->jid);
+
+			if (jid) {
+				service_data = g_new0(struct jabber_disco_service_data, 1);
+				service_data->jid = g_strdup(item->jid);
+
+				service = purple_disco_list_service_new(PURPLE_DISCO_SERVICE_TYPE_CHAT,
+						jid->node, item->name, PURPLE_DISCO_ADD, service_data);
+
+				purple_disco_list_service_add(list, service, parent);
+
+				jabber_id_free(jid);
+			}
+		} else {
+			JabberIq *iq;
+			struct _disco_data *req_data;
+			char *full_node;
+
+			if (parent_node && !item->node)
+				continue;
+
+			if (parent_node)
+				full_node = g_strconcat(parent_node, "/", item->node, NULL);
+			else
+				full_node = g_strdup(item->node);
+
+			req_data = g_new0(struct _disco_data, 1);
+			req_data->list_data = list_data;
+			req_data->parent = parent;
+			req_data->node = full_node;
+
+			++list_data->fetch_count;
+			purple_disco_list_ref(list);
+
+			iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info");
+			xmlnode_set_attrib(iq->node, "to", item->jid);
+			if (full_node)
+				xmlnode_set_attrib(xmlnode_get_child(iq->node, "query"),
+				                   "node", full_node);
+			jabber_iq_set_callback(iq, jabber_disco_service_info_cb, req_data);
+
+			jabber_iq_send(iq);
+		}
+	}
+
+	g_slist_foreach(items, (GFunc)jabber_disco_item_free, NULL);
+	g_slist_free(items);
+
+	if (list_data->fetch_count == 0)
+		purple_disco_list_set_in_progress(list, FALSE);
+
+	purple_disco_list_unref(list);
+
+	g_free(disco_data->node);
+	g_free(disco_data);
+}
+
+static void
+jabber_disco_service_info_cb(JabberStream *js, const char *from,
+                             JabberIqType type, const char *id,
+                             xmlnode *packet, gpointer data)
+{
+	struct _disco_data *disco_data;
+	struct jabber_disco_list_data *list_data;
+	xmlnode *query, *identity, *child;
+	const char *anode;
+	char *aname, *node;
+
+	PurpleDiscoList *list;
+	PurpleDiscoService *parent, *service;
+	PurpleDiscoServiceType service_type;
+	PurpleDiscoServiceFlags flags;
+	struct jabber_disco_service_data *service_data;
+
+	disco_data = data;
+	list_data = disco_data->list_data;
+	list = list_data->list;
+	parent = disco_data->parent;
+
+	node = disco_data->node;
+	disco_data->node = NULL;
+	g_free(disco_data);
+
+	--list_data->fetch_count;
+
+	if (!purple_disco_list_get_in_progress(list)) {
+		purple_disco_list_unref(list);
+		return;
+	}
+
+	if (!from || type == JABBER_IQ_ERROR
+			|| (!(query = xmlnode_get_child(packet, "query")))
+			|| (!(identity = xmlnode_get_child(query, "identity")))) {
+		if (list_data->fetch_count == 0)
+			purple_disco_list_set_in_progress(list, FALSE);
+
+		purple_disco_list_unref(list);
+		return;
+	}
+
+	service_type = jabber_disco_category_from_string(
+			xmlnode_get_attrib(identity, "category"));
+
+	/* Default to allowing things to be add-able */
+	flags = PURPLE_DISCO_ADD;
+
+	for (child = xmlnode_get_child(query, "feature"); child;
+			child = xmlnode_get_next_twin(child)) {
+		const char *var;
+
+		if (!(var = xmlnode_get_attrib(child, "var")))
+			continue;
+		
+		if (g_str_equal(var, "jabber:iq:register"))
+			flags |= PURPLE_DISCO_REGISTER;
+		
+		if (g_str_equal(var, "http://jabber.org/protocol/disco#items"))
+			flags |= PURPLE_DISCO_BROWSE;
+
+		if (g_str_equal(var, "http://jabber.org/protocol/muc")) {
+			flags |= PURPLE_DISCO_BROWSE;
+			service_type = PURPLE_DISCO_SERVICE_TYPE_CHAT;
+		}
+	}
+
+	if ((anode = xmlnode_get_attrib(query, "node")))
+		aname = g_strconcat(from, anode, NULL);
+	else
+		aname = g_strdup(from);
+
+	service_data = g_new0(struct jabber_disco_service_data, 1);
+	service_data->jid = g_strdup(from);
+	if (anode)
+		service_data->node = g_strdup(anode);
+
+	service = purple_disco_list_service_new(service_type, aname,
+			xmlnode_get_attrib(identity, "name"), flags, service_data);
+	g_free(aname);
+
+	if (service_type == PURPLE_DISCO_SERVICE_TYPE_GATEWAY)
+		purple_disco_service_set_gateway_type(service,
+				jabber_disco_type_from_string(xmlnode_get_attrib(identity,
+				                                                 "type")));
+
+	purple_disco_list_service_add(list, service, parent);
+
+	if (list_data->fetch_count == 0)
+		purple_disco_list_set_in_progress(list, FALSE);
+
+	purple_disco_list_unref(list);
+
+	g_free(node);
+}
+
+static void
+jabber_disco_server_items_cb(JabberStream *js, const char *from,
+                             JabberIqType type, const char *id,
+                             xmlnode *packet, gpointer data)
+{
+	struct jabber_disco_list_data *list_data;
+	xmlnode *query, *child;
+	gboolean has_items = FALSE;
+
+	list_data = data;
+	--list_data->fetch_count;
+
+	if (!from || type == JABBER_IQ_ERROR ||
+			!purple_disco_list_get_in_progress(list_data->list)) {
+		purple_disco_list_unref(list_data->list);
+		return;
+	}
+
+	query = xmlnode_get_child(packet, "query");
+
+	for(child = xmlnode_get_child(query, "item"); child;
+			child = xmlnode_get_next_twin(child)) {
+		JabberIq *iq;
+		const char *jid;
+		struct _disco_data *disco_data;
+
+		if(!(jid = xmlnode_get_attrib(child, "jid")))
+			continue;
+
+		disco_data = g_new0(struct _disco_data, 1);
+		disco_data->list_data = list_data;
+
+		has_items = TRUE;
+		++list_data->fetch_count;
+		purple_disco_list_ref(list_data->list);
+		iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#info");
+		xmlnode_set_attrib(iq->node, "to", jid);
+		jabber_iq_set_callback(iq, jabber_disco_service_info_cb, disco_data);
+
+		jabber_iq_send(iq);
+	}
+
+	if (!has_items)
+		purple_disco_list_set_in_progress(list_data->list, FALSE);
+
+	purple_disco_list_unref(list_data->list);
+}
+
+static void
+jabber_disco_server_info_cb(JabberStream *js, const char *who, JabberCapabilities caps, gpointer data)
+{
+	struct jabber_disco_list_data *list_data;
+
+	list_data = data;
+	--list_data->fetch_count;
+
+	if (!list_data->list) {
+		purple_disco_list_unref(list_data->list);
+
+		if (list_data->fetch_count == 0)
+			jabber_disco_list_data_destroy(list_data);
+
+		return;
+	}
+
+	if (caps & JABBER_CAP_ITEMS) {
+		JabberIq *iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items");
+		xmlnode_set_attrib(iq->node, "to", who);
+		jabber_iq_set_callback(iq, jabber_disco_server_items_cb, list_data);
+
+		++list_data->fetch_count;
+		purple_disco_list_ref(list_data->list);
+
+		jabber_iq_send(iq);
+	} else {
+		purple_notify_error(NULL, _("Error"), _("Server doesn't support service discovery"), NULL); 
+		purple_disco_list_set_in_progress(list_data->list, FALSE);
+	}
+
+	purple_disco_list_unref(list_data->list);
+}
+
+static void discolist_cancel_cb(struct jabber_disco_list_data *list_data, const char *server)
+{
+	purple_disco_list_set_in_progress(list_data->list, FALSE);
+	purple_disco_list_unref(list_data->list);
+}
+
+static void discolist_ok_cb(struct jabber_disco_list_data *list_data, const char *server)
+{
+	JabberStream *js;
+
+	js = list_data->js;
+
+	if (!server || !*server) {
+		purple_notify_error(js->gc, _("Invalid Server"), _("Invalid Server"), NULL);
+
+		purple_disco_list_set_in_progress(list_data->list, FALSE);
+		purple_disco_list_unref(list_data->list);
+		return;
+	}
+
+	list_data->server = g_strdup(server);
+	if (js->last_disco_server)
+		g_free(js->last_disco_server);
+	js->last_disco_server = g_strdup(server);
+
+	purple_disco_list_set_in_progress(list_data->list, TRUE);
+
+	++list_data->fetch_count;
+	jabber_disco_info_do(js, list_data->server, jabber_disco_server_info_cb, list_data);
+}
+
+static void
+jabber_disco_service_close(PurpleDiscoService *service)
+{
+	struct jabber_disco_service_data *data;
+
+	data = purple_disco_service_get_protocol_data(service);
+	g_free(data->jid);
+	g_free(data->node);
+	g_free(data);
+}
+
+#if 0
+static void
+jabber_disco_cancel(PurpleDiscoList *list)
+{
+	/* This space intentionally letft blank */
+}
+#endif
+
+static void
+jabber_disco_list_expand(PurpleDiscoList *list, PurpleDiscoService *service)
+{
+	PurpleAccount *account;
+	PurpleConnection *pc;
+	JabberStream *js;
+	struct jabber_disco_list_data *list_data;
+	struct jabber_disco_service_data *service_data;
+	struct _disco_data *disco_data;
+
+	account = purple_disco_list_get_account(list);
+	pc = purple_account_get_connection(account);
+	js = purple_connection_get_protocol_data(pc);
+
+	list_data = purple_disco_list_get_protocol_data(list);
+
+	++list_data->fetch_count;
+	purple_disco_list_ref(list);
+	disco_data = g_new0(struct _disco_data, 1);
+	disco_data->list_data = list_data;
+	disco_data->parent = service;
+
+	service_data = purple_disco_service_get_protocol_data(service);
+
+	jabber_disco_items_do(js, service_data->jid, service_data->node,
+	                      jabber_disco_service_items_cb, disco_data);
+}
+
+static void
+jabber_disco_service_register(PurpleConnection *gc, PurpleDiscoService *service)
+{
+	JabberStream *js = purple_connection_get_protocol_data(gc);
+
+	jabber_register_gateway(js, purple_disco_service_get_name(service));
+}
+
+
+PurpleDiscoList *
+jabber_disco_get_list(PurpleConnection *gc)
+{
+	PurpleAccount *account;
+	PurpleDiscoList *list;
+	JabberStream *js;
+	struct jabber_disco_list_data *disco_list_data;
+
+	account = purple_connection_get_account(gc);
+	js = purple_connection_get_protocol_data(gc);
+
+	/* We start with a ref */
+	list = purple_disco_list_new(account);
+
+	disco_list_data = g_new0(struct jabber_disco_list_data, 1);
+	disco_list_data->list = list;
+	disco_list_data->js = js;
+	purple_disco_list_set_protocol_data(list, disco_list_data,
+	                                    disco_proto_data_destroy_cb);
+	purple_disco_list_set_service_close_func(list, jabber_disco_service_close);
+#if 0
+	purple_disco_list_set_cancel_func(list, jabber_disco_cancel);
+#endif
+	purple_disco_list_set_expand_func(list, jabber_disco_list_expand);
+	purple_disco_list_set_register_func(list, jabber_disco_service_register);
+
+	purple_request_input(gc, _("Server name request"), _("Enter an XMPP Server"),
+			_("Select an XMPP server to query"),
+			js->last_disco_server ? js->last_disco_server : js->user->domain,
+			FALSE, FALSE, NULL,
+			_("Find Services"), PURPLE_CALLBACK(discolist_ok_cb),
+			_("Cancel"), PURPLE_CALLBACK(discolist_cancel_cb),
+			account, NULL, NULL, disco_list_data);
+
+	return list;
+}
+
+static void
+jabber_disco_items_cb(JabberStream *js, const char *from, JabberIqType type,
+                      const char *id, xmlnode *packet, gpointer data)
+{
+	struct _jabber_disco_items_cb_data *jdicd;
+	xmlnode *query, *child;
+	const char *node = NULL;
+	GSList *items = NULL;
+
+	jdicd = data;
+
+	query = xmlnode_get_child(packet, "query");
+
+	if (query)
+		node = xmlnode_get_attrib(query, "node");
+
+	if (!from || !query || type == JABBER_IQ_ERROR) {
+		jdicd->callback(js, from, node, NULL, jdicd->data);
+		g_free(jdicd);
+		return;
+	}
+
+	for (child = xmlnode_get_child(query, "item"); child;
+			child = xmlnode_get_next_twin(child)) {
+		JabberDiscoItem *item;
+
+		item = g_new0(JabberDiscoItem, 1);
+		item->jid  = g_strdup(xmlnode_get_attrib(child, "jid"));
+		item->node = g_strdup(xmlnode_get_attrib(child, "node"));
+		item->name = g_strdup(xmlnode_get_attrib(child, "name"));
+
+		items = g_slist_prepend(items, item);
+	}
+
+	items = g_slist_reverse(items);
+	jdicd->callback(js, from, node, items, jdicd->data);
+	g_free(jdicd);
+}
+
+void jabber_disco_items_do(JabberStream *js, const char *who, const char *node,
+		JabberDiscoItemsCallback *callback, gpointer data)
+{
+	struct _jabber_disco_items_cb_data *jdicd;
+	JabberIq *iq;
+
+	jdicd = g_new0(struct _jabber_disco_items_cb_data, 1);
+	jdicd->data = data;
+	jdicd->callback = callback;
+
+	iq = jabber_iq_new_query(js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items");
+	if (node)
+		xmlnode_set_attrib(xmlnode_get_child(iq->node, "query"), "node", node);
+
+	xmlnode_set_attrib(iq->node, "to", who);
+
+	jabber_iq_set_callback(iq, jabber_disco_items_cb, jdicd);
+	jabber_iq_send(iq);
+}
+
+void jabber_disco_item_free(JabberDiscoItem *item)
+{
+	g_free((char *)item->jid);
+	g_free((char *)item->node);
+	g_free((char *)item->name);
+	g_free(item);
+}
--- a/libpurple/protocols/jabber/disco.h	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/protocols/jabber/disco.h	Wed Apr 29 23:20:51 2009 +0000
@@ -1,5 +1,5 @@
 /**
- * @file iq.h JabberID handlers
+ * @file disco.h Jabber Service Discovery
  *
  * purple
  *
@@ -24,9 +24,18 @@
 
 #include "jabber.h"
 
+typedef struct _JabberDiscoItem {
+	const char *jid;  /* MUST */
+	const char *node; /* SHOULD */
+	const char *name; /* MAY */
+} JabberDiscoItem;
+
 typedef void (JabberDiscoInfoCallback)(JabberStream *js, const char *who,
 		JabberCapabilities capabilities, gpointer data);
 
+typedef void (JabberDiscoItemsCallback)(JabberStream *js, const char *jid,
+		const char *node, GSList *items, gpointer data);
+
 void jabber_disco_info_parse(JabberStream *js, const char *from,
                              JabberIqType type, const char *id, xmlnode *in_query);
 void jabber_disco_items_parse(JabberStream *js, const char *from,
@@ -37,4 +46,10 @@
 void jabber_disco_info_do(JabberStream *js, const char *who,
 		JabberDiscoInfoCallback *callback, gpointer data);
 
+PurpleDiscoList *jabber_disco_get_list(PurpleConnection *gc);
+
+void jabber_disco_items_do(JabberStream *js, const char *jid, const char *node,
+		JabberDiscoItemsCallback *callback, gpointer data);
+void jabber_disco_item_free(JabberDiscoItem *);
+
 #endif /* PURPLE_JABBER_DISCO_H_ */
--- a/libpurple/protocols/jabber/jabber.c	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Wed Apr 29 23:20:51 2009 +0000
@@ -767,8 +767,6 @@
 	js->fd = -1;
 	js->iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
 			g_free, g_free);
-	js->disco_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
-			g_free, g_free);
 	js->buddies = g_hash_table_new_full(g_str_hash, g_str_equal,
 			g_free, (GDestroyNotify)jabber_buddy_free);
 	js->chats = g_hash_table_new_full(g_str_hash, g_str_equal,
@@ -1090,6 +1088,24 @@
 	jabber_iq_send(iq);
 }
 
+static const struct {
+	const char *name;
+	const char *label;
+} registration_fields[] = {
+	{ "email",   N_("Email") },
+	{ "nick",    N_("Nickname") },
+	{ "first",   N_("First name") },
+	{ "last",    N_("Last name") },
+	{ "address", N_("Address") },
+	{ "city",    N_("City") },
+	{ "state",   N_("State") },
+	{ "zip",     N_("Postal code") },
+	{ "phone",   N_("Phone") },
+	{ "url",     N_("URL") },
+	{ "date",    N_("Date") },
+	{ NULL, NULL }
+};
+
 void jabber_register_parse(JabberStream *js, const char *from, JabberIqType type,
                            const char *id, xmlnode *query)
 {
@@ -1097,10 +1113,11 @@
 	PurpleRequestFields *fields;
 	PurpleRequestFieldGroup *group;
 	PurpleRequestField *field;
-	xmlnode *x, *y;
+	xmlnode *x, *y, *node;
 	char *instructions;
 	JabberRegisterCBData *cbdata;
 	gboolean registered = FALSE;
+	int i;
 
 	if (type != JABBER_IQ_RESULT)
 		return;
@@ -1157,74 +1174,53 @@
 	group = purple_request_field_group_new(NULL);
 	purple_request_fields_add_group(fields, group);
 
-	if(js->registration)
-		field = purple_request_field_string_new("username", _("Username"), js->user->node, FALSE);
-	else
-		field = purple_request_field_string_new("username", _("Username"), NULL, FALSE);
-
-	purple_request_field_group_add_field(group, field);
-
-	if(js->registration)
-		field = purple_request_field_string_new("password", _("Password"),
-									purple_connection_get_password(js->gc), FALSE);
-	else
-		field = purple_request_field_string_new("password", _("Password"), NULL, FALSE);
-
-	purple_request_field_string_set_masked(field, TRUE);
-	purple_request_field_group_add_field(group, field);
-
-	if(xmlnode_get_child(query, "name")) {
+	if((node = xmlnode_get_child(query, "username"))) {
+		char *data = xmlnode_get_data(node);
+		if(js->registration)
+			field = purple_request_field_string_new("username", _("Username"), data ? data : js->user->node, FALSE);
+		else
+			field = purple_request_field_string_new("username", _("Username"), data, FALSE);
+
+		purple_request_field_group_add_field(group, field);
+		g_free(data);
+	}
+	if((node = xmlnode_get_child(query, "password"))) {
+		if(js->registration)
+			field = purple_request_field_string_new("password", _("Password"),
+										purple_connection_get_password(js->gc), FALSE);
+		else {
+			char *data = xmlnode_get_data(node);
+			field = purple_request_field_string_new("password", _("Password"), data, FALSE);
+			g_free(data);
+		}
+
+		purple_request_field_string_set_masked(field, TRUE);
+		purple_request_field_group_add_field(group, field);
+	}
+
+	if((node = xmlnode_get_child(query, "name"))) {
 		if(js->registration)
 			field = purple_request_field_string_new("name", _("Name"),
 													purple_account_get_alias(js->gc->account), FALSE);
-		else
-			field = purple_request_field_string_new("name", _("Name"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "email")) {
-		field = purple_request_field_string_new("email", _("Email"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "nick")) {
-		field = purple_request_field_string_new("nick", _("Nickname"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "first")) {
-		field = purple_request_field_string_new("first", _("First name"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "last")) {
-		field = purple_request_field_string_new("last", _("Last name"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "address")) {
-		field = purple_request_field_string_new("address", _("Address"), NULL, FALSE);
+		else {
+			char *data = xmlnode_get_data(node);
+			field = purple_request_field_string_new("name", _("Name"), data, FALSE);
+			g_free(data);
+		}
 		purple_request_field_group_add_field(group, field);
 	}
-	if(xmlnode_get_child(query, "city")) {
-		field = purple_request_field_string_new("city", _("City"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "state")) {
-		field = purple_request_field_string_new("state", _("State"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "zip")) {
-		field = purple_request_field_string_new("zip", _("Postal code"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
+
+	for (i = 0; registration_fields[i].name != NULL; ++i) {
+		if ((node = xmlnode_get_child(query, registration_fields[i].name))) {
+			char *data = xmlnode_get_data(node);
+			field = purple_request_field_string_new(registration_fields[i].name,
+			                                        _(registration_fields[i].label),
+			                                        data, FALSE);
+			purple_request_field_group_add_field(group, field);
+			g_free(data);
+		}
 	}
-	if(xmlnode_get_child(query, "phone")) {
-		field = purple_request_field_string_new("phone", _("Phone"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "url")) {
-		field = purple_request_field_string_new("url", _("URL"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
-	if(xmlnode_get_child(query, "date")) {
-		field = purple_request_field_string_new("date", _("Date"), NULL, FALSE);
-		purple_request_field_group_add_field(group, field);
-	}
+
 	if(registered) {
 		field = purple_request_field_bool_new("unregister", _("Unregister"), FALSE);
 		purple_request_field_group_add_field(group, field);
@@ -1297,8 +1293,6 @@
 	js->registration = TRUE;
 	js->iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
 			g_free, g_free);
-	js->disco_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
-			g_free, g_free);
 	js->user = jabber_id_new(purple_account_get_username(account));
 	js->next_id = g_random_int();
 	js->old_length = 0;
@@ -1486,8 +1480,6 @@
 
 	if(js->iq_callbacks)
 		g_hash_table_destroy(js->iq_callbacks);
-	if(js->disco_callbacks)
-		g_hash_table_destroy(js->disco_callbacks);
 	if(js->buddies)
 		g_hash_table_destroy(js->buddies);
 	if(js->chats)
@@ -1555,6 +1547,7 @@
 	g_free(js->old_uri);
 	g_free(js->old_track);
 	g_free(js->expected_rspauth);
+	g_free(js->last_disco_server);
 
 	if (js->keepalive_timeout != -1)
 		purple_timeout_remove(js->keepalive_timeout);
--- a/libpurple/protocols/jabber/jabber.h	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Wed Apr 29 23:20:51 2009 +0000
@@ -44,6 +44,8 @@
 	JABBER_CAP_ADHOC		  = 1 << 12,
 	JABBER_CAP_BLOCKING       = 1 << 13,
 
+	JABBER_CAP_ITEMS          = 1 << 14,
+
 	JABBER_CAP_RETRIEVED      = 1 << 31
 } JabberCapabilities;
 
@@ -53,12 +55,12 @@
 #include <glib.h>
 #include "circbuffer.h"
 #include "connection.h"
+#include "dnsquery.h"
 #include "dnssrv.h"
 #include "media.h"
 #include "mediamanager.h"
 #include "roomlist.h"
 #include "sslconn.h"
-#include "dnsquery.h"
 
 #include "iq.h"
 #include "jutil.h"
@@ -151,7 +153,6 @@
 	GList *user_directories;
 
 	GHashTable *iq_callbacks;
-	GHashTable *disco_callbacks;
 	int next_id;
 
 	GList *bs_proxies;
@@ -266,6 +267,12 @@
 	int stun_port;
 	PurpleDnsQueryData *stun_query;
 	/* later add stuff to handle TURN relays... */
+
+	/**
+	 * The last server the user disco'd (or NULL) via the server discovery
+	 * API.
+	 */
+	char *last_disco_server;
 };
 
 typedef gboolean (JabberFeatureEnabled)(JabberStream *js, const gchar *namespace);
--- a/libpurple/protocols/jabber/libxmpp.c	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/protocols/jabber/libxmpp.c	Wed Apr 29 23:20:51 2009 +0000
@@ -34,6 +34,7 @@
 #include "iq.h"
 #include "jabber.h"
 #include "chat.h"
+#include "disco.h"
 #include "message.h"
 #include "roster.h"
 #include "si.h"
@@ -117,10 +118,12 @@
 	jabber_unregister_account,		/* unregister_user */
 	jabber_send_attention,			/* send_attention */
 	jabber_attention_types,			/* attention_types */
+
 	sizeof(PurplePluginProtocolInfo),       /* struct_size */
 	NULL, /* get_account_text_table */
 	jabber_initiate_media,          /* initiate_media */
 	jabber_get_media_caps,                  /* get_media_caps */
+	jabber_disco_get_list,			/* disco_get_list */
 };
 
 static gboolean load_plugin(PurplePlugin *plugin)
--- a/libpurple/prpl.h	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/prpl.h	Wed Apr 29 23:20:51 2009 +0000
@@ -70,6 +70,7 @@
 #include "proxy.h"
 #include "plugin.h"
 #include "roomlist.h"
+#include "disco.h"
 #include "status.h"
 #include "whiteboard.h"
 
@@ -481,6 +482,11 @@
 	 */
 	PurpleMediaCaps (*get_media_caps)(PurpleAccount *account,
 					  const char *who);
+
+	/**
+	 * Service discovery prpl callbacks
+	 */
+	PurpleDiscoList *(*disco_get_list)(PurpleConnection *gc);
 };
 
 #define PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl, member) \
--- a/libpurple/purple.h.in	Wed Apr 29 17:50:14 2009 +0000
+++ b/libpurple/purple.h.in	Wed Apr 29 23:20:51 2009 +0000
@@ -79,6 +79,7 @@
 #include <prpl.h>
 #include <request.h>
 #include <roomlist.h>
+#include <disco.h>
 #include <savedstatuses.h>
 #include <server.h>
 #include <signals.h>
--- a/pidgin/Makefile.am	Wed Apr 29 17:50:14 2009 +0000
+++ b/pidgin/Makefile.am	Wed Apr 29 23:20:51 2009 +0000
@@ -90,6 +90,7 @@
 	gtkconv.c \
 	gtkdebug.c \
 	gtkdialogs.c \
+	gtkdisco.c \
 	gtkdnd-hints.c \
 	gtkdocklet.c \
 	gtkdocklet-x11.c \
@@ -149,6 +150,7 @@
 	gtkconvwin.h \
 	gtkdebug.h \
 	gtkdialogs.h \
+	gtkdisco.h \
 	gtkdnd-hints.h \
 	gtkdocklet.h \
 	gtkeventloop.h \
--- a/pidgin/Makefile.mingw	Wed Apr 29 17:50:14 2009 +0000
+++ b/pidgin/Makefile.mingw	Wed Apr 29 23:20:51 2009 +0000
@@ -65,6 +65,7 @@
 			gtkconv.c \
 			gtkdebug.c \
 			gtkdialogs.c \
+			gtkdisco.c \
 			gtkdnd-hints.c \
 			gtkdocklet.c \
 			gtkeventloop.c \
--- a/pidgin/gtkblist.c	Wed Apr 29 17:50:14 2009 +0000
+++ b/pidgin/gtkblist.c	Wed Apr 29 23:20:51 2009 +0000
@@ -49,6 +49,7 @@
 #include "gtkconv.h"
 #include "gtkdebug.h"
 #include "gtkdialogs.h"
+#include "gtkdisco.h"
 #include "gtkft.h"
 #include "gtklog.h"
 #include "gtkmenutray.h"
@@ -3380,6 +3381,7 @@
 	{ N_("/Tools/Pr_ivacy"), NULL, pidgin_privacy_dialog_show, 0, "<Item>", NULL },
 	{ "/Tools/sep2", NULL, NULL, 0, "<Separator>", NULL },
 	{ N_("/Tools/_File Transfers"), "<CTL>T", pidgin_xfer_dialog_show, 0, "<StockItem>", PIDGIN_STOCK_TOOLBAR_TRANSFER },
+	{ N_("/Tools/Service _Discovery"), NULL, pidgin_disco_dialog_show, 0, "<Item>", NULL },
 	{ N_("/Tools/R_oom List"), NULL, pidgin_roomlist_dialog_show, 0, "<Item>", NULL },
 	{ N_("/Tools/System _Log"), NULL, gtk_blist_show_systemlog_cb, 3, "<Item>", NULL },
 	{ "/Tools/sep3", NULL, NULL, 0, "<Separator>", NULL },
@@ -4263,6 +4265,9 @@
 
 	widget = gtk_item_factory_get_widget(gtkblist->ift, N_("/Tools/Room List"));
 	gtk_widget_set_sensitive(widget, pidgin_roomlist_is_showable());
+
+	widget = gtk_item_factory_get_widget(gtkblist->ift, N_("/Tools/Service Discovery"));
+	gtk_widget_set_sensitive(widget, pidgin_disco_is_showable());
 }
 
 static void
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkdisco.c	Wed Apr 29 23:20:51 2009 +0000
@@ -0,0 +1,582 @@
+/**
+ * @file gtkdisco.c GTK+ Service Discovery UI
+ * @ingroup pidgin
+ */
+
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#include "internal.h"
+#include "pidgin.h"
+#include "gtkutils.h"
+#include "debug.h"
+#include "disco.h"
+
+#include "gtkdisco.h"
+
+typedef struct _PidginDiscoDialog {
+	GtkWidget *window;
+	GtkWidget *account_widget;
+	
+	GtkWidget *sw;
+	GtkWidget *progress;
+
+	GtkWidget *stop_button;
+	GtkWidget *list_button;
+	GtkWidget *register_button;
+	GtkWidget *add_button;
+	GtkWidget *close_button;
+
+	PurpleAccount *account;
+	PurpleDiscoList *discolist;
+} PidginDiscoDialog;
+
+typedef struct _PidginDiscoList {
+	PidginDiscoDialog *dialog;
+	GtkTreeStore *model;
+	GtkWidget *tree;
+	GHashTable *services;
+} PidginDiscoList;
+
+struct _menu_cb_info {
+	PurpleDiscoList *list;
+	PurpleDiscoService *service;
+};
+
+enum {
+	PIXBUF_COLUMN = 0,
+	NAME_COLUMN,
+	DESCRIPTION_COLUMN,
+	SERVICE_COLUMN,
+	NUM_OF_COLUMNS
+};
+
+static void pidgin_disco_create_tree(PidginDiscoList *pdl);
+
+static void dialog_select_account_cb(GObject *w, PurpleAccount *account,
+				     PidginDiscoDialog *dialog)
+{
+	dialog->account = account;
+}
+
+static void register_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	struct _menu_cb_info *info = g_object_get_data(G_OBJECT(button), "disco-info");
+	PurpleAccount *account;
+	PurpleConnection *gc;
+
+	account = purple_disco_list_get_account(info->list);
+	gc = purple_account_get_connection(account);
+
+	purple_disco_service_register(info->service);
+}
+
+static void list_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	PurpleConnection *gc;
+	PidginDiscoList *pdl;
+
+	gc = purple_account_get_connection(dialog->account);
+	if (!gc)
+		return;
+
+	gtk_widget_set_sensitive(dialog->add_button, FALSE);
+	gtk_widget_set_sensitive(dialog->register_button, FALSE);
+
+	if (dialog->discolist != NULL) {
+		pdl = purple_disco_list_get_ui_data(dialog->discolist);
+		gtk_widget_destroy(pdl->tree);
+		purple_disco_list_unref(dialog->discolist);
+	}
+
+	dialog->discolist = purple_disco_get_list(gc);
+	if (!dialog->discolist)
+		return;
+	/* We keep a copy... */
+	purple_disco_list_ref(dialog->discolist);
+
+	pdl = purple_disco_list_get_ui_data(dialog->discolist);
+	pdl->dialog = dialog;
+
+	pidgin_disco_create_tree(pdl);
+
+	if (dialog->account_widget)
+		gtk_widget_set_sensitive(dialog->account_widget, FALSE);
+}
+
+static void add_room_to_blist_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	struct _menu_cb_info *info = g_object_get_data(G_OBJECT(button), "disco-info");
+	PurpleAccount *account;
+	const char *name;
+
+	g_return_if_fail(info != NULL);
+
+	account = purple_disco_list_get_account(info->list);
+	name = purple_disco_service_get_name(info->service);
+
+	if (purple_disco_service_get_type(info->service) == PURPLE_DISCO_SERVICE_TYPE_CHAT)
+		purple_blist_request_add_chat(account, NULL, NULL, name);
+	else
+		purple_blist_request_add_buddy(account, name, NULL, NULL);
+}
+
+static void
+selection_changed_cb(GtkTreeSelection *selection, PidginDiscoList *pdl)
+{
+	PurpleDiscoService *service;
+	PurpleDiscoServiceFlags flags;
+	GtkTreeIter iter;
+	GValue val;
+	static struct _menu_cb_info *info;
+	PidginDiscoDialog *dialog = pdl->dialog;
+
+	if (gtk_tree_selection_get_selected(selection, NULL, &iter)) {
+		val.g_type = 0;
+		gtk_tree_model_get_value(GTK_TREE_MODEL(pdl->model), &iter, SERVICE_COLUMN, &val);
+		service = g_value_get_pointer(&val);
+		if (!service) {
+			gtk_widget_set_sensitive(dialog->add_button, FALSE);
+			gtk_widget_set_sensitive(dialog->register_button, FALSE);
+			return;
+		}
+
+		info = g_new0(struct _menu_cb_info, 1);
+		info->list = dialog->discolist;
+		info->service = service;
+
+		g_object_set_data_full(G_OBJECT(dialog->add_button), "disco-info",
+		                       info, g_free);
+		g_object_set_data(G_OBJECT(dialog->register_button), "disco-info", info);
+
+		flags = purple_disco_service_get_flags(service);
+
+		gtk_widget_set_sensitive(dialog->add_button, flags & PURPLE_DISCO_ADD);
+		gtk_widget_set_sensitive(dialog->register_button, flags & PURPLE_DISCO_REGISTER);
+	} else {
+		gtk_widget_set_sensitive(dialog->add_button, FALSE);
+		gtk_widget_set_sensitive(dialog->register_button, FALSE);
+	}
+}
+
+static void
+row_expanded_cb(GtkTreeView *tree, GtkTreeIter *arg1, GtkTreePath *rg2,
+                gpointer user_data)
+{
+	PidginDiscoList *pdl;
+	PurpleDiscoService *service;
+	GValue val;
+
+	pdl = user_data;
+
+	val.g_type = 0;
+	gtk_tree_model_get_value(GTK_TREE_MODEL(pdl->model), arg1, SERVICE_COLUMN,
+	                         &val);
+	service = g_value_get_pointer(&val);
+	purple_disco_service_expand(service);
+}
+
+static gint
+delete_win_cb(GtkWidget *w, GdkEventAny *e, gpointer d)
+{
+	PidginDiscoDialog *dialog = d;
+
+	if (dialog->discolist && purple_disco_list_get_in_progress(dialog->discolist))
+		purple_disco_cancel_get_list(dialog->discolist);
+
+	if (dialog->discolist) {
+		PidginDiscoList *pdl = purple_disco_list_get_ui_data(dialog->discolist);
+
+		if (pdl)
+			pdl->dialog = NULL;
+		purple_disco_list_unref(dialog->discolist);
+	}
+
+	g_free(dialog);
+
+	return FALSE;
+}
+
+static void stop_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	purple_disco_cancel_get_list(dialog->discolist);
+
+	if (dialog->account_widget)
+		gtk_widget_set_sensitive(dialog->account_widget, TRUE);
+
+	gtk_widget_set_sensitive(dialog->stop_button, FALSE);
+	gtk_widget_set_sensitive(dialog->list_button, TRUE);
+	gtk_widget_set_sensitive(dialog->add_button, FALSE);
+}
+
+static void close_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	GtkWidget *window = dialog->window;
+
+	delete_win_cb(NULL, NULL, dialog);
+	gtk_widget_destroy(window);
+}
+
+static gboolean account_filter_func(PurpleAccount *account)
+{
+	PurpleConnection *conn = purple_account_get_connection(account);
+	PurplePluginProtocolInfo *prpl_info = NULL;
+
+	if (conn)
+		prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(conn->prpl);
+
+	return (prpl_info && PURPLE_PROTOCOL_PLUGIN_HAS_FUNC(prpl_info, disco_get_list));
+}
+
+gboolean
+pidgin_disco_is_showable()
+{
+	GList *c;
+	PurpleConnection *gc;
+
+	for (c = purple_connections_get_all(); c != NULL; c = c->next) {
+		gc = c->data;
+
+		if (account_filter_func(purple_connection_get_account(gc)))
+			return TRUE;
+	}
+
+	return FALSE;
+}
+
+static void pidgin_disco_create_tree(PidginDiscoList *pdl)
+{
+	GtkCellRenderer *text_renderer, *pixbuf_renderer;
+	GtkTreeViewColumn *column;
+	GtkTreeSelection *selection;
+
+	pdl->model = gtk_tree_store_new(NUM_OF_COLUMNS,
+			GDK_TYPE_PIXBUF,	/* PIXBUF_COLUMN */
+			G_TYPE_STRING,		/* NAME_COLUMN */
+			G_TYPE_STRING,		/* DESCRIPTION_COLUMN */
+			G_TYPE_POINTER		/* SERVICE_COLUMN */
+	);
+
+	pdl->tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pdl->model));
+	gtk_tree_view_set_rules_hint(GTK_TREE_VIEW(pdl->tree), TRUE);
+
+	selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(pdl->tree));
+	g_signal_connect(G_OBJECT(selection), "changed",
+					 G_CALLBACK(selection_changed_cb), pdl);
+
+	g_object_unref(pdl->model);
+
+	gtk_container_add(GTK_CONTAINER(pdl->dialog->sw), pdl->tree);
+	gtk_widget_show(pdl->tree);
+
+	text_renderer = gtk_cell_renderer_text_new();
+	pixbuf_renderer = gtk_cell_renderer_pixbuf_new();
+	
+	column = gtk_tree_view_column_new();
+	gtk_tree_view_column_set_title(column, _("Name"));
+
+	gtk_tree_view_column_pack_start(column,  pixbuf_renderer, FALSE);
+	gtk_tree_view_column_set_attributes(column, pixbuf_renderer,
+			"pixbuf", PIXBUF_COLUMN, NULL);
+
+	gtk_tree_view_column_pack_start(column, text_renderer, TRUE);
+	gtk_tree_view_column_set_attributes(column, text_renderer,
+			"text", NAME_COLUMN, NULL);
+
+	gtk_tree_view_column_set_sizing(GTK_TREE_VIEW_COLUMN(column),
+	                                GTK_TREE_VIEW_COLUMN_GROW_ONLY);
+	gtk_tree_view_column_set_resizable(GTK_TREE_VIEW_COLUMN(column), TRUE);
+	gtk_tree_view_column_set_sort_column_id(GTK_TREE_VIEW_COLUMN(column), NAME_COLUMN);
+	gtk_tree_view_column_set_reorderable(GTK_TREE_VIEW_COLUMN(column), TRUE);
+	gtk_tree_view_append_column(GTK_TREE_VIEW(pdl->tree), column);
+
+	column = gtk_tree_view_column_new_with_attributes(_("Description"), text_renderer,
+				"text", DESCRIPTION_COLUMN, NULL);
+	gtk_tree_view_column_set_sizing(GTK_TREE_VIEW_COLUMN(column),
+	                                GTK_TREE_VIEW_COLUMN_GROW_ONLY);
+	gtk_tree_view_column_set_resizable(GTK_TREE_VIEW_COLUMN(column), TRUE);
+	gtk_tree_view_column_set_sort_column_id(GTK_TREE_VIEW_COLUMN(column), DESCRIPTION_COLUMN);
+	gtk_tree_view_column_set_reorderable(GTK_TREE_VIEW_COLUMN(column), TRUE);
+	gtk_tree_view_append_column(GTK_TREE_VIEW(pdl->tree), column);
+
+	g_signal_connect(G_OBJECT(pdl->tree), "row-expanded", G_CALLBACK(row_expanded_cb), pdl);
+}
+
+static PidginDiscoDialog*
+pidgin_disco_dialog_new_with_account(PurpleAccount *account)
+{
+	PidginDiscoDialog *dialog;
+	GtkWidget *window, *vbox, *vbox2, *bbox;
+
+	dialog = g_new0(PidginDiscoDialog, 1);
+	dialog->account = account;
+
+	/* Create the window. */
+	dialog->window = window = pidgin_create_dialog(_("Service Discovery"), PIDGIN_HIG_BORDER, "service discovery", TRUE);
+
+	g_signal_connect(G_OBJECT(window), "delete_event",
+					 G_CALLBACK(delete_win_cb), dialog);
+
+	/* Create the parent vbox for everything. */
+	vbox = pidgin_dialog_get_vbox_with_properties(GTK_DIALOG(window), FALSE, PIDGIN_HIG_BORDER);
+
+	vbox2 = gtk_vbox_new(FALSE, PIDGIN_HIG_BORDER);
+	gtk_container_add(GTK_CONTAINER(vbox), vbox2);
+	gtk_widget_show(vbox2);
+
+	/* accounts dropdown list */
+	dialog->account_widget = pidgin_account_option_menu_new(dialog->account, FALSE,
+	                         G_CALLBACK(dialog_select_account_cb), account_filter_func, dialog);
+	if (!dialog->account) /* this is normally null, and we normally don't care what the first selected item is */
+		dialog->account = pidgin_account_option_menu_get_selected(dialog->account_widget);
+	pidgin_add_widget_to_vbox(GTK_BOX(vbox2), _("_Account:"), NULL, dialog->account_widget, TRUE, NULL);
+
+	/* scrolled window */
+	dialog->sw = gtk_scrolled_window_new(NULL, NULL);
+	gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(dialog->sw),
+	                                    GTK_SHADOW_IN);
+	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(dialog->sw),
+	                               GTK_POLICY_AUTOMATIC,
+	                               GTK_POLICY_AUTOMATIC);
+	gtk_box_pack_start(GTK_BOX(vbox2), dialog->sw, TRUE, TRUE, 0);
+	gtk_widget_set_size_request(dialog->sw, -1, 250);
+	gtk_widget_show(dialog->sw);
+
+	/* progress bar */
+	dialog->progress = gtk_progress_bar_new();
+	gtk_progress_bar_set_pulse_step(GTK_PROGRESS_BAR(dialog->progress), 0.1);
+	gtk_box_pack_start(GTK_BOX(vbox2), dialog->progress, FALSE, FALSE, 0);
+	gtk_widget_show(dialog->progress);
+
+
+	/* button box */
+	bbox = pidgin_dialog_get_action_area(GTK_DIALOG(window));
+	gtk_box_set_spacing(GTK_BOX(bbox), PIDGIN_HIG_BOX_SPACE);
+	gtk_button_box_set_layout(GTK_BUTTON_BOX(bbox), GTK_BUTTONBOX_END);
+
+	/* stop button */
+	dialog->stop_button = pidgin_dialog_add_button(GTK_DIALOG(window), GTK_STOCK_STOP,
+	                 G_CALLBACK(stop_button_cb), dialog);
+	gtk_widget_set_sensitive(dialog->stop_button, FALSE);
+
+	/* list button */
+	dialog->list_button = pidgin_pixbuf_button_from_stock(_("_Browse"), GTK_STOCK_REFRESH,
+	                                                    PIDGIN_BUTTON_HORIZONTAL);
+	gtk_box_pack_start(GTK_BOX(bbox), dialog->list_button, FALSE, FALSE, 0);
+	g_signal_connect(G_OBJECT(dialog->list_button), "clicked",
+	                 G_CALLBACK(list_button_cb), dialog);
+	gtk_widget_show(dialog->list_button);
+
+	/* register button */
+	dialog->register_button = pidgin_dialog_add_button(GTK_DIALOG(dialog->window), _("Register"),
+	                 G_CALLBACK(register_button_cb), dialog);
+	gtk_widget_set_sensitive(dialog->register_button, FALSE);
+
+	/* add button */
+	dialog->add_button = pidgin_pixbuf_button_from_stock(_("_Add"), GTK_STOCK_ADD,
+	                                                    PIDGIN_BUTTON_HORIZONTAL);
+	gtk_box_pack_start(GTK_BOX(bbox), dialog->add_button, FALSE, FALSE, 0);
+	g_signal_connect(G_OBJECT(dialog->add_button), "clicked",
+	                 G_CALLBACK(add_room_to_blist_cb), dialog);
+	gtk_widget_set_sensitive(dialog->add_button, FALSE);
+	gtk_widget_show(dialog->add_button);
+
+	/* close button */
+	dialog->close_button = pidgin_dialog_add_button(GTK_DIALOG(window), GTK_STOCK_CLOSE,
+					 G_CALLBACK(close_button_cb), dialog);
+
+	/* show the dialog window and return the dialog */
+	gtk_widget_show(dialog->window);
+
+	return dialog;
+}
+
+void
+pidgin_disco_dialog_show(void)
+{
+	pidgin_disco_dialog_new_with_account(NULL);
+}
+
+void
+pidgin_disco_dialog_show_with_account(PurpleAccount* account)
+{
+	PidginDiscoDialog *dialog = pidgin_disco_dialog_new_with_account(account);
+
+	if (!dialog)
+		return;
+
+	list_button_cb(GTK_BUTTON(dialog->list_button), dialog);
+}
+
+static void
+pidgin_disco_create(PurpleDiscoList *list)
+{
+	PidginDiscoList *pdl = g_new0(PidginDiscoList, 1);
+
+	purple_disco_list_set_ui_data(list, pdl);
+
+	pdl->services = g_hash_table_new_full(NULL, NULL, NULL,
+			(GDestroyNotify)gtk_tree_row_reference_free);
+}
+
+
+static void
+pidgin_disco_destroy(PurpleDiscoList *list)
+{
+	PidginDiscoList *pdl;
+
+	pdl = purple_disco_list_get_ui_data(list);
+
+	g_hash_table_destroy(pdl->services);
+	g_free(pdl);
+
+	purple_disco_list_set_ui_data(list, NULL);
+}
+
+static void pidgin_disco_in_progress(PurpleDiscoList *list, gboolean in_progress)
+{
+	PidginDiscoList *pdl;
+	PidginDiscoDialog *dialog;
+	
+	pdl = purple_disco_list_get_ui_data(list);
+	if (!pdl)
+		return;
+
+	dialog = pdl->dialog;
+	if (!dialog)
+		return;
+
+	if (in_progress) {
+		if (dialog->account_widget)
+			gtk_widget_set_sensitive(dialog->account_widget, FALSE);
+		gtk_widget_set_sensitive(dialog->stop_button, TRUE);
+		gtk_widget_set_sensitive(dialog->list_button, FALSE);
+	} else {
+		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress), 0.0);
+		if (dialog->account_widget)
+			gtk_widget_set_sensitive(dialog->account_widget, TRUE);
+		gtk_widget_set_sensitive(dialog->stop_button, FALSE);
+		gtk_widget_set_sensitive(dialog->list_button, TRUE);
+	}
+}
+
+static void pidgin_disco_add_service(PurpleDiscoList *list, PurpleDiscoService *service, PurpleDiscoService *parent)
+{
+	PidginDiscoList *pdl;
+	PidginDiscoDialog *dialog;
+	PurpleDiscoServiceType type;
+	const char *gateway_type;
+	GtkTreeIter iter, parent_iter, child;
+	char *filename = NULL;
+	GdkPixbuf *pixbuf = NULL;
+	gboolean append = TRUE;
+
+	pdl = purple_disco_list_get_ui_data(list);
+	dialog = pdl->dialog;
+
+	purple_debug_info("disco", "Add_service \"%s\"\n", purple_disco_service_get_name(service));
+
+	gtk_progress_bar_pulse(GTK_PROGRESS_BAR(dialog->progress));
+
+	if (parent) {
+		GtkTreeRowReference *rr;
+		GtkTreePath *path;
+
+		rr = g_hash_table_lookup(pdl->services, parent);
+		path = gtk_tree_row_reference_get_path(rr);
+		if (path) {
+			gtk_tree_model_get_iter(GTK_TREE_MODEL(pdl->model), &parent_iter, path);
+			gtk_tree_path_free(path);
+
+			if (gtk_tree_model_iter_children(GTK_TREE_MODEL(pdl->model), &child,
+			                                 &parent_iter)) {
+				PurpleDiscoList *tmp;
+				gtk_tree_model_get(GTK_TREE_MODEL(pdl->model), &child,
+				                   SERVICE_COLUMN, &tmp, -1);
+				if (!tmp)
+					append = FALSE;
+			}
+		}
+	}
+
+	if (append)
+		gtk_tree_store_append(pdl->model, &iter, (parent ? &parent_iter : NULL));
+	else
+		iter = child;
+
+	if (purple_disco_service_get_flags(service) & PURPLE_DISCO_BROWSE) {
+		GtkTreeRowReference *rr;
+		GtkTreePath *path;
+
+		gtk_tree_store_append(pdl->model, &child, &iter);
+
+		path = gtk_tree_model_get_path(GTK_TREE_MODEL(pdl->model), &iter);
+		rr = gtk_tree_row_reference_new(GTK_TREE_MODEL(pdl->model), path);
+		g_hash_table_insert(pdl->services, service, rr);
+		gtk_tree_path_free(path);
+	}
+
+	type = purple_disco_service_get_type(service);
+	gateway_type = purple_disco_service_get_gateway_type(service);
+
+	if (type == PURPLE_DISCO_SERVICE_TYPE_GATEWAY && gateway_type) {
+		char *tmp = g_strconcat(gateway_type, ".png", NULL);
+		filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "protocols", "22", tmp, NULL);
+		g_free(tmp);
+#if 0
+	} else if (type == PURPLE_DISCO_SERVICE_TYPE_USER) {
+		filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "status", "22", "person.png", NULL);
+#endif
+	} else if (type == PURPLE_DISCO_SERVICE_TYPE_CHAT)
+		filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "status", "22", "chat.png", NULL);
+
+	if (filename) {
+		pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
+		g_free(filename);
+	}
+
+	gtk_tree_store_set(pdl->model, &iter,
+			PIXBUF_COLUMN, pixbuf,
+			NAME_COLUMN, purple_disco_service_get_name(service),
+			DESCRIPTION_COLUMN, purple_disco_service_get_description(service),
+			SERVICE_COLUMN, service,
+			-1);
+
+	if (pixbuf)
+		g_object_unref(pixbuf);
+}
+
+static PurpleDiscoUiOps ops = {
+	pidgin_disco_dialog_show_with_account,
+	pidgin_disco_create,
+	pidgin_disco_destroy,
+	pidgin_disco_add_service,
+	pidgin_disco_in_progress,
+	/* padding */
+	NULL,
+	NULL,
+	NULL,
+	NULL
+};
+
+void pidgin_disco_init() {
+	purple_disco_set_ui_ops(&ops);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/gtkdisco.h	Wed Apr 29 23:20:51 2009 +0000
@@ -0,0 +1,56 @@
+/**
+ * @file gtkdisco.c GTK+ Service Discovery UI
+ * @ingroup pidgin
+ */
+
+/* pidgin
+ *
+ * Pidgin is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
+ */
+
+#ifndef _GTK_DISCO_H_
+#define _GTK_DISCO_H_
+
+
+/**
+ * Initializes the service discovery subsystem.
+ */
+void pidgin_disco_init(void);
+
+/**
+ * Determines if showing the service discovery dialog is a valid action.
+ *
+ * @return TRUE if there are accounts online that support service
+ *         discovery.  Otherwise return FALSE.
+ */
+gboolean pidgin_disco_is_showable(void);
+
+/**
+ * Shows a new service discovery dialog.
+ */
+void pidgin_disco_dialog_show(void);
+
+/**
+ * Shows a new service discovery dialog and fetches the list for the specified account.
+ *
+ * @param account The account to use.
+ */
+void pidgin_disco_dialog_show_with_account(PurpleAccount *account);
+
+#endif /* _GTK_DISCO_H_ */
--- a/pidgin/gtkmain.c	Wed Apr 29 17:50:14 2009 +0000
+++ b/pidgin/gtkmain.c	Wed Apr 29 23:20:51 2009 +0000
@@ -48,6 +48,7 @@
 #include "gtkconv.h"
 #include "gtkdebug.h"
 #include "gtkdialogs.h"
+#include "gtkdisco.h"
 #include "gtkdocklet.h"
 #include "gtkeventloop.h"
 #include "gtkft.h"
@@ -308,6 +309,7 @@
 	pidgin_privacy_init();
 	pidgin_xfers_init();
 	pidgin_roomlist_init();
+	pidgin_disco_init();
 	pidgin_log_init();
 	pidgin_docklet_init();
 	pidgin_smileys_init();