libpurple/protocols/jabber/message.c

Mon, 04 Sep 2023 23:10:47 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Mon, 04 Sep 2023 23:10:47 -0500
changeset 42306
aee8ab56690f
parent 42305
a3895b6d3621
child 42348
991ae41441ab
permissions
-rw-r--r--

Add the conversation as a parameter to PurpleProtocolChat->send

This is an incremental update so that all of the implemenations can start
moving in this direction. Eventually this be something like
PurpleProtocol->send_message or something which will be used for both ims and
chats.

Testing Done:
Joined an XMPP muc and sent some messages.

Bugs closed: PIDGIN-17826

Reviewed at https://reviews.imfreedom.org/r/2596/

/*
 * purple - Jabber Protocol Plugin
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 *
 */
#include <glib/gi18n-lib.h>

#include <purple.h>

#include "adhoccommands.h"
#include "buddy.h"
#include "chat.h"
#include "data.h"
#include "message.h"
#include "pep.h"
#include "iq.h"

#include <string.h>

static GString *jm_body_with_oob(JabberMessage *jm) {
	GList *etc;
	GString *body = g_string_new("");

	if(jm->xhtml)
		g_string_append(body, jm->xhtml);
	else if(jm->body)
		g_string_append(body, jm->body);

	for(etc = jm->etc; etc; etc = etc->next) {
		PurpleXmlNode *x = etc->data;
		const char *xmlns = purple_xmlnode_get_namespace(x);
		if(purple_strequal(xmlns, NS_OOB_X_DATA)) {
			PurpleXmlNode *url, *desc;
			char *urltxt, *desctxt;

			url = purple_xmlnode_get_child(x, "url");
			desc = purple_xmlnode_get_child(x, "desc");

			if(!url)
				continue;

			urltxt = purple_xmlnode_get_data(url);
			desctxt = desc ? purple_xmlnode_get_data(desc) : urltxt;

			if(body->len && !purple_strequal(body->str, urltxt))
				g_string_append_printf(body, "<br/><a href='%s'>%s</a>",
						urltxt, desctxt);
			else
				g_string_printf(body, "<a href='%s'>%s</a>",
						urltxt, desctxt);

			g_free(urltxt);

			if(desctxt != urltxt)
				g_free(desctxt);
		}
	}

	return body;
}

void jabber_message_free(JabberMessage *jm)
{
	g_free(jm->from);
	g_free(jm->to);
	g_free(jm->id);
	g_free(jm->subject);
	g_free(jm->body);
	g_free(jm->xhtml);
	g_free(jm->password);
	g_free(jm->error);
	g_free(jm->thread_id);
	g_list_free(jm->etc);
	g_list_free(jm->eventitems);

	g_clear_pointer(&jm->sent, g_date_time_unref);

	g_free(jm);
}

