# HG changeset patch # User Richard Laager # Date 1213197616 0 # Node ID c45d05bd58ed844486cf2fb69ed929440064e50d # Parent 9cb1e75854f1a956544cd30d4a0930ed3957f319# Parent 9d83ae2c1a4ffd47fa522f52e649f4762f9af957 propagate from branch 'im.pidgin.pidgin' (head 7be65dacd56b6536cf745747e39a29f4f7f2644b) to branch 'im.pidgin.xmpp.custom_smiley' (head a21aa16c365d48c3255358530932190b7263cc9d) diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/Makefile.am --- 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 \ diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/Makefile.mingw --- 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 \ diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/buddy.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); +} + diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/buddy.h --- 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_ */ diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/chat.c --- 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); } diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/data.c --- /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 +#include +#include + +#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); +} diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/data.h --- /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 + +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 */ diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/iq.c --- 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); diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/jabber.c --- 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); } diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/libxmpp.c --- 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); } diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/message.c --- 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 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 tags within the XHTML and replace those + tags with the value of their "alt" attributes */ + if (g_str_has_prefix(&(markup[pos]), "")) { + pos2 += 2; + break; + } + } + + /* note, if the above loop didn't find the end of the 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); +} diff -r 9cb1e75854f1 -r c45d05bd58ed libpurple/protocols/jabber/message.h --- 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_ */