changeset 23449:c45d05bd58ed

propagate from branch 'im.pidgin.pidgin' (head 7be65dacd56b6536cf745747e39a29f4f7f2644b) to branch 'im.pidgin.xmpp.custom_smiley' (head a21aa16c365d48c3255358530932190b7263cc9d)
author Richard Laager <rlaager@wiktel.com>
date Wed, 11 Jun 2008 15:20:16 +0000
parents 9cb1e75854f1 (current diff) 9d83ae2c1a4f (diff)
children 1de1494a13e5
files libpurple/protocols/jabber/buddy.c libpurple/protocols/jabber/jabber.c
diffstat 12 files changed, 927 insertions(+), 8 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/protocols/jabber/Makefile.am	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/Makefile.am	Wed Jun 11 15:20:16 2008 +0000
@@ -11,6 +11,8 @@
 			  buddy.h \
 			  chat.c \
 			  chat.h \
+			  data.c \
+			  data.h \
 			  disco.c \
 			  disco.h \
 			  google.c \
--- a/libpurple/protocols/jabber/Makefile.mingw	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/Makefile.mingw	Wed Jun 11 15:20:16 2008 +0000
@@ -48,6 +48,7 @@
 			buddy.c \
 			caps.c \
 			chat.c \
+			data.c \
 			disco.c \
 			google.c \
 			iq.c \
--- a/libpurple/protocols/jabber/buddy.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/buddy.c	Wed Jun 11 15:20:16 2008 +0000
@@ -2501,5 +2501,38 @@
 			js);
 }
 
+gboolean
+jabber_resource_has_capability(const JabberBuddyResource *jbr, const gchar *cap)
+{
+	const GList *iter = NULL;
 
+	if (!jbr->caps) {
+		purple_debug_error("jabber",
+			"Unable to find caps: nothing known about buddy\n");
+		return FALSE;
+	}
 
+	for (iter = jbr->caps->features ; iter ; iter = g_list_next(iter)) {
+		purple_debug_info("jabber", "Found cap: %s\n", (char *)iter->data);
+		if (strcmp(iter->data, cap) == 0) {
+			return TRUE;
+		}
+	}
+
+	return FALSE;
+}
+
+gboolean
+jabber_buddy_has_capability(const JabberBuddy *jb, const gchar *cap)
+{
+	JabberBuddyResource *jbr = jabber_buddy_find_resource((JabberBuddy*)jb, NULL);
+
+	if (!jbr) {
+		purple_debug_error("jabber",
+			"Unable to find caps: buddy might be offline\n");
+		return FALSE;
+	}
+
+	return jabber_resource_has_capability(jbr, cap);
+}
+
--- a/libpurple/protocols/jabber/buddy.h	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/buddy.h	Wed Jun 11 15:20:16 2008 +0000
@@ -119,4 +119,8 @@
 
 void jabber_vcard_fetch_mine(JabberStream *js);
 
+gboolean jabber_resource_has_capability(const JabberBuddyResource *jbr,
+										const gchar *cap);
+gboolean jabber_buddy_has_capability(const JabberBuddy *jb, const gchar *cap);
+
 #endif /* _PURPLE_JABBER_BUDDY_H_ */
--- a/libpurple/protocols/jabber/chat.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/chat.c	Wed Jun 11 15:20:16 2008 +0000
@@ -31,6 +31,7 @@
 #include "message.h"
 #include "presence.h"
 #include "xdata.h"
+#include "data.h"
 
 GList *jabber_chat_info(PurpleConnection *gc)
 {
@@ -678,6 +679,9 @@
 		xmlnode_insert_data(status, msg, -1);
 	}
 	jabber_send(chat->js, presence);