static void handle_chat(JabberMessage *jm)
{
	const gchar *contact = jm->outgoing ? jm->to : jm->from;
	JabberID *jid = jabber_id_new(contact);

	PurpleConnection *gc;
	PurpleConversationManager *manager;
	PurpleAccount *account;
	PurpleMessageFlags flags = 0;
	JabberBuddy *jb;
	JabberBuddyResource *jbr;
	GString *body;

	if(!jid)
		return;

	manager = purple_conversation_manager_get_default();

	gc = jm->js->gc;
	account = purple_connection_get_account(gc);

	jb = jabber_buddy_find(jm->js, contact, TRUE);
	jbr = jabber_buddy_find_resource(jb, jid->resource);

	if (jbr && jm->chat_state != JM_STATE_NONE)
		jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED;

	switch(jm->chat_state) {
		case JM_STATE_COMPOSING:
			purple_serv_got_typing(gc, contact, 0, PURPLE_IM_TYPING);
			break;
		case JM_STATE_PAUSED:
			purple_serv_got_typing(gc, contact, 0, PURPLE_IM_TYPED);
			break;
		case JM_STATE_GONE: {
			PurpleConversation *im = NULL;

			im = purple_conversation_manager_find_im(manager, account, contact);

			if (im && jid->node && jid->domain) {
				char buf[256];
				PurpleBuddy *buddy;

				g_snprintf(buf, sizeof(buf), "%s@%s", jid->node, jid->domain);

				if ((buddy = purple_blist_find_buddy(account, buf))) {
					const char *who;
					char *escaped;

					who = purple_buddy_get_alias(buddy);
					escaped = g_markup_escape_text(who, -1);

					g_snprintf(buf, sizeof(buf),
					           _("%s has left the conversation."), escaped);
					g_free(escaped);

					/* At some point when we restructure PurpleConversation,
					 * this should be able to be implemented by removing the
					 * user from the conversation like we do with chats now. */
					purple_conversation_write_system_message(im, buf, 0);
				}
			}
			purple_serv_got_typing_stopped(gc, contact);
			break;
		}
		default:
			purple_serv_got_typing_stopped(gc, contact);
	}

	body = jm_body_with_oob(jm);

	if(body && body->len) {
		if (jid->resource) {
			/*
			 * We received a message from a specific resource, so
			 * we probably want a reply to go to this specific
			 * resource (i.e. bind/lock the conversation to this
			 * resource).
			 *
			 * This works because purple_im_conversation_send gets the name
			 * from purple_conversation_get_name()
			 */
			PurpleConversation *im;

			im = purple_conversation_manager_find_im(manager, account, contact);
			if (im && !purple_strequal(contact,
					purple_conversation_get_name(im))) {
				purple_debug_info("jabber", "Binding conversation to %s\n",
				                  contact);
				purple_conversation_set_name(im, contact);
			}
		}

		if(jbr) {
			/* Treat SUPPORTED as a terminal with no escape :) */
			if (jbr->chat_states != JABBER_CHAT_STATES_SUPPORTED) {
				if (jm->chat_state != JM_STATE_NONE)
					jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED;
				else
					jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED;
			}

			g_free(jbr->thread_id);
			jbr->thread_id = g_strdup(jm->thread_id);
		}

		if(jm->forwarded) {
			flags |= PURPLE_MESSAGE_FORWARDED;
		}
		flags |= jm->outgoing ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV;

		purple_serv_got_im(gc, contact, body->str, flags,
		                   (time_t)g_date_time_to_unix(jm->sent));
	}

	jabber_id_free(jid);

	if(body)
		g_string_free(body, TRUE);
}

static void handle_headline(JabberMessage *jm)
{
	char *title;
	GString *body;

	if(!jm->xhtml && !jm->body)
		return; /* ignore headlines without any content */

	body = jm_body_with_oob(jm);
	title = g_strdup_printf(_("Message from %s"), jm->from);

	purple_notify_formatted(jm->js->gc, title, jm->subject ? jm->subject : title,
			NULL, body->str, NULL, NULL);

	g_free(title);
	g_string_free(body, TRUE);
}

static void handle_groupchat(JabberMessage *jm)
{
	JabberID *jid = jabber_id_new(jm->from);
	JabberChat *chat;
	PurpleMessageFlags messageFlags = 0;

	if(!jid)
		return;

	chat = jabber_chat_find(jm->js, jid->node, jid->domain);

	if(!chat)
		return;

	if(jm->subject) {
		purple_chat_conversation_set_topic(chat->conv, jid->resource,
				jm->subject);
		messageFlags |= PURPLE_MESSAGE_NO_LOG;
		if(!jm->xhtml && !jm->body) {
			char *msg, *tmp, *tmp2;
			tmp = g_markup_escape_text(jm->subject, -1);
			tmp2 = purple_markup_linkify(tmp);
			if(jid->resource)
				msg = g_strdup_printf(_("%s has set the topic to: %s"), jid->resource, tmp2);
			else
				msg = g_strdup_printf(_("The topic is: %s"), tmp2);
			purple_conversation_write_system_message(PURPLE_CONVERSATION(chat->conv),
				msg, messageFlags);
			g_free(tmp);
			g_free(tmp2);
			g_free(msg);
		}
	}

	if(jm->xhtml || jm->body) {
		if(jid->resource) {
			time_t sent = (time_t)g_date_time_to_unix(jm->sent);

			purple_serv_got_chat_in(jm->js->gc, chat->id, jid->resource,
							messageFlags | (jm->delayed ? PURPLE_MESSAGE_DELAYED : 0),
							jm->xhtml ? jm->xhtml : jm->body, sent);
		} else if(chat->muc) {
			purple_conversation_write_system_message(
				PURPLE_CONVERSATION(chat->conv),
				jm->xhtml ? jm->xhtml : jm->body, messageFlags);
		}
	}

	jabber_id_free(jid);
}

