changeset 27002:b709ab0cb4bc

propagate from branch 'im.pidgin.pidgin' (head a24ba8f3be7fe637dd9123af47b4c500944f40ae) to branch 'im.pidgin.cpw.darkrain42.xmpp.disco' (head d75cba73217aa91d4e2547cd9084ca64c3610eaf)
author Paul Aurich <paul@darkrain42.org>
date Wed, 03 Jun 2009 04:32:19 +0000
parents 7358c1f6df19 (current diff) 1c8d7165df6b (diff)
children 61d817c4c935
files configure.ac libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h
diffstat 13 files changed, 1565 insertions(+), 92 deletions(-) [+]
line wrap: on
line diff
--- a/configure.ac	Tue Jun 02 21:41:03 2009 +0000
+++ b/configure.ac	Wed Jun 03 04:32:19 2009 +0000
@@ -2476,6 +2476,7 @@
 		   pidgin/pixmaps/emotes/small/16/Makefile
 		   pidgin/plugins/Makefile
 		   pidgin/plugins/cap/Makefile
+		   pidgin/plugins/disco/Makefile
 		   pidgin/plugins/gestures/Makefile
 		   pidgin/plugins/gevolution/Makefile
 		   pidgin/plugins/musicmessaging/Makefile
--- a/libpurple/protocols/jabber/disco.c	Tue Jun 02 21:41:03 2009 +0000
+++ b/libpurple/protocols/jabber/disco.c	Wed Jun 03 04:32:19 2009 +0000
@@ -1,5 +1,5 @@
 /*
- * purple - Jabber Protocol Plugin
+ * purple - Jabber Service Discovery
  *
  * Copyright (C) 2003, Nathan Walp <faceprint@faceprint.com>
  *
@@ -22,6 +22,7 @@
 #include "internal.h"
 #include "prefs.h"
 #include "debug.h"
+#include "request.h"
 
 #include "adhoccommands.h"
 #include "buddy.h"
@@ -40,6 +41,11 @@
 	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); \
@@ -163,11 +169,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");
@@ -176,13 +182,42 @@
 		}
 		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;
 		JabberBuddyResource *jbr = NULL;
 		JabberCapabilities capabilities = JABBER_CAP_NONE;
-		struct _jabber_disco_info_cb_data *jdicd;
 
 		if((jid = jabber_id_new(from))) {
 			if(jid->resource && (jb = jabber_buddy_find(js, from, TRUE)))
@@ -193,7 +228,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;
 
@@ -245,6 +280,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;
 				}
@@ -260,19 +297,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)))
@@ -284,7 +314,6 @@
 			capabilities = jbr->capabilities;
 
 		jdicd->callback(js, from, capabilities, jdicd->data);
-		g_hash_table_remove(js->disco_callbacks, from);
 	}
 }
 
@@ -529,10 +558,10 @@
 	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);
 }
+
--- a/libpurple/protocols/jabber/disco.h	Tue Jun 02 21:41:03 2009 +0000
+++ b/libpurple/protocols/jabber/disco.h	Wed Jun 03 04:32:19 2009 +0000
@@ -1,5 +1,5 @@
 /**
- * @file disco.h service discovery 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,
--- a/libpurple/protocols/jabber/jabber.c	Tue Jun 02 21:41:03 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Wed Jun 03 04:32:19 2009 +0000
@@ -766,8 +766,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,
@@ -1089,6 +1087,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)
 {
@@ -1096,10 +1112,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;
@@ -1156,74 +1173,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);
@@ -1296,8 +1292,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)
--- a/libpurple/protocols/jabber/jabber.h	Tue Jun 02 21:41:03 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Wed Jun 03 04:32:19 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"
@@ -153,7 +155,6 @@
 	GList *user_directories;
 
 	GHashTable *iq_callbacks;
-	GHashTable *disco_callbacks;
 	int next_id;
 
 	GList *bs_proxies;
--- a/libpurple/protocols/jabber/libxmpp.c	Tue Jun 02 21:41:03 2009 +0000
+++ b/libpurple/protocols/jabber/libxmpp.c	Wed Jun 03 04:32:19 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,6 +118,7 @@
 	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 */
--- a/pidgin/gtkutils.c	Tue Jun 02 21:41:03 2009 +0000
+++ b/pidgin/gtkutils.c	Wed Jun 03 04:32:19 2009 +0000
@@ -525,7 +525,7 @@
 	GtkWidget *item = gtk_menu_get_active(GTK_MENU(menu));
 	if (p_item)
 		(*p_item) = item;
-	return g_object_get_data(G_OBJECT(item), "aop_per_item_data");
+	return item ? g_object_get_data(G_OBJECT(item), "aop_per_item_data") : NULL;
 }
 
 static void
--- a/pidgin/plugins/Makefile.am	Tue Jun 02 21:41:03 2009 +0000
+++ b/pidgin/plugins/Makefile.am	Wed Jun 03 04:32:19 2009 +0000
@@ -1,4 +1,4 @@
-DIST_SUBDIRS = cap gestures gevolution musicmessaging perl ticker
+DIST_SUBDIRS = cap disco gestures gevolution musicmessaging perl ticker
 
 if BUILD_GEVOLUTION
 GEVOLUTION_DIR = gevolution
@@ -26,6 +26,7 @@
 	$(GEVOLUTION_DIR) \
 	$(MUSICMESSAGING_DIR) \
 	$(PERL_DIR) \