+
+	jabber_data_delete_associated_with_conv(chat->conv);
+
 	xmlnode_free(presence);
 	g_free(room_jid);
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/jabber/data.c	Wed Jun 11 15:20:16 2008 +0000
@@ -0,0 +1,320 @@
+/*
+ * 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 Library 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 02110-1301,  USA
+ */
+ 
+#include <stdlib.h>
+#include <glib.h>
+#include <string.h>
+
+#include "data.h"
+#include "debug.h"
+#include "xmlnode.h"
+#include "util.h"
+#include "conversation.h"
+#include "iq.h"
+
+/* hash table to store locally supplied data objects, by conversation and
+ 	alt text (smiley shortcut) */
+static GHashTable *local_datas_by_alt = NULL;
+
+/* hash table to store locally supplied data objects, by content id */
+static GHashTable *local_datas_by_cid = NULL;
+
+/* remote supplied data objects by content id */
+static GHashTable *remote_datas_by_cid = NULL;
+
+
+void
+jabber_data_init(void)
+{
+	/* setup hash tables for storing data instances here */
+	purple_debug_info("jabber", "Setting up data handling\n");
+
+	local_datas_by_alt = g_hash_table_new(NULL, NULL);
+	local_datas_by_cid = g_hash_table_new(NULL, NULL);
+	remote_datas_by_cid = g_hash_table_new(NULL, NULL);
+}
+
+JabberData *
+jabber_data_create_from_data(gconstpointer rawdata, gsize size, const char *type,
+							 const char *alt)
+{
+	JabberData *data = g_new0(JabberData, 1);
+	gchar *checksum = purple_util_get_image_checksum(rawdata, size);
+	gchar cid[256];
+
+	/* is there some better way to generate a content ID? */
+	g_snprintf(cid, sizeof(cid), "%s@%s", checksum, g_get_host_name());
+	g_free(checksum);
+
+	data->cid = g_strdup(cid);
+	data->type = g_strdup(type);
+	data->alt = g_strdup(alt);
+	data->size = size;
+
+	data->data = g_memdup(rawdata, size);
+
+	return data;
+}
+
+JabberData *
+jabber_data_create_from_xml(xmlnode *tag)
+{
+	JabberData *data = g_new0(JabberData, 1);
+	gsize size;
+	gpointer raw_data = NULL;
+
+	if (data == NULL) {
+		purple_debug_error("jabber", "Could not allocate data object\n");
+		g_free(data);
+		return NULL;
+	}
+
+	/* check if this is a "data" tag */
+	if (strcmp(tag->name, "data") != 0) {
+		purple_debug_error("jabber", "Invalid data element");
+		g_free(data);
+		return NULL;
+	}
+
+	data->cid = g_strdup(xmlnode_get_attrib(tag, "cid"));
+	data->type = g_strdup(xmlnode_get_attrib(tag, "type"));
+	data->alt = g_strdup(xmlnode_get_attrib(tag, "alt"));
+
+	raw_data = xmlnode_get_data(tag);
+	data->data = purple_base64_decode(raw_data, &size);
+	data->size = size;
+
+	g_free(raw_data);
+
+	return data;
+}
+
+
+void
+jabber_data_delete(JabberData *data)
+{
+	g_free(data->cid);
+	g_free(data->alt);
+	g_free(data->type);
+	g_free(data->data);
+	g_free(data);
+}
+
+const char *
+jabber_data_get_cid(const JabberData *data)
+{
+	return data->cid;
+}
+
+const char *
+jabber_data_get_alt(const JabberData *data)
+{
+	return data->alt;
+}
+
+const char *
+jabber_data_get_type(const JabberData *data)
+{
+	return data->type;
+}
+
+gsize
+jabber_data_get_size(const JabberData *data)
+{
+	return data->size;
+}
+
+gpointer
+jabber_data_get_data(const JabberData *data)
+{
+	return data->data;
+}
+
+xmlnode *
+jabber_data_get_xml_definition(const JabberData *data)
+{
+	xmlnode *tag = xmlnode_new("data");
+	char *base64data = purple_base64_encode(data->data, data->size);
+
+	xmlnode_set_namespace(tag, XEP_0231_NAMESPACE);
+	xmlnode_set_attrib(tag, "alt", data->alt);
+	xmlnode_set_attrib(tag, "cid", data->cid);
+	xmlnode_set_attrib(tag, "type", data->type);
+
+	xmlnode_insert_data(tag, base64data, -1);
+
+	g_free(base64data);
+
+	return tag;
+}
+
+xmlnode *
+jabber_data_get_xhtml_im(const JabberData *data)
+{
+	xmlnode *img = xmlnode_new("img");
+	char src[128];
+
+	xmlnode_set_attrib(img, "alt", data->alt);
+	g_snprintf(src, sizeof(src), "cid:%s", data->cid);
+	xmlnode_set_attrib(img, "src", src);
+
+	return img;
+}
+
+xmlnode *
+jabber_data_get_xml_request(const gchar *cid)
+{
+	xmlnode *tag = xmlnode_new("data");
+
+	xmlnode_set_namespace(tag, XEP_0231_NAMESPACE);
+	xmlnode_set_attrib(tag, "cid", cid);
+
+	return tag;
+}
+
+const JabberData *
+jabber_data_find_local_by_alt(const PurpleConversation *conv, const char *alt)
+{
+	GHashTable *local_datas = g_hash_table_lookup(local_datas_by_alt, conv);
+
+	if (local_datas) {
+		return g_hash_table_lookup(local_datas, alt);
+	} else {
+		return NULL;
+	}
+}
+
+
+const JabberData *
+jabber_data_find_local_by_cid(const PurpleConversation *conv, const char *cid)
+{
+	GHashTable *local_datas = g_hash_table_lookup(local_datas_by_cid, conv);
+
+	if (local_datas) {
+		return g_hash_table_lookup(local_datas, cid);
+	} else {
+		return NULL;
+	}
+}
+
+const JabberData *
+jabber_data_find_remote_by_cid(const PurpleConversation *conv, const char *cid)
+{
+	GHashTable *remote_datas = g_hash_table_lookup(remote_datas_by_cid, conv);
+
+	if (remote_datas) {
+		return g_hash_table_lookup(remote_datas, cid);
+	} else {
+		return NULL;
+	}
+}
+
+void
+jabber_data_associate_local_with_conv(JabberData *data, PurpleConversation *conv)
+{
+	GHashTable *datas_by_alt = g_hash_table_lookup(local_datas_by_alt, conv);
+	GHashTable *datas_by_cid = g_hash_table_lookup(local_datas_by_cid, conv);
+
+	if (!datas_by_alt) {
+		datas_by_alt = g_hash_table_new(g_str_hash, g_str_equal);
+		g_hash_table_insert(local_datas_by_alt, conv, datas_by_alt);
+	}
+
+	if (!datas_by_cid) {
+		datas_by_cid = g_hash_table_new(g_str_hash, g_str_equal);
+		g_hash_table_insert(local_datas_by_cid, conv, datas_by_cid);
+	}
+
+	g_hash_table_insert(datas_by_alt, g_strdup(jabber_data_get_alt(data)), data);
+	g_hash_table_insert(datas_by_cid, g_strdup(jabber_data_get_cid(data)), data);
+}
+
+void
+jabber_data_associate_remote_with_conv(JabberData *data, PurpleConversation *conv)
+{
+	GHashTable *datas_by_cid = g_hash_table_lookup(remote_datas_by_cid, conv);
+
+	if (!datas_by_cid) {
+		datas_by_cid = g_hash_table_new(g_str_hash, g_str_equal);
+		g_hash_table_insert(remote_datas_by_cid, conv, datas_by_cid);
+	}
+
+	g_hash_table_insert(datas_by_cid, g_strdup(jabber_data_get_cid(data)), data);
+}
+
+static void
+jabber_data_delete_from_hash_table(gpointer key, gpointer value,
+								   gpointer user_data)
+{
+	JabberData *data = (JabberData *) value;
+	jabber_data_delete(data);
+	g_free(key);
+}
+
+void
+jabber_data_delete_associated_with_conv(PurpleConversation *conv)
+{
+	GHashTable *local_datas = g_hash_table_lookup(local_datas_by_cid, conv);
+	GHashTable *remote_datas = g_hash_table_lookup(remote_datas_by_cid, conv);
+	GHashTable *local_datas_alt = g_hash_table_lookup(local_datas_by_alt, conv);
+
+	if (local_datas) {
+		g_hash_table_foreach(local_datas, jabber_data_delete_from_hash_table,
+							 NULL);
+		g_hash_table_destroy(local_datas);
+		g_hash_table_remove(local_datas_by_cid, conv);
+	}
+	if (remote_datas) {
+		g_hash_table_foreach(remote_datas, jabber_data_delete_from_hash_table,
+							 NULL);
+		g_hash_table_destroy(remote_datas);
+		g_hash_table_remove(remote_datas_by_cid, conv);
+	}
+	if (local_datas_alt) {
+		g_hash_table_destroy(local_datas_alt);
+		g_hash_table_remove(local_datas_by_alt, conv);
+	}
+}
+
+void
+jabber_data_parse(JabberStream *js, xmlnode *packet)
+{
+	JabberIq *result = NULL;
+	const char *who = xmlnode_get_attrib(packet, "from");
+	const PurpleConnection *gc = js->gc;
+	const PurpleAccount *account = purple_connection_get_account(gc);
+	const PurpleConversation *conv =
+		purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY, who, account);
+	xmlnode *data_node = xmlnode_get_child(packet, "data");
+	const JabberData *data =
+		jabber_data_find_local_by_cid(conv, xmlnode_get_attrib(data_node, "cid"));
+
+	if (!conv || !data) {
+		xmlnode *item_not_found = xmlnode_new("item-not-found");
+
+		result = jabber_iq_new(js, JABBER_IQ_ERROR);
+		xmlnode_set_attrib(result->node, "to", who);
+		xmlnode_set_attrib(result->node, "id", xmlnode_get_attrib(packet, "id"));
+		xmlnode_insert_child(result->node, item_not_found);
+	} else {
+		result = jabber_iq_new(js, JABBER_IQ_RESULT);
+		xmlnode_set_attrib(result->node, "to", who);
+		xmlnode_set_attrib(result->node, "id", xmlnode_get_attrib(packet, "id"));
+		xmlnode_insert_child(result->node,
+							 jabber_data_get_xml_definition(data));
+	}
+	jabber_iq_send(result);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/jabber/data.h	Wed Jun 11 15:20:16 2008 +0000
@@ -0,0 +1,83 @@
+/*
+ * 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 Library 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 02110-1301,  USA
+ */
+ 
+#ifndef JABBER_DATA_H
+#define JABBER_DATA_H
+
+#include "xmlnode.h"
+#include "conversation.h"
+#include "jabber.h"
+
+#define XEP_0231_NAMESPACE "urn:xmpp:tmp:data-element"
+#define XEP_0231_IB_IMAGE_NAMESPACE "urn:xmpp:tmp:data-element:inband-image"
+
+#include <glib.h>
+
+typedef struct {
+	char *cid;
+	char *alt;
+	char *type;
+	gsize size;
+	gpointer data;
+} JabberData;
+
+void jabber_data_init(void);
+
+/* creates a JabberData instance from raw data */
+JabberData *jabber_data_create_from_data(gconstpointer data, gsize size,
+										 const char *type, const char *alt);
+
+/* create a JabberData instance from an XML "data" element (as defined by
+  XEP 0231 */
+JabberData *jabber_data_create_from_xml(xmlnode *tag);
+
+void jabber_data_delete(JabberData *data);
+
+const char *jabber_data_get_cid(const JabberData *data);
+const char *jabber_data_get_alt(const JabberData *data);
+const char *jabber_data_get_type(const JabberData *data);
+
+gsize jabber_data_get_size(const JabberData *data);
+gpointer jabber_data_get_data(const JabberData *data);
+
+/* returns the XML definition for the data element */
+xmlnode *jabber_data_get_xml_definition(const JabberData *data);
+
+/* returns an XHTML-IM "img" tag given a data instance */
+xmlnode *jabber_data_get_xhtml_im(const JabberData *data);
+
+/* returns a data request element (to be included in an iq stanza) for requesting
+  data */
+xmlnode *jabber_data_get_xml_request(const gchar *cid);
+
+/* lookup functions */
+const JabberData *jabber_data_find_local_by_alt(const PurpleConversation *conv,
+												const char *alt);
+const JabberData *jabber_data_find_local_by_cid(const PurpleConversation *conv,
+												const char *cid);
+const JabberData *jabber_data_find_remote_by_cid(const PurpleConversation *conv,
+												 const char *cid);
+
+/* associate data objects with a conversation */
+void jabber_data_associate_local_with_conv(JabberData *data, PurpleConversation *conv);
+void jabber_data_associate_remote_with_conv(JabberData *data, PurpleConversation *conv);
+void jabber_data_delete_associated_with_conv(PurpleConversation *conv);
+
+/* handles iq requests */
+void jabber_data_parse(JabberStream *js, xmlnode *packet);
+
+
+#endif /* JABBER_DATA_H */
--- a/libpurple/protocols/jabber/iq.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/iq.c	Wed Jun 11 15:20:16 2008 +0000
@@ -33,6 +33,7 @@
 #include "si.h"
 #include "ping.h"
 #include "adhoccommands.h"
