libpurple/protocols/jabber/jabber.c

Mon, 04 Sep 2023 23:04:09 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Mon, 04 Sep 2023 23:04:09 -0500
changeset 42305
a3895b6d3621
parent 42298
4671ff5c65d6
child 42410
563e7a17c220
permissions
-rw-r--r--

Add the conversation as a parameter to PurpleProtocolIM->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:
Compiled and send a message to the echo bot.

Bugs closed: PIDGIN-17825

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

/*
 * 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 <config.h>

#include <errno.h>

#include <glib/gi18n-lib.h>

#include <gplugin.h>
#include <gplugin-native.h>

#include <purple.h>

#include "auth.h"
#include "buddy.h"
#include "caps.h"
#include "chat.h"
#include "data.h"
#include "disco.h"
#include "ibb.h"
#include "iq.h"
#include "jutil.h"
#include "message.h"
#include "parser.h"
#include "presence.h"
#include "jabber.h"
#include "roster.h"
#include "oob.h"
#include "ping.h"
#include "si.h"
#include "xdata.h"
#include "pep.h"
#include "adhoccommands.h"
#include "xmpp.h"

#include "jingle/jingle.h"
#include "jingle/content.h"
#include "jingle/iceudp.h"
#include "jingle/rawudp.h"
#include "jingle/rtp.h"
#include "jingle/session.h"

#define PING_TIMEOUT 60
/* Send a whitespace keepalive to the server if we haven't sent
 * anything in the last 120 seconds
 */
#define DEFAULT_INACTIVITY_TIME 120

GList *jabber_features = NULL;
GList *jabber_identities = NULL;

static PurpleProtocol *xmpp_protocol = NULL;

static GHashTable *jabber_cmds = NULL; /* PurpleProtocol * => GSList of ids */

static gint plugin_ref = 0;

static void jabber_send_raw(PurpleProtocolServer *protocol_server, JabberStream *js, const gchar *data, gint len);
static void jabber_remove_feature(const gchar *namespace);
static gboolean jabber_initiate_media(PurpleProtocolMedia *media, PurpleAccount *account, const char *who, PurpleMediaSessionType type);
static PurpleMediaCaps jabber_get_media_caps(PurpleProtocolMedia *media, PurpleAccount *account, const char *who);

static void jabber_stream_init(JabberStream *js)
{
	char *open_stream;

	g_free(js->stream_id);
	js->stream_id = NULL;

	open_stream = g_strdup_printf("<stream:stream to='%s' "
				          "xmlns='" NS_XMPP_CLIENT "' "
						  "xmlns:stream='" NS_XMPP_STREAMS "' "
						  "version='1.0'>",
						  js->user->domain);
	/* setup the parser fresh for each stream */
	jabber_parser_setup(js);
	jabber_send_raw(NULL, js, open_stream, -1);
	js->reinit = FALSE;
	g_free(open_stream);
}

static void
jabber_session_initialized_cb(JabberStream *js, G_GNUC_UNUSED const char *from,
                              JabberIqType type, G_GNUC_UNUSED const char *id,
                              G_GNUC_UNUSED PurpleXmlNode *packet,
                              G_GNUC_UNUSED gpointer data)
{
	if (type == JABBER_IQ_RESULT) {
		jabber_disco_items_server(js);
	} else {
		purple_connection_error(js->gc,
			PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
			("Error initializing session"));
	}
}

static void jabber_session_init(JabberStream *js)
{
	JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET);
	PurpleXmlNode *session;

	jabber_iq_set_callback(iq, jabber_session_initialized_cb, NULL);

	session = purple_xmlnode_new_child(iq->node, "session");
	purple_xmlnode_set_namespace(session, NS_XMPP_SESSION);

	jabber_iq_send(iq);
}

static void
jabber_bind_result_cb(JabberStream *js, G_GNUC_UNUSED const char *from,
                      JabberIqType type, G_GNUC_UNUSED const char *id,
                      PurpleXmlNode *packet, G_GNUC_UNUSED gpointer data)
{
	PurpleXmlNode *bind;

	if (type == JABBER_IQ_RESULT &&
			(bind = purple_xmlnode_get_child_with_namespace(packet, "bind", NS_XMPP_BIND))) {
		PurpleXmlNode *jid;
		char *full_jid;
		if((jid = purple_xmlnode_get_child(bind, "jid")) && (full_jid = purple_xmlnode_get_data(jid))) {
			jabber_id_free(js->user);

			js->user = jabber_id_new(full_jid);
			if (js->user == NULL) {
				purple_connection_error(js->gc,
					PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
					_("Invalid response from server"));
				g_free(full_jid);
				return;
			}

			js->user_jb = jabber_buddy_find(js, full_jid, TRUE);
			js->user_jb->subscription |= JABBER_SUB_BOTH;

			purple_connection_set_display_name(js->gc, full_jid);

			g_free(full_jid);
		}
	} else {
		PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR;
		char *msg = jabber_parse_error(js, packet, &reason);
		purple_connection_error(js->gc, reason, msg);
		g_free(msg);

		return;
	}

	jabber_session_init(js);
}

static char *
jabber_prep_resource(char *input)
{
	const gchar *hostname = NULL, *dot = NULL;
	gchar *result = NULL;

	/* Empty resource == don't send any */
	if (input == NULL || *input == '\0')
		return NULL;

	if (strstr(input, "__HOSTNAME__") == NULL)
		return g_strdup(input);

	/* Replace __HOSTNAME__ with hostname */
	hostname = g_get_host_name();

	/* We want only the short hostname, not the FQDN - this will prevent the
	 * resource string from being unreasonably long on systems which stuff the
	 * whole FQDN in the hostname */
	if ((dot = strchr(hostname, '.')) != NULL) {
		gchar *short_hostname = g_strndup(hostname, dot - hostname);
		result = purple_strreplace(input, "__HOSTNAME__", short_hostname);
		g_free(short_hostname);
	} else {
		result = purple_strreplace(input, "__HOSTNAME__", hostname);
	}

	return result;
}

static gboolean
jabber_process_starttls(JabberStream *js, PurpleXmlNode *packet)
{
	PurpleAccount *account = NULL;
	PurpleXmlNode *starttls = NULL;

	/* It's a secure BOSH connection, just return FALSE and skip, without doing
	 * anything extra. XEP-0206 (XMPP Over BOSH): The client SHOULD ignore any
	 * Transport Layer Security (TLS) feature since BOSH channel encryption
	 * SHOULD be negotiated at the HTTP layer.
	 *
	 * Note: we are already receiving STARTTLS at this point from a SSL/TLS BOSH
	 * connection, so it is not necessary to check if SSL is supported.
	 */
	if (js->bosh && jabber_bosh_connection_is_ssl(js->bosh)) {
		return FALSE;
	}

	/* Otherwise, it's a standard XMPP connection, or a HTTP (insecure) BOSH connection.
	 * We request STARTTLS for standard XMPP connections, but we do nothing for insecure
	 * BOSH connections, per XEP-0206. */
	if(!js->bosh) {
		jabber_send_raw(NULL, js,
				"<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>", -1);
		return TRUE;
	}

	/* It's an insecure standard XMPP connection, or an insecure BOSH connection, let's
	 * ignore STARTTLS even it's required by the server to prevent disabling HTTP BOSH
	 * entirely (sysadmin is responsible to provide HTTPS-only BOSH if security is required),
	 * and emit errors if encryption is required by the user. */
	starttls = purple_xmlnode_get_child(packet, "starttls");
	if(!js->bosh && purple_xmlnode_get_child(starttls, "required")) {
		purple_connection_error(js->gc,
				PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
				_("Server requires TLS/SSL, but no TLS/SSL support was found."));
		return TRUE;
	}

	account = purple_connection_get_account(js->gc);
	if (purple_strequal("require_tls", purple_account_get_string(account, "connection_security", JABBER_DEFAULT_REQUIRE_TLS))) {
		purple_connection_error(js->gc,
				PURPLE_CONNECTION_ERROR_NO_SSL_SUPPORT,
				_("You require encryption, but no TLS/SSL support was found."));
		return TRUE;
	}

	return FALSE;
}

void jabber_stream_features_parse(JabberStream *js, PurpleXmlNode *packet)
{
	PurpleAccount *account = purple_connection_get_account(js->gc);
	const char *connection_security =
		purple_account_get_string(account, "connection_security", JABBER_DEFAULT_REQUIRE_TLS);

	if (purple_xmlnode_get_child(packet, "starttls")) {
		if (jabber_process_starttls(js, packet)) {
			jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION);
			return;
		}
	} else if (purple_strequal(connection_security, "require_tls") && !jabber_stream_is_ssl(js)) {
		purple_connection_error(js->gc,
			 PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR,
			_("You require encryption, but it is not available on this server."));
		return;
	}

	if(purple_xmlnode_get_child(packet, "mechanisms")) {
		jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING);
		jabber_auth_start(js, packet);
	} else if(purple_xmlnode_get_child(packet, "bind")) {
		PurpleXmlNode *bind, *resource;
		char *requested_resource;
		JabberIq *iq = jabber_iq_new(js, JABBER_IQ_SET);
		bind = purple_xmlnode_new_child(iq->node, "bind");
		purple_xmlnode_set_namespace(bind, NS_XMPP_BIND);
		requested_resource = jabber_prep_resource(js->user->resource);

		if (requested_resource != NULL) {
			resource = purple_xmlnode_new_child(bind, "resource");
			purple_xmlnode_insert_data(resource, requested_resource, -1);
			g_free(requested_resource);
		}

		jabber_iq_set_callback(iq, jabber_bind_result_cb, NULL);

		jabber_iq_send(iq);
	} else if (purple_xmlnode_get_child_with_namespace(packet, "ver", NS_ROSTER_VERSIONING)) {
		js->server_caps |= JABBER_CAP_ROSTER_VERSIONING;
	} else /* if(purple_xmlnode_get_child_with_namespace(packet, "auth")) */ {
		/* If we get an empty stream:features packet, or we explicitly get
		 * an auth feature with namespace http://jabber.org/features/iq-auth
		 * we should revert back to iq:auth authentication, even though we're
		 * connecting to an XMPP server.  */
		jabber_stream_set_state(js, JABBER_STREAM_AUTHENTICATING);
		jabber_auth_start_old(js);
	}
}

static void jabber_stream_handle_error(JabberStream *js, PurpleXmlNode *packet)
{
	PurpleConnectionError reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR;
	char *msg = jabber_parse_error(js, packet, &reason);

	purple_connection_error(js->gc, reason, msg);

	g_free(msg);
}

static void tls_init(JabberStream *js);

void jabber_process_packet(JabberStream *js, PurpleXmlNode **packet)
{
	const char *name;
	const char *xmlns;

	purple_signal_emit(purple_connection_get_protocol(js->gc), "jabber-receiving-xmlnode", js->gc, packet);

	/* if the signal leaves us with a null packet, we're done */
	if(NULL == *packet)
		return;

	name = (*packet)->name;
	xmlns = purple_xmlnode_get_namespace(*packet);

	if (purple_strequal(name, "iq")) {
		jabber_iq_parse(js, *packet);
	} else if (purple_strequal(name, "presence")) {
		jabber_presence_parse(js, *packet);
	} else if (purple_strequal(name, "message")) {
		jabber_message_parse(js, *packet);
	} else if (purple_strequal(xmlns, NS_XMPP_STREAMS)) {
		if (purple_strequal(name, "features"))
			jabber_stream_features_parse(js, *packet);
		else if (purple_strequal(name, "error"))
			jabber_stream_handle_error(js, *packet);
	} else if (purple_strequal(xmlns, NS_XMPP_SASL)) {
		if (js->state != JABBER_STREAM_AUTHENTICATING)
			purple_debug_warning("jabber", "Ignoring spurious SASL stanza %s\n", name);
		else {
			if (purple_strequal(name, "challenge"))
				jabber_auth_handle_challenge(js, *packet);
			else if (purple_strequal(name, "success"))
				jabber_auth_handle_success(js, *packet);
			else if (purple_strequal(name, "failure"))
				jabber_auth_handle_failure(js, *packet);
		}
	} else if (purple_strequal(xmlns, NS_XMPP_TLS)) {
		if (js->state != JABBER_STREAM_INITIALIZING_ENCRYPTION ||
		    G_IS_TLS_CONNECTION(js->stream)) {
			purple_debug_warning("jabber", "Ignoring spurious %s\n", name);
		} else {
			if (purple_strequal(name, "proceed"))
				tls_init(js);
			/* TODO: Handle <failure/>, I guess? */
		}
	} else {
		purple_debug_warning("jabber", "Unknown packet: %s\n", name);
	}
}

static void
jabber_push_bytes_cb(GObject *source, GAsyncResult *res, gpointer data)
{
	PurpleQueuedOutputStream *stream = PURPLE_QUEUED_OUTPUT_STREAM(source);
	JabberStream *js = data;
	gboolean result;
	GError *error = NULL;

	result = purple_queued_output_stream_push_bytes_finish(stream, res, &error);

	if (!result) {
		purple_queued_output_stream_clear_queue(stream);

		if (error->code != G_IO_ERROR_CANCELLED) {
			g_prefix_error(&error, "%s", _("Lost connection with server: "));
			purple_connection_take_error(js->gc, error);
		} else {
			g_error_free(error);
		}
	}
}

static gboolean do_jabber_send_raw(JabberStream *js, const char *data, int len)
{
	GBytes *output;
	gboolean success = TRUE;

	g_return_val_if_fail(len > 0, FALSE);

	if (js->state == JABBER_STREAM_CONNECTED)
		jabber_stream_restart_inactivity_timer(js);

	output = g_bytes_new(data, len);
	purple_queued_output_stream_push_bytes_async(
	        js->output, output, G_PRIORITY_DEFAULT, js->cancellable,
	        jabber_push_bytes_cb, js);
	g_bytes_unref(output);

	return success;
}

