diff libpurple/protocols/jabber/caps.c @ 17882: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
children 7c79957207c3
line wrap: on
line diff
--- /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);
+	}
+}
+