+#include "data.h"
 
 #ifdef _WIN32
 #include "utsname.h"
@@ -355,6 +356,12 @@
 		return;
 	}
 
+	if (xmlnode_get_child_with_namespace(packet, "data",
+		    "urn:xmpp:tmp:data-element")) {
+		jabber_data_parse(js, packet);
+		return;
+	}
+
 	/* If we get here, send the default error reply mandated by XMPP-CORE */
 	if(type && (!strcmp(type, "set") || !strcmp(type, "get"))) {
 		JabberIq *iq = jabber_iq_new(js, JABBER_IQ_ERROR);
--- a/libpurple/protocols/jabber/jabber.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Wed Jun 11 15:20:16 2008 +0000
@@ -42,6 +42,7 @@
 #include "auth.h"
 #include "buddy.h"
 #include "chat.h"
+#include "data.h"
 #include "disco.h"
 #include "google.h"
 #include "iq.h"
@@ -57,6 +58,7 @@
 #include "pep.h"
 #include "adhoccommands.h"
 
+
 #define JABBER_CONNECT_STEPS (js->gsc ? 9 : 5)
 
 static PurplePlugin *my_protocol = NULL;
@@ -610,7 +612,8 @@
 	JabberStream *js;
 	JabberBuddy *my_jb = NULL;
 
-	gc->flags |= PURPLE_CONNECTION_HTML;
+	gc->flags |= PURPLE_CONNECTION_HTML |
+		PURPLE_CONNECTION_ALLOW_CUSTOM_SMILEY;
 	js = gc->proto_data = g_new0(JabberStream, 1);
 	js->gc = gc;
 	js->fd = -1;
@@ -1861,6 +1864,10 @@
 	JabberID *jid;
 	JabberBuddy *jb;
 	JabberBuddyResource *jbr;
+	PurpleAccount *account = purple_connection_get_account(gc);
+	PurpleConversation *conv =
+		purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY,
+			who, account);
 
 	if(!(jid = jabber_id_new(who)))
 		return;