+	disco \
 	ticker
 
 plugindir = $(libdir)/pidgin
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/disco/Makefile.am	Wed Jun 03 04:32:19 2009 +0000
@@ -0,0 +1,23 @@
+plugindir = $(libdir)/pidgin
+
+xmppdisco_la_LDFLAGS = -module -avoid-version
+
+if PLUGINS
+
+plugin_LTLIBRARIES = xmppdisco.la
+
+xmppdisco_la_SOURCES = \
+	gtkdisco.c \
+	xmppdisco.c
+
+xmppdisco_la_LIBADD = $(GTK_LIBS)
+
+endif
+
+AM_CPPFLAGS = \
+	-DDATADIR=\"$(datadir)\" \
+	-I$(top_srcdir)/libpurple \
+	-I$(top_builddir)/libpurple \
+	-I$(top_srcdir)/pidgin \
+	$(DEBUG_CFLAGS) \
+	$(GTK_CFLAGS)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/disco/gtkdisco.c	Wed Jun 03 04:32:19 2009 +0000
@@ -0,0 +1,583 @@
+/**
+ * @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 "debug.h"
+#include "gtkutils.h"
+#include "pidgin.h"
+#include "request.h"
+
+#include "gtkdisco.h"
+#include "xmppdisco.h"
+
+GList *dialogs = NULL;
+
+struct _menu_cb_info {
+	PidginDiscoList *list;
+	XmppDiscoService *service;
+};
+
+enum {
+	PIXBUF_COLUMN = 0,
+	NAME_COLUMN,
+	DESCRIPTION_COLUMN,
+	SERVICE_COLUMN,
+	NUM_OF_COLUMNS
+};
+
+static void
+pidgin_disco_list_destroy(PidginDiscoList *list)
+{
+	g_hash_table_destroy(list->services);
+	if (list->dialog && list->dialog->discolist == list)
+		list->dialog->discolist = NULL;
+
+	if (list->tree) {
+		gtk_widget_destroy(list->tree);
+		list->tree = NULL;
+	}
+
+	g_free((gchar*)list->server);
+	g_free(list);
+}
+
+PidginDiscoList *pidgin_disco_list_ref(PidginDiscoList *list)
+{
+	g_return_val_if_fail(list != NULL, NULL);
+
+	++list->ref;
+    purple_debug_misc("xmppdisco", "reffing list, ref count now %d\n", list->ref);
+
+	return list;
+}
+
+void pidgin_disco_list_unref(PidginDiscoList *list)
+{
+	g_return_if_fail(list != NULL);
+
+	--list->ref;
+
+	purple_debug_misc("xmppdisco", "unreffing list, ref count now %d\n", list->ref);
+	if (list->ref == 0)
+		pidgin_disco_list_destroy(list);
+}
+
+void pidgin_disco_list_set_in_progress(PidginDiscoList *list, gboolean in_progress)
+{
+	PidginDiscoDialog *dialog = list->dialog;
+
+	if (!dialog)
+		return;
+
+	list->in_progress = in_progress;
+
+	if (in_progress) {
+		gtk_widget_set_sensitive(dialog->account_widget, FALSE);
+		gtk_widget_set_sensitive(dialog->stop_button, TRUE);
+		gtk_widget_set_sensitive(dialog->browse_button, FALSE);
+	} else {
+		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(dialog->progress), 0.0);
+
+		gtk_widget_set_sensitive(dialog->account_widget, TRUE);
+
+		gtk_widget_set_sensitive(dialog->stop_button, FALSE);
+		gtk_widget_set_sensitive(dialog->browse_button, TRUE);
+/*
+		gtk_widget_set_sensitive(dialog->register_button, FALSE);
+		gtk_widget_set_sensitive(dialog->add_button, FALSE);
+*/
+	}
+}
+
+static void pidgin_disco_create_tree(PidginDiscoList *pdl);
+
+static void dialog_select_account_cb(GObject *w, PurpleAccount *account,
+				     PidginDiscoDialog *dialog)
+{
+	dialog->account = account;
+	gtk_widget_set_sensitive(dialog->browse_button, account != NULL);
+}
+
+static void register_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	struct _menu_cb_info *info = g_object_get_data(G_OBJECT(button), "disco-info");
+
+	xmpp_disco_service_register(info->service);
+}
+
+static void discolist_cancel_cb(PidginDiscoList *pdl, const char *server)
+{
+	pidgin_disco_list_set_in_progress(pdl, FALSE);
+	pidgin_disco_list_unref(pdl);
+}
+
+static void discolist_ok_cb(PidginDiscoList *pdl, const char *server)
+{
+	gtk_widget_set_sensitive(pdl->dialog->browse_button, TRUE);
+
+	if (!server || !*server) {
+		purple_notify_error(my_plugin, _("Invalid Server"), _("Invalid Server"),
+		                    NULL);
+
+		pidgin_disco_list_set_in_progress(pdl, FALSE);
+		pidgin_disco_list_unref(pdl);
+		return;
+	}
+
+	pdl->server = g_strdup(server);
+	pidgin_disco_list_set_in_progress(pdl, TRUE);
+	xmpp_disco_start(pdl);
+}
+
+static void browse_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	PurpleConnection *pc;
+	PidginDiscoList *pdl;
+	const char *username;
+	const char *at, *slash;
+	char *server = NULL;
+
+	pc = purple_account_get_connection(dialog->account);
+	if (!pc)
+		return;
+
+	gtk_widget_set_sensitive(dialog->browse_button, FALSE);
+	gtk_widget_set_sensitive(dialog->add_button, FALSE);
+	gtk_widget_set_sensitive(dialog->register_button, FALSE);
+
+	if (dialog->discolist != NULL) {
+		if (dialog->discolist->tree) {
+			gtk_widget_destroy(dialog->discolist->tree);
+			dialog->discolist->tree = NULL;
+		}
+		pidgin_disco_list_unref(dialog->discolist);
+	}
+
+	pdl = dialog->discolist = g_new0(PidginDiscoList, 1);
+	pdl->services = g_hash_table_new_full(NULL, NULL, NULL,
+			(GDestroyNotify)gtk_tree_row_reference_free);
+	pdl->pc = pc;
+	/* We keep a copy... */
+	pidgin_disco_list_ref(pdl);
+
+	pdl->dialog = dialog;
+	pidgin_disco_create_tree(pdl);
+
+	if (dialog->account_widget)
+		gtk_widget_set_sensitive(dialog->account_widget, FALSE);
+
+	username = purple_account_get_username(dialog->account);
+	at = g_utf8_strchr(username, -1, '@');
+	slash = g_utf8_strchr(username, -1, '/');
+	if (at && !slash) {
+		server = g_strdup_printf("%s", at + 1);
+	} else if (at && slash && at + 1 < slash) {
+		server = g_strdup_printf("%.*s", (int)(slash - (at + 1)), at + 1);
+	}
+
+	if (server == NULL)
+		/* This shouldn't ever happen since the account is connected */
+		server = g_strdup("jabber.org");
+
+	purple_request_input(my_plugin, _("Server name request"), _("Enter an XMPP Server"),
+			_("Select an XMPP server to query"),
+			server, FALSE, FALSE, NULL,
+			_("Find Services"), PURPLE_CALLBACK(discolist_ok_cb),
+			_("Cancel"), PURPLE_CALLBACK(discolist_cancel_cb),
+			purple_connection_get_account(pc), NULL, NULL, pdl);
+
+	g_free(server);
+}
+
+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_connection_get_account(info->list->pc);
+	name = info->service->name;
+
+	if (info->service->type == XMPP_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)
+{
+	XmppDiscoService *service;
+	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);
+
+		gtk_widget_set_sensitive(dialog->add_button, service->flags & XMPP_DISCO_ADD);
+		gtk_widget_set_sensitive(dialog->register_button, service->flags & XMPP_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;
+	XmppDiscoService *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);
+	xmpp_disco_service_expand(service);
+}
+
+static void
+destroy_win_cb(GtkWidget *window, gpointer d)
+{
+	PidginDiscoDialog *dialog = d;
+	PidginDiscoList *list = dialog->discolist;
+
+	if (list) {
+		list->dialog = NULL;
+
+		if (list->in_progress)
+			list->in_progress = FALSE;
+
+		pidgin_disco_list_unref(list);
+	}
+
+	dialogs = g_list_remove(dialogs, d);
+	g_free(dialog);
+}
+
+static void stop_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	pidgin_disco_list_set_in_progress(dialog->discolist, FALSE);
+}
+
+static void close_button_cb(GtkButton *button, PidginDiscoDialog *dialog)
+{
+	GtkWidget *window = dialog->window;
+
+	gtk_widget_destroy(window);
+}
+
+static gboolean account_filter_func(PurpleAccount *account)
+{
+	return purple_strequal(purple_account_get_protocol_id(account), XMPP_PLUGIN_ID);
+}
+
+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);
+}
+
+void pidgin_disco_signed_off_cb(PurpleConnection *pc)
+{
+	GList *node;
+
+	for (node = dialogs; node; node = node->next) {
+		PidginDiscoDialog *dialog = node->data;
+		PidginDiscoList *list = dialog->discolist;
+
+		if (list && list->pc == pc) {
+			if (list->in_progress)
+				pidgin_disco_list_set_in_progress(list, FALSE);
+
+			if (list->tree) {
+				gtk_widget_destroy(list->tree);
+				list->tree = NULL;
+			}
+
+			pidgin_disco_list_unref(list);
+			dialog->discolist = NULL;
+
+			gtk_widget_set_sensitive(dialog->browse_button,
+					pidgin_account_option_menu_get_selected(dialog->account_widget) != NULL);
+
+			gtk_widget_set_sensitive(dialog->register_button, FALSE);
+			gtk_widget_set_sensitive(dialog->add_button, FALSE);
+		}
+	}
+}
+
+void pidgin_disco_dialogs_destroy_all(void)
+{
+	while (dialogs) {
+		PidginDiscoDialog *dialog = dialogs->data;
+
+		gtk_widget_destroy(dialog->window);
+		/* destroy_win_cb removes the dialog from the list */
+	}
+}
+
+PidginDiscoDialog *pidgin_disco_dialog_new(void)
+{
+	PidginDiscoDialog *dialog;
+	GtkWidget *window, *vbox, *vbox2, *bbox;
+
+	dialog = g_new0(PidginDiscoDialog, 1);
+	dialogs = g_list_prepend(dialogs, dialog);
+
+	/* Create the window. */
+	dialog->window = window = pidgin_create_dialog(_("Service Discovery"), PIDGIN_HIG_BORDER, "service discovery", TRUE);
+
+	g_signal_connect(G_OBJECT(window), "destroy",
+					 G_CALLBACK(destroy_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(NULL, FALSE,
+	                         G_CALLBACK(dialog_select_account_cb), account_filter_func, dialog);
+	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);
+
+	/* browse button */
+	dialog->browse_button = pidgin_pixbuf_button_from_stock(_("_Browse"), GTK_STOCK_REFRESH,
+	                                                    PIDGIN_BUTTON_HORIZONTAL);
+	gtk_box_pack_start(GTK_BOX(bbox), dialog->browse_button, FALSE, FALSE, 0);
+	g_signal_connect(G_OBJECT(dialog->browse_button), "clicked",
+	                 G_CALLBACK(browse_button_cb), dialog);
+	gtk_widget_set_sensitive(dialog->browse_button, dialog->account != NULL);
+	gtk_widget_show(dialog->browse_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_add_service(PidginDiscoList *pdl, XmppDiscoService *service, XmppDiscoService *parent)
+{
+	PidginDiscoDialog *dialog;
+	GtkTreeIter iter, parent_iter, child;
+	char *filename = NULL;
+	GdkPixbuf *pixbuf = NULL;
+	gboolean append = TRUE;
+
+	dialog = pdl->dialog;
+	g_return_if_fail(dialog != NULL);
+
+	purple_debug_info("xmppdisco", "Adding service \"%s\"\n", service->name);
+
+	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)) {
+				PidginDiscoList *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 (service->flags & XMPP_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);
+	}
+
+	if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY && service->gateway_type) {
+		char *tmp = g_strconcat(service->gateway_type, ".png", NULL);
+		filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "protocols", "22", tmp, NULL);
+		g_free(tmp);
+#if 0
+	} else if (service->type == XMPP_DISCO_SERVICE_TYPE_USER) {
+		filename = g_build_filename(DATADIR, "pixmaps", "pidgin", "status", "22", "person.png", NULL);
+#endif
+	} else if (service->type == XMPP_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, service->name,
+			DESCRIPTION_COLUMN, service->description,
+			SERVICE_COLUMN, service,
+			-1);
+
+	if (pixbuf)
+		g_object_unref(pixbuf);
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/disco/gtkdisco.h	Wed Jun 03 04:32:19 2009 +0000
@@ -0,0 +1,79 @@
+/* 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 PIDGIN_XMPP_DISCO_UI_H
+#define PIDGIN_XMPP_DISCO_UI_H
+
+typedef struct _PidginDiscoDialog PidginDiscoDialog;
+typedef struct _PidginDiscoList PidginDiscoList;
+
+#include "xmppdisco.h"
+
+struct _PidginDiscoDialog {
+	GtkWidget *window;
+	GtkWidget *account_widget;
+
+	GtkWidget *sw;
+	GtkWidget *progress;
+
+	GtkWidget *stop_button;
+	GtkWidget *browse_button;
+	GtkWidget *register_button;
+	GtkWidget *add_button;
+	GtkWidget *close_button;
+
+	PurpleAccount *account;
+	PidginDiscoList *discolist;
+};
+
+struct _PidginDiscoList {
+	PurpleConnection *pc;
+	gboolean in_progress;
+	const gchar *server;
+
+	gint ref;
+	guint fetch_count;
+
+	PidginDiscoDialog *dialog;
+	GtkTreeStore *model;
+	GtkWidget *tree;
+	GHashTable *services;
+};
+
+/**
+ * Shows a new service discovery dialog.
+ */
+PidginDiscoDialog *pidgin_disco_dialog_new(void);
+
+/**
+ * Destroy all the open dialogs (called when unloading the plugin).
+ */
+void pidgin_disco_dialogs_destroy_all(void);
+void pidgin_disco_signed_off_cb(PurpleConnection *pc);
+
+void pidgin_disco_add_service(PidginDiscoList *list, XmppDiscoService *service,
+                              XmppDiscoService *parent);
+
+PidginDiscoList *pidgin_disco_list_ref(PidginDiscoList *list);
+void pidgin_disco_list_unref(PidginDiscoList *list);
+
+void pidgin_disco_list_set_in_progress(PidginDiscoList *list, gboolean in_progress);
+#endif /* PIDGIN_XMPP_DISCO_UI_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/disco/xmppdisco.c	Wed Jun 03 04:32:19 2009 +0000
@@ -0,0 +1,646 @@
+/*
+ * Purple - XMPP Service Disco Browser
+ *
+ * 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 "debug.h"
+#include "signals.h"
+#include "version.h"
+
+#include "gtkconv.h"
+#include "gtkimhtml.h"
+#include "gtkplugin.h"
+
+#include "xmppdisco.h"
+#include "gtkdisco.h"
+
+/* Variables */
+PurplePlugin *my_plugin = NULL;
+static GHashTable *iq_callbacks = NULL;
+static gboolean iq_listening = FALSE;
+
+typedef void (*XmppIqCallback)(PurpleConnection *pc, const char *type,
+                              const char *id, const char *from, xmlnode *iq,
+							  gpointer data);
+
+struct xmpp_iq_cb_data
+{
+	gpointer context;
+	PurpleConnection *pc;
+	XmppIqCallback cb;
+};
+
+struct item_data {
+	PidginDiscoList *list;
+	XmppDiscoService *parent;
+	char *name;
+	char *node; /* disco#info replies don't always include the node */
+};
+
+static char*
+generate_next_id()
+{
+	static guint32 index = 0;
+
+	if (index == 0) {
+		do {
+			index = g_random_int();
+		} while (index == 0);
+	}
+
+	return g_strdup_printf("purpledisco%x", index++);
+}
+
+static gboolean
+remove_iq_callbacks_by_pc(gpointer key, gpointer value, gpointer user_data)
+{
+	struct xmpp_iq_cb_data *cb_data = value;
+
+	if (cb_data && cb_data->pc == user_data) {
+		/*
+		 * This is a hack. All the IQ callback datas in this code are
+		 * the same structure so that we can free them here. Ideally they'd
+		 * be objects and this would be polymorphic. That's overkill, here.
+		 */
+		struct item_data *item_data = cb_data->context;
+
+		if (item_data) {
+			pidgin_disco_list_unref(item_data->list);
+			g_free(item_data->name);
+			g_free(item_data->node);
+			g_free(item_data);
+		}
+
+		return TRUE;
+	} else
+		return FALSE;
+}
+
+static gboolean
+xmpp_iq_received(PurpleConnection *pc, const char *type, const char *id,
+                 const char *from, xmlnode *iq)
+{
+	struct xmpp_iq_cb_data *cb_data;
+
+	cb_data = g_hash_table_lookup(iq_callbacks, id);
+	if (!cb_data)
+		return FALSE;
+
+	cb_data->cb(cb_data->pc, type, id, from, iq, cb_data->context);
+
+	g_hash_table_remove(iq_callbacks, id);
+	if (g_hash_table_size(iq_callbacks) == 0) {
+		PurplePlugin *prpl = purple_connection_get_prpl(pc);
+		iq_listening = FALSE;
+		purple_signal_disconnect(prpl, "jabber-receiving-iq", my_plugin,
+		                         PURPLE_CALLBACK(xmpp_iq_received));
+	}
+
+	/* Om nom nom nom */
+	return TRUE;
+}
+
+static void
+xmpp_iq_register_callback(PurpleConnection *pc, gchar *id, gpointer data,
+                          XmppIqCallback cb)
+{
+	struct xmpp_iq_cb_data *cbdata = g_new0(struct xmpp_iq_cb_data, 1);
+
+	cbdata->context = data;
+	cbdata->cb = cb;
+	cbdata->pc = pc;
+
+	g_hash_table_insert(iq_callbacks, id, cbdata);
+
+	if (!iq_listening) {
+    	PurplePlugin *prpl = purple_plugins_find_with_id(XMPP_PLUGIN_ID);
+		iq_listening = TRUE;
+		purple_signal_connect(prpl, "jabber-receiving-iq", my_plugin,
+		                      PURPLE_CALLBACK(xmpp_iq_received), NULL);
+	}
+}
+
+static void
+xmpp_disco_info_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
+                   const char *node, XmppIqCallback cb)
+{
+	PurplePlugin *prpl;
+	PurplePluginProtocolInfo *prpl_info;
+	xmlnode *iq, *query;
+	char *id = generate_next_id();
+	char *str;
+
+	iq = xmlnode_new("iq");
+	xmlnode_set_attrib(iq, "type", "get");
+	xmlnode_set_attrib(iq, "to", jid);
+	xmlnode_set_attrib(iq, "id", id);
+
+	query = xmlnode_new_child(iq, "query");
+	xmlnode_set_namespace(query, NS_DISCO_INFO);
+	if (node)
+		xmlnode_set_attrib(query, "node", node);
+
+	xmpp_iq_register_callback(pc, id, cbdata, cb);
+
+	str = xmlnode_to_str(iq, NULL);
+	prpl = purple_connection_get_prpl(pc);
+	prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
+	prpl_info->send_raw(pc, str, -1);
+	g_free(str);
+}
+
+static void
+xmpp_disco_items_do(PurpleConnection *pc, gpointer cbdata, const char *jid,
+                    const char *node, XmppIqCallback cb)
+{
+	PurplePlugin *prpl;
+	PurplePluginProtocolInfo *prpl_info;
+	xmlnode *iq, *query;
+	char *id = generate_next_id();
+	char *str;
+
+	iq = xmlnode_new("iq");
+	xmlnode_set_attrib(iq, "type", "get");
+	xmlnode_set_attrib(iq, "to", jid);
+	xmlnode_set_attrib(iq, "id", id);
+
+	query = xmlnode_new_child(iq, "query");
+	xmlnode_set_namespace(query, NS_DISCO_ITEMS);
+	if (node)
+		xmlnode_set_attrib(query, "node", node);
+
+	xmpp_iq_register_callback(pc, id, cbdata, cb);
+
+	str = xmlnode_to_str(iq, NULL);
+	prpl = purple_connection_get_prpl(pc);
+	prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
+	prpl_info->send_raw(pc, str, -1);
+	g_free(str);
+}
+
+static XmppDiscoServiceType
+disco_service_type_from_identity(xmlnode *identity)
+{
+	const char *category, *type;
+
+	if (!identity)
+		return XMPP_DISCO_SERVICE_TYPE_OTHER;
+
+	category = xmlnode_get_attrib(identity, "category");
+	type = xmlnode_get_attrib(identity, "type");
+
+	if (!category)
+		return XMPP_DISCO_SERVICE_TYPE_OTHER;
+
+	if (g_str_equal(category, "conference"))
+		return XMPP_DISCO_SERVICE_TYPE_CHAT;
+	else if (g_str_equal(category, "directory"))
+		return XMPP_DISCO_SERVICE_TYPE_DIRECTORY;
+	else if (g_str_equal(category, "gateway"))
+		return XMPP_DISCO_SERVICE_TYPE_GATEWAY;
+	else if (g_str_equal(category, "pubsub")) {
+		if (!type || g_str_equal(type, "collection"))
+			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION;
+		else if (g_str_equal(type, "leaf"))
+			return XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF;
+		else if (g_str_equal(type, "service"))
+			return XMPP_DISCO_SERVICE_TYPE_OTHER;
+		else {
+			purple_debug_warning("xmppdisco", "Unknown pubsub type '%s'\n", type);
+			return XMPP_DISCO_SERVICE_TYPE_OTHER;
+		}
+	}
+
+	return XMPP_DISCO_SERVICE_TYPE_OTHER;
+}
+
+static const struct {
+    const char *from;
+    const char *to;
+} disco_type_mappings[] = {
+    { "gadu-gadu", "gadu-gadu" }, /* the prpl is prpl-gg, but list_icon returns "gadu-gadu" */
+    { "sametime",  "meanwhile" },
+    { "myspaceim", "myspace" },
+    { "xmpp",      "jabber" }, /* prpl-jabber (mentioned in case the prpl is renamed so this line will match) */
+    { NULL,        NULL }
+};
+
+static const gchar *
+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
+got_info_cb(PurpleConnection *pc, const char *type, const char *id,
+            const char *from, xmlnode *iq, gpointer data)
+{
+	struct item_data *item_data = data;
+	PidginDiscoList *list = item_data->list;
+	xmlnode *query;
+
+	--list->fetch_count;
+
+	if (!list->in_progress)
+		goto out;
+
+	if (g_str_equal(type, "result") &&
+			(query = xmlnode_get_child(iq, "query"))) {
+		xmlnode *identity = xmlnode_get_child(query, "identity");
+		XmppDiscoService *service;
+		xmlnode *feature;
+
+		service = g_new0(XmppDiscoService, 1);
+		service->list = item_data->list;
+		purple_debug_info("xmppdisco", "parent for %s is %p\n", from, item_data->parent);
+		service->parent = item_data->parent;
+		service->flags = XMPP_DISCO_ADD;
+		service->type = disco_service_type_from_identity(identity);
+
+		if (item_data->node) {
+			if (item_data->name) {
+				service->name = item_data->name;
+				item_data->name = NULL;
+			} else
+				service->name = g_strdup(item_data->node);
+
+			service->node = item_data->node;
+			item_data->node = NULL;
+
+			if (service->type == XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION)
+				service->flags |= XMPP_DISCO_BROWSE;
+		} else
+			service->name = g_strdup(from);
+
+		if (item_data->name) {
+			service->description = item_data->name;
+			item_data->name = NULL;
+		} else if (identity)
+			service->description = g_strdup(xmlnode_get_attrib(identity, "name"));
+
+		/* TODO: Overlap with service->name a bit */
+		service->jid = g_strdup(from);
+
+		for (feature = xmlnode_get_child(query, "feature"); feature;
+				feature = xmlnode_get_next_twin(feature)) {
+			const char *var;
+			if (!(var = xmlnode_get_attrib(feature, "var")))
+				continue;
+
+			if (g_str_equal(var, NS_REGISTER))
+				service->flags |= XMPP_DISCO_REGISTER;
+			else if (g_str_equal(var, NS_DISCO_ITEMS))
+				service->flags |= XMPP_DISCO_BROWSE;
+			else if (g_str_equal(var, NS_MUC)) {
+				service->flags |= XMPP_DISCO_BROWSE;
+				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
+			}
+		}
+
+		if (service->type == XMPP_DISCO_SERVICE_TYPE_GATEWAY)
+			service->gateway_type = g_strdup(disco_type_from_string(
+					xmlnode_get_attrib(identity, "type")));
+
+		pidgin_disco_add_service(list, service, service->parent);
+	}
+
+out:
+	if (list->fetch_count == 0)
+		pidgin_disco_list_set_in_progress(list, FALSE);
+
+	g_free(item_data->name);
+	g_free(item_data->node);
+	g_free(item_data);
+	pidgin_disco_list_unref(list);
+}
+
+static void
+got_items_cb(PurpleConnection *pc, const char *type, const char *id,
+             const char *from, xmlnode *iq, gpointer data)
+{
+	struct item_data *item_data = data;
+	PidginDiscoList *list = item_data->list;
+	xmlnode *query;
+
+	--list->fetch_count;
+
+	if (!list->in_progress)
+		goto out;
+
+	if (g_str_equal(type, "result") &&
+			(query = xmlnode_get_child(iq, "query"))) {
+		xmlnode *item;
+
+		for (item = xmlnode_get_child(query, "item"); item;
+				item = xmlnode_get_next_twin(item)) {
+			const char *jid = xmlnode_get_attrib(item, "jid");
+			const char *name = xmlnode_get_attrib(item, "name");
+			const char *node = xmlnode_get_attrib(item, "node");
+
+			if (item_data->parent->type == XMPP_DISCO_SERVICE_TYPE_CHAT) {
+				/* This is a hacky first-order approximation. Any MUC
+				 * component that has a >1 level hierarchy (a Yahoo MUC
+				 * transport component probably does) will violate this.
+				 *
+				 * On the other hand, this is better than querying all the
+				 * chats at conference.jabber.org to enumerate them.
+				 */
+				XmppDiscoService *service = g_new0(XmppDiscoService, 1);
+				service->list = item_data->list;
+				service->parent = item_data->parent;
+				service->flags = XMPP_DISCO_ADD;
+				service->type = XMPP_DISCO_SERVICE_TYPE_CHAT;
+
+				service->name = g_strdup(name);
+				service->jid = g_strdup(jid);
+				service->node = g_strdup(node);
+				pidgin_disco_add_service(list, service, item_data->parent);
+			} else {
+				struct item_data *item_data2 = g_new0(struct item_data, 1);
+
+				item_data2->list = item_data->list;
+				item_data2->parent = item_data->parent;
+				item_data2->name = g_strdup(name);
+				item_data2->node = g_strdup(node);
+
+				++list->fetch_count;
+				pidgin_disco_list_ref(list);
+				xmpp_disco_info_do(pc, item_data2, jid, node, got_info_cb);
+			}
+		}
+	}
+
+out:
+	if (list->fetch_count == 0)
+		pidgin_disco_list_set_in_progress(list, FALSE);
+
+	g_free(item_data);
+	pidgin_disco_list_unref(list);
+}
+
+static void
+server_items_cb(PurpleConnection *pc, const char *type, const char *id,
+                const char *from, xmlnode *iq, gpointer data)
+{
+	struct item_data *cb_data = data;
+	PidginDiscoList *list = cb_data->list;
+	xmlnode *query;
+
+	g_free(cb_data);
+	--list->fetch_count;
+
+	if (g_str_equal(type, "result") &&
+			(query = xmlnode_get_child(iq, "query"))) {
+		xmlnode *item;
+
+		for (item = xmlnode_get_child(query, "item"); item;
+				item = xmlnode_get_next_twin(item)) {
+			const char *jid = xmlnode_get_attrib(item, "jid");
+			struct item_data *item_data;
+
+			if (!jid)
+				continue;
+
+			item_data = g_new0(struct item_data, 1);
+			item_data->list = list;
+
+			++list->fetch_count;
+			pidgin_disco_list_ref(list);
+			xmpp_disco_info_do(pc, item_data, jid, NULL, got_info_cb);
+		}
+	}
+
+	if (list->fetch_count == 0)
+		pidgin_disco_list_set_in_progress(list, FALSE);
+
+	pidgin_disco_list_unref(list);
+}
+
+static void
+server_info_cb(PurpleConnection *pc, const char *type, const char *id,
+               const char *from, xmlnode *iq, gpointer data)
+{
+	struct item_data *cb_data = data;
+	PidginDiscoList *list = cb_data->list;
+	xmlnode *query;
+	gboolean items = FALSE;
+
+	--list->fetch_count;
+
+	if (g_str_equal(type, "result") &&
+			(query = xmlnode_get_child(iq, "query"))) {
+		xmlnode *feature;
+
+		for (feature = xmlnode_get_child(query, "feature"); feature;
+				feature = xmlnode_get_next_twin(feature)) {
+			const char *var = xmlnode_get_attrib(feature, "var");
+			if (purple_strequal(var, NS_DISCO_ITEMS)) {
+				items = TRUE;
+				break;
+			}
+		}
+	}
+
+	if (items) {
+		xmpp_disco_items_do(pc, cb_data, from, NULL /* node */, server_items_cb);
+		++list->fetch_count;
+		pidgin_disco_list_ref(list);
+	} else {
+		purple_notify_error(my_plugin, _("Error"),
+		                    _("Server does not support service discovery"),
+		                   NULL);
+		pidgin_disco_list_set_in_progress(list, FALSE);
+		g_free(cb_data);
+	}
+
+	pidgin_disco_list_unref(list);
+}
+
+void xmpp_disco_start(PidginDiscoList *list)
+{
+	struct item_data *cb_data;
+
+	g_return_if_fail(list != NULL);
+
+	++list->fetch_count;
+	pidgin_disco_list_ref(list);
+
+	cb_data = g_new0(struct item_data, 1);
+	cb_data->list = list;
+
+	xmpp_disco_info_do(list->pc, cb_data, list->server, NULL, server_info_cb);
+}
+
+void xmpp_disco_service_expand(XmppDiscoService *service)
+{
+	struct item_data *item_data;
+
+	g_return_if_fail(service != NULL);
+
+	if (service->expanded)
+		return;
+
+	item_data = g_new0(struct item_data, 1);
+	item_data->list = service->list;
+	item_data->parent = service;
+
+	++service->list->fetch_count;
+	pidgin_disco_list_ref(service->list);
+
+	pidgin_disco_list_set_in_progress(service->list, TRUE);
+
+	xmpp_disco_items_do(service->list->pc, item_data, service->jid, service->node,
+	                    got_items_cb);
+	service->expanded = TRUE;
+}
+
+void xmpp_disco_service_register(XmppDiscoService *service)
+{
+	PurplePlugin *prpl;
+	PurplePluginProtocolInfo *prpl_info;
+	xmlnode *iq, *query;
+	char *id = generate_next_id();
+	char *str;
+
+	iq = xmlnode_new("iq");
+	xmlnode_set_attrib(iq, "type", "get");
+	xmlnode_set_attrib(iq, "to", service->jid);
+	xmlnode_set_attrib(iq, "id", id);
+
+	query = xmlnode_new_child(iq, "query");
+	xmlnode_set_namespace(query, NS_REGISTER);
+
+	str = xmlnode_to_str(iq, NULL);
+	prpl = purple_connection_get_prpl(service->list->pc);
+	prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(prpl);
+	prpl_info->send_raw(service->list->pc, str, -1);
+	g_free(str);
+	g_free(id);
+}
+
+static void
+create_dialog(PurplePluginAction *action)
+{
+	pidgin_disco_dialog_new();
+}
+
+static GList *
+actions(PurplePlugin *plugin, gpointer context)
+{
+	GList *l = NULL;
+	PurplePluginAction *action = NULL;
+
+	action = purple_plugin_action_new(_("XMPP Service Discovery"),
+	                                  create_dialog);
+	l = g_list_prepend(l, action);
+
+	return l;
+}
+
+static void
+signed_off_cb(PurpleConnection *pc, gpointer unused)
+{
+	/* Deal with any dialogs */
+	pidgin_disco_signed_off_cb(pc);
+
+	/* Remove all the IQ callbacks for this connection */
+	g_hash_table_foreach_remove(iq_callbacks, remove_iq_callbacks_by_pc, pc);
+}
+
+static gboolean
+plugin_load(PurplePlugin *plugin)
+{
+    PurplePlugin *xmpp_prpl;
+
+	my_plugin = plugin;
+
+    xmpp_prpl = purple_plugins_find_with_id(XMPP_PLUGIN_ID);
+    if (NULL == xmpp_prpl)
+        return FALSE;
+
+	purple_signal_connect(purple_connections_get_handle(), "signing-off",
+	                      plugin, PURPLE_CALLBACK(signed_off_cb), NULL);
+
+	iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
+
+	return TRUE;
+}
+
+static gboolean
+plugin_unload(PurplePlugin *plugin)
+{
+	g_hash_table_destroy(iq_callbacks);
+	iq_callbacks = NULL;
+
+	purple_signals_disconnect_by_handle(plugin);
+	pidgin_disco_dialogs_destroy_all();
+
+	return TRUE;
+}
+
+static PurplePluginInfo info =
+{
+	PURPLE_PLUGIN_MAGIC,
+	PURPLE_MAJOR_VERSION,
+	PURPLE_MINOR_VERSION,
+	PURPLE_PLUGIN_STANDARD,
+	PIDGIN_PLUGIN_TYPE,
+	0,
+	NULL,
+	PURPLE_PRIORITY_DEFAULT,
+	"gtk-xmppdisco",
+	N_("XMPP Service Discovery"),
+	DISPLAY_VERSION,
+	N_("Allows browsing and registering services."),
+	N_("This plugin is useful for registering with legacy transports or other "
+	   "XMPP services."),
+	"Paul Aurich <paul@darkrain42.org>",
+	PURPLE_WEBSITE,
+	plugin_load,
+	plugin_unload,
+	NULL,               /**< destroy    */
+	NULL,               /**< ui_info    */
+	NULL,               /**< extra_info */
+	NULL,               /**< prefs_info */
+	actions,
+
+	/* padding */
+	NULL,
+	NULL,
+	NULL,
+	NULL
+};
+
+static void
+init_plugin(PurplePlugin *plugin)
+{
+}
+
+PURPLE_INIT_PLUGIN(xmppdisco, init_plugin, info)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pidgin/plugins/disco/xmppdisco.h	Wed Jun 03 04:32:19 2009 +0000
@@ -0,0 +1,107 @@
+/* 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 PIDGIN_XMPP_DISCO_H
+#define PIDGIN_XMPP_DISCO_H
+
+typedef struct _XmppDiscoService XmppDiscoService;
+
+#include "gtkdisco.h"
+
+#define XMPP_PLUGIN_ID      "prpl-jabber"
+#define NS_DISCO_INFO       "http://jabber.org/protocol/disco#info"
+#define NS_DISCO_ITEMS      "http://jabber.org/protocol/disco#items"
+#define NS_MUC              "http://jabber.org/protocol/muc"
+#define NS_REGISTER         "jabber:iq:register"
+
+#include "plugin.h"
+extern PurplePlugin *my_plugin;
+
+/**
+ * The types of services.
+ */
+typedef enum
+{
+    XMPP_DISCO_SERVICE_TYPE_UNSET,
+    /**
+     * A registerable gateway to another protocol. An example would be
+     * XMPP legacy transports.
+     */
+    XMPP_DISCO_SERVICE_TYPE_GATEWAY,
+
+    /**
+     * A directory (e.g. allows the user to search for other users).
+     */
+    XMPP_DISCO_SERVICE_TYPE_DIRECTORY,
+
+    /**
+     * A chat (multi-user conversation).
+     */
+    XMPP_DISCO_SERVICE_TYPE_CHAT,
+
+	/**
+	 * A pubsub collection (contains nodes)
+	 */
+	XMPP_DISCO_SERVICE_TYPE_PUBSUB_COLLECTION,
+
+	/**
+	 * A pubsub leaf (contains stuff, not nodes).
+	 */
+	XMPP_DISCO_SERVICE_TYPE_PUBSUB_LEAF,
+
+	/**
+     * Something else. Do we need more categories?
+     */
+    XMPP_DISCO_SERVICE_TYPE_OTHER
+} XmppDiscoServiceType;
+
+/**
+ * The flags of services.
+ */
+typedef enum
+{
+    XMPP_DISCO_NONE          = 0x0000,
+    XMPP_DISCO_ADD           = 0x0001, /**< Supports an 'add' operation */
+    XMPP_DISCO_BROWSE        = 0x0002, /**< Supports browsing */
+    XMPP_DISCO_REGISTER      = 0x0004  /**< Supports a 'register' operation */
+} XmppDiscoServiceFlags;
+
+struct _XmppDiscoService {
+	PidginDiscoList *list;
+	gchar *name;
+	gchar *description;
+
+	gchar *gateway_type;
+	XmppDiscoServiceType type;
+	XmppDiscoServiceFlags flags;
+
+	XmppDiscoService *parent;
+	gchar *jid;
+	gchar *node;
+	gboolean expanded;
+};
+
+void xmpp_disco_start(PidginDiscoList *list);
+
+void xmpp_disco_service_expand(XmppDiscoService *service);
+void xmpp_disco_service_register(XmppDiscoService *service);
+
+#endif /* PIDGIN_XMPP_DISCO_H */