static void
jabber_send_raw(G_GNUC_UNUSED PurpleProtocolServer *protocol_server,
                JabberStream *js, const char *data, gint len)
{
	PurpleConnection *gc;
	PurpleAccount *account;

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

	g_return_if_fail(data != NULL);

	/* because printing a tab to debug every minute gets old */
	if (!purple_strequal(data, "\t")) {
		const char *username;
		char *text = NULL, *last_part = NULL, *tag_start = NULL;

		/* Because debug logs with plaintext passwords make me sad */
		if (!purple_debug_is_unsafe() && js->state != JABBER_STREAM_CONNECTED &&
				/* Either <auth> or <query><password>... */
				(((tag_start = strstr(data, "<auth ")) &&
					strstr(data, "xmlns='" NS_XMPP_SASL "'")) ||
				((tag_start = strstr(data, "<query ")) &&
					strstr(data, "xmlns='jabber:iq:auth'>") &&
					(tag_start = strstr(tag_start, "<password>"))))) {
			char *data_start, *tag_end = strchr(tag_start, '>');
			text = g_strdup(data);

			/* Better to print out some wacky debugging than crash
			 * due to a plugin sending bad xml */
			if (tag_end == NULL)
				tag_end = tag_start;

			data_start = text + (tag_end - data) + 1;

			last_part = strchr(data_start, '<');
			*data_start = '\0';
		}

		username = purple_connection_get_display_name(gc);
		if(username == NULL) {
			PurpleContactInfo *info = PURPLE_CONTACT_INFO(account);
			username = purple_contact_info_get_username(info);
		}

		purple_debug_misc("jabber", "Sending%s (%s): %s%s%s\n",
				jabber_stream_is_ssl(js) ? " (ssl)" : "", username,
				text ? text : data,
				last_part ? "password removed" : "",
				last_part ? last_part : "");

		g_free(text);
	}

	purple_signal_emit(purple_connection_get_protocol(gc), "jabber-sending-text", gc, &data);
	if (data == NULL)
		return;

	if (len == -1)
		len = strlen(data);

	if (js->bosh)
		jabber_bosh_connection_send(js->bosh, data);
	else
		do_jabber_send_raw(js, data, len);
}

static gint
jabber_protocol_send_raw(G_GNUC_UNUSED PurpleProtocolServer *protocol_server,
                         PurpleConnection *gc, const gchar *buf, gint len)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);

	g_return_val_if_fail(js != NULL, -1);
	/* TODO: It's probably worthwhile to restrict this to when the account
	 * state is CONNECTED, but I can /almost/ envision reasons for wanting
	 * to do things during the connection process.
	 */

	jabber_send_raw(NULL, js, buf, len);
	return (len < 0 ? (int)strlen(buf) : len);
}

static void
jabber_send_signal_cb(PurpleConnection *pc, PurpleXmlNode **packet,
                      G_GNUC_UNUSED gpointer unused)
{
	JabberStream *js;
	char *txt;
	int len;

	if (NULL == packet)
		return;

	PURPLE_ASSERT_CONNECTION_IS_VALID(pc);

	js = purple_connection_get_protocol_data(pc);

	if (NULL == js)
		return;

	if (js->bosh)
		if (purple_strequal((*packet)->name, "message") ||
				purple_strequal((*packet)->name, "iq") ||
				purple_strequal((*packet)->name, "presence"))
			purple_xmlnode_set_namespace(*packet, NS_XMPP_CLIENT);
	txt = purple_xmlnode_to_str(*packet, &len);
	jabber_send_raw(NULL, js, txt, len);
	g_free(txt);
}

void jabber_send(JabberStream *js, PurpleXmlNode *packet)
{
	purple_signal_emit(purple_connection_get_protocol(js->gc), "jabber-sending-xmlnode", js->gc, &packet);
}

static gboolean jabber_keepalive_timeout(PurpleConnection *gc)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);
	purple_connection_error(gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
					_("Ping timed out"));
	js->keepalive_timeout = 0;
	return FALSE;
}

static void
jabber_keepalive(G_GNUC_UNUSED PurpleProtocolServer *protocol_server,
                 PurpleConnection *gc)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);

	if (js->keepalive_timeout == 0) {
		jabber_keepalive_ping(js);
		js->keepalive_timeout = g_timeout_add_seconds(120,
				G_SOURCE_FUNC(jabber_keepalive_timeout), gc);
	}
}

static int
jabber_get_keepalive_interval(G_GNUC_UNUSED PurpleProtocolServer *protocol_server)
{
	return PING_TIMEOUT;
}

static gboolean
jabber_recv_cb(GObject *stream, gpointer data)
{
	PurpleConnection *gc = data;
	JabberStream *js = purple_connection_get_protocol_data(gc);
	gssize len;
	gchar buf[4096];
	GError *error = NULL;

	PURPLE_ASSERT_CONNECTION_IS_VALID(gc);

	do {
		len = g_pollable_input_stream_read_nonblocking(
		        G_POLLABLE_INPUT_STREAM(stream), buf, sizeof(buf) - 1,
		        js->cancellable, &error);
		if (len == 0) {
			purple_connection_error(js->gc,
			                        PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
			                        _("Server closed the connection"));
			js->inpa = 0;
			return G_SOURCE_REMOVE;
		} else if (len < 0) {
			if (error->code == G_IO_ERROR_WOULD_BLOCK) {
				g_error_free(error);
				return G_SOURCE_CONTINUE;
			} else if (error->code == G_IO_ERROR_CANCELLED) {
				g_error_free(error);
			} else {
				g_prefix_error(&error, "%s",
				               _("Lost connection with server: "));
				purple_connection_take_error(js->gc, error);
			}
			js->inpa = 0;
			return G_SOURCE_REMOVE;
		}

		purple_connection_update_last_received(gc);
		buf[len] = '\0';
		purple_debug_misc("jabber", "Recv (%" G_GSSIZE_FORMAT "): %s", len,
		                  buf);
		jabber_parser_process(js, buf, len);
		if(js->reinit)
			jabber_stream_init(js);
	} while (len > 0);

	return G_SOURCE_CONTINUE;
}

static void
jabber_stream_connect_finish(JabberStream *js, GIOStream *stream)
{
	GSource *source;

	js->stream = stream;
	js->input = g_object_ref(g_io_stream_get_input_stream(js->stream));
	js->output = purple_queued_output_stream_new(
	        g_io_stream_get_output_stream(js->stream));

	if (js->state == JABBER_STREAM_CONNECTING) {
		jabber_send_raw(NULL, js, "<?xml version='1.0' ?>", -1);
	}

	jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING);
	source = g_pollable_input_stream_create_source(
	        G_POLLABLE_INPUT_STREAM(js->input), js->cancellable);
	g_source_set_callback(source, G_SOURCE_FUNC(jabber_recv_cb), js->gc, NULL);
	js->inpa = g_source_attach(source, NULL);
	g_source_unref(source);
}

static void
jabber_login_callback(GObject *source_object, GAsyncResult *res, gpointer data)
{
	GSocketClient *client = G_SOCKET_CLIENT(source_object);
	JabberStream *js = data;
	GSocketConnection *conn;
	GIOStream *stream;
	gboolean is_old_ssl = g_socket_client_get_tls(client);
	GError *error = NULL;

	conn = g_socket_client_connect_to_host_finish(client, res, &error);
	if (conn == NULL) {
		if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			g_error_free(error);
			return;
		} else if (is_old_ssl) {
			/* Old-style SSL only makes a direct connection, or fails. */
			purple_connection_take_error(js->gc, error);
			return;
		}
		g_error_free(error);

		purple_connection_error(js->gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		                        _("Unable to connect"));

		purple_connection_error(js->gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		                        _("Unable to connect"));

		return;
	}

	if (is_old_ssl) {
		stream = G_IO_STREAM(g_tcp_wrapper_connection_get_base_io_stream(
		        G_TCP_WRAPPER_CONNECTION(conn)));
	} else {
		stream = G_IO_STREAM(conn);
	}

	jabber_stream_connect_finish(js, stream);

	if (is_old_ssl) {
		/* Tell the app that we're doing encryption */
		jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION);
	}
}

static void
tls_handshake_cb(GObject *source_object, GAsyncResult *res, gpointer data)
{
	JabberStream *js = data;
	GError *error = NULL;

	if (!g_tls_connection_handshake_finish(G_TLS_CONNECTION(source_object), res,
	                                       &error)) {
		if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			/* Connection already closed/freed. Escape. */
		} else if (g_error_matches(error, G_TLS_ERROR, G_TLS_ERROR_HANDSHAKE)) {
			/* In Gio, a handshake error is because of the cert */
			purple_connection_error(js->gc,
			                        PURPLE_CONNECTION_ERROR_CERT_OTHER_ERROR,
			                        _("SSL peer presented an invalid certificate"));
		} else {
			/* Report any other errors as handshake failing */
			purple_connection_error(js->gc,
			                        PURPLE_CONNECTION_ERROR_ENCRYPTION_ERROR,
			                        _("SSL Handshake Failed"));
		}

		g_error_free(error);
		return;
	}

	jabber_stream_connect_finish(js, js->stream);

	/* Tell the app that we're doing encryption */
	jabber_stream_set_state(js, JABBER_STREAM_INITIALIZING_ENCRYPTION);
}

static void tls_init(JabberStream *js)
{
	GSocketConnectable *identity;
	GIOStream *tls_conn;
	GError *error = NULL;

	g_clear_handle_id(&js->inpa, g_source_remove);
	js->input = NULL;
	g_filter_output_stream_set_close_base_stream(
	        G_FILTER_OUTPUT_STREAM(js->output), FALSE);
	g_output_stream_close(G_OUTPUT_STREAM(js->output), js->cancellable, NULL);
	js->output = NULL;

	identity = g_network_address_new(js->certificate_CN, 0);
	tls_conn = g_tls_client_connection_new(js->stream, identity, &error);
	g_object_unref(identity);

	if (tls_conn == NULL) {
		purple_debug_warning("jabber",
		                     "Error creating TLS client connection: %s",
		                     error->message);
		g_clear_error(&error);
		purple_connection_error(js->gc, PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
		                        _("SSL Connection Failed"));
		return;
	}

	g_clear_object(&js->stream);
	js->stream = G_IO_STREAM(tls_conn);

	g_tls_connection_handshake_async(G_TLS_CONNECTION(tls_conn),
	                                 G_PRIORITY_DEFAULT, js->cancellable,
	                                 tls_handshake_cb, js);
}

static void
srv_resolved_cb(GObject *source_object, GAsyncResult *result, gpointer data)
{
	GSocketClient *client = G_SOCKET_CLIENT(source_object);
	JabberStream *js = data;
	GSocketConnection *conn;
	GError *error = NULL;

	conn = g_socket_client_connect_to_service_finish(client, result, &error);
	if (error) {
		if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			/* Do nothing; cancelled. */

		} else if (g_error_matches(error, G_RESOLVER_ERROR,
		                           G_RESOLVER_ERROR_NOT_FOUND)) {
			/* If there was no response, then attempt fallback behaviour of XMPP
			 * Core 3.2.2. */
			purple_debug_warning(
			        "jabber",
			        "SRV lookup failed, proceeding with normal connection : %s",
			        error->message);

			g_socket_client_connect_to_host_async(
			        js->client, js->user->domain,
			        purple_account_get_int(
			                purple_connection_get_account(js->gc), "port",
			                5222),
			        js->cancellable, jabber_login_callback, js);

		} else {
			/* If resolving failed or connecting failed, then just error out, as
			 * in XMPP Core 3.2.1 step 8. */
			purple_connection_g_error(js->gc, error);
		}

		g_error_free(error);
		return;
	}

	jabber_stream_connect_finish(js, G_IO_STREAM(conn));
}

static JabberStream *
jabber_stream_new(PurpleAccount *account)
{
	PurpleConnection *gc = purple_account_get_connection(account);
	PurpleContactInfo *info = PURPLE_CONTACT_INFO(account);
	GProxyResolver *resolver;
	GError *error = NULL;
	JabberStream *js;
	PurplePresence *presence;
	gchar *user;
	gchar *slash;

	resolver = purple_proxy_get_proxy_resolver(account, &error);
	if (resolver == NULL) {
		purple_debug_error("jabber", "Unable to get account proxy resolver: %s",
		                   error->message);
		g_error_free(error);
		return NULL;
	}

	js = g_new0(JabberStream, 1);
	purple_connection_set_protocol_data(gc, js);
	js->gc = gc;
	js->http_conns = soup_session_new_with_options("proxy-resolver", resolver,
	                                               NULL);
	g_object_unref(resolver);

	/* we might want to expose this at some point */
	js->cancellable = g_cancellable_new();

	user = g_strdup(purple_contact_info_get_username(info));
	/* jabber_id_new doesn't accept "user@domain/" as valid */
	slash = strchr(user, '/');
	if (slash && *(slash + 1) == '\0')
		*slash = '\0';
	js->user = jabber_id_new(user);

	if (!js->user) {
		purple_connection_error(gc,
			PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
			_("Invalid XMPP ID"));
		g_free(user);
		/* Destroying the connection will free the JabberStream */
		return NULL;
	}

	if (!js->user->node || *(js->user->node) == '\0') {
		purple_connection_error(gc,
			PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
			_("Invalid XMPP ID. Username portion must be set."));
		g_free(user);
		/* Destroying the connection will free the JabberStream */
		return NULL;
	}

	if (!js->user->domain || *(js->user->domain) == '\0') {
		purple_connection_error(gc,
			PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
			_("Invalid XMPP ID. Domain must be set."));
		g_free(user);
		/* Destroying the connection will free the JabberStream */
		return NULL;
	}

	js->buddies = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, (GDestroyNotify)jabber_buddy_free);

	/* This is overridden during binding, but we need it here
	 * in case the server only does legacy non-sasl auth!.
	 */
	purple_connection_set_display_name(gc, user);

	js->user_jb = jabber_buddy_find(js, user, TRUE);
	g_free(user);
	if (!js->user_jb) {
		/* This basically *can't* fail, but for good measure... */
		purple_connection_error(gc,
			PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
			_("Invalid XMPP ID"));
		/* Destroying the connection will free the JabberStream */
		g_return_val_if_reached(NULL);
	}

	js->user_jb->subscription |= JABBER_SUB_BOTH;

	js->iq_callbacks = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, (GDestroyNotify)jabber_iq_callbackdata_free);
	js->chats = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, (GDestroyNotify)jabber_chat_free);
	js->next_id = g_random_int();
	js->keepalive_timeout = 0;
	js->max_inactivity = DEFAULT_INACTIVITY_TIME;
	/* Set the default protocol version to 1.0. Overridden in parser.c. */
	js->protocol_version.major = 1;
	js->protocol_version.minor = 0;
	js->sessions = NULL;

	/* if we are idle, set idle-ness on the stream (this could happen if we get
		disconnected and the reconnects while being idle. I don't think it makes
		sense to do this when registering a new account... */
	presence = purple_account_get_presence(account);
	if (purple_presence_is_idle(presence)) {
		GDateTime *idle = purple_presence_get_idle_time(presence);

		js->idle = 0;
		if(idle != NULL) {
			js->idle = g_date_time_to_unix(idle);
		}
	}

	return js;
}