@@ -1875,6 +1882,8 @@
 			jabber_message_conv_closed(js, who);
 	}
 
+	jabber_data_delete_associated_with_conv(conv);
+
 	jabber_id_free(jid);
 }
 
--- a/libpurple/protocols/jabber/libxmpp.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/libxmpp.c	Wed Jun 11 15:20:16 2008 +0000
@@ -43,6 +43,7 @@
 #include "pep.h"
 #include "usertune.h"
 #include "caps.h"
+#include "data.h"
 
 static PurplePluginProtocolInfo prpl_info =
 {
@@ -240,6 +241,13 @@
 	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
 						  option);
 
+	/* this should probably be part of global smiley theme settings later on,
+	  shared with MSN */
+	option = purple_account_option_bool_new(_("Show Custom Smileys"),
+		"custom_smileys", TRUE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options,
+		option);
+
 	jabber_init_plugin(plugin);
 
 	purple_prefs_remove("/plugins/prpl/jabber");
@@ -262,11 +270,16 @@
 	
 	jabber_tune_init();
 	jabber_caps_init();
+	jabber_data_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);
-	jabber_add_feature("buzz", "http://www.xmpp.org/extensions/xep-0224.html#ns", jabber_buzz_isenabled);
-	
+	jabber_add_feature("buzz", "http://www.xmpp.org/extensions/xep-0224.html#ns",
+					   jabber_buzz_isenabled);
+	/* this string will need to be updated when XEP-0231 turns "draft" */
+	jabber_add_feature("smileys", XEP_0231_IB_IMAGE_NAMESPACE,
+					   jabber_custom_smileys_isenabled);
+
 	jabber_pep_register_handler("avatar", AVATARNAMESPACEMETA, jabber_buddy_avatar_update_metadata);
 }
 