static void handle_groupchat_invite(JabberMessage *jm)
{
	GHashTable *components;
	JabberID *jid = jabber_id_new(jm->to);

	if(!jid)
		return;

	components = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);

	g_hash_table_replace(components, "room", g_strdup(jid->node));
	g_hash_table_replace(components, "server", g_strdup(jid->domain));
	g_hash_table_replace(components, "handle", g_strdup(jm->js->user->node));
	g_hash_table_replace(components, "password", g_strdup(jm->password));

	jabber_id_free(jid);
	purple_serv_got_chat_invite(jm->js->gc, jm->to, jm->from, jm->body, components);
}

static void handle_error(JabberMessage *jm)
{
	char *buf;

	if(!jm->body)
		return;

	buf = g_strdup_printf(_("Message delivery to %s failed: %s"),
			jm->from, jm->error ? jm->error : "");

	purple_notify_formatted(jm->js->gc, _("XMPP Message Error"), _("XMPP Message Error"), buf,
			jm->xhtml ? jm->xhtml : jm->body, NULL, NULL);

	g_free(buf);
}

static gchar *
jabber_message_xml_to_string_strip_img_smileys(PurpleXmlNode *xhtml)
{
	gchar *markup = purple_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")) {
			PurpleXmlNode *img = NULL;
			int pos2 = pos;
			const gchar *src;

			for (; pos2 < len ; pos2++) {
				if (g_str_has_prefix(&(markup[pos2]), "/>")) {
					pos2 += 2;
					break;
				} else if (g_str_has_prefix(&(markup[pos2]), "</img>")) {
					pos2 += 5;
					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 purple_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 = purple_xmlnode_from_str(&(markup[pos]), pos2 - pos);
			src = purple_xmlnode_get_attrib(img, "src");

			if (g_str_has_prefix(src, "cid:")) {
				const gchar *alt = purple_xmlnode_get_attrib(img, "alt");
				/* if the "alt" attribute is empty, put the cid as smiley string */
				if (alt && alt[0] != '\0') {
					/* if the "alt" is the same as the CID, as Jabbim does,
					 this prevents linkification... */
					if (purple_email_is_valid(alt)) {
						gchar *safe_alt = g_strdup_printf("smiley:%s", alt);
						out = g_string_append(out, safe_alt);
						g_free(safe_alt);
					} else {
						gchar *alt_escaped = g_markup_escape_text(alt, -1);
						out = g_string_append(out, alt_escaped);
						g_free(alt_escaped);
					}
				} else {
					out = g_string_append(out, src);
				}
				pos += pos2 - pos;
			} else {
				out = g_string_append_c(out, markup[pos]);
				pos++;
			}

			purple_xmlnode_free(img);

		} else {
			out = g_string_append_c(out, markup[pos]);
			pos++;
		}
	}

	g_free(markup);
	return g_string_free(out, FALSE);
}