static void
jabber_stream_connect(JabberStream *js)
{
	PurpleConnection *gc = js->gc;
	PurpleAccount *account = purple_connection_get_account(gc);
	const char *connect_server = purple_account_get_string(account,
			"connect_server", "");
	const char *bosh_url = purple_account_get_string(account,
			"bosh_url", "");
	GError *error = NULL;

	jabber_stream_set_state(js, JABBER_STREAM_CONNECTING);

	/* If both BOSH and a Connect Server are specified, we prefer BOSH. I'm not
	 * attached to that choice, though.
	 */
	if (*bosh_url) {
		js->bosh = jabber_bosh_connection_new(js, bosh_url);
		if (!js->bosh) {
			purple_connection_error(gc,
				PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
				_("Malformed BOSH URL"));
		}

		return;
	}

	js->client = purple_gio_socket_client_new(account, &error);
	if (js->client == NULL) {
		purple_connection_take_error(gc, error);
		return;
	}

	js->certificate_CN = g_strdup(connect_server[0] ? connect_server : js->user->domain);

	/* if they've got old-ssl mode going, we probably want to ignore SRV lookups */
	if (purple_strequal("old_ssl", purple_account_get_string(account, "connection_security", JABBER_DEFAULT_REQUIRE_TLS))) {
		g_socket_client_set_tls(js->client, TRUE);
		g_socket_client_connect_to_host_async(
		        js->client, js->certificate_CN,
		        purple_account_get_int(account, "port", 5223), js->cancellable,
		        jabber_login_callback, js);
		return;
	}

	/* no old-ssl, so if they've specified a connect server, we'll use that, otherwise we'll
	 * invoke the magic of SRV lookups, to figure out host and port */
	if(connect_server[0]) {
		g_socket_client_connect_to_host_async(
		        js->client, connect_server,
		        purple_account_get_int(account, "port", 5222), js->cancellable,
		        jabber_login_callback, js);
	} else {
		g_socket_client_connect_to_service_async(js->client, js->user->domain,
		                                         "xmpp-client", js->cancellable,
		                                         srv_resolved_cb, js);
	}
}

static void
jabber_login(G_GNUC_UNUSED PurpleProtocol *protocol, PurpleAccount *account) {
	PurpleConnection *gc = purple_account_get_connection(account);
	JabberStream *js;
	PurpleImage *image;

	purple_connection_set_flags(gc, PURPLE_CONNECTION_FLAG_HTML |
		PURPLE_CONNECTION_FLAG_NO_IMAGES);
	js = jabber_stream_new(account);
	if (js == NULL)
		return;

	/* replace old default proxies with the new default: NULL
	 * TODO: these can eventually be removed */
	if (purple_strequal("proxy.jabber.org", purple_account_get_string(account, "ft_proxies", ""))
			|| purple_strequal("proxy.eu.jabber.org", purple_account_get_string(account, "ft_proxies", "")))
		purple_account_set_string(account, "ft_proxies", NULL);

	/*
	 * Calculate the avatar hash for our current image so we know (when we
	 * fetch our vCard and PEP avatar) if we should send our avatar to the
	 * server.
	 */
	image = purple_buddy_icons_find_account_icon(account);
	if (image != NULL) {
		js->initial_avatar_hash = g_compute_checksum_for_data(
			G_CHECKSUM_SHA1,
			purple_image_get_data(image),
			purple_image_get_data_size(image)
		);
		g_object_unref(image);
	}

	jabber_stream_connect(js);
}

/* TODO: As Will pointed out in IRC, after being notified by the core to
 * shutdown, we should async. wait for the server to send us the stream
 * termination before destroying everything. That seems like it would require
 * changing the semantics of protocol's close(), so it's a good idea for 3.0.0.
 */
static void
jabber_close(G_GNUC_UNUSED PurpleProtocol *protocol, PurpleConnection *gc) {
	JabberStream *js = purple_connection_get_protocol_data(gc);

	/* Close all of the open Jingle sessions on this stream */
	jingle_terminate_sessions(js);

	if (js->bosh) {
		jabber_bosh_connection_destroy(js->bosh);
		js->bosh = NULL;
	} else if (js->output != NULL) {
		/* We should emit the stream termination message here
		 * normally, but since we destroy the jabber stream just
		 * after, it has no way to effectively go out on the
		 * wire. Moreover, it causes a connection lost error in
		 * the output queued stream that triggers an
		 * heap-use-after-free error in jabber_push_bytes_cb().
		 *
		 * This case happens when disabling the jabber account
		 * from the dialog box.
		 *
		 * jabber_send_raw(js, "</stream:stream>", -1);
		 */
		g_clear_handle_id(&js->inpa, g_source_remove);
		purple_gio_graceful_close(js->stream, js->input,
		                          G_OUTPUT_STREAM(js->output));
	}

	g_clear_object(&js->output);
	g_clear_object(&js->input);
	g_clear_object(&js->stream);

	jabber_buddy_remove_all_pending_buddy_info_requests(js);

	jabber_parser_free(js);

	g_clear_pointer(&js->iq_callbacks, g_hash_table_destroy);
	g_clear_pointer(&js->buddies, g_hash_table_destroy);
	g_clear_pointer(&js->chats, g_hash_table_destroy);

	g_list_free_full(js->chat_servers, g_free);

	g_list_free_full(js->bs_proxies, (GDestroyNotify)jabber_bytestreams_streamhost_free);

	if (js->http_conns) {
		soup_session_abort(js->http_conns);
		g_object_unref(js->http_conns);
	}

	g_free(js->stream_id);
	g_clear_pointer(&js->user, jabber_id_free);
	g_free(js->initial_avatar_hash);
	g_free(js->avatar_hash);
	g_free(js->caps_hash);

	if (js->auth_mech && js->auth_mech->dispose)
		js->auth_mech->dispose(js);
	g_free(js->serverFQDN);
	g_list_free_full(js->commands, (GDestroyNotify)jabber_adhoc_commands_free);
	g_free(js->server_name);
	g_free(js->certificate_CN);
	g_free(js->old_msg);
	g_free(js->old_avatarhash);

	g_clear_handle_id(&js->keepalive_timeout, g_source_remove);
	g_clear_handle_id(&js->inactivity_timer, g_source_remove);
	g_clear_handle_id(&js->conn_close_timeout, g_source_remove);

	g_cancellable_cancel(js->cancellable);
	g_object_unref(G_OBJECT(js->cancellable));

	g_free(js);

	purple_connection_set_protocol_data(gc, NULL);
}

void jabber_stream_set_state(JabberStream *js, JabberStreamState state)
{
	js->state = state;
	if(state == JABBER_STREAM_INITIALIZING) {
		jabber_stream_init(js);
	} else if(state == JABBER_STREAM_CONNECTED) {
		/* Send initial presence */
		jabber_presence_send(js, TRUE);
		/* Start up the inactivity timer */
		jabber_stream_restart_inactivity_timer(js);

		purple_connection_set_state(js->gc, PURPLE_CONNECTION_STATE_CONNECTED);
	}
}

char *jabber_get_next_id(JabberStream *js)
{
	return g_strdup_printf("purple%x", js->next_id++);
}


static void
jabber_idle_set(G_GNUC_UNUSED PurpleProtocolServer *protocol_server,
                PurpleConnection *gc, gint idle)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);

	js->idle = idle ? time(NULL) - idle : idle;

	/* send out an updated prescence */
	purple_debug_info("jabber", "sending updated presence for idle\n");
	jabber_presence_send(js, FALSE);
}

void
jabber_blocklist_parse_push(G_GNUC_UNUSED JabberStream *js,
                            G_GNUC_UNUSED const char *from,
                            G_GNUC_UNUSED JabberIqType type,
                            G_GNUC_UNUSED const char *id,
                            G_GNUC_UNUSED PurpleXmlNode *child)
{
#if 0
	JabberIq *result;
	PurpleXmlNode *item;
	PurpleAccount *account;
	gboolean is_block;
	GSList *deny;

	if (!jabber_is_own_account(js, from)) {
		PurpleXmlNode *error, *x;
		result = jabber_iq_new(js, JABBER_IQ_ERROR);
		purple_xmlnode_set_attrib(result->node, "id", id);
		if (from)
			purple_xmlnode_set_attrib(result->node, "to", from);

		error = purple_xmlnode_new_child(result->node, "error");
		purple_xmlnode_set_attrib(error, "type", "cancel");
		x = purple_xmlnode_new_child(error, "not-allowed");
		purple_xmlnode_set_namespace(x, NS_XMPP_STANZAS);

		jabber_iq_send(result);
		return;
	}

	account = purple_connection_get_account(js->gc);
	is_block = purple_strequal(child->name, "block");

	item = purple_xmlnode_get_child(child, "item");
	if (!is_block && item == NULL) {
		/* Unblock everyone */
		purple_debug_info("jabber", "Received unblock push. Unblocking everyone.\n");

		while ((deny = purple_account_privacy_get_denied(account)) != NULL) {
			purple_account_privacy_deny_remove(account, deny->data, TRUE);
		}
	} else if (item == NULL) {
		/* An empty <block/> is bogus */
		PurpleXmlNode *error, *x;
		result = jabber_iq_new(js, JABBER_IQ_ERROR);
		purple_xmlnode_set_attrib(result->node, "id", id);

		error = purple_xmlnode_new_child(result->node, "error");
		purple_xmlnode_set_attrib(error, "type", "modify");
		x = purple_xmlnode_new_child(error, "bad-request");
		purple_xmlnode_set_namespace(x, NS_XMPP_STANZAS);

		jabber_iq_send(result);
		return;
	} else {
		for ( ; item; item = purple_xmlnode_get_next_twin(item)) {
			const char *jid = purple_xmlnode_get_attrib(item, "jid");
			if (jid == NULL || *jid == '\0')
				continue;

			if (is_block)
				purple_account_privacy_deny_add(account, jid, TRUE);
			else
				purple_account_privacy_deny_remove(account, jid, TRUE);
		}
	}

	result = jabber_iq_new(js, JABBER_IQ_RESULT);
	purple_xmlnode_set_attrib(result->node, "id", id);
	jabber_iq_send(result);
#endif
}

#if 0
static void jabber_blocklist_parse(JabberStream *js, const char *from,
                                   JabberIqType type, const char *id,
                                   PurpleXmlNode *packet, gpointer data)
{
	PurpleXmlNode *blocklist, *item;
	PurpleAccount *account;
	GSList *deny;

	blocklist = purple_xmlnode_get_child_with_namespace(packet,
			"blocklist", NS_SIMPLE_BLOCKING);
	account = purple_connection_get_account(js->gc);

	if (type == JABBER_IQ_ERROR || blocklist == NULL)
		return;

	/* This is the only privacy method supported by XEP-0191 */
	purple_account_set_privacy_type(account, PURPLE_ACCOUNT_PRIVACY_DENY_USERS);

	/*
	 * TODO: When account->deny is something more than a hash table, this can
	 * be re-written to find the set intersection and difference.
	 */
	while ((deny = purple_account_privacy_get_denied(account)))
		purple_account_privacy_deny_remove(account, deny->data, TRUE);

	item = purple_xmlnode_get_child(blocklist, "item");
	while (item != NULL) {
		const char *jid = purple_xmlnode_get_attrib(item, "jid");
		purple_account_privacy_deny_add(account, jid, TRUE);
		item = purple_xmlnode_get_next_twin(item);
	}
}
#endif

void
jabber_request_block_list(G_GNUC_UNUSED JabberStream *js)
{
#if 0
	JabberIq *iq;
	PurpleXmlNode *blocklist;

	iq = jabber_iq_new(js, JABBER_IQ_GET);

	blocklist = purple_xmlnode_new_child(iq->node, "blocklist");
	purple_xmlnode_set_namespace(blocklist, NS_SIMPLE_BLOCKING);

	jabber_iq_set_callback(iq, jabber_blocklist_parse, NULL);

	jabber_iq_send(iq);
#endif
}