--- a/libpurple/protocols/jabber/message.c	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/message.c	Wed Jun 11 15:20:16 2008 +0000
@@ -27,10 +27,15 @@
 
 #include "buddy.h"
 #include "chat.h"
+#include "data.h"
 #include "google.h"
 #include "message.h"
 #include "xmlnode.h"
 #include "pep.h"
+#include "smiley.h"
+#include "iq.h"
+
+#include <string.h>
 
 void jabber_message_free(JabberMessage *jm)
 {
@@ -312,6 +317,186 @@
 	g_free(str);
 }
 
+/* used internally by the functions below */
+typedef struct {
+	gchar *cid;
+	gchar *alt;
+} JabberSmileyRef;
+
+static GList *
+_jabber_message_get_refs_from_xmlnode(const xmlnode *message)
+{
+	xmlnode *child;
+	GList *refs = NULL;
+
+	for (child = xmlnode_get_child(message, "img") ; child ;
+		 child = xmlnode_get_next_twin(child)) {
+		const gchar *src = xmlnode_get_attrib(child, "src");
+		const gssize len = strlen(src);
+
+		if (len > 4 && g_str_has_prefix(src, "cid:")) {
+			JabberSmileyRef *ref = g_new0(JabberSmileyRef, 1);
+			ref->cid = g_strdup(&(src[4]));
+			ref->alt = g_strdup(xmlnode_get_attrib(child, "alt"));
+			refs = g_list_append(refs, ref);
+		}
+	}
+
+	for (child = message->child ; child ; child = child->next) {
+		refs = g_list_concat(refs,
+			_jabber_message_get_refs_from_xmlnode(child));
+	}
+
+	return refs;
+}
+
+static GList *
+jabber_message_get_refs_from_xmlnode(const xmlnode *message)
+{
+	GList *refs = _jabber_message_get_refs_from_xmlnode(message);
+	GHashTable *unique_refs = g_hash_table_new(g_str_hash, g_str_equal);
+	GList *result = NULL;
+	GList *iterator = NULL;
+
+	for (iterator = refs ; iterator ; iterator = g_list_next(iterator)) {
+		JabberSmileyRef *ref = (JabberSmileyRef *) iterator->data;
+		if (!g_hash_table_lookup(unique_refs, ref->cid)) {
+			JabberSmileyRef *new_ref = g_new0(JabberSmileyRef, 1);
+			new_ref->cid = g_strdup(ref->cid);
+			new_ref->alt = g_strdup(ref->alt);
+			g_hash_table_insert(unique_refs, ref->cid, ref);
+			result = g_list_append(result, new_ref);
+		}
+	}
+
+	for (iterator = refs ; iterator ; iterator = g_list_next(iterator)) {
+		JabberSmileyRef *ref = (JabberSmileyRef *) iterator->data;
+		g_free(ref->cid);
+		g_free(ref->alt);
+		g_free(ref);
+	}
+
+	g_hash_table_destroy(unique_refs);
+
+	return result;
+}
+
+
+
+static gchar *
+jabber_message_xml_to_string_strip_img_smileys(xmlnode *xhtml)
+{
+	const gchar *markup = xmlnode_to_str(xhtml, NULL);
+	int len = strlen(markup);
+	int pos = 0;
+	GString *out = g_string_new(NULL);
+
+	while (pos < len) {
+		/* this is a bit cludgy, maybe there is a better way to do this...
+		  we need to find all <img> tags within the XHTML and replace those
+			tags with the value of their "alt" attributes */
+		if (g_str_has_prefix(&(markup[pos]), "<img")) {
+			xmlnode *img = NULL;
+			int pos2 = pos;
+			const gchar *src;
+
+			for (; pos2 < len ; pos2++) {
+				if (g_str_has_prefix(&(markup[pos2]), "/>")) {
+					pos2 += 2;
+					break;
+				}
+			}
+
+			/* note, if the above loop didn't find the end of the <img> tag,
+			  it the parsed string will be until the end of the input string,
+			  in which case xmlnode_from_str will bail out and return NULL,
+			  in this case the "if" statement below doesn't trigger and the
+			  text is copied unchanged */
+			img = xmlnode_from_str(&(markup[pos]), pos2 - pos);
+			src = xmlnode_get_attrib(img, "src");
+
+			if (g_str_has_prefix(src, "cid:")) {
+				const gchar *alt = xmlnode_get_attrib(img, "alt");
+				gchar *escaped = g_markup_escape_text(alt, -1);
+				out = g_string_append(out, escaped);
+				pos += pos2 - pos;
+				g_free(escaped);
+			} else {
+				out = g_string_append_c(out, markup[pos]);
+				pos++;
+			}
+
+			xmlnode_free(img);
+
+		} else {
+			out = g_string_append_c(out, markup[pos]);
+			pos++;
+		}
+	}
+
+	return g_string_free(out, FALSE);
+}
+
+static void
+jabber_message_add_remote_smileys_to_conv(PurpleConversation *conv,
+	const xmlnode *message)
+{
+	xmlnode *data_tag;
+	for (data_tag = xmlnode_get_child(message, "data") ; data_tag ;
+		 data_tag = xmlnode_get_next_twin(data_tag)) {
+		const gchar *cid = xmlnode_get_attrib(data_tag, "cid");
+		const JabberData *data = jabber_data_find_remote_by_cid(conv, cid);
+
+		if (!data) {
+			/* we haven't cached this already, let's add it */
+			JabberData *new_data = jabber_data_create_from_xml(data_tag);
+			jabber_data_associate_remote_with_conv(new_data, conv);
+		}
+	}
+}
+
+static void
+jabber_message_get_data_cb(JabberStream *js, xmlnode *packet, gpointer data)
+{
+	PurpleConversation *conv = (PurpleConversation *) data;
+	xmlnode *data_element = xmlnode_get_child(packet, "data");
+	xmlnode *item_not_found = xmlnode_get_child(packet, "item-not-found");
+
+	/* did we get a data element as result? */
+	if (data_element) {
+		JabberData *data = jabber_data_create_from_xml(data_element);
+
+		if (data) {
+			jabber_data_associate_remote_with_conv(data, conv);
+			purple_conv_custom_smiley_write(conv, jabber_data_get_alt(data),
+											jabber_data_get_data(data),
+											jabber_data_get_size(data));
+			purple_conv_custom_smiley_close(conv, jabber_data_get_alt(data));
+		}
+
+	} else if (item_not_found) {
+		purple_debug_info("jabber",
+			"Responder didn't recognize requested data\n");
+	} else {
+		purple_debug_error("jabber", "Unknown response to data request\n");
+	}
+}
+
+static void
+jabber_message_send_data_request(JabberStream *js, PurpleConversation *conv,
+								 const gchar *cid, const gchar *who)
+{
+	JabberIq *request = jabber_iq_new(js, JABBER_IQ_GET);
+	xmlnode *data_request = jabber_data_get_xml_request(cid);
+
+	xmlnode_set_attrib(request->node, "to", who);
+	jabber_iq_set_callback(request, jabber_message_get_data_cb, conv);
+	xmlnode_insert_child(request->node, data_request);
+
+	jabber_iq_send(request);
+}
+
+
 void jabber_message_parse(JabberStream *js, xmlnode *packet)
 {
 	JabberMessage *jm;
@@ -368,14 +553,99 @@
 		} else if(!strcmp(child->name, "html") && !strcmp(xmlns,"http://jabber.org/protocol/xhtml-im")) {
 			if(!jm->xhtml && xmlnode_get_child(child, "body")) {
 				char *c;
-				jm->xhtml = xmlnode_to_str(child, NULL);
-			        /* Convert all newlines to whitespace. Technically, even regular, non-XML HTML is supposed to ignore newlines, but Pidgin has, as convention
-			 	 * treated \n as a newline for compatibility with other protocols
+
+				const PurpleConnection *gc = js->gc;
+				const gchar *who = xmlnode_get_attrib(packet, "from");
+				PurpleAccount *account = purple_connection_get_account(gc);
+				PurpleConversation *conv = NULL;
+				const GList *smiley_refs = NULL;
+				gchar *reformatted_xhtml;
+
+				if (purple_account_get_bool(account, "custom_smileys", TRUE)) {
+					/* find a list of smileys ("cid" and "alt" text pairs)
+					  occuring in the message */
+					smiley_refs = jabber_message_get_refs_from_xmlnode(child);
+
+					if (jm->type == JABBER_MESSAGE_GROUPCHAT) {
+						JabberID *jid = jabber_id_new(jm->from);
+						JabberChat *chat = NULL;
+
+						if (jid) {
+							chat = jabber_chat_find(js, jid->node, jid->domain);
+							conv = chat->conv;
+						}
+
+						jabber_id_free(jid);
+					} else {
+						conv =
+							purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY,
+								who, account);
+					}
+
+					/* if the conversation doesn't exist yet we need to create it
+				  	now */
+					if (!conv) {
+						/* if a message of this type is initiating a conversation,
+					  	that must be an IM */
+						conv = purple_conversation_new(PURPLE_CONV_TYPE_IM,
+							account, who);
+					}
+
+					/* process any newly provided smileys */
+					jabber_message_add_remote_smileys_to_conv(conv, packet);
+				}
+
+				/* reformat xhtml so that img tags with a "cid:" src gets
+				  translated to the bare text of the emoticon (the "alt" attrib) */
+				/* this is done also when custom smiley retrieval is turned off,
+				  this way the receiver always sees the shortcut instead */
+				reformatted_xhtml =
+					jabber_message_xml_to_string_strip_img_smileys(child);
+
+				jm->xhtml = reformatted_xhtml;
+
+				/* add known custom emoticons to the conversation */
+				/* note: if there were no smileys in the incoming message, or
+				  	if receiving custom smileys is turned off, smiley_refs will
+					be NULL */
+				for (; smiley_refs ; smiley_refs = g_list_next(smiley_refs)) {
+					const JabberSmileyRef *ref =
+						(JabberSmileyRef *) smiley_refs->data;
+					const gchar *cid = ref->cid;
+					const gchar *alt = ref->alt;
+
+					if (purple_conv_custom_smiley_add(conv, alt, "cid", cid,
+						    TRUE)) {
+						const JabberData *data =
+								jabber_data_find_remote_by_cid(conv, cid);
+						/* if data is already known, we add write it immediatly */
+						if (data) {
+							purple_conv_custom_smiley_write(conv, alt,
+								jabber_data_get_data(data),
+								jabber_data_get_size(data));
+							purple_conv_custom_smiley_close(conv, alt);
+						} else {
+							/* we need to request the smiley (data) */
+							jabber_message_send_data_request(js, conv, cid, who);
+						}
+					}
+				}
+
+			    /* Convert all newlines to whitespace. Technically, even regular, non-XML HTML is supposed to ignore newlines, but Pidgin has, as convention
+				 * treated \n as a newline for compatibility with other protocols
 				 */
 				for (c = jm->xhtml; *c != '\0'; c++) {
-					if (*c == '\n') 
+					if (*c == '\n')
 						*c = ' ';
 				}