void jabber_message_parse(JabberStream *js, PurpleXmlNode *packet)
{
	JabberMessage *jm;
	const char *id, *from, *to, *type;
	PurpleXmlNode *child = NULL, *received = NULL;
	gboolean signal_return;
	gboolean delayed = FALSE, is_outgoing = FALSE, is_forwarded = FALSE;
	GDateTime *timestamp = g_date_time_new_now_utc();

	/* Check if we have a carbons received element from our own account. */
	from = purple_xmlnode_get_attrib(packet, "from");
	if(from != NULL && jabber_is_own_account(js, from)) {
		PurpleXmlNode *forwarded = NULL;

		/* We check if this is a received carbon first. */
		received = purple_xmlnode_get_child_with_namespace(packet, "received",
		                                                   NS_MESSAGE_CARBONS);
		if(received != NULL) {
			forwarded = purple_xmlnode_get_child_with_namespace(received,
			                                                    "forwarded",
			                                                    NS_FORWARD);
		} else {
			PurpleXmlNode *sent = NULL;

			sent = purple_xmlnode_get_child_with_namespace(packet, "sent",
			                                               NS_MESSAGE_CARBONS);
			if(sent != NULL) {
				forwarded = purple_xmlnode_get_child_with_namespace(sent,
				                                                    "forwarded",
				                                                    NS_FORWARD);
				is_outgoing = TRUE;
			}
		}

		if(forwarded != NULL) {
			PurpleXmlNode *fwd_msg = NULL;

			fwd_msg = purple_xmlnode_get_child_with_namespace(forwarded,
			                                                  "message",
			                                                  NS_XMPP_CLIENT);
			if(fwd_msg != NULL) {
				PurpleXmlNode *delay = NULL;

				/* We have a forwarded message, so update the packet to point
				 * to it directly.
				 */
				packet = fwd_msg;
				is_forwarded = TRUE;

				/* Now check if it was a delayed message and if so, grab the
				 * timestamp that the server sent.
				 */
				delay = purple_xmlnode_get_child_with_namespace(forwarded,
				                                                "delay",
				                                                NS_DELAYED_DELIVERY);
				if(delay != NULL) {
					GDateTime *delayed_ts = NULL;
					GTimeZone *tz = g_time_zone_new_utc();
					const gchar *ts = purple_xmlnode_get_attrib(delay,
					                                            "stamp");

					delayed_ts = g_date_time_new_from_iso8601(ts, tz);
					g_time_zone_unref(tz);

					if(delayed_ts != NULL) {
						delayed = TRUE;
						g_date_time_unref(timestamp);
						timestamp = delayed_ts;
					}
				}
			}
		}
	}

	/* If the message was forwarded, packet is now pointing to the forwarded
	 * message.
	 */
	from = purple_xmlnode_get_attrib(packet, "from");
	id = purple_xmlnode_get_attrib(packet, "id");
	to = purple_xmlnode_get_attrib(packet, "to");
	type = purple_xmlnode_get_attrib(packet, "type");

	signal_return = GPOINTER_TO_INT(purple_signal_emit_return_1(purple_connection_get_protocol(js->gc),
			"jabber-receiving-message", js->gc, type, id, from, to, packet));
	if (signal_return)
		return;

	jm = g_new0(JabberMessage, 1);
	jm->js = js;
	jm->sent = timestamp;
	jm->delayed = delayed;
	jm->chat_state = JM_STATE_NONE;
	jm->forwarded = is_forwarded;
	jm->outgoing = is_outgoing;

	if(type) {
		if(purple_strequal(type, "normal"))
			jm->type = JABBER_MESSAGE_NORMAL;
		else if(purple_strequal(type, "chat"))
			jm->type = JABBER_MESSAGE_CHAT;
		else if(purple_strequal(type, "groupchat"))
			jm->type = JABBER_MESSAGE_GROUPCHAT;
		else if(purple_strequal(type, "headline"))
			jm->type = JABBER_MESSAGE_HEADLINE;
		else if(purple_strequal(type, "error"))
			jm->type = JABBER_MESSAGE_ERROR;
		else
			jm->type = JABBER_MESSAGE_OTHER;
	} else {
		jm->type = JABBER_MESSAGE_NORMAL;
	}

	jm->from = g_strdup(from);
	jm->to   = g_strdup(to);
	jm->id   = g_strdup(id);

	for(child = packet->child; child; child = child->next) {
		const char *xmlns = purple_xmlnode_get_namespace(child);
		if(child->type != PURPLE_XMLNODE_TYPE_TAG)
			continue;

		if(purple_strequal(child->name, "error")) {
			const char *code = purple_xmlnode_get_attrib(child, "code");
			char *code_txt = NULL;
			char *text = purple_xmlnode_get_data(child);
			if (!text) {
				PurpleXmlNode *enclosed_text_node;

				if ((enclosed_text_node = purple_xmlnode_get_child(child, "text")))
					text = purple_xmlnode_get_data(enclosed_text_node);
			}

			if(code)
				code_txt = g_strdup_printf(_("(Code %s)"), code);

			if(!jm->error)
				jm->error = g_strdup_printf("%s%s%s",
						text ? text : "",
						text && code_txt ? " " : "",
						code_txt ? code_txt : "");

			g_free(code_txt);
			g_free(text);
		} else if (xmlns == NULL) {
			/* QuLogic: Not certain this is correct, but it would have happened
			   with the previous code. */
			if(purple_strequal(child->name, "x"))
				jm->etc = g_list_append(jm->etc, child);
			/* The following tests expect xmlns != NULL */
			continue;
		} else if(purple_strequal(child->name, "subject") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
			if(!jm->subject) {
				jm->subject = purple_xmlnode_get_data(child);
				if(!jm->subject)
					jm->subject = g_strdup("");
			}
		} else if(purple_strequal(child->name, "thread") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
			if(!jm->thread_id)
				jm->thread_id = purple_xmlnode_get_data(child);
		} else if(purple_strequal(child->name, "body") && purple_strequal(xmlns, NS_XMPP_CLIENT)) {
			if(!jm->body) {
				char *msg = purple_xmlnode_get_data(child);
				char *escaped = g_markup_escape_text(msg, -1);
				jm->body = purple_strdup_withhtml(escaped);
				g_free(escaped);
				g_free(msg);
			}
		} else if(purple_strequal(child->name, "html") && purple_strequal(xmlns, NS_XHTML_IM)) {
			if(!jm->xhtml && purple_xmlnode_get_child(child, "body")) {
				char *c;
				gchar *reformatted_xhtml;

				purple_xmlnode_strip_prefixes(child);

				/* 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;

			    /* 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')
						*c = ' ';
				}
			}
		} else if(purple_strequal(child->name, "active") && purple_strequal(xmlns,"http://jabber.org/protocol/chatstates")) {
			jm->chat_state = JM_STATE_ACTIVE;
		} else if(purple_strequal(child->name, "composing") && purple_strequal(xmlns,"http://jabber.org/protocol/chatstates")) {
			jm->chat_state = JM_STATE_COMPOSING;
		} else if(purple_strequal(child->name, "paused") && purple_strequal(xmlns,"http://jabber.org/protocol/chatstates")) {
			jm->chat_state = JM_STATE_PAUSED;
		} else if(purple_strequal(child->name, "inactive") && purple_strequal(xmlns,"http://jabber.org/protocol/chatstates")) {
			jm->chat_state = JM_STATE_INACTIVE;
		} else if(purple_strequal(child->name, "gone") && purple_strequal(xmlns,"http://jabber.org/protocol/chatstates")) {
			jm->chat_state = JM_STATE_GONE;
		} else if(purple_strequal(child->name, "event") && purple_strequal(xmlns,"http://jabber.org/protocol/pubsub#event")) {
			PurpleXmlNode *items;
			jm->type = JABBER_MESSAGE_EVENT;
			for(items = purple_xmlnode_get_child(child,"items"); items; items = items->next)
				jm->eventitems = g_list_append(jm->eventitems, items);
		} else if(purple_strequal(child->name, "delay") && purple_strequal(xmlns, NS_DELAYED_DELIVERY)) {
			const char *stamp = purple_xmlnode_get_attrib(child, "stamp");
			if(stamp != NULL) {
				GDateTime *delayed_ts = NULL;
				GTimeZone *tz = g_time_zone_new_utc();

				delayed_ts = g_date_time_new_from_iso8601(stamp, tz);
				g_time_zone_unref(tz);

				if(delayed_ts != NULL) {
					jm->delayed = TRUE;
					g_date_time_unref(jm->sent);
					jm->sent = delayed_ts;
				}
			}
		} else if(purple_strequal(child->name, "x")) {
			if(purple_strequal(xmlns, NS_DELAYED_DELIVERY_LEGACY)) {
				const char *stamp = purple_xmlnode_get_attrib(child, "stamp");

				if(stamp != NULL) {
					GDateTime *delayed_ts = NULL;
					GTimeZone *tz = g_time_zone_new_utc();

					delayed_ts = g_date_time_new_from_iso8601(stamp, tz);
					g_time_zone_unref(tz);

					if(delayed_ts != NULL) {
						jm->delayed = TRUE;
						g_date_time_unref(jm->sent);
						jm->sent = delayed_ts;
					}
				}
			} else if(purple_strequal(xmlns, "jabber:x:conference") &&
					jm->type != JABBER_MESSAGE_GROUPCHAT_INVITE &&
					jm->type != JABBER_MESSAGE_ERROR) {
				const char *jid = purple_xmlnode_get_attrib(child, "jid");
				if(jid) {
					const char *reason = purple_xmlnode_get_attrib(child, "reason");
					const char *password = purple_xmlnode_get_attrib(child, "password");

					jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE;
					g_free(jm->to);
					jm->to = g_strdup(jid);

					if (reason) {
						g_free(jm->body);
						jm->body = g_strdup(reason);
					}

					if (password) {
						g_free(jm->password);
						jm->password = g_strdup(password);
					}
				}
			} else if(purple_strequal(xmlns, "http://jabber.org/protocol/muc#user") &&
					jm->type != JABBER_MESSAGE_ERROR) {
				PurpleXmlNode *invite = purple_xmlnode_get_child(child, "invite");
				if(invite) {
					PurpleXmlNode *reason, *password;
					const char *jid = purple_xmlnode_get_attrib(invite, "from");
					g_free(jm->to);
					jm->to = jm->from;
					jm->from = g_strdup(jid);
					if((reason = purple_xmlnode_get_child(invite, "reason"))) {
						g_free(jm->body);
						jm->body = purple_xmlnode_get_data(reason);
					}
					if((password = purple_xmlnode_get_child(child, "password"))) {
						g_free(jm->password);
						jm->password = purple_xmlnode_get_data(password);
					}

					jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE;
				}
			} else {
				jm->etc = g_list_append(jm->etc, child);
			}
		} else if (purple_strequal(child->name, "query")) {
			const char *node = purple_xmlnode_get_attrib(child, "node");
			if (purple_strequal(xmlns, NS_DISCO_ITEMS)
					&& purple_strequal(node, "http://jabber.org/protocol/commands")) {
				jabber_adhoc_got_list(js, jm->from, child);
			}
		}
	}

	switch(jm->type) {
		case JABBER_MESSAGE_OTHER:
			purple_debug_info("jabber",
					"Received message of unknown type: %s\n", type);
			G_GNUC_FALLTHROUGH;
		case JABBER_MESSAGE_NORMAL:
		case JABBER_MESSAGE_CHAT:
			handle_chat(jm);
			break;
		case JABBER_MESSAGE_HEADLINE:
			handle_headline(jm);
			break;
		case JABBER_MESSAGE_GROUPCHAT:
			handle_groupchat(jm);
			break;
		case JABBER_MESSAGE_GROUPCHAT_INVITE:
			handle_groupchat_invite(jm);
			break;
		case JABBER_MESSAGE_EVENT:
			jabber_handle_event(jm);
			break;
		case JABBER_MESSAGE_ERROR:
			handle_error(jm);
			break;
	}
	jabber_message_free(jm);
}

void jabber_message_send(JabberMessage *jm)
{
	PurpleXmlNode *message, *child;
	const char *type = NULL;

	message = purple_xmlnode_new("message");

	switch(jm->type) {
		case JABBER_MESSAGE_NORMAL:
			type = "normal";
			break;
		case JABBER_MESSAGE_CHAT:
		case JABBER_MESSAGE_GROUPCHAT_INVITE:
			type = "chat";
			break;
		case JABBER_MESSAGE_HEADLINE:
			type = "headline";
			break;
		case JABBER_MESSAGE_GROUPCHAT:
			type = "groupchat";
			break;
		case JABBER_MESSAGE_ERROR:
			type = "error";
			break;
		case JABBER_MESSAGE_OTHER:
		default:
			type = NULL;
			break;
	}

	if(type)
		purple_xmlnode_set_attrib(message, "type", type);

	if (jm->id)
		purple_xmlnode_set_attrib(message, "id", jm->id);

	purple_xmlnode_set_attrib(message, "to", jm->to);

	if(jm->thread_id) {
		child = purple_xmlnode_new_child(message, "thread");
		purple_xmlnode_insert_data(child, jm->thread_id, -1);
	}

	child = NULL;
	switch(jm->chat_state)
	{
		case JM_STATE_ACTIVE:
			child = purple_xmlnode_new_child(message, "active");
			break;
		case JM_STATE_COMPOSING:
			child = purple_xmlnode_new_child(message, "composing");
			break;
		case JM_STATE_PAUSED:
			child = purple_xmlnode_new_child(message, "paused");
			break;
		case JM_STATE_INACTIVE:
			child = purple_xmlnode_new_child(message, "inactive");
			break;
		case JM_STATE_GONE:
			child = purple_xmlnode_new_child(message, "gone");
			break;
		case JM_STATE_NONE:
			/* yep, nothing */
			break;
	}
	if(child)
		purple_xmlnode_set_namespace(child, "http://jabber.org/protocol/chatstates");

	if(jm->subject) {
		child = purple_xmlnode_new_child(message, "subject");
		purple_xmlnode_insert_data(child, jm->subject, -1);
	}

	if(jm->body) {
		child = purple_xmlnode_new_child(message, "body");
		purple_xmlnode_insert_data(child, jm->body, -1);
	}

	if(jm->xhtml) {
		if ((child = purple_xmlnode_from_str(jm->xhtml, -1))) {
			purple_xmlnode_insert_child(message, child);
		} else {
			purple_debug_error("jabber",
					"XHTML translation/validation failed, returning: %s\n",
					jm->xhtml);
		}
	}

	jabber_send(jm->js, message);

	purple_xmlnode_free(message);
}

/*
 * Compare the XHTML and plain strings passed in for "equality". Any HTML markup
 * other than <br/> (matches a newline) in the XHTML will cause this to return
 * FALSE.
 */
static gboolean
jabber_xhtml_plain_equal(const char *xhtml_escaped, const char *plain)
{
	int i = 0;
	int j = 0;
	gboolean ret;
	char *xhtml = purple_unescape_html(xhtml_escaped);

	while (xhtml[i] && plain[j]) {
		if (xhtml[i] == plain[j]) {
			i += 1;
			j += 1;
			continue;
		}

		if (plain[j] == '\n' && !strncmp(xhtml+i, "<br/>", 5)) {
			i += 5;
			j += 1;
			continue;
		}

		g_free(xhtml);
		return FALSE;
	}

	/* Are we at the end of both strings? */
	ret = (xhtml[i] == plain[j]) && (xhtml[i] == '\0');
	g_free(xhtml);
	return ret;
}

int
jabber_message_send_im(G_GNUC_UNUSED PurpleProtocolIM *pim,
                       PurpleConnection *gc,
                       G_GNUC_UNUSED PurpleConversation *conversation,
                       PurpleMessage *msg)
{
	JabberMessage *jm;
	JabberBuddy *jb;
	JabberBuddyResource *jbr;
	char *xhtml;
	char *tmp;
	char *resource;
	const gchar *rcpt = purple_message_get_recipient(msg);

	if (!rcpt || purple_message_is_empty(msg))
		return 0;

	resource = jabber_get_resource(rcpt);

	jb = jabber_buddy_find(purple_connection_get_protocol_data(gc), rcpt, TRUE);
	jbr = jabber_buddy_find_resource(jb, resource);

	g_free(resource);

	jm = g_new0(JabberMessage, 1);
	jm->js = purple_connection_get_protocol_data(gc);
	jm->type = JABBER_MESSAGE_CHAT;
	jm->chat_state = JM_STATE_ACTIVE;
	jm->to = g_strdup(rcpt);
	jm->id = jabber_get_next_id(jm->js);

	if(jbr) {
		if(jbr->thread_id)
			jm->thread_id = jbr->thread_id;

		if (jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
			jm->chat_state = JM_STATE_NONE;
		else {
			/* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
			   jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */
		}
	}

	tmp = purple_utf8_strip_unprintables(purple_message_get_contents(msg));
	purple_markup_html_to_xhtml(tmp, &xhtml, &jm->body);
	g_free(tmp);

	/*
	 * For backward compatibility with user expectations or for those not on
	 * the user's roster, allow sending XHTML-IM markup.
	 */
	if (jbr == NULL || jbr->caps == NULL ||
			jabber_resource_has_capability(jbr, NS_XHTML_IM)) {
		if (!jabber_xhtml_plain_equal(xhtml, jm->body))
			/* Wrap the message in <p/> for great interoperability justice. */
			jm->xhtml = g_strdup_printf("<html xmlns='" NS_XHTML_IM "'><body xmlns='" NS_XHTML "'><p>%s</p></body></html>", xhtml);
	}

	g_free(xhtml);

	jabber_message_send(jm);
	jabber_message_free(jm);
	return 1;
}

gint
jabber_message_send_chat(G_GNUC_UNUSED PurpleProtocolChat *protocol_chat,
                         PurpleConnection *gc, gint id,
                         G_GNUC_UNUSED PurpleConversation *conversation,
                         PurpleMessage *msg)
{
	JabberChat *chat;
	JabberMessage *jm;
	JabberStream *js;
	char *xhtml;
	char *tmp;

	if (!gc || purple_message_is_empty(msg))
		return 0;

	js = purple_connection_get_protocol_data(gc);
	chat = jabber_chat_find_by_id(js, id);

	if(!chat)
		return 0;

	jm = g_new0(JabberMessage, 1);
	jm->js = purple_connection_get_protocol_data(gc);
	jm->type = JABBER_MESSAGE_GROUPCHAT;
	jm->to = g_strdup_printf("%s@%s", chat->room, chat->server);
	jm->id = jabber_get_next_id(jm->js);

	tmp = purple_utf8_strip_unprintables(purple_message_get_contents(msg));
	purple_markup_html_to_xhtml(tmp, &xhtml, &jm->body);
	g_free(tmp);

	if (chat->xhtml && !jabber_xhtml_plain_equal(xhtml, jm->body))
		/* Wrap the message in <p/> for greater interoperability justice. */
		jm->xhtml = g_strdup_printf("<html xmlns='" NS_XHTML_IM "'><body xmlns='" NS_XHTML "'><p>%s</p></body></html>", xhtml);

	g_free(xhtml);

	jabber_message_send(jm);
	jabber_message_free(jm);

	return 1;
}

unsigned int
jabber_send_typing(G_GNUC_UNUSED PurpleProtocolIM *pim, PurpleConnection *gc,
                   const char *who, PurpleIMTypingState state)
{
	JabberStream *js;
	JabberMessage *jm;
	JabberBuddy *jb;
	JabberBuddyResource *jbr;
	char *resource;

	js = purple_connection_get_protocol_data(gc);
	jb = jabber_buddy_find(js, who, TRUE);
	if (!jb)
		return 0;

	resource = jabber_get_resource(who);
	jbr = jabber_buddy_find_resource(jb, resource);
	g_free(resource);

	/* We know this entity doesn't support chat states */
	if (jbr && jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
		return 0;

	/* *If* we don't have presence /and/ the buddy can't see our
	 * presence, don't send typing notifications.
	 */
	if (!jbr && !(jb->subscription & JABBER_SUB_FROM))
		return 0;

	/* TODO: figure out threading */
	jm = g_new0(JabberMessage, 1);
	jm->js = js;
	jm->type = JABBER_MESSAGE_CHAT;
	jm->to = g_strdup(who);
	jm->id = jabber_get_next_id(jm->js);

	if(PURPLE_IM_TYPING == state)
		jm->chat_state = JM_STATE_COMPOSING;
	else if(PURPLE_IM_TYPED == state)
		jm->chat_state = JM_STATE_PAUSED;
	else
		jm->chat_state = JM_STATE_ACTIVE;

	/* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
		jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */

	jabber_message_send(jm);
	jabber_message_free(jm);

	return 0;
}

mercurial