#if 0
static void
jabber_add_deny(PurpleProtocolPrivacy *privacy, PurpleConnection *gc,
                const char *who)
{
	JabberStream *js;
	JabberIq *iq;
	PurpleXmlNode *block, *item;

	g_return_if_fail(who != NULL && *who != '\0');

	js = purple_connection_get_protocol_data(gc);
	if (js == NULL)
		return;

	if (!(js->server_caps & JABBER_CAP_BLOCKING))
	{
		purple_notify_error(NULL, _("Server doesn't support blocking"),
			_("Server doesn't support blocking"), NULL,
			purple_request_cpar_from_connection(gc));
		return;
	}

	iq = jabber_iq_new(js, JABBER_IQ_SET);

	block = purple_xmlnode_new_child(iq->node, "block");
	purple_xmlnode_set_namespace(block, NS_SIMPLE_BLOCKING);

	item = purple_xmlnode_new_child(block, "item");
	purple_xmlnode_set_attrib(item, "jid", who);

	jabber_iq_send(iq);
}

static void
jabber_remove_deny(PurpleProtocolPrivacy *privacy, PurpleConnection *gc,
                   const char *who)
{
	JabberStream *js;
	JabberIq *iq;
	PurpleXmlNode *unblock, *item;

	g_return_if_fail(who != NULL && *who != '\0');

	js = purple_connection_get_protocol_data(gc);
	if (js == NULL)
		return;

	if (!(js->server_caps & JABBER_CAP_BLOCKING))
		return;

	iq = jabber_iq_new(js, JABBER_IQ_SET);

	unblock = purple_xmlnode_new_child(iq->node, "unblock");
	purple_xmlnode_set_namespace(unblock, NS_SIMPLE_BLOCKING);

	item = purple_xmlnode_new_child(unblock, "item");
	purple_xmlnode_set_attrib(item, "jid", who);

	jabber_iq_send(iq);
}
#endif

void jabber_add_feature(const char *namespace, JabberFeatureEnabled cb) {
	JabberFeature *feat;

	g_return_if_fail(namespace != NULL);

	feat = g_new0(JabberFeature,1);
	feat->namespace = g_strdup(namespace);
	feat->is_enabled = cb;

	/* try to remove just in case it already exists in the list */
	jabber_remove_feature(namespace);

	jabber_features = g_list_append(jabber_features, feat);
}

static void jabber_feature_free(JabberFeature *feature) {
	g_return_if_fail(feature != NULL);

	g_free(feature->namespace);
	g_free(feature);
}

static void
jabber_remove_feature(const char *namespace) {
	GList *feature;
	for(feature = jabber_features; feature; feature = feature->next) {
		JabberFeature *feat = (JabberFeature*)feature->data;
		if(purple_strequal(feat->namespace, namespace)) {
			jabber_feature_free(feat);
			jabber_features = g_list_delete_link(jabber_features, feature);
			break;
		}
	}
}

gint
jabber_identity_compare(gconstpointer a, gconstpointer b)
{
	const JabberIdentity *ac;
	const JabberIdentity *bc;
	gint cat_cmp;
	gint typ_cmp;

	ac = a;
	bc = b;

	cat_cmp = g_strcmp0(ac->category, bc->category);
	if (cat_cmp != 0) {
		return cat_cmp;
	}

	typ_cmp = g_strcmp0(ac->type, bc->type);
	if (typ_cmp != 0) {
		return typ_cmp;
	}

	return g_strcmp0(ac->lang, bc->lang);
}

JabberIdentity *jabber_identity_new(const gchar *category, const gchar *type,
				    const gchar *lang, const gchar *name)
{
	JabberIdentity *id = g_new0(JabberIdentity, 1);
	id->category = g_strdup(category);
	id->type = g_strdup(type);
	id->lang = g_strdup(lang);
	id->name = g_strdup(name);
	return id;
}

void jabber_identity_free(JabberIdentity *id)
{
	g_return_if_fail(id != NULL);

	g_free(id->category);
	g_free(id->type);
	g_free(id->lang);
	g_free(id->name);
	g_free(id);
}

/*
 * jabber_add_identity:
 * @category: The category of the identity.
 * @type: The type of the identity.
 * @language: (nullable): The language localization of the name.
 * @name: The name of the identity.
 *
 * Adds an identity to this jabber library instance. For list of valid values
 * visit the website of the XMPP Registrar
 * (http://xmpp.org/registrar/disco-categories.html#client)
 *
 * Like with jabber_add_feature, if you call this while accounts are connected,
 * Bad Things will happen.
 */
static void
jabber_add_identity(const gchar *category, const gchar *type,
                    const gchar *lang, const gchar *name)
{
	GList *identity;
	JabberIdentity *ident;

	/* both required according to XEP-0030 */
	g_return_if_fail(category != NULL);
	g_return_if_fail(type != NULL);

	ident = jabber_identity_new(category, type, lang, name);

	/* Check if this identity is already there... */
	identity = g_list_find_custom(jabber_identities, ident, jabber_identity_compare);
	if (identity != NULL) {
		jabber_identity_free(ident);
		return;
	}

	jabber_identities = g_list_insert_sorted(jabber_identities, ident,
	                                         jabber_identity_compare);
}

void jabber_bytestreams_streamhost_free(JabberBytestreamsStreamhost *sh)
{
	g_return_if_fail(sh != NULL);

	g_free(sh->jid);
	g_free(sh->host);
	g_free(sh->zeroconf);
	g_free(sh);
}

gboolean jabber_stream_is_ssl(JabberStream *js)
{
	return (js->bosh && jabber_bosh_connection_is_ssl(js->bosh)) ||
	       (!js->bosh && G_IS_TLS_CONNECTION(js->stream));
}

static gboolean
inactivity_cb(gpointer data)
{
	JabberStream *js = data;

	/* We want whatever is sent to set this.  It's okay because
	 * the eventloop unsets it via the return FALSE.
	 */
	js->inactivity_timer = 0;

	if (js->bosh) {
		jabber_bosh_connection_send_keepalive(js->bosh);
	} else {
		jabber_send_raw(NULL, js, "\t", 1);
	}

	return FALSE;
}

void jabber_stream_restart_inactivity_timer(JabberStream *js)
{
	g_clear_handle_id(&js->inactivity_timer, g_source_remove);

	g_return_if_fail(js->max_inactivity > 0);

	js->inactivity_timer =
		g_timeout_add_seconds(js->max_inactivity,
		                           inactivity_cb, js);
}

static const char *
jabber_list_emblem(G_GNUC_UNUSED PurpleProtocolClient *client, PurpleBuddy *b)
{
	JabberStream *js;
	JabberBuddy *jb = NULL;
	PurpleConnection *gc = purple_account_get_connection(purple_buddy_get_account(b));

	if(!gc)
		return NULL;

	js = purple_connection_get_protocol_data(gc);
	if(js)
		jb = jabber_buddy_find(js, purple_buddy_get_name(b), FALSE);

	if(!PURPLE_BUDDY_IS_ONLINE(b)) {
		if(jb && (jb->subscription & JABBER_SUB_PENDING ||
					!(jb->subscription & JABBER_SUB_TO)))
			return "not-authorized";
	}

	if (jb) {
		JabberBuddyResource *jbr = jabber_buddy_find_resource(jb, NULL);
		if (jbr) {
			const gchar *client_type =
				jabber_resource_get_identity_category_type(jbr, "client");

			if (client_type) {
				if (purple_strequal(client_type, "phone")) {
					return "mobile";
				} else if (purple_strequal(client_type, "web")) {
					return "external";
				} else if (purple_strequal(client_type, "handheld")) {
					return "hiptop";
				} else if (purple_strequal(client_type, "bot")) {
					return "bot";
				}
				/* the default value "pc" falls through and has no emblem */
			}
		}
	}

	return NULL;
}

static GList *
jabber_status_types(G_GNUC_UNUSED PurpleProtocol *protocol,
                    G_GNUC_UNUSED PurpleAccount *account)
{
	PurpleStatusType *type;
	GList *types = NULL;
	GValue *priority_value;
	GValue *buzz_enabled;

	priority_value = purple_value_new(G_TYPE_INT);
	g_value_set_int(priority_value, 1);
	buzz_enabled = purple_value_new(G_TYPE_BOOLEAN);
	g_value_set_boolean(buzz_enabled, TRUE);
	type = purple_status_type_new_with_attrs(PURPLE_STATUS_AVAILABLE,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_ONLINE),
			NULL, TRUE, TRUE, FALSE,
			"priority", _("Priority"), priority_value,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			"nick", _("Nickname"), purple_value_new(G_TYPE_STRING),
			"buzz", _("Allow Buzz"), buzz_enabled,
			NULL);
	types = g_list_prepend(types, type);

	priority_value = purple_value_new(G_TYPE_INT);
	g_value_set_int(priority_value, 1);
	buzz_enabled = purple_value_new(G_TYPE_BOOLEAN);
	g_value_set_boolean(buzz_enabled, TRUE);
	type = purple_status_type_new_with_attrs(PURPLE_STATUS_AVAILABLE,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_CHAT),
			_("Chatty"), TRUE, TRUE, FALSE,
			"priority", _("Priority"), priority_value,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			"nick", _("Nickname"), purple_value_new(G_TYPE_STRING),
			"buzz", _("Allow Buzz"), buzz_enabled,
			NULL);
	types = g_list_prepend(types, type);

	priority_value = purple_value_new(G_TYPE_INT);
	g_value_set_int(priority_value, 0);
	buzz_enabled = purple_value_new(G_TYPE_BOOLEAN);
	g_value_set_boolean(buzz_enabled, TRUE);
	type = purple_status_type_new_with_attrs(PURPLE_STATUS_AWAY,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_AWAY),
			NULL, TRUE, TRUE, FALSE,
			"priority", _("Priority"), priority_value,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			"nick", _("Nickname"), purple_value_new(G_TYPE_STRING),
			"buzz", _("Allow Buzz"), buzz_enabled,
			NULL);
	types = g_list_prepend(types, type);

	priority_value = purple_value_new(G_TYPE_INT);
	g_value_set_int(priority_value, 0);
	buzz_enabled = purple_value_new(G_TYPE_BOOLEAN);
	g_value_set_boolean(buzz_enabled, TRUE);
	type = purple_status_type_new_with_attrs(PURPLE_STATUS_EXTENDED_AWAY,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_XA),
			NULL, TRUE, TRUE, FALSE,
			"priority", _("Priority"), priority_value,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			"nick", _("Nickname"), purple_value_new(G_TYPE_STRING),
			"buzz", _("Allow Buzz"), buzz_enabled,
			NULL);
	types = g_list_prepend(types, type);

	priority_value = purple_value_new(G_TYPE_INT);
	g_value_set_int(priority_value, 0);
	type = purple_status_type_new_with_attrs(PURPLE_STATUS_UNAVAILABLE,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_DND),
			_("Do Not Disturb"), TRUE, TRUE, FALSE,
			"priority", _("Priority"), priority_value,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			"nick", _("Nickname"), purple_value_new(G_TYPE_STRING),
			NULL);
	types = g_list_prepend(types, type);

	/*
	if(js->protocol_version == JABBER_PROTO_0_9)
		"Invisible"
	*/

	type = purple_status_type_new_with_attrs(PURPLE_STATUS_OFFLINE,
			jabber_buddy_state_get_status_id(JABBER_BUDDY_STATE_UNAVAILABLE),
			NULL, TRUE, TRUE, FALSE,
			"message", _("Message"), purple_value_new(G_TYPE_STRING),
			NULL);
	types = g_list_prepend(types, type);

	return g_list_reverse(types);
}

static void
jabber_password_change_result_cb(JabberStream *js,
                                 G_GNUC_UNUSED const char *from,
                                 JabberIqType type,
                                 G_GNUC_UNUSED const char *id,
                                 PurpleXmlNode *packet, gpointer data)
{
	if (type == JABBER_IQ_RESULT) {
		PurpleAccount *account = purple_connection_get_account(js->gc);
		PurpleCredentialManager *manager = NULL;

		purple_notify_info(js->gc, _("Password Changed"), _("Password "
			"Changed"), _("Your password has been changed."),
			purple_request_cpar_from_connection(js->gc));

		manager = purple_credential_manager_get_default();
		purple_credential_manager_write_password_async(manager, account,
		                                               (const gchar *)data,
		                                               NULL, NULL, NULL);
	} else {
		char *msg = jabber_parse_error(js, packet, NULL);

		purple_notify_error(js->gc, _("Error changing password"),
			_("Error changing password"), msg,
			purple_request_cpar_from_connection(js->gc));
		g_free(msg);
	}

	g_free(data);
}

static void
jabber_password_change_cb(JabberStream *js, PurpleRequestPage *page) {
	const char *p1, *p2;
	JabberIq *iq;
	PurpleXmlNode *query, *y;

	p1 = purple_request_page_get_string(page, "password1");
	p2 = purple_request_page_get_string(page, "password2");

	if(!purple_strequal(p1, p2)) {
		purple_notify_error(js->gc, NULL,
			_("New passwords do not match."), NULL,
			purple_request_cpar_from_connection(js->gc));
		return;
	}

	iq = jabber_iq_new_query(js, JABBER_IQ_SET, "jabber:iq:register");

	purple_xmlnode_set_attrib(iq->node, "to", js->user->domain);

	query = purple_xmlnode_get_child(iq->node, "query");

	y = purple_xmlnode_new_child(query, "username");
	purple_xmlnode_insert_data(y, js->user->node, -1);
	y = purple_xmlnode_new_child(query, "password");
	purple_xmlnode_insert_data(y, p1, -1);

	jabber_iq_set_callback(iq, jabber_password_change_result_cb, g_strdup(p1));

	jabber_iq_send(iq);
}