+
+				/* we don't need the list of CIDs anymore */
+				for (; smiley_refs ; smiley_refs = g_list_next(smiley_refs)) {
+					JabberSmileyRef *ref = (JabberSmileyRef *) smiley_refs->data;
+					g_free(ref->cid);
+					g_free(ref->alt);
+					g_free(ref);
+				}
 			}
 		} else if(!strcmp(child->name, "active") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
 			jm->chat_state = JM_STATE_ACTIVE;
@@ -503,6 +773,120 @@
 	jabber_message_free(jm);
 }
 
+static const gchar *
+jabber_message_get_mimetype_from_ext(const gchar *ext)
+{
+	if (strcmp(ext, "png") == 0) {
+		return "image/png";
+	} else if (strcmp(ext, "gif") == 0) {
+		return "image/gif";
+	} else if (strcmp(ext, "jpg") == 0) {
+		return "image/jpeg";
+	} else if (strcmp(ext, "tif") == 0) {
+		return "image/tif";
+	} else {
+		return "image/x-icon"; /* or something... */
+	}
+}
+
+static GList *
+jabber_message_xhtml_find_smileys(const char *xhtml)
+{
+	GList *smileys = purple_smileys_get_all();
+	GList *found_smileys = NULL;
+
+	for (; smileys ; smileys = g_list_delete_link(smileys, smileys)) {
+		PurpleSmiley *smiley = (PurpleSmiley *) smileys->data;
+		const gchar *shortcut = purple_smiley_get_shortcut(smiley);
+		const gssize len = strlen(shortcut);
+
+		gchar *escaped = g_markup_escape_text(shortcut, len);
+		const char *pos = strstr(xhtml, escaped);
+
+		if (pos) {
+			found_smileys = g_list_append(found_smileys, smiley);
+		}
+
+		g_free(escaped);
+	}
+
+	return found_smileys;
+}
+
+static gchar *
+jabber_message_get_smileyfied_xhtml(const PurpleConversation *conv,
+	const gchar *xhtml, const GList *smileys)
+{
+	/* create XML element for all smileys (img tags) */
+	GString *result = g_string_new(NULL);
+	int pos = 0;
+	int length = strlen(xhtml);
+
+	while (pos < length) {
+		const GList *iterator;
+		gboolean found_smiley = FALSE;
+
+		for (iterator = smileys ; iterator ;
+			iterator = g_list_next(iterator)) {
+			const PurpleSmiley *smiley = (PurpleSmiley *) iterator->data;
+			const gchar *shortcut = purple_smiley_get_shortcut(smiley);
+			const gssize len = strlen(shortcut);
+			gchar *escaped = g_markup_escape_text(shortcut, len);
+
+			if (g_str_has_prefix(&(xhtml[pos]), escaped)) {
+				/* we found the current smiley at this position */
+				const JabberData *data =
+					jabber_data_find_local_by_alt(conv, shortcut);
+				xmlnode *img = jabber_data_get_xhtml_im(data);
+				int len;
+				gchar *img_text = xmlnode_to_str(img, &len);
+
+				found_smiley = TRUE;
+				result = g_string_append(result, img_text);
+				g_free(img_text);
+				pos += strlen(escaped);
+				g_free(escaped);
+				break;
+			} else {
+				/* cleanup from the before the next round... */
+				g_free(escaped);
+			}
+		}
+		if (!found_smiley) {
+			/* there was no smiley here, just copy one byte */
+			result = g_string_append_c(result, xhtml[pos]);
+			pos++;
+		}
+	}
+
+	return g_string_free(result, FALSE);
+}
+
+static gboolean
+jabber_conv_support_custom_smileys(const PurpleConnection *gc,
+								   const PurpleConversation *conv,
+								   const gchar *who)
+{
+	JabberStream *js = (JabberStream *) gc->proto_data;
+	JabberBuddy *jb = jabber_buddy_find(js, who, FALSE);
+
+	if (!jb) {
+		purple_debug_error("jabber",
+			"jabber_conv_support_custom smileys: could not find buddy\n");
+		return FALSE;
+	}
+
+	switch (purple_conversation_get_type(conv)) {
+		/* for the time being, we will not support custom smileys in MUCs */
+		case PURPLE_CONV_TYPE_IM:
+			return jabber_buddy_has_capability(jb, XEP_0231_IB_IMAGE_NAMESPACE);
+			break;
+		default:
+			return FALSE;
+			break;
+	}
+}
+
 void jabber_message_send(JabberMessage *jm)
 {
 	xmlnode *message, *child;
@@ -588,7 +972,55 @@
 	}
 
 	if(jm->xhtml) {
-		child = xmlnode_from_str(jm->xhtml, -1);
+		PurpleAccount *account = purple_connection_get_account(jm->js->gc);
+		PurpleConversation *conv =
+			purple_find_conversation_with_account(PURPLE_CONV_TYPE_ANY, jm->to,
+				account);
+
+		if (jabber_conv_support_custom_smileys(jm->js->gc, conv, jm->to)) {
+			GList *found_smileys = jabber_message_xhtml_find_smileys(jm->xhtml);
+
+			if (found_smileys) {
+				gchar *smileyfied_xhtml = NULL;
+				const GList *iterator;
+
+				for (iterator = found_smileys; iterator ;
+					iterator = g_list_next(iterator)) {
+					const PurpleSmiley *smiley =
+							(PurpleSmiley *) iterator->data;
+					const gchar *shortcut = purple_smiley_get_shortcut(smiley);
+					const JabberData *data =
+							jabber_data_find_local_by_alt(conv, shortcut);
+
+					/* if data has not been sent before, include data */
+					if (!data) {
+						PurpleStoredImage *image =
+								purple_smiley_get_stored_image(smiley);
+						const gchar *ext = purple_imgstore_get_extension(image);
+
+						JabberData *new_data =
+							jabber_data_create_from_data(purple_imgstore_get_data(image),
+														purple_imgstore_get_size(image),
+														jabber_message_get_mimetype_from_ext(ext),
+														shortcut);
+						jabber_data_associate_local_with_conv(new_data, conv);
+						xmlnode_insert_child(message,
+							jabber_data_get_xml_definition(new_data));
+					}
+				}
+
+				smileyfied_xhtml =
+					jabber_message_get_smileyfied_xhtml(conv, jm->xhtml,
+						found_smileys);
+				child = xmlnode_from_str(smileyfied_xhtml, -1);
+				g_free(smileyfied_xhtml);
+				g_list_free(found_smileys);
+			} else {
+				child = xmlnode_from_str(jm->xhtml, -1);
+			}
+		} else {
+			child = xmlnode_from_str(jm->xhtml, -1);
+		}
 		if(child) {
 			xmlnode_insert_child(message, child);
 		} else {
@@ -762,3 +1194,11 @@
 	return js->allowBuzz;
 }
 
+gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname,
+										 const gchar *namespace)
+{
+	const PurpleConnection *gc = js->gc;
+	PurpleAccount *account = purple_connection_get_account(gc);
+
+	return purple_account_get_bool(account, "custom_smileys", TRUE);
+}
--- a/libpurple/protocols/jabber/message.h	Wed Jun 11 02:42:23 2008 +0000
+++ b/libpurple/protocols/jabber/message.h	Wed Jun 11 15:20:16 2008 +0000
@@ -80,4 +80,7 @@
 
 gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *shortname, const gchar *namespace);
 
+gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *shortname,
+										 const gchar *namespace);
+
 #endif /* _PURPLE_JABBER_MESSAGE_H_ */