changeset 17609:f88b3a093cba

Implemented ad-hoc commands for the buddy action menu (untested), implemented the receiving end of XEP-0115: Entity Capabilities. Note that this seems not to be reliable right now, since some clients seem to have a very broken [read: completely non-functional] implementation (most notably Gajim and the py-transports).
author Andreas Monitzer <pidgin@monitzer.com>
date Sat, 23 Jun 2007 02:57:21 +0000
parents a8b1159fd95b
children 9a19c46adf66
files libpurple/protocols/jabber/adhoccommands.c libpurple/protocols/jabber/adhoccommands.h libpurple/protocols/jabber/buddy.c libpurple/protocols/jabber/buddy.h libpurple/protocols/jabber/caps.c libpurple/protocols/jabber/caps.h libpurple/protocols/jabber/disco.c libpurple/protocols/jabber/jabber.h libpurple/protocols/jabber/libxmpp.c libpurple/protocols/jabber/presence.c
diffstat 10 files changed, 748 insertions(+), 5 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/protocols/jabber/adhoccommands.c	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/adhoccommands.c	Sat Jun 23 02:57:21 2007 +0000
@@ -38,9 +38,61 @@
 	GList *actionslist;
 } JabberAdHocActionInfo;
 
-/*static void do_adhoc_parse_iq(JabberStream *js, xmlnode *packet, gpointer data) {
-	jabber_adhoc_parse(js, packet);
-}*/
+void jabber_adhoc_disco_result_cb(JabberStream *js, xmlnode *packet, gpointer data) {
+	const char *from = xmlnode_get_attrib(packet, "from");
+	const char *type = xmlnode_get_attrib(packet, "type");
+	const char *node;
+	xmlnode *query, *item;
+	JabberID *jabberid;
+	JabberBuddy *jb;
+	JabberBuddyResource *jbr = NULL;
+	
+	if(strcmp(type, "result"))
+		return;
+	
+	query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#items");
+	if(!query)
+		return;
+	node = xmlnode_get_attrib(query,"node");
+	if(!node || strcmp(node, "http://jabber.org/protocol/commands"))
+		return;
+	
+	if((jabberid = jabber_id_new(from))) {
+		if(jabberid->resource && (jb = jabber_buddy_find(js, from, TRUE)))
+			jbr = jabber_buddy_find_resource(jb, jabberid->resource);
+		jabber_id_free(jabberid);
+	}
+	
+	if(!jbr)
+		return;
+	
+	if(jbr->commands) {
+		/* since the list we just received is complete, wipe the old one */
+		while(jbr->commands) {
+			JabberAdHocCommands *cmd = jbr->commands->data;
+			g_free(cmd->jid);
+			g_free(cmd->node);
+			g_free(cmd->name);
+			g_free(cmd);
+			jbr->commands = g_list_delete_link(jbr->commands, jbr->commands);
+		}
+	}
+	
+	for(item = query->child; item; item = item->next) {
+		JabberAdHocCommands *cmd;
+		if(item->type != XMLNODE_TYPE_TAG)
+			continue;
+		if(strcmp(item->name, "item"))
+			continue;
+		cmd = g_new0(JabberAdHocCommands, 1);
+		
+		cmd->jid = g_strdup(xmlnode_get_attrib(item,"jid"));
+		cmd->node = g_strdup(xmlnode_get_attrib(item,"node"));
+		cmd->name = g_strdup(xmlnode_get_attrib(item,"name"));
+		
+		jbr->commands = g_list_append(jbr->commands,cmd);
+	}
+}
 
 static void do_adhoc_action_cb(JabberStream *js, xmlnode *result, const char *actionhandle, gpointer user_data) {
 	xmlnode *command;
@@ -128,3 +180,22 @@
 		jabber_x_data_request_with_actions(js,xdata,actionslist,actionindex,do_adhoc_action_cb,actionInfo);
 	}
 }
+
+void jabber_adhoc_execute(PurpleBlistNode *node, gpointer data) {
+	if (PURPLE_BLIST_NODE_IS_BUDDY(node)) {
+		JabberAdHocCommands *cmd = data;
+		PurpleBuddy *buddy = (PurpleBuddy *) node;
+		JabberStream *js = purple_account_get_connection(buddy->account)->proto_data;
+		JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET);
+		xmlnode *command = xmlnode_new_child(iq->node,"command");
+		xmlnode_set_attrib(iq->node,"to",cmd->jid);
+		xmlnode_set_namespace(command,"http://jabber.org/protocol/commands");
+		xmlnode_set_attrib(command,"node",cmd->node);
+		xmlnode_set_attrib(command,"action","execute");
+		
+		/* we don't need to set a callback, since jabber_adhoc_parse is run for all replies */
+		
+		jabber_iq_send(iq);
+	}
+}
+
--- a/libpurple/protocols/jabber/adhoccommands.h	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/adhoccommands.h	Sat Jun 23 02:57:21 2007 +0000
@@ -28,4 +28,8 @@
 
 void jabber_adhoc_parse(JabberStream *js, xmlnode *packet);
 
+void jabber_adhoc_disco_result_cb(JabberStream *js, xmlnode *packet, gpointer data);
+
+void jabber_adhoc_execute(PurpleBlistNode *node, gpointer data);
+
 #endif /* _PURPLE_JABBER_ADHOCCOMMANDS_H_ */
--- a/libpurple/protocols/jabber/buddy.c	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/buddy.c	Sat Jun 23 02:57:21 2007 +0000
@@ -35,6 +35,7 @@
 #include "presence.h"
 #include "xdata.h"
 #include "pep.h"
+#include "adhoccommands.h"
 
 typedef struct {
 	long idle_seconds;
@@ -117,7 +118,6 @@
 		int priority, JabberBuddyState state, const char *status)
 {
 	JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, resource);
-
 	if(!jbr) {
 		jbr = g_new0(JabberBuddyResource, 1);
 		jbr->jb = jb;
@@ -142,6 +142,17 @@
 	g_return_if_fail(jbr != NULL);
 
 	jbr->jb->resources = g_list_remove(jbr->jb->resources, jbr);
+	
+	while(jbr->commands) {
+		JabberAdHocCommands *cmd = jbr->commands->data;
+		g_free(cmd->jid);
+		g_free(cmd->node);
+		g_free(cmd->name);
+		g_free(cmd);
+		jbr->commands = g_list_delete_link(jbr->commands, jbr->commands);
+	}
+	
+	jabber_caps_free_clientinfo(jbr->caps);
 
 	g_free(jbr->name);
 	g_free(jbr->status);
@@ -1625,6 +1636,7 @@
 	PurpleConnection *gc = purple_account_get_connection(buddy->account);
 	JabberStream *js = gc->proto_data;
 	JabberBuddy *jb = jabber_buddy_find(js, buddy->name, TRUE);
+	GList *jbrs;
 
 	GList *m = NULL;
 	PurpleMenuAction *act;
@@ -1688,6 +1700,19 @@
 									 NULL, NULL);
 		m = g_list_append(m, act);
 	}
+	
+	/* add all ad hoc commands to the action menu */
+	for(jbrs = jb->resources; jbrs; jbrs = g_list_next(jbrs)) {
+		JabberBuddyResource *jbr = jbrs->data;
+		GList *commands;
+		if (!jbr->commands)
+			continue;
+		for(commands = jbr->commands; commands; commands = g_list_next(commands)) {
+			JabberAdHocCommands *cmd = commands->data;
+			act = purple_menu_action_new(cmd->name, PURPLE_CALLBACK(jabber_adhoc_execute), cmd, NULL);
+			m = g_list_append(m, act);
+		}
+	}
 
 	return m;
 }
--- a/libpurple/protocols/jabber/buddy.h	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/buddy.h	Sat Jun 23 02:57:21 2007 +0000
@@ -34,6 +34,7 @@
 } JabberBuddyState;
 
 #include "jabber.h"
+#include "caps.h"
 
 #define AVATARNAMESPACEDATA "http://www.xmpp.org/extensions/xep-0084.html#ns-data"
 #define AVATARNAMESPACEMETA "http://www.xmpp.org/extensions/xep-0084.html#ns-metadata"
@@ -56,6 +57,12 @@
 	} subscription;
 } JabberBuddy;
 
+typedef struct _JabberAdHocCommands {
+	char *jid;
+	char *node;
+	char *name;
+} JabberAdHocCommands;
+
 typedef struct _JabberBuddyResource {
 	JabberBuddy *jb;
 	char *name;
@@ -74,6 +81,8 @@
 		char *name;
 		char *os;
 	} client;
+	JabberCapsClientInfo *caps;
+	GList *commands;
 } JabberBuddyResource;
 
 void jabber_buddy_free(JabberBuddy *jb);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/jabber/caps.c	Sat Jun 23 02:57:21 2007 +0000
@@ -0,0 +1,529 @@
+/*
+ * purple - Jabber Protocol Plugin
+ *
+ * Copyright (C) 2007, Andreas Monitzer <andy@monitzer.com>
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA	 02111-1307	 USA
+ *
+ */
+
+#include "caps.h"
+#include <string.h>
+#include "internal.h"
+#include "util.h"
+#include "iq.h"
+
+#define JABBER_CAPS_FILENAME "xmpp-caps.xml"
+
+static GHashTable *capstable = NULL; /* JabberCapsKey -> JabberCapsValue */
+
+typedef struct _JabberCapsKey {
+	char *node;
+	char *ver;
+} JabberCapsKey;
+
+typedef struct _JabberCapsValueExt {
+	GList *identities; /* JabberCapsIdentity */
+	GList *features; /* char * */
+} JabberCapsValueExt;
+
+typedef struct _JabberCapsValue {
+	GList *identities; /* JabberCapsIdentity */
+	GList *features; /* char * */
+	GHashTable *ext; /* char * -> JabberCapsValueExt */
+} JabberCapsValue;
+
+static guint jabber_caps_hash(gconstpointer key) {
+	const JabberCapsKey *name = key;
+	guint nodehash = g_str_hash(name->node);
+	guint verhash = g_str_hash(name->ver);
+	
+	return nodehash ^ verhash;
+}
+
+static gboolean jabber_caps_compare(gconstpointer v1, gconstpointer v2) {
+	const JabberCapsKey *name1 = v1;
+	const JabberCapsKey *name2 = v2;
+
+	return strcmp(name1->node,name2->node) == 0 && strcmp(name1->ver,name2->ver) == 0;
+}
+
+static void jabber_caps_destroy_key(gpointer key) {
+	JabberCapsKey *keystruct = key;
+	g_free(keystruct->node);
+	g_free(keystruct->ver);
+	g_free(keystruct);
+}
+
+static void jabber_caps_destroy_value(gpointer value) {
+	JabberCapsValue *valuestruct = value;
+	while(valuestruct->identities) {
+		JabberCapsIdentity *id = valuestruct->identities->data;
+		g_free(id->category);
+		g_free(id->type);
+		g_free(id->name);
+		g_free(id);
+		
+		valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities);
+	}
+	while(valuestruct->features) {
+		g_free(valuestruct->features->data);
+		valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features);
+	}
+	g_hash_table_destroy(valuestruct->ext);
+	g_free(valuestruct);
+}
+
+static void jabber_caps_ext_destroy_value(gpointer value) {
+	JabberCapsValueExt *valuestruct = value;
+	while(valuestruct->identities) {
+		JabberCapsIdentity *id = valuestruct->identities->data;
+		g_free(id->category);
+		g_free(id->type);
+		g_free(id->name);
+		g_free(id);
+		
+		valuestruct->identities = g_list_delete_link(valuestruct->identities,valuestruct->identities);
+	}
+	while(valuestruct->features) {
+		g_free(valuestruct->features->data);
+		valuestruct->features = g_list_delete_link(valuestruct->features,valuestruct->features);
+	}
+	g_free(valuestruct);
+}
+
+static void jabber_caps_load(void);
+
+void jabber_caps_init(void) {
+	capstable = g_hash_table_new_full(jabber_caps_hash, jabber_caps_compare, jabber_caps_destroy_key, jabber_caps_destroy_value);
+	jabber_caps_load();
+}
+
+static void jabber_caps_load(void) {
+	xmlnode *capsdata = purple_util_read_xml_from_file(JABBER_CAPS_FILENAME, "XMPP capabilities cache");
+	xmlnode *client;
+	if(!capsdata || strcmp(capsdata->name, "capabilities"))
+		return;
+	
+	for(client = capsdata->child; client; client = client->next) {
+		if(client->type != XMLNODE_TYPE_TAG)
+			continue;
+		if(!strcmp(client->name, "client")) {
+			JabberCapsKey *key = g_new0(JabberCapsKey, 1);
+			JabberCapsValue *value = g_new0(JabberCapsValue, 1);
+			xmlnode *child;
+			key->node = g_strdup(xmlnode_get_attrib(client,"node"));
+			key->ver  = g_strdup(xmlnode_get_attrib(client,"ver"));
+			value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value);
+			for(child = client->child; child; child = child->next) {
+				if(child->type != XMLNODE_TYPE_TAG)
+					continue;
+				if(!strcmp(child->name,"feature")) {
+					const char *var = xmlnode_get_attrib(child, "var");
+					if(!var)
+						continue;
+					value->features = g_list_append(value->features,g_strdup(var));
+				} else if(!strcmp(child->name,"identity")) {
+					const char *category = xmlnode_get_attrib(child, "category");
+					const char *type = xmlnode_get_attrib(child, "type");
+					const char *name = xmlnode_get_attrib(child, "name");
+					
+					JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1);
+					id->category = g_strdup(category);
+					id->type = g_strdup(type);
+					id->name = g_strdup(name);
+					
+					value->identities = g_list_append(value->identities,id);
+				} else if(!strcmp(child->name,"ext")) {
+					const char *identifier = xmlnode_get_attrib(child, "identifier");
+					if(identifier) {
+						xmlnode *extchild;
+						
+						JabberCapsValueExt *extvalue = g_new0(JabberCapsValueExt, 1);
+						
+						for(extchild = child->child; extchild; extchild = extchild->next) {
+							if(extchild->type != XMLNODE_TYPE_TAG)
+								continue;
+							if(!strcmp(extchild->name,"feature")) {
+								const char *var = xmlnode_get_attrib(extchild, "var");
+								if(!var)
+									continue;
+								extvalue->features = g_list_append(extvalue->features,g_strdup(var));
+							} else if(!strcmp(extchild->name,"identity")) {
+								const char *category = xmlnode_get_attrib(extchild, "category");
+								const char *type = xmlnode_get_attrib(extchild, "type");
+								const char *name = xmlnode_get_attrib(extchild, "name");
+								
+								JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1);
+								id->category = g_strdup(category);
+								id->type = g_strdup(type);
+								id->name = g_strdup(name);
+								
+								extvalue->identities = g_list_append(extvalue->identities,id);
+							}
+						}
+						g_hash_table_replace(value->ext, g_strdup(identifier), extvalue);
+					}
+				}
+			}
+			g_hash_table_replace(capstable, key, value);
+		}
+	}
+}
+
+static void jabber_caps_store_ext(gpointer key, gpointer value, gpointer user_data) {
+	const char *extname = key;
+	JabberCapsValueExt *props = value;
+	xmlnode *root = user_data;
+	xmlnode *ext = xmlnode_new_child(root,"ext");
+	GList *iter;
+	
+	xmlnode_set_attrib(ext,"identifier",extname);
+	
+	for(iter = props->identities; iter; iter = g_list_next(iter)) {
+		JabberCapsIdentity *id = iter->data;
+		xmlnode *identity = xmlnode_new_child(ext, "identity");
+		xmlnode_set_attrib(identity, "category", id->category);
+		xmlnode_set_attrib(identity, "type", id->type);
+		xmlnode_set_attrib(identity, "name", id->name);
+	}
+	
+	for(iter = props->features; iter; iter = g_list_next(iter)) {
+		const char *feat = iter->data;
+		xmlnode *feature = xmlnode_new_child(ext, "feature");
+		xmlnode_set_attrib(feature, "var", feat);
+	}
+}
+
+static void jabber_caps_store_client(gpointer key, gpointer value, gpointer user_data) {
+	JabberCapsKey *clientinfo = key;
+	JabberCapsValue *props = value;
+	xmlnode *root = user_data;
+	xmlnode *client = xmlnode_new_child(root,"client");
+	GList *iter;
+
+	xmlnode_set_attrib(client,"node",clientinfo->node);
+	xmlnode_set_attrib(client,"ver",clientinfo->ver);
+	
+	for(iter = props->identities; iter; iter = g_list_next(iter)) {
+		JabberCapsIdentity *id = iter->data;
+		xmlnode *identity = xmlnode_new_child(client, "identity");
+		xmlnode_set_attrib(identity, "category", id->category);
+		xmlnode_set_attrib(identity, "type", id->type);
+		xmlnode_set_attrib(identity, "name", id->name);
+	}
+
+	for(iter = props->features; iter; iter = g_list_next(iter)) {
+		const char *feat = iter->data;
+		xmlnode *feature = xmlnode_new_child(client, "feature");
+		xmlnode_set_attrib(feature, "var", feat);
+	}
+	
+	g_hash_table_foreach(props->ext,jabber_caps_store_ext,client);
+}
+
+static void jabber_caps_store(void) {
+	xmlnode *root = xmlnode_new("capabilities");
+	g_hash_table_foreach(capstable, jabber_caps_store_client, root);
+	purple_util_write_data_to_file(JABBER_CAPS_FILENAME, xmlnode_to_formatted_str(root, NULL), -1);
+}
+
+/* this function assumes that all information is available locally */
+static JabberCapsClientInfo *jabber_caps_collect_info(const char *node, const char *ver, GList *ext) {
+	JabberCapsClientInfo *result = g_new0(JabberCapsClientInfo, 1);
+	JabberCapsKey *key = g_new0(JabberCapsKey, 1);
+	JabberCapsValue *caps;
+	GList *iter;
+	
+	key->node = g_strdup(node);
+	key->ver = g_strdup(ver);
+	
+	caps = g_hash_table_lookup(capstable,key);
+	
+	g_free(key->node);
+	g_free(key->ver);
+	g_free(key);
+	
+	/* join all information */
+	for(iter = caps->identities; iter; iter = g_list_next(iter)) {
+		JabberCapsIdentity *id = iter->data;
+		JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1);
+		newid->category = g_strdup(id->category);
+		newid->type = g_strdup(id->type);
+		newid->name = g_strdup(id->name);
+		
+		result->identities = g_list_append(result->identities,newid);
+	}
+	for(iter = caps->features; iter; iter = g_list_next(iter)) {
+		const char *feat = iter->data;
+		char *newfeat = g_strdup(feat);
+		
+		result->features = g_list_append(result->features,newfeat);
+	}
+	
+	for(iter = ext; iter; iter = g_list_next(iter)) {
+		const char *ext = iter->data;
+		JabberCapsValueExt *extinfo = g_hash_table_lookup(caps->ext,ext);
+		
+		if(extinfo) {
+			for(iter = extinfo->identities; iter; iter = g_list_next(iter)) {
+				JabberCapsIdentity *id = iter->data;
+				JabberCapsIdentity *newid = g_new0(JabberCapsIdentity, 1);
+				newid->category = g_strdup(id->category);
+				newid->type = g_strdup(id->type);
+				newid->name = g_strdup(id->name);
+				
+				result->identities = g_list_append(result->identities,newid);
+			}
+			for(iter = extinfo->features; iter; iter = g_list_next(iter)) {
+				const char *feat = iter->data;
+				char *newfeat = g_strdup(feat);
+				
+				result->features = g_list_append(result->features,newfeat);
+			}
+		}
+	}
+	return result;
+}
+
+void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo) {
+	if(!clientinfo)
+		return;
+	while(clientinfo->identities) {
+		JabberCapsIdentity *id = clientinfo->identities->data;
+		g_free(id->category);
+		g_free(id->type);
+		g_free(id->name);
+		g_free(id);
+		
+		clientinfo->identities = g_list_remove_link(clientinfo->identities,clientinfo->identities);
+	}
+	while(clientinfo->features) {
+		char *feat = clientinfo->features->data;
+		g_free(feat);
+		
+		clientinfo->features = g_list_remove_link(clientinfo->features,clientinfo->features);
+	}
+	
+	g_free(clientinfo);
+}
+
+typedef struct _jabber_caps_cbplususerdata {
+	jabber_caps_get_info_cb cb;
+	gpointer user_data;
+	
+	char *who;
+	char *node;
+	char *ver;
+	GList *ext;
+	unsigned extOutstanding;
+} jabber_caps_cbplususerdata;
+
+static void jabber_caps_get_info_check_completion(jabber_caps_cbplususerdata *userdata) {
+	if(userdata->extOutstanding == 0) {
+		userdata->cb(jabber_caps_collect_info(userdata->node, userdata->ver, userdata->ext), userdata->user_data);
+		g_free(userdata->who);
+		g_free(userdata->node);
+		g_free(userdata->ver);
+		while(userdata->ext) {
+			g_free(userdata->ext->data);
+			userdata->ext = g_list_remove_link(userdata->ext,userdata->ext);
+		}
+		g_free(userdata);
+	}
+}
+
+static void jabber_caps_ext_iqcb(JabberStream *js, xmlnode *packet, gpointer data) {
+	/* collect data and fetch all exts */
+	xmlnode *query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#info");
+	xmlnode *child;
+	jabber_caps_cbplususerdata *userdata = data;
+	JabberCapsKey *clientkey = g_new0(JabberCapsKey, 1);
+	JabberCapsValue *client;
+	JabberCapsValueExt *value = g_new0(JabberCapsValueExt, 1);
+	const char *node = xmlnode_get_attrib(query, "node");
+	const char *key;
+	
+	--userdata->extOutstanding;
+	
+	if(node) {
+		clientkey->node = g_strdup(userdata->node);
+		clientkey->ver = g_strdup(userdata->ver);
+		
+		client = g_hash_table_lookup(capstable,clientkey);
+		
+		g_free(clientkey->node);
+		g_free(clientkey->ver);
+		g_free(clientkey);
+		
+		/* split node by #, key either points to \0 or the correct ext afterwards */
+		for(key = node; key[0] != '\0'; ++key) {
+			if(key[0] == '#') {
+				++key;
+				break;
+			}
+		}
+
+		for(child = query->child; child; child = child->next) {
+			if(child->type != XMLNODE_TYPE_TAG)
+				continue;
+			if(!strcmp(child->name,"feature")) {
+				const char *var = xmlnode_get_attrib(child, "var");
+				if(!var)
+					continue;
+				value->features = g_list_append(value->features,g_strdup(var));
+			} else if(!strcmp(child->name,"identity")) {
+				const char *category = xmlnode_get_attrib(child, "category");
+				const char *type = xmlnode_get_attrib(child, "type");
+				const char *name = xmlnode_get_attrib(child, "name");
+				
+				JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1);
+				id->category = g_strdup(category);
+				id->type = g_strdup(type);
+				id->name = g_strdup(name);
+				
+				value->identities = g_list_append(value->identities,id);
+			}
+		}
+		g_hash_table_replace(client->ext, g_strdup(key), value);
+		
+		jabber_caps_store();
+	}
+	
+	jabber_caps_get_info_check_completion(userdata);
+}
+
+static void jabber_caps_client_iqcb(JabberStream *js, xmlnode *packet, gpointer data) {
+	/* collect data and fetch all exts */
+	xmlnode *query = xmlnode_get_child_with_namespace(packet,"query","http://jabber.org/protocol/disco#info");
+	xmlnode *child;
+	GList *iter;
+	jabber_caps_cbplususerdata *userdata = data;
+	JabberCapsKey *key = g_new0(JabberCapsKey, 1);
+	JabberCapsValue *value = g_new0(JabberCapsValue, 1);
+	key->node = g_strdup(userdata->node);
+	key->ver = g_strdup(userdata->ver);
+	
+	value->ext = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, jabber_caps_ext_destroy_value);
+
+	for(child = query->child; child; child = child->next) {
+		if(child->type != XMLNODE_TYPE_TAG)
+			continue;
+		if(!strcmp(child->name,"feature")) {
+			const char *var = xmlnode_get_attrib(child, "var");
+			if(!var)
+				continue;
+			value->features = g_list_append(value->features,g_strdup(var));
+		} else if(!strcmp(child->name,"identity")) {
+			const char *category = xmlnode_get_attrib(child, "category");
+			const char *type = xmlnode_get_attrib(child, "type");
+			const char *name = xmlnode_get_attrib(child, "name");
+			
+			JabberCapsIdentity *id = g_new0(JabberCapsIdentity, 1);
+			id->category = g_strdup(category);
+			id->type = g_strdup(type);
+			id->name = g_strdup(name);
+			
+			value->identities = g_list_append(value->identities,id);
+		}
+	}
+	g_hash_table_replace(capstable, key, value);
+				
+	/* fetch all exts */
+	for(iter = userdata->ext; iter; iter = g_list_next(iter)) {
+		JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info");
+		xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info");
+		char *node = g_strdup_printf("%s#%s", node, (const char*)iter->data);
+		xmlnode_set_attrib(query, "node", node);
+		g_free(node);
+		xmlnode_set_attrib(iq->node, "to", userdata->who);
+
+		jabber_iq_set_callback(iq,jabber_caps_ext_iqcb,userdata);
+		jabber_iq_send(iq);
+	}
+	
+	jabber_caps_store();
+	
+	jabber_caps_get_info_check_completion(userdata);
+}
+
+void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data) {
+	JabberCapsValue *client;
+	JabberCapsKey *key = g_new0(JabberCapsKey, 1);
+	char *originalext = g_strdup(ext);
+	char *oneext, *ctx;
+	jabber_caps_cbplususerdata *userdata = g_new0(jabber_caps_cbplususerdata, 1);
+	userdata->cb = cb;
+	userdata->user_data = user_data;
+	userdata->who = g_strdup(who);
+	userdata->node = g_strdup(node);
+	userdata->ver = g_strdup(ver);
+	
+	if(originalext)
+		for(oneext = strtok_r(originalext, " ", &ctx); oneext; oneext = strtok_r(NULL, " ", &ctx)) {
+			userdata->ext = g_list_append(userdata->ext,g_strdup(oneext));
+			++userdata->extOutstanding;
+		}
+	g_free(originalext);
+	
+	key->node = g_strdup(node);
+	key->ver = g_strdup(ver);
+	
+	client = g_hash_table_lookup(capstable, key);
+	
+	g_free(key->node);
+	g_free(key->ver);
+	g_free(key);
+	
+	if(!client) {
+		JabberIq *iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info");
+		xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info");
+		char *nodever = g_strdup_printf("%s#%s", node, ver);
+		xmlnode_set_attrib(query, "node", nodever);
+		g_free(nodever);
+		xmlnode_set_attrib(iq->node, "to", who);
+		
+		jabber_iq_set_callback(iq,jabber_caps_client_iqcb,userdata);
+		jabber_iq_send(iq);
+	} else {
+		GList *iter; 
+		/* fetch unknown exts only */
+		for(iter = userdata->ext; iter; iter = g_list_next(iter)) {
+			JabberCapsValueExt *extvalue = g_hash_table_lookup(client->ext, (const char*)iter->data);
+			JabberIq *iq;
+			xmlnode *query;
+			char *nodever;
+			
+			if(extvalue) {
+				/* we already have this ext, don't bother with it */
+				--userdata->extOutstanding;
+				continue;
+			}
+			
+			iq = jabber_iq_new_query(js,JABBER_IQ_GET,"http://jabber.org/protocol/disco#info");
+			query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#info");
+			nodever = g_strdup_printf("%s#%s", node, (const char*)iter->data);
+			xmlnode_set_attrib(query, "node", nodever);
+			g_free(nodever);
+			xmlnode_set_attrib(iq->node, "to", who);
+			
+			jabber_iq_set_callback(iq,jabber_caps_ext_iqcb,userdata);
+			jabber_iq_send(iq);
+		}
+		/* maybe we have all data available anyways? This is the ideal case where no network traffic is necessary */
+		jabber_caps_get_info_check_completion(userdata);
+	}
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/jabber/caps.h	Sat Jun 23 02:57:21 2007 +0000
@@ -0,0 +1,49 @@
+/*
+ * purple - Jabber Protocol Plugin
+ *
+ * Copyright (C) 2007, Andreas Monitzer <andy@monitzer.com>
+ *
+ * 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., 59 Temple Place, Suite 330, Boston, MA	 02111-1307	 USA
+ *
+ */
+
+#ifndef _PURPLE_JABBER_CAPS_H_
+#define _PURPLE_JABBER_CAPS_H_
+
+typedef struct _JabberCapsClientInfo JabberCapsClientInfo;
+
+#include "jabber.h"
+
+/* Implementation of XEP-0115 */
+
+typedef struct _JabberCapsIdentity {
+	char *category;
+	char *type;
+	char *name;
+} JabberCapsIdentity;
+
+struct _JabberCapsClientInfo {
+	GList *identities; /* JabberCapsIdentity */
+	GList *features; /* char * */
+};
+
+typedef void (*jabber_caps_get_info_cb)(JabberCapsClientInfo *info, gpointer user_data);
+
+void jabber_caps_init(void);
+
+void jabber_caps_get_info(JabberStream *js, const char *who, const char *node, const char *ver, const char *ext, jabber_caps_get_info_cb cb, gpointer user_data);
+void jabber_caps_free_clientinfo(JabberCapsClientInfo *clientinfo);
+
+#endif /* _PURPLE_JABBER_CAPS_H_ */
--- a/libpurple/protocols/jabber/disco.c	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/disco.c	Sat Jun 23 02:57:21 2007 +0000
@@ -31,6 +31,8 @@
 #include "presence.h"
 #include "roster.h"
 #include "pep.h"
+#include "adhoccommands.h"
+
 
 struct _jabber_disco_info_cb_data {
 	gpointer data;
@@ -210,6 +212,9 @@
 					capabilities |= JABBER_CAP_IQ_REGISTER;
 				else if(!strcmp(var, "http://www.xmpp.org/extensions/xep-0199.html#ns"))
 					capabilities |= JABBER_CAP_PING;
+				else if(!strcmp(var, "http://jabber.org/protocol/commands")) {
+					capabilities |= JABBER_CAP_ADHOC;
+				}
 			}
 		}
 
--- a/libpurple/protocols/jabber/jabber.h	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Sat Jun 23 02:57:21 2007 +0000
@@ -41,6 +41,7 @@
 	JABBER_CAP_GOOGLE_ROSTER  = 1 << 10,
 	
 	JABBER_CAP_PING			  = 1 << 11,
+	JABBER_CAP_ADHOC		  = 1 << 12,
 	
 	JABBER_CAP_RETRIEVED	  = 1 << 31
 } JabberCapabilities;
--- a/libpurple/protocols/jabber/libxmpp.c	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/libxmpp.c	Sat Jun 23 02:57:21 2007 +0000
@@ -40,6 +40,7 @@
 #include "google.h"
 #include "pep.h"
 #include "usertune.h"
+#include "caps.h"
 
 static PurplePluginProtocolInfo prpl_info =
 {
@@ -234,6 +235,7 @@
         jabber_pep_init();
 		
 		jabber_tune_init();
+		jabber_caps_init();
 
 		jabber_add_feature("avatarmeta", AVATARNAMESPACEMETA, jabber_pep_namespace_only_when_pep_enabled_cb);
 		jabber_add_feature("avatardata", AVATARNAMESPACEDATA, jabber_pep_namespace_only_when_pep_enabled_cb);
--- a/libpurple/protocols/jabber/presence.c	Fri Jun 22 11:52:50 2007 +0000
+++ b/libpurple/protocols/jabber/presence.c	Sat Jun 23 02:57:21 2007 +0000
@@ -36,6 +36,7 @@
 #include "presence.h"
 #include "iq.h"
 #include "jutil.h"
+#include "adhoccommands.h"
 
 #include "usertune.h"
 
@@ -337,6 +338,35 @@
 	}
 }
 
+typedef struct _JabberPresenceCapabilities {
+	JabberStream *js;
+	JabberBuddyResource *jbr;
+	char *from;
+} JabberPresenceCapabilities;
+
+static void jabber_presence_set_capabilities(JabberCapsClientInfo *info, gpointer user_data) {
+	JabberPresenceCapabilities *userdata = user_data;
+	GList *iter;
+	
+	if(userdata->jbr->caps)
+		jabber_caps_free_clientinfo(userdata->jbr->caps);
+	userdata->jbr->caps = info;
+	
+	for(iter = info->features; iter; iter = g_list_next(iter)) {
+		if(!strcmp((const char*)iter->data, "http://jabber.org/protocol/commands")) {
+			JabberIq *iq = jabber_iq_new_query(userdata->js, JABBER_IQ_GET, "http://jabber.org/protocol/disco#items");
+			xmlnode *query = xmlnode_get_child_with_namespace(iq->node,"query","http://jabber.org/protocol/disco#items");
+			xmlnode_set_attrib(iq->node, "to", userdata->from);
+			xmlnode_set_attrib(query, "node", "http://jabber.org/protocol/commands");
+			
+			jabber_iq_set_callback(iq, jabber_adhoc_disco_result_cb, NULL);
+			jabber_iq_send(iq);
+			break;
+		}
+	}
+	g_free(user_data);
+}
+
 void jabber_presence_parse(JabberStream *js, xmlnode *packet)
 {
 	const char *from = xmlnode_get_attrib(packet, "from");
@@ -358,6 +388,7 @@
 	xmlnode *y;
 	gboolean muc = FALSE;
 	char *avatar_hash = NULL;
+	xmlnode *caps = NULL;
 
 	if(!(jb = jabber_buddy_find(js, from, TRUE)))
 		return;
@@ -420,8 +451,10 @@
 
 
 	for(y = packet->child; y; y = y->next) {
+		const char *xmlns;
 		if(y->type != XMLNODE_TYPE_TAG)
 			continue;
+		xmlns = xmlnode_get_namespace(y);
 
 		if(!strcmp(y->name, "status")) {
 			g_free(status);
@@ -432,9 +465,11 @@
 				priority = atoi(p);
 				g_free(p);
 			}
-		} else if(!strcmp(y->name, "delay")) {
+		} else if(!strcmp(y->name, "delay") && !strcmp(xmlns, "urn:xmpp:delay")) {
 			/* XXX: compare the time.  jabber:x:delay can happen on presence packets that aren't really and truly delayed */
 			delayed = TRUE;
+		} else if(!strcmp(y->name, "c") && !strcmp(xmlns, "http://jabber.org/protocol/caps")) {
+			caps = y; /* store for later, when creating buddy resource */
 		} else if(!strcmp(y->name, "x")) {
 			const char *xmlns = xmlnode_get_namespace(y);
 			if(xmlns && !strcmp(xmlns, "jabber:x:delay")) {
@@ -656,6 +691,19 @@
 		} else {
 			jbr = jabber_buddy_track_resource(jb, jid->resource, priority,
 					state, status);
+			if(caps) {
+				const char *node = xmlnode_get_attrib(caps,"node");
+				const char *ver = xmlnode_get_attrib(caps,"ver");
+				const char *ext = xmlnode_get_attrib(caps,"ext");
+				
+				if(node && ver) {
+					JabberPresenceCapabilities *userdata = g_new0(JabberPresenceCapabilities, 1);
+					userdata->js = js;
+					userdata->jbr = jbr;
+					userdata->from = g_strdup(from);
+					jabber_caps_get_info(js, from, node, ver, ext, jabber_presence_set_capabilities, userdata);
+				}
+			}
 		}
 
 		if((found_jbr = jabber_buddy_find_resource(jb, NULL))) {