static void
jabber_password_change(G_GNUC_UNUSED GSimpleAction *action, GVariant *parameter,
                       G_GNUC_UNUSED gpointer data)
{
	const char *account_id = NULL;
	PurpleAccountManager *manager = NULL;
	PurpleAccount *account = NULL;
	PurpleConnection *connection = NULL;
	JabberStream *js = NULL;
	PurpleRequestPage *page;
	PurpleRequestGroup *group;
	PurpleRequestField *field;

	if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
		g_critical("XMPP Change Password action parameter is of incorrect type %s",
		           g_variant_get_type_string(parameter));
	}

	account_id = g_variant_get_string(parameter, NULL);
	manager = purple_account_manager_get_default();
	account = purple_account_manager_find_by_id(manager, account_id);
	connection = purple_account_get_connection(account);
	g_clear_object(&account);
	js = purple_connection_get_protocol_data(connection);

	page = purple_request_page_new();
	group = purple_request_group_new(NULL);
	purple_request_page_add_group(page, group);

	field = purple_request_field_string_new("password1", _("Password"),
			"", FALSE);
	purple_request_field_string_set_masked(PURPLE_REQUEST_FIELD_STRING(field),
	                                       TRUE);
	purple_request_field_set_required(field, TRUE);
	purple_request_group_add_field(group, field);

	field = purple_request_field_string_new("password2", _("Password (again)"),
			"", FALSE);
	purple_request_field_string_set_masked(PURPLE_REQUEST_FIELD_STRING(field),
	                                       TRUE);
	purple_request_field_set_required(field, TRUE);
	purple_request_group_add_field(group, field);

	purple_request_fields(connection, _("Change XMPP Password"),
	                      _("Change XMPP Password"),
	                      _("Please enter your new password"),
	                      page,
	                      _("OK"), G_CALLBACK(jabber_password_change_cb),
	                      _("Cancel"), NULL,
	                      purple_request_cpar_from_connection(connection), js);
}

static const gchar *
xmpp_protocol_actions_get_prefix(G_GNUC_UNUSED PurpleProtocolActions *actions)
{
	return "prpl-xmpp";
}

static GActionGroup *
xmpp_protocol_actions_get_action_group(G_GNUC_UNUSED PurpleProtocolActions *actions,
                                       PurpleConnection *connection)
{
	JabberStream *js = purple_connection_get_protocol_data(connection);
	GSimpleActionGroup *group = NULL;
	GActionEntry entries[] = {
		{
			.name = "set-user-info",
			.activate = jabber_setup_set_info,
			.parameter_type = "s",
		},
		{
			.name = "change-password",
			.activate = jabber_password_change,
			.parameter_type = "s",
		},
	};
	gsize nentries = G_N_ELEMENTS(entries);

	group = g_simple_action_group_new();
	g_action_map_add_action_entries(G_ACTION_MAP(group), entries, nentries,
	                                NULL);

	if(js->pep) {
		jabber_pep_add_action_entries(group);
	}

#if 0
	if(js->commands) {
		jabber_adhoc_add_server_action_entries(js, group);
	}
#endif

	return G_ACTION_GROUP(group);
}

static GMenu *
xmpp_protocol_actions_get_menu(G_GNUC_UNUSED PurpleProtocolActions *actions,
                               G_GNUC_UNUSED PurpleConnection *connection)
{
	GMenu *menu = NULL;
	GMenuItem *item = NULL;

	menu = g_menu_new();

	item = g_menu_item_new(_("Set User Info..."), "prpl-xmpp.set-user-info");
	g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
	                          "account");
	g_menu_append_item(menu, item);
	g_object_unref(item);

	item = g_menu_item_new(_("Change Password..."),
	                       "prpl-xmpp.change-password");
	g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
	                          "account");
	g_menu_append_item(menu, item);
	g_object_unref(item);

	jabber_pep_append_menu(menu);

#if 0
	if(js->commands) {
		jabber_adhoc_append_server_menu(js, menu);
	}
#endif

	return menu;
}

static PurpleChat *
jabber_find_blist_chat(G_GNUC_UNUSED PurpleProtocolClient *client,
                       PurpleAccount *account, const char *name)
{
	PurpleBlistNode *gnode, *cnode;
	JabberID *jid;

	if(!(jid = jabber_id_new(name)))
		return NULL;

	for (gnode = purple_blist_get_default_root(); gnode;
	     gnode = purple_blist_node_get_sibling_next(gnode)) {
		for(cnode = purple_blist_node_get_first_child(gnode);
				cnode;
				cnode = purple_blist_node_get_sibling_next(cnode)) {
			PurpleChat *chat = (PurpleChat*)cnode;
			const char *room, *server;
			GHashTable *components;
			if(!PURPLE_IS_CHAT(cnode))
				continue;

			if (purple_chat_get_account(chat) != account)
				continue;

			components = purple_chat_get_components(chat);
			if(!(room = g_hash_table_lookup(components, "room")))
				continue;
			if(!(server = g_hash_table_lookup(components, "server")))
				continue;

			/* FIXME: Collate is wrong in a few cases here; this should be prepped */
			if(jid->node && jid->domain &&
					!g_utf8_collate(room, jid->node) && !g_utf8_collate(server, jid->domain)) {
				jabber_id_free(jid);
				return chat;
			}
		}
	}
	jabber_id_free(jid);
	return NULL;
}

static void
jabber_convo_closed(G_GNUC_UNUSED PurpleProtocolClient *client,
                    PurpleConnection *gc, const char *who)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);
	JabberID *jid;
	JabberBuddy *jb;
	JabberBuddyResource *jbr;

	if(!(jid = jabber_id_new(who)))
		return;

	if((jb = jabber_buddy_find(js, who, TRUE)) &&
			(jbr = jabber_buddy_find_resource(jb, jid->resource))) {
		g_free(jbr->thread_id);
		jbr->thread_id = NULL;
	}

	jabber_id_free(jid);
}

static const gchar *
jabber_client_normalize(G_GNUC_UNUSED PurpleProtocolClient *client,
                        PurpleAccount *account, const char *who)
{
	return jabber_normalize(account, who);
}

char *jabber_parse_error(JabberStream *js,
                         PurpleXmlNode *packet,
                         PurpleConnectionError *reason)
{
	PurpleXmlNode *error;
	const char *code = NULL, *text = NULL;
	const char *xmlns = purple_xmlnode_get_namespace(packet);
	char *cdata = NULL;

#define SET_REASON(x) \
	if(reason != NULL) { *reason = x; }

	if((error = purple_xmlnode_get_child(packet, "error"))) {
		PurpleXmlNode *t = purple_xmlnode_get_child_with_namespace(error, "text", NS_XMPP_STANZAS);
		if (t)
			cdata = purple_xmlnode_get_data(t);

		code = purple_xmlnode_get_attrib(error, "code");

		/* Stanza errors */
		if(purple_xmlnode_get_child(error, "bad-request")) {
			text = _("Bad Request");
		} else if(purple_xmlnode_get_child(error, "conflict")) {
			SET_REASON(PURPLE_CONNECTION_ERROR_NAME_IN_USE);
			text = _("Conflict");
		} else if(purple_xmlnode_get_child(error, "feature-not-implemented")) {
			text = _("Feature Not Implemented");
		} else if(purple_xmlnode_get_child(error, "forbidden")) {
			text = _("Forbidden");
		} else if(purple_xmlnode_get_child(error, "gone")) {
			text = _("Gone");
		} else if(purple_xmlnode_get_child(error, "internal-server-error")) {
			text = _("Internal Server Error");
		} else if(purple_xmlnode_get_child(error, "item-not-found")) {
			text = _("Item Not Found");
		} else if(purple_xmlnode_get_child(error, "jid-malformed")) {
			text = _("Malformed XMPP ID");
		} else if(purple_xmlnode_get_child(error, "not-acceptable")) {
			text = _("Not Acceptable");
		} else if(purple_xmlnode_get_child(error, "not-allowed")) {
			text = _("Not Allowed");
		} else if(purple_xmlnode_get_child(error, "not-authorized")) {
			text = _("Not Authorized");
		} else if(purple_xmlnode_get_child(error, "payment-required")) {
			text = _("Payment Required");
		} else if(purple_xmlnode_get_child(error, "recipient-unavailable")) {
			text = _("Recipient Unavailable");
		} else if(purple_xmlnode_get_child(error, "redirect")) {
			/* XXX */
		} else if(purple_xmlnode_get_child(error, "registration-required")) {
			text = _("Registration Required");
		} else if(purple_xmlnode_get_child(error, "remote-server-not-found")) {
			text = _("Remote Server Not Found");
		} else if(purple_xmlnode_get_child(error, "remote-server-timeout")) {
			text = _("Remote Server Timeout");
		} else if(purple_xmlnode_get_child(error, "resource-constraint")) {
			text = _("Server Overloaded");
		} else if(purple_xmlnode_get_child(error, "service-unavailable")) {
			text = _("Service Unavailable");
		} else if(purple_xmlnode_get_child(error, "subscription-required")) {
			text = _("Subscription Required");
		} else if(purple_xmlnode_get_child(error, "unexpected-request")) {
			text = _("Unexpected Request");
		} else if(purple_xmlnode_get_child(error, "undefined-condition")) {
			text = _("Unknown Error");
		}
	} else if(purple_strequal(xmlns, NS_XMPP_SASL)) {
		/* Most common reason can be the default */
		SET_REASON(PURPLE_CONNECTION_ERROR_NETWORK_ERROR);
		if(purple_xmlnode_get_child(packet, "aborted")) {
			text = _("Authorization Aborted");
		} else if(purple_xmlnode_get_child(packet, "incorrect-encoding")) {
			text = _("Incorrect encoding in authorization");
		} else if(purple_xmlnode_get_child(packet, "invalid-authzid")) {
			text = _("Invalid authzid");
		} else if(purple_xmlnode_get_child(packet, "invalid-mechanism")) {
			text = _("Invalid Authorization Mechanism");
		} else if(purple_xmlnode_get_child(packet, "mechanism-too-weak")) {
			SET_REASON(PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE);
			text = _("Authorization mechanism too weak");
		} else if(purple_xmlnode_get_child(packet, "not-authorized")) {
			SET_REASON(PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED);
			/* Clear the password if it isn't being saved */
			if (!purple_account_get_remember_password(purple_connection_get_account(js->gc))) {
				PurpleAccount *account = purple_connection_get_account(js->gc);
				PurpleCredentialManager *manager = NULL;

				manager = purple_credential_manager_get_default();
				purple_credential_manager_clear_password_async(manager, account,
				                                               NULL, NULL,
				                                               NULL);
			}
			text = _("Not Authorized");
		} else if(purple_xmlnode_get_child(packet, "temporary-auth-failure")) {
			text = _("Temporary Authentication Failure");
		} else {
			SET_REASON(PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED);
			text = _("Authentication Failure");
		}
	} else if(purple_strequal(packet->name, "stream:error") ||
			 (purple_strequal(packet->name, "error") &&
				purple_strequal(xmlns, NS_XMPP_STREAMS))) {
		/* Most common reason as default: */
		SET_REASON(PURPLE_CONNECTION_ERROR_NETWORK_ERROR);
		if(purple_xmlnode_get_child(packet, "bad-format")) {
			text = _("Bad Format");
		} else if(purple_xmlnode_get_child(packet, "bad-namespace-prefix")) {
			text = _("Bad Namespace Prefix");
		} else if(purple_xmlnode_get_child(packet, "conflict")) {
			SET_REASON(PURPLE_CONNECTION_ERROR_NAME_IN_USE);
			text = _("Resource Conflict");
		} else if(purple_xmlnode_get_child(packet, "connection-timeout")) {
			text = _("Connection Timeout");
		} else if(purple_xmlnode_get_child(packet, "host-gone")) {
			text = _("Host Gone");
		} else if(purple_xmlnode_get_child(packet, "host-unknown")) {
			text = _("Host Unknown");
		} else if(purple_xmlnode_get_child(packet, "improper-addressing")) {
			text = _("Improper Addressing");
		} else if(purple_xmlnode_get_child(packet, "internal-server-error")) {
			text = _("Internal Server Error");
		} else if(purple_xmlnode_get_child(packet, "invalid-id")) {
			text = _("Invalid ID");
		} else if(purple_xmlnode_get_child(packet, "invalid-namespace")) {
			text = _("Invalid Namespace");
		} else if(purple_xmlnode_get_child(packet, "invalid-xml")) {
			text = _("Invalid XML");
		} else if(purple_xmlnode_get_child(packet, "nonmatching-hosts")) {
			text = _("Non-matching Hosts");
		} else if(purple_xmlnode_get_child(packet, "not-authorized")) {
			text = _("Not Authorized");
		} else if(purple_xmlnode_get_child(packet, "policy-violation")) {
			text = _("Policy Violation");
		} else if(purple_xmlnode_get_child(packet, "remote-connection-failed")) {
			text = _("Remote Connection Failed");
		} else if(purple_xmlnode_get_child(packet, "resource-constraint")) {
			text = _("Resource Constraint");
		} else if(purple_xmlnode_get_child(packet, "restricted-xml")) {
			text = _("Restricted XML");
		} else if(purple_xmlnode_get_child(packet, "see-other-host")) {
			text = _("See Other Host");
		} else if(purple_xmlnode_get_child(packet, "system-shutdown")) {
			text = _("System Shutdown");
		} else if(purple_xmlnode_get_child(packet, "undefined-condition")) {
			text = _("Undefined Condition");
		} else if(purple_xmlnode_get_child(packet, "unsupported-encoding")) {
			text = _("Unsupported Encoding");
		} else if(purple_xmlnode_get_child(packet, "unsupported-stanza-type")) {
			text = _("Unsupported Stanza Type");
		} else if(purple_xmlnode_get_child(packet, "unsupported-version")) {
			text = _("Unsupported Version");
		} else if(purple_xmlnode_get_child(packet, "xml-not-well-formed")) {
			text = _("XML Not Well Formed");
		} else {
			text = _("Stream Error");
		}
	}

#undef SET_REASON

	if(text || cdata) {
		char *ret = g_strdup_printf("%s%s%s", code ? code : "",
				code ? ": " : "", text ? text : cdata);
		g_free(cdata);
		return ret;
	} else {
		return NULL;
	}
}

static PurpleCmdRet
jabber_cmd_chat_config(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                       G_GNUC_UNUSED char **args, G_GNUC_UNUSED char **error,
                       G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat)
		return PURPLE_CMD_RET_FAILED;

	jabber_chat_request_room_configure(chat);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_register(PurpleConversation *conv,
                         G_GNUC_UNUSED const char *cmd,
                         G_GNUC_UNUSED char **args, G_GNUC_UNUSED char **error,
                         G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat)
		return PURPLE_CMD_RET_FAILED;

	jabber_chat_register(chat);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_topic(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                      char **args, G_GNUC_UNUSED char **error,
                      G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat)
		return PURPLE_CMD_RET_FAILED;

	if (args && args[0] && *args[0])
		jabber_chat_change_topic(chat, args[0]);
	else {
		const char *cur = purple_chat_conversation_get_topic(PURPLE_CHAT_CONVERSATION(conv));
		char *buf, *tmp, *tmp2;

		if (cur) {
			tmp = g_markup_escape_text(cur, -1);
			tmp2 = purple_markup_linkify(tmp);
			buf = g_strdup_printf(_("current topic is: %s"), tmp2);
			g_free(tmp);
			g_free(tmp2);
		} else
			buf = g_strdup(_("No topic is set"));
		purple_conversation_write_system_message(conv, buf, PURPLE_MESSAGE_NO_LOG);
		g_free(buf);
	}

	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_nick(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                     char **args, char **error, G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if(!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	if (!jabber_resourceprep_validate(args[0])) {
		*error = g_strdup(_("Invalid nickname"));
		return PURPLE_CMD_RET_FAILED;
	}

	if (jabber_chat_change_nick(chat, args[0]))
		return PURPLE_CMD_RET_OK;
	else
		return PURPLE_CMD_RET_FAILED;
}

static PurpleCmdRet
jabber_cmd_chat_part(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                     char **args, G_GNUC_UNUSED char **error,
                     G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat)
		return PURPLE_CMD_RET_FAILED;

	jabber_chat_part(chat, args ? args[0] : NULL);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_ban(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                    char **args, char **error, G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if(!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	if(!jabber_chat_ban_user(chat, args[0], args[1])) {
		*error = g_strdup_printf(_("Unable to ban user %s"), args[0]);
		return PURPLE_CMD_RET_FAILED;
	}

	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_affiliate(PurpleConversation *conv,
                          G_GNUC_UNUSED const char *cmd, char **args,
                          char **error, G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	if (!purple_strequal(args[0], "owner") &&
	    !purple_strequal(args[0], "admin") &&
	    !purple_strequal(args[0], "member") &&
	    !purple_strequal(args[0], "outcast") &&
	    !purple_strequal(args[0], "none")) {
		*error = g_strdup_printf(_("Unknown affiliation: \"%s\""), args[0]);
		return PURPLE_CMD_RET_FAILED;
	}

	if (args[1]) {
		int i;
		char **nicks = g_strsplit(args[1], " ", -1);

		for (i = 0; nicks[i]; ++i)
			if (!jabber_chat_affiliate_user(chat, nicks[i], args[0])) {
				*error = g_strdup_printf(_("Unable to affiliate user %s as \"%s\""), nicks[i], args[0]);
				g_strfreev(nicks);
				return PURPLE_CMD_RET_FAILED;
			}

		g_strfreev(nicks);
	} else {
		jabber_chat_affiliation_list(chat, args[0]);
	}

	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_role(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                     char **args, char **error, G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if (!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	if (!purple_strequal(args[0], "moderator") &&
	    !purple_strequal(args[0], "participant") &&
	    !purple_strequal(args[0], "visitor") &&
	    !purple_strequal(args[0], "none")) {
		*error = g_strdup_printf(_("Unknown role: \"%s\""), args[0]);
		return PURPLE_CMD_RET_FAILED;
	}

	if (args[1]) {
		int i;
		char **nicks = g_strsplit(args[1], " ", -1);

		for (i = 0; nicks[i]; i++)
			if (!jabber_chat_role_user(chat, nicks[i], args[0], NULL)) {
				*error = g_strdup_printf(_("Unable to set role \"%s\" for user: %s"),
										 args[0], nicks[i]);
				g_strfreev(nicks);
				return PURPLE_CMD_RET_FAILED;
			}

		g_strfreev(nicks);
	} else {
		jabber_chat_role_list(chat, args[0]);
	}
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_invite(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                       char **args, G_GNUC_UNUSED char **error,
                       G_GNUC_UNUSED gpointer data)
{
	if(!args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	jabber_chat_invite(purple_conversation_get_connection(conv),
			purple_chat_conversation_get_id(PURPLE_CHAT_CONVERSATION(conv)), args[1] ? args[1] : "",
			args[0]);

	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_join(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                     char **args, char **error, G_GNUC_UNUSED gpointer data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));
	GHashTable *components;
	JabberID *jid = NULL;
	const char *room = NULL, *server = NULL, *handle = NULL;

	if (!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

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

	if (strchr(args[0], '@'))
		jid = jabber_id_new(args[0]);
	if (jid) {
		room   = jid->node;
		server = jid->domain;
		handle = jid->resource ? jid->resource : chat->handle;
	} else {
		/* If jabber_id_new failed, the user may have just passed in
		 * a room name.  For backward compatibility, handle that here.
		 */
		if (strchr(args[0], '@')) {
			*error = g_strdup(_("Invalid XMPP ID"));
			return PURPLE_CMD_RET_FAILED;
		}

		room   = args[0];
		server = chat->server;
		handle = chat->handle;
	}

	g_hash_table_insert(components, "room", (gpointer)room);
	g_hash_table_insert(components, "server", (gpointer)server);
	g_hash_table_insert(components, "handle", (gpointer)handle);

	if (args[1])
		g_hash_table_insert(components, "password", args[1]);

	jabber_chat_join(purple_conversation_get_connection(conv), components);

	g_hash_table_destroy(components);
	jabber_id_free(jid);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_kick(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                     char **args, char **error, G_GNUC_UNUSED void *data)
{
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));

	if(!chat || !args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	if(!jabber_chat_role_user(chat, args[0], "none", args[1])) {
		*error = g_strdup_printf(_("Unable to kick user %s"), args[0]);
		return PURPLE_CMD_RET_FAILED;
	}

	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_chat_msg(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                    char **args, G_GNUC_UNUSED char **error,
                    G_GNUC_UNUSED void *data)
{
	PurpleAccount *account = NULL;
	PurpleConnection *pc = NULL;
	PurpleProtocol *prpl = NULL;
	PurpleMessage *msg = NULL;
	JabberChat *chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));
	char *who;
	const gchar *me = NULL;

	if (!chat)
		return PURPLE_CMD_RET_FAILED;

	account = purple_connection_get_account(pc);
	me = purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(account));

	who = g_strdup_printf("%s@%s/%s", chat->room, chat->server, args[0]);
	pc = purple_conversation_get_connection(conv);
	prpl = purple_connection_get_protocol(pc);

	msg = purple_message_new_outgoing(me, who, args[1], 0);

	jabber_message_send_im(PURPLE_PROTOCOL_IM(prpl), pc, NULL, msg);

	g_free(who);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
jabber_cmd_ping(PurpleConversation *conv, G_GNUC_UNUSED const char *cmd,
                char **args, char **error, G_GNUC_UNUSED void *data)
{
	PurpleAccount *account;
	PurpleConnection *pc;

	if(!args || !args[0])
		return PURPLE_CMD_RET_FAILED;

	account = purple_conversation_get_account(conv);
	pc = purple_account_get_connection(account);

	if(!jabber_ping_jid(purple_connection_get_protocol_data(pc), args[0])) {
		*error = g_strdup_printf(_("Unable to ping user %s"), args[0]);
		return PURPLE_CMD_RET_FAILED;
	}

	return PURPLE_CMD_RET_OK;
}

static gboolean
jabber_offline_message(G_GNUC_UNUSED PurpleProtocolClient *client,
                       G_GNUC_UNUSED PurpleBuddy *buddy)
{
	return TRUE;
}

static gboolean
jabber_audio_enabled(G_GNUC_UNUSED JabberStream *js,
                     G_GNUC_UNUSED const char *namespace)
{
	PurpleMediaManager *manager = purple_media_manager_get();
	PurpleMediaCaps caps = purple_media_manager_get_ui_caps(manager);

	return (caps & (PURPLE_MEDIA_CAPS_AUDIO | PURPLE_MEDIA_CAPS_AUDIO_SINGLE_DIRECTION));
}

static gboolean
jabber_video_enabled(G_GNUC_UNUSED JabberStream *js,
                     G_GNUC_UNUSED const char *namespace)
{
	PurpleMediaManager *manager = purple_media_manager_get();
	PurpleMediaCaps caps = purple_media_manager_get_ui_caps(manager);

	return (caps & (PURPLE_MEDIA_CAPS_VIDEO | PURPLE_MEDIA_CAPS_VIDEO_SINGLE_DIRECTION));
}

typedef struct {
	PurpleProtocolMedia *media;
	PurpleAccount *account;
	gchar *who;
	PurpleMediaSessionType type;

} JabberMediaRequest;

static void
jabber_media_cancel_cb(JabberMediaRequest *request,
                       G_GNUC_UNUSED PurpleRequestPage *page)
{
	g_free(request->who);
	g_free(request);
}

static void
jabber_media_ok_cb(JabberMediaRequest *request, PurpleRequestPage *page) {
	const gchar *selected = purple_request_page_get_choice(page, "resource");
	gchar *who = g_strdup_printf("%s/%s", request->who, selected);
	jabber_initiate_media(request->media, request->account, who, request->type);

	g_free(who);
	g_free(request->who);
	g_free(request);
}

static gboolean
jabber_initiate_media(PurpleProtocolMedia *media, PurpleAccount *account,
                      const gchar *who, PurpleMediaSessionType type)
{
	PurpleConnection *gc = purple_account_get_connection(account);
	JabberStream *js = purple_connection_get_protocol_data(gc);
	JabberBuddy *jb;
	JabberBuddyResource *jbr = NULL;
	char *resource = NULL;

	if (!js) {
		purple_debug_error("jabber",
				"jabber_initiate_media: NULL stream\n");
		return FALSE;
	}

	jb = jabber_buddy_find(js, who, FALSE);

	if(!jb || !jb->resources ||
			(((resource = jabber_get_resource(who)) != NULL)
			 && (jbr = jabber_buddy_find_resource(jb, resource)) == NULL)) {
		/* no resources online, we're trying to initiate with someone
		 * whose presence we're not subscribed to, or
		 * someone who is offline.  Let's inform the user */
		char *msg;

		if(!jb) {
			msg = g_strdup_printf(_("Unable to initiate media with %s: invalid JID"), who);
		} else if(jb->subscription & JABBER_SUB_TO && !jb->resources) {
			msg = g_strdup_printf(_("Unable to initiate media with %s: user is not online"), who);
		} else if(resource) {
			msg = g_strdup_printf(_("Unable to initiate media with %s: resource is not online"), who);
		} else {
			msg = g_strdup_printf(_("Unable to initiate media with %s: not subscribed to user presence"), who);
		}

		purple_notify_error(account, _("Media Initiation Failed"),
			_("Media Initiation Failed"), msg,
			purple_request_cpar_from_connection(gc));
		g_free(msg);
		g_free(resource);
		return FALSE;
	} else if(jbr != NULL) {
		/* they've specified a resource, no need to ask or
		 * default or anything, just do it */

		g_free(resource);

		return jingle_rtp_initiate_media(js, who, type);
	} else if(!jb->resources->next) {
		/* only 1 resource online (probably our most common case)
		 * so no need to ask who to initiate with */
		gchar *name;
		gboolean result;
		jbr = jb->resources->data;
		name = g_strdup_printf("%s/%s", who, jbr->name);
		result = jabber_initiate_media(media, account, name, type);
		g_free(name);
		return result;
	} else {
		/* we've got multiple resources,
		 * we need to pick one to initiate with */
		GList *l;
		char *msg;
		PurpleRequestPage *page = NULL;
		PurpleRequestField *field = NULL;
		PurpleRequestFieldChoice *choice = NULL;
		PurpleRequestGroup *group = NULL;
		JabberMediaRequest *request;

		field = purple_request_field_choice_new("resource", _("Resource"), 0);
		choice = PURPLE_REQUEST_FIELD_CHOICE(field);
		for(l = jb->resources; l; l = l->next)
		{
			JabberBuddyResource *ljbr = l->data;
			PurpleMediaCaps caps;
			gchar *name;
			name = g_strdup_printf("%s/%s", who, ljbr->name);
			caps = jabber_get_media_caps(media, account, name);
			g_free(name);

			if ((type & PURPLE_MEDIA_AUDIO) &&
					(type & PURPLE_MEDIA_VIDEO)) {
				if (caps & PURPLE_MEDIA_CAPS_AUDIO_VIDEO) {
					jbr = ljbr;
					purple_request_field_choice_add_full(choice, jbr->name,
					                                     g_strdup(jbr->name),
					                                     g_free);
				}
			} else if (type & (PURPLE_MEDIA_AUDIO) &&
					(caps & PURPLE_MEDIA_CAPS_AUDIO)) {
				jbr = ljbr;
				purple_request_field_choice_add_full(choice, jbr->name,
				                                     g_strdup(jbr->name),
				                                     g_free);
			}else if (type & (PURPLE_MEDIA_VIDEO) &&
					(caps & PURPLE_MEDIA_CAPS_VIDEO)) {
				jbr = ljbr;
				purple_request_field_choice_add_full(choice, jbr->name,
				                                     g_strdup(jbr->name),
				                                     g_free);
			}
		}

		if (jbr == NULL) {
			purple_debug_error("jabber",
					"No resources available\n");
			return FALSE;
		}

		if(g_list_length(purple_request_field_choice_get_elements(choice)) <= 1) {
			gchar *name;
			gboolean result;
			g_object_unref(field);
			name = g_strdup_printf("%s/%s", who, jbr->name);
			result = jabber_initiate_media(media, account, name, type);
			g_free(name);
			return result;
		}

		msg = g_strdup_printf(_("Please select the resource of %s with which you would like to start a media session."), who);
		page = purple_request_page_new();
		group =	purple_request_group_new(NULL);
		request = g_new0(JabberMediaRequest, 1);
		request->media = media;
		request->account = account;
		request->who = g_strdup(who);
		request->type = type;

		purple_request_group_add_field(group, field);
		purple_request_page_add_group(page, group);
		purple_request_fields(account, _("Select a Resource"), msg,
				NULL, page, _("Initiate Media"),
				G_CALLBACK(jabber_media_ok_cb), _("Cancel"),
				G_CALLBACK(jabber_media_cancel_cb),
				purple_request_cpar_from_account(account),
				request);

		g_free(msg);
		return TRUE;
	}

	return FALSE;
}

static PurpleMediaCaps
jabber_get_media_caps(G_GNUC_UNUSED PurpleProtocolMedia *media,
                      PurpleAccount *account, const char *who)
{
	PurpleConnection *gc = purple_account_get_connection(account);
	JabberStream *js = purple_connection_get_protocol_data(gc);
	JabberBuddy *jb;
	JabberBuddyResource *jbr;
	PurpleMediaCaps total = PURPLE_MEDIA_CAPS_NONE;
	gchar *resource;
	GList *specific = NULL, *l;

	if (!js) {
		purple_debug_info("jabber",
				"jabber_can_do_media: NULL stream\n");
		return FALSE;
	}

	jb = jabber_buddy_find(js, who, FALSE);

	if (!jb || !jb->resources) {
		/* no resources online, we're trying to get caps for someone
		 * whose presence we're not subscribed to, or
		 * someone who is offline. */
		return total;

	} else if ((resource = jabber_get_resource(who)) != NULL) {
		/* they've specified a resource, no need to ask or
		 * default or anything, just do it */
		jbr = jabber_buddy_find_resource(jb, resource);
		g_free(resource);

		if (!jbr) {
			purple_debug_error("jabber", "jabber_get_media_caps:"
					" Can't find resource %s\n", who);
			return total;
		}

		l = specific = g_list_prepend(specific, jbr);

	} else {
		/* we've got multiple resources, combine their caps */
		l = jb->resources;
	}

	for (; l; l = l->next) {
		PurpleMediaCaps caps = PURPLE_MEDIA_CAPS_NONE;
		jbr = l->data;

		if (jabber_resource_has_capability(jbr,
				JINGLE_APP_RTP_SUPPORT_AUDIO))
			caps |= PURPLE_MEDIA_CAPS_AUDIO_SINGLE_DIRECTION |
					PURPLE_MEDIA_CAPS_AUDIO;
		if (jabber_resource_has_capability(jbr,
				JINGLE_APP_RTP_SUPPORT_VIDEO))
			caps |= PURPLE_MEDIA_CAPS_VIDEO_SINGLE_DIRECTION |
					PURPLE_MEDIA_CAPS_VIDEO;
		if (caps & PURPLE_MEDIA_CAPS_AUDIO && caps &
				PURPLE_MEDIA_CAPS_VIDEO)
			caps |= PURPLE_MEDIA_CAPS_AUDIO_VIDEO;
		if (caps != PURPLE_MEDIA_CAPS_NONE) {
			if (!jabber_resource_has_capability(jbr,
					JINGLE_TRANSPORT_ICEUDP) &&
					!jabber_resource_has_capability(jbr,
					JINGLE_TRANSPORT_RAWUDP)) {
				purple_debug_info("jingle-rtp", "Buddy doesn't "
						"support the same transport types\n");
				caps = PURPLE_MEDIA_CAPS_NONE;
			} else
				caps |= PURPLE_MEDIA_CAPS_MODIFY_SESSION |
						PURPLE_MEDIA_CAPS_CHANGE_DIRECTION;
		}

		total |= caps;
	}

	g_clear_list(&specific, NULL);

	return total;
}

static gboolean
jabber_can_receive_file(G_GNUC_UNUSED PurpleProtocolXfer *prplxfer,
                        PurpleConnection *gc, const char *who)
{
	JabberStream *js = purple_connection_get_protocol_data(gc);

	if (js) {
		JabberBuddy *jb = jabber_buddy_find(js, who, FALSE);
		GList *iter;
		gboolean has_resources_without_caps = FALSE;

		/* if we didn't find a JabberBuddy, we don't have presence for this
		 buddy, let's assume they can receive files, disco should tell us
		 when actually trying */
		if (jb == NULL)
			return TRUE;

		/* find out if there is any resources without caps */
		for (iter = jb->resources; iter ; iter = g_list_next(iter)) {
			JabberBuddyResource *jbr = (JabberBuddyResource *) iter->data;

			if (!jabber_resource_know_capabilities(jbr)) {
				has_resources_without_caps = TRUE;
			}
		}

		if (has_resources_without_caps) {
			/* there is at least one resource which we don't have caps for,
			 let's assume they can receive files... */
			return TRUE;
		} else {
			/* we have caps for all the resources, see if at least one has
			 right caps */
			for (iter = jb->resources; iter ; iter = g_list_next(iter)) {
				JabberBuddyResource *jbr = (JabberBuddyResource *) iter->data;

				if (jabber_resource_has_capability(jbr, NS_SI_FILE_TRANSFER)
			    	&& (jabber_resource_has_capability(jbr,
			    			NS_BYTESTREAMS)
			        	|| jabber_resource_has_capability(jbr, NS_IBB))) {
					return TRUE;
				}
			}
			return FALSE;
		}
	} else {
		return TRUE;
	}
}

static void
jabber_register_commands(PurpleProtocol *protocol)
{
	GSList *commands = NULL;
	PurpleCmdId id;
	const gchar *proto_id = purple_protocol_get_id(protocol);

	id = purple_cmd_register("config", "", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id,
		jabber_cmd_chat_config, _("config:  Configure a chat room."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("configure", "", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id,
		jabber_cmd_chat_config, _("configure:  Configure a chat room."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("nick", "s", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id,
		jabber_cmd_chat_nick, _("nick &lt;new nickname&gt;:  "
		"Change your nickname."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("part", "s", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_part,
		_("part [message]:  Leave the room."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("register", "", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id,
		jabber_cmd_chat_register,
		_("register:  Register with a chat room."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	/* XXX: there needs to be a core /topic cmd, methinks */
	id = purple_cmd_register("topic", "s", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_topic,
		_("topic [new topic]:  View or change the topic."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("ban", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_ban,
		_("ban &lt;user&gt; [reason]:  Ban a user from the room."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("affiliate", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id,
		jabber_cmd_chat_affiliate, _("affiliate "
		"&lt;owner|admin|member|outcast|none&gt; [nick1] [nick2] ...: "
		"Get the users with an affiliation or set users' affiliation "
		"with the room."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("role", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_role,
		_("role &lt;moderator|participant|visitor|none&gt; [nick1] "
		"[nick2] ...: Get the users with a role or set users' role "
		"with the room."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("invite", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_invite,
		_("invite &lt;user&gt; [message]:  Invite a user to the room."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("join", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_join,
		_("join: &lt;room[@server]&gt; [password]:  Join a chat."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("kick", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY |
		PURPLE_CMD_FLAG_ALLOW_WRONG_ARGS, proto_id, jabber_cmd_chat_kick,
		_("kick &lt;user&gt; [reason]:  Kick a user from the room."),
		NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("msg", "ws", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id,
		jabber_cmd_chat_msg, _("msg &lt;user&gt; &lt;message&gt;:  "
		"Send a private message to another user."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	id = purple_cmd_register("ping", "w", PURPLE_CMD_P_PROTOCOL,
		PURPLE_CMD_FLAG_CHAT | PURPLE_CMD_FLAG_IM |
		PURPLE_CMD_FLAG_PROTOCOL_ONLY, proto_id, jabber_cmd_ping,
		_("ping &lt;jid&gt;:  Ping a user/component/server."), NULL);
	commands = g_slist_prepend(commands, GUINT_TO_POINTER(id));

	g_hash_table_insert(jabber_cmds, protocol, commands);
}

static void cmds_free_func(gpointer value)
{
	GSList *commands = value;
	g_slist_free_full(commands,
	                  (GDestroyNotify)(GCallback)purple_cmd_unregister);
}

static void jabber_unregister_commands(PurpleProtocol *protocol)
{
	g_hash_table_remove(jabber_cmds, protocol);
}

static gboolean
find_acct_cb(PurpleAccount *account, const gchar *protocol) {
	const gchar *account_protocol_id = NULL;

	account_protocol_id = purple_account_get_protocol_id(account);
	return (purple_strequal(protocol, account_protocol_id) &&
	        purple_account_is_connected(account));
}

static PurpleAccount *find_acct(const char *protocol, const char *acct_id)
{
	PurpleAccountManager *manager = NULL;
	PurpleAccount *acct = NULL;

	manager = purple_account_manager_get_default();

	/* If we have a specific acct, use it */
	if (acct_id) {
		acct = purple_account_manager_find(manager, acct_id, protocol);
		if (acct && !purple_account_is_connected(acct)) {
			g_clear_object(&acct);
		}
	} else { /* Otherwise find an active account for the protocol */
		acct = purple_account_manager_find_custom(manager,
		                                          (GEqualFunc)find_acct_cb,
		                                          protocol);
	}

	return acct;
}

static gboolean
xmpp_uri_handler(const char *proto, const char *user, GHashTable *params,
		gpointer user_data)
{
	PurpleProtocol *protocol = (PurpleProtocol *)user_data;
	const gchar *acct_id = NULL;
	PurpleAccount *acct;

	g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), FALSE);

	if (g_ascii_strcasecmp(proto, "xmpp"))
		return FALSE;

	if (params != NULL) {
		acct_id = g_hash_table_lookup(params, "account");
	}

	acct = find_acct(XMPP_PROTOCOL_ID, acct_id);

	if (!acct)
		return FALSE;

	/* xmpp:romeo@montague.net?message;subject=Test%20Message;body=Here%27s%20a%20test%20message */
	/* params is NULL if the URI has no '?' (or anything after it) */
	if (!params || g_hash_table_lookup_extended(params, "message", NULL, NULL)) {
		if (user && *user) {
			PurpleConversation *im = purple_im_conversation_new(acct, user);
			const gchar *body = NULL;

			purple_conversation_present(im);

			if(params != NULL) {
				body = g_hash_table_lookup(params, "body");
			}

			if(body && *body) {
				purple_conversation_send_confirm(im, body);
			}

			g_clear_object(&acct);

			return TRUE;
		}
	} else if (g_hash_table_lookup_extended(params, "roster", NULL, NULL)) {
		char *name = g_hash_table_lookup(params, "name");
		if (user && *user) {
			purple_blist_request_add_buddy(acct, user, NULL, name);
			g_clear_object(&acct);
			return TRUE;
		}
	} else if (g_hash_table_lookup_extended(params, "join", NULL, NULL)) {
		PurpleConnection *gc = purple_account_get_connection(acct);
		if (user && *user) {
			GHashTable *params = jabber_chat_info_defaults(gc, user);
			jabber_chat_join(gc, params);
		}
		g_clear_object(&acct);
		return TRUE;
	}

	g_clear_object(&acct);

	return FALSE;
}

static void
jabber_do_init(void)
{
	PurpleUi *ui = purple_core_get_ui();
	const gchar *ui_type;
	const gchar *type = "pc"; /* default client type, if unknown or
								unspecified */
	const gchar *ui_name = NULL;

	jabber_cmds = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, cmds_free_func);

	ui_type = ui ? purple_ui_get_client_type(ui) : NULL;
	if (ui_type) {
		if (purple_strequal(ui_type, "pc") ||
			purple_strequal(ui_type, "console") ||
			purple_strequal(ui_type, "phone") ||
			purple_strequal(ui_type, "handheld") ||
			purple_strequal(ui_type, "web") ||
			purple_strequal(ui_type, "bot")) {
			type = ui_type;
		}
	}

	if (ui)
		ui_name = purple_ui_get_name(ui);
	if (ui_name == NULL)
		ui_name = PACKAGE;

	jabber_add_identity("client", type, NULL, ui_name);

	/* initialize jabber_features list */
	jabber_add_feature(NS_LAST_ACTIVITY, NULL);
	jabber_add_feature(NS_OOB_IQ_DATA, NULL);
	jabber_add_feature(NS_ENTITY_TIME, NULL);
	jabber_add_feature("jabber:iq:version", NULL);
	jabber_add_feature("jabber:x:conference", NULL);
	jabber_add_feature(NS_BYTESTREAMS, NULL);
	jabber_add_feature("http://jabber.org/protocol/caps", NULL);
	jabber_add_feature("http://jabber.org/protocol/chatstates", NULL);
	jabber_add_feature(NS_DISCO_INFO, NULL);
	jabber_add_feature(NS_DISCO_ITEMS, NULL);
	jabber_add_feature(NS_IBB, NULL);
	jabber_add_feature("http://jabber.org/protocol/muc", NULL);
	jabber_add_feature("http://jabber.org/protocol/muc#user", NULL);
	jabber_add_feature("http://jabber.org/protocol/si", NULL);
	jabber_add_feature(NS_SI_FILE_TRANSFER, NULL);
	jabber_add_feature(NS_XHTML_IM, NULL);
	jabber_add_feature(NS_PING, NULL);

	/* Bits Of Binary */
	jabber_add_feature(NS_BOB, NULL);

	/* Jingle features! */
	jabber_add_feature(JINGLE, NULL);

	jabber_add_feature(JINGLE_APP_RTP, NULL);
	jabber_add_feature(JINGLE_APP_RTP_SUPPORT_AUDIO, jabber_audio_enabled);
	jabber_add_feature(JINGLE_APP_RTP_SUPPORT_VIDEO, jabber_video_enabled);
	jabber_add_feature(JINGLE_TRANSPORT_RAWUDP, NULL);
	jabber_add_feature(JINGLE_TRANSPORT_ICEUDP, NULL);

	g_signal_connect(G_OBJECT(purple_media_manager_get()), "ui-caps-changed",
			G_CALLBACK(jabber_caps_broadcast_change), NULL);

	/* reverse order of unload_plugin */
	jabber_iq_init();
	jabber_presence_init();
	jabber_caps_init();
	/* PEP things should be init via jabber_pep_init, not here */
	jabber_pep_init();
	jabber_data_init();
	jabber_bosh_init();

	/* TODO: Implement adding and retrieving own features via IPC API */

	jabber_ibb_init();
	jabber_si_init();

	jabber_auth_init();
}

static void
jabber_do_uninit(void)
{
	/* reverse order of jabber_do_init */
	jabber_bosh_uninit();
	jabber_data_uninit();
	jabber_si_uninit();
	jabber_ibb_uninit();
	/* PEP things should be uninit via jabber_pep_uninit, not here */
	jabber_pep_uninit();
	jabber_caps_uninit();
	jabber_presence_uninit();
	jabber_iq_uninit();

	g_signal_handlers_disconnect_by_func(purple_media_manager_get(),
	                                     jabber_caps_broadcast_change, NULL);

	jabber_auth_uninit();
	g_clear_list(&jabber_features, (GDestroyNotify)jabber_feature_free);
	g_clear_list(&jabber_identities, (GDestroyNotify)jabber_identity_free);

	g_clear_pointer(&jabber_cmds, g_hash_table_destroy);
}

static void jabber_init_protocol(PurpleProtocol *protocol)
{
	++plugin_ref;

	if (plugin_ref == 1)
		jabber_do_init();

	jabber_register_commands(protocol);

	purple_signal_register(protocol, "jabber-register-namespace-watcher",
			purple_marshal_VOID__POINTER_POINTER,
			G_TYPE_NONE, 2,
			G_TYPE_STRING,  /* node */
			G_TYPE_STRING); /* namespace */

	purple_signal_register(protocol, "jabber-unregister-namespace-watcher",
			purple_marshal_VOID__POINTER_POINTER,
			G_TYPE_NONE, 2,
			G_TYPE_STRING,  /* node */
			G_TYPE_STRING); /* namespace */

	purple_signal_connect(protocol, "jabber-register-namespace-watcher",
			protocol, G_CALLBACK(jabber_iq_signal_register), NULL);
	purple_signal_connect(protocol, "jabber-unregister-namespace-watcher",
			protocol, G_CALLBACK(jabber_iq_signal_unregister), NULL);


	purple_signal_register(protocol, "jabber-receiving-xmlnode",
			purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_POINTER); /* pointer to a PurpleXmlNode* */

	purple_signal_register(protocol, "jabber-sending-xmlnode",
			purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_POINTER); /* pointer to a PurpleXmlNode* */

	/*
	 * Do not remove this or the plugin will fail. Completely. You have been
	 * warned!
	 */
	purple_signal_connect_priority(protocol, "jabber-sending-xmlnode",
			protocol, G_CALLBACK(jabber_send_signal_cb),
			NULL, PURPLE_SIGNAL_PRIORITY_HIGHEST);

	purple_signal_register(protocol, "jabber-sending-text",
			     purple_marshal_VOID__POINTER_POINTER, G_TYPE_NONE, 2,
			     PURPLE_TYPE_CONNECTION,
			     G_TYPE_POINTER); /* pointer to a string */

	purple_signal_register(protocol, "jabber-receiving-message",
			purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER_POINTER,
			G_TYPE_BOOLEAN, 6,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_STRING, /* type */
			G_TYPE_STRING, /* id */
			G_TYPE_STRING, /* from */
			G_TYPE_STRING, /* to */
			PURPLE_TYPE_XMLNODE);

	purple_signal_register(protocol, "jabber-receiving-iq",
			purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
			G_TYPE_BOOLEAN, 5,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_STRING, /* type */
			G_TYPE_STRING, /* id */
			G_TYPE_STRING, /* from */
			PURPLE_TYPE_XMLNODE);

	purple_signal_register(protocol, "jabber-watched-iq",
			purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER_POINTER,
			G_TYPE_BOOLEAN, 5,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_STRING, /* type */
			G_TYPE_STRING, /* id */
			G_TYPE_STRING, /* from */
			PURPLE_TYPE_XMLNODE); /* child */

	purple_signal_register(protocol, "jabber-receiving-presence",
			purple_marshal_BOOLEAN__POINTER_POINTER_POINTER_POINTER,
			G_TYPE_BOOLEAN, 4,
			PURPLE_TYPE_CONNECTION,
			G_TYPE_STRING, /* type */
			G_TYPE_STRING, /* from */
			PURPLE_TYPE_XMLNODE);
}

static void jabber_uninit_protocol(PurpleProtocol *protocol)
{
	g_return_if_fail(plugin_ref > 0);

	purple_signals_unregister_by_instance(protocol);
	jabber_unregister_commands(protocol);

	--plugin_ref;
	if (plugin_ref == 0)
		jabber_do_uninit();
}

static PurpleBuddyIconSpec *
jabber_protocol_get_buddy_icon_spec(G_GNUC_UNUSED PurpleProtocol *protocol) {
	return purple_buddy_icon_spec_new("png",
	                                  32, 32, 96, 96, 0,
	                                  PURPLE_ICON_SCALE_SEND |
	                                  PURPLE_ICON_SCALE_DISPLAY);
}

static void
jabber_protocol_init(G_GNUC_UNUSED JabberProtocol *self) {
}

static void
jabber_protocol_class_init(JabberProtocolClass *klass)
{
	PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);

	protocol_class->get_buddy_icon_spec = jabber_protocol_get_buddy_icon_spec;

	protocol_class->login = jabber_login;
	protocol_class->close = jabber_close;
	protocol_class->status_types = jabber_status_types;
}

static void
jabber_protocol_class_finalize(G_GNUC_UNUSED JabberProtocolClass *klass)
{
}

static void
xmpp_protocol_actions_iface_init(PurpleProtocolActionsInterface *iface)
{
	iface->get_prefix = xmpp_protocol_actions_get_prefix;
	iface->get_action_group = xmpp_protocol_actions_get_action_group;
	iface->get_menu = xmpp_protocol_actions_get_menu;
}

static void
jabber_protocol_client_iface_init(PurpleProtocolClientInterface *client_iface)
{
	client_iface->list_emblem     = jabber_list_emblem;
	client_iface->blist_node_menu = jabber_blist_node_menu;
	client_iface->convo_closed    = jabber_convo_closed;
	client_iface->normalize       = jabber_client_normalize;
	client_iface->find_blist_chat = jabber_find_blist_chat;
	client_iface->offline_message = jabber_offline_message;
}

static void
jabber_protocol_server_iface_init(PurpleProtocolServerInterface *server_iface)
{
	server_iface->set_info               = jabber_set_info;
	server_iface->get_info               = jabber_buddy_get_info;
	server_iface->set_status             = jabber_set_status;
	server_iface->set_idle               = jabber_idle_set;
	server_iface->add_buddy              = jabber_roster_add_buddy;
	server_iface->remove_buddy           = jabber_roster_remove_buddy;
	server_iface->keepalive              = jabber_keepalive;
	server_iface->get_keepalive_interval = jabber_get_keepalive_interval;
	server_iface->alias_buddy            = jabber_roster_alias_change;
	server_iface->group_buddy            = jabber_roster_group_change;
	server_iface->rename_group           = jabber_roster_group_rename;
	server_iface->set_buddy_icon         = jabber_set_buddy_icon;
	server_iface->send_raw               = jabber_protocol_send_raw;
}

static void
jabber_protocol_im_iface_init(PurpleProtocolIMInterface *im_iface)
{
	im_iface->send        = jabber_message_send_im;
	im_iface->send_typing = jabber_send_typing;
}

static GHashTable *
jabber_protocol_chat_info_defaults(G_GNUC_UNUSED PurpleProtocolChat *protocol_chat,
                                   PurpleConnection *connection,
                                   const gchar *name)
{
	return jabber_chat_info_defaults(connection, name);
}

static void
jabber_protocol_chat_join(G_GNUC_UNUSED PurpleProtocolChat *protocol_chat,
                          PurpleConnection *connection, GHashTable *components)
{
	jabber_chat_join(connection, components);
}

static void
jabber_protocol_chat_invite(G_GNUC_UNUSED PurpleProtocolChat *protocol_chat,
                            PurpleConnection *connection,  gint id,
                            const gchar *message, const gchar *who)
{
	jabber_chat_invite(connection, id, message, who);
}

static void
jabber_protocol_chat_iface_init(PurpleProtocolChatInterface *chat_iface)
{
	chat_iface->info               = jabber_chat_info;
	chat_iface->info_defaults      = jabber_protocol_chat_info_defaults;
	chat_iface->join               = jabber_protocol_chat_join;
	chat_iface->get_name           = jabber_get_chat_name;
	chat_iface->invite             = jabber_protocol_chat_invite;
	chat_iface->leave              = jabber_chat_leave;
	chat_iface->send               = jabber_message_send_chat;
	chat_iface->get_user_real_name = jabber_chat_user_real_name;
	chat_iface->set_topic          = jabber_chat_set_topic;
}

static void
jabber_protocol_roomlist_iface_init(PurpleProtocolRoomlistInterface *roomlist_iface)
{
	roomlist_iface->get_list       = jabber_roomlist_get_list;
	roomlist_iface->cancel         = jabber_roomlist_cancel;
	roomlist_iface->room_serialize = jabber_roomlist_room_serialize;
}

static void
jabber_protocol_media_iface_init(PurpleProtocolMediaInterface *media_iface)
{
	media_iface->initiate_session = jabber_initiate_media;
	media_iface->get_caps         = jabber_get_media_caps;
}

static void
jabber_protocol_xfer_iface_init(PurpleProtocolXferInterface *xfer_iface)
{
	xfer_iface->can_receive = jabber_can_receive_file;
	xfer_iface->send_file   = jabber_si_xfer_send;
	xfer_iface->new_xfer    = jabber_si_new_xfer;
}

G_DEFINE_DYNAMIC_TYPE_EXTENDED(
	JabberProtocol,
	jabber_protocol,
	PURPLE_TYPE_PROTOCOL,
	G_TYPE_FLAG_ABSTRACT,
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_ACTIONS,
	                              xmpp_protocol_actions_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CLIENT,
	                              jabber_protocol_client_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_SERVER,
	                              jabber_protocol_server_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_IM,
	                              jabber_protocol_im_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CHAT,
	                              jabber_protocol_chat_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_ROOMLIST,
	                              jabber_protocol_roomlist_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_MEDIA,
	                              jabber_protocol_media_iface_init)
	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_XFER,
	                              jabber_protocol_xfer_iface_init))

static GPluginPluginInfo *
jabber_query(G_GNUC_UNUSED GError **error)
{
	return purple_plugin_info_new(
		"id",           "prpl-xmpp",
		"name",         "XMPP Protocols",
		"version",      DISPLAY_VERSION,
		"category",     N_("Protocol"),
		"summary",      N_("XMPP Protocol Plugin"),
		"description",  N_("XMPP Protocol Plugin"),
		"website",      PURPLE_WEBSITE,
		"abi-version",  PURPLE_ABI_VERSION,
		"flags",        PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
		                PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
		NULL
	);
}

static gboolean
jabber_load(GPluginPlugin *plugin, GError **error)
{
	PurpleProtocolManager *manager = purple_protocol_manager_get_default();

	jingle_session_register(plugin);

	jingle_transport_register(plugin);
	jingle_iceudp_register(plugin);
	jingle_rawudp_register(plugin);

	jingle_content_register(plugin);
	jingle_rtp_register(plugin);

	jabber_protocol_register_type(G_TYPE_MODULE(plugin));

	xmpp_protocol_register(plugin);

	jabber_oob_xfer_register(G_TYPE_MODULE(plugin));
	jabber_si_xfer_register(G_TYPE_MODULE(plugin));

	xmpp_protocol = xmpp_protocol_new();
	if(!purple_protocol_manager_register(manager, xmpp_protocol, error)) {
		g_clear_object(&xmpp_protocol);

		return FALSE;
	}

	purple_signal_connect(purple_get_core(), "uri-handler", xmpp_protocol,
		G_CALLBACK(xmpp_uri_handler), xmpp_protocol);

	jabber_init_protocol(xmpp_protocol);

	return TRUE;
}

static gboolean
jabber_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
              G_GNUC_UNUSED gboolean shutdown, GError **error)
{
	PurpleProtocolManager *manager = purple_protocol_manager_get_default();

	if(!purple_protocol_manager_unregister(manager, xmpp_protocol, error)) {
		return FALSE;
	}

	purple_signal_disconnect(purple_get_core(), "uri-handler",
			xmpp_protocol, G_CALLBACK(xmpp_uri_handler));

	jabber_uninit_protocol(xmpp_protocol);

	g_clear_object(&xmpp_protocol);

	return TRUE;
}

GPLUGIN_NATIVE_PLUGIN_DECLARE(jabber)

mercurial