view src/protocols/jabber/jabber.c @ 2856:b1e300a85678

[gaim-migrate @ 2869] rewrote the html parser in gtkimhtml. yes, that's really all i did. the reason for the massive change is because i added a length argument, which then needed to be propogated down to everything that would ever receive anything that would get drawn to the window. the new parser isn't any faster. that wasn't my goal. it's much more understandable now (hopefully, anyway). committer: Tailor Script <tailor@pidgin.im>
author Eric Warmenhoven <eric@warmenhoven.org>
date Sat, 08 Dec 2001 09:48:52 +0000
parents b917845dad3c
children b68c648618a3
line wrap: on
line source

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 * gaim
 *
 * Some code copyright (C) 1998-1999, Mark Spencer <markster@marko.net>
 * libfaim code copyright 1998, 1999 Adam Fritzler <afritz@auk.cx>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif


#include <netdb.h>
#include <unistd.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <sys/socket.h>
#include <sys/utsname.h>
#include <sys/stat.h>
#include "multi.h"
#include "prpl.h"
#include "gaim.h"
#ifdef MAX
#undef MAX
#endif
#ifdef MIN
#undef MIN
#endif
#include "jabber.h"
#include "proxy.h"

#include "pixmaps/available.xpm"
#include "pixmaps/available-away.xpm"
#include "pixmaps/available-chat.xpm"
#include "pixmaps/available-xa.xpm"
#include "pixmaps/available-dnd.xpm"

/* The priv member of gjconn's is a gaim_connection for now. */
#define GJ_GC(x) ((struct gaim_connection *)(x)->priv)

#define IQID_AUTH "__AUTH__"

#define IQ_NONE -1
#define IQ_AUTH 0
#define IQ_ROSTER 1

#define UC_AWAY (0x02 | UC_UNAVAILABLE)
#define UC_CHAT  0x04
#define UC_XA   (0x08 | UC_UNAVAILABLE)
#define UC_DND  (0x10 | UC_UNAVAILABLE)

#define DEFAULT_SERVER "jabber.org"
#define DEFAULT_GROUPCHAT "conference.jabber.org"
#define DEFAULT_PORT 5222

#define USEROPT_PORT 0

typedef struct gjconn_struct {
	/* Core structure */
	pool p;			/* Memory allocation pool */
	int state;		/* Connection state flag */
	int fd;			/* Connection file descriptor */
	jid user;		/* User info */
	char *pass;		/* User passwd */

	/* Stream stuff */
	int id;			/* id counter for jab_getid() function */
	char idbuf[9];		/* temporary storage for jab_getid() */
	char *sid;		/* stream id from server, for digest auth */
	XML_Parser parser;	/* Parser instance */
	xmlnode current;	/* Current node in parsing instance.. */

	/* Event callback ptrs */
	void (*on_state)(struct gjconn_struct *j, int state);
	void (*on_packet)(struct gjconn_struct *j, jpacket p);

	void *priv;

} *gjconn, gjconn_struct;

typedef void (*gjconn_state_h)(gjconn j, int state);
typedef void (*gjconn_packet_h)(gjconn j, jpacket p);

static gjconn gjab_new(char *user, char *pass, void *priv);
static void gjab_delete(gjconn j);
static void gjab_state_handler(gjconn j, gjconn_state_h h);
static void gjab_packet_handler(gjconn j, gjconn_packet_h h);
static void gjab_start(gjconn j);
static void gjab_stop(gjconn j);
/*
static int gjab_getfd(gjconn j);
static jid gjab_getjid(gjconn j);
static char *gjab_getsid(gjconn j);
*/
static char *gjab_getid(gjconn j);
static void gjab_send(gjconn j, xmlnode x);
static void gjab_send_raw(gjconn j, const char *str);
static void gjab_recv(gjconn j);
static void gjab_auth(gjconn j);

struct jabber_data {
	gjconn jc;
	gboolean did_import;
	GSList *pending_chats;
	GSList *existing_chats;
	GHashTable *hash;
	time_t idle;
	gboolean die;
};

struct jabber_chat {
	jid Jid;
	struct gaim_connection *gc;
	struct conversation *b;
	int id;
};

static char *jabber_name()
{
	return "Jabber";
}

#define STATE_EVT(arg) if(j->on_state) { (j->on_state)(j, (arg) ); }

static char *create_valid_jid(const char *given, char *server, char *resource)
{
	char *valid;

	if (!strchr(given, '@'))
		valid = g_strdup_printf("%s@%s/%s", given, server, resource);
	else if (!strchr(strchr(given, '@'), '/'))
		valid = g_strdup_printf("%s/%s", given, resource);
	else
		valid = g_strdup(given);

	return valid;
}

static gjconn gjab_new(char *user, char *pass, void *priv)
{
	pool p;
	gjconn j;

	if (!user)
		return (NULL);

	p = pool_new();
	if (!p)
		return (NULL);
	j = pmalloc_x(p, sizeof(gjconn_struct), 0);
	if (!j)
		return (NULL);
	j->p = p;

	j->user = jid_new(p, user);
	j->pass = pstrdup(p, pass);

	j->state = JCONN_STATE_OFF;
	j->id = 1;
	j->fd = -1;

	j->priv = priv;

	return j;
}

static void gjab_delete(gjconn j)
{
	if (!j)
		return;

	gjab_stop(j);
	pool_free(j->p);
}

static void gjab_state_handler(gjconn j, gjconn_state_h h)
{
	if (!j)
		return;

	j->on_state = h;
}

static void gjab_packet_handler(gjconn j, gjconn_packet_h h)
{
	if (!j)
		return;

	j->on_packet = h;
}

static void gjab_stop(gjconn j)
{
	if (!j || j->state == JCONN_STATE_OFF)
		return;

	j->state = JCONN_STATE_OFF;
	gjab_send_raw(j, "</stream:stream>");
	close(j->fd);
	j->fd = -1;
	XML_ParserFree(j->parser);
	j->parser = NULL;
}

/*
static int gjab_getfd(gjconn j)
{
	if (j)
		return j->fd;
	else
		return -1;
}

static jid gjab_getjid(gjconn j)
{
	if (j)
		return (j->user);
	else
		return NULL;
}

static char *gjab_getsid(gjconn j)
{
	if (j)
		return (j->sid);
	else
		return NULL;
}
*/

static char *gjab_getid(gjconn j)
{
	snprintf(j->idbuf, 8, "%d", j->id++);
	return &j->idbuf[0];
}

static void gjab_send(gjconn j, xmlnode x)
{
	if (j && j->state != JCONN_STATE_OFF) {
		char *buf = xmlnode2str(x);
		if (buf)
			write(j->fd, buf, strlen(buf));
		debug_printf("gjab_send: %s\n", buf);
	}
}

static void gjab_send_raw(gjconn j, const char *str)
{
	if (j && j->state != JCONN_STATE_OFF) {
		write(j->fd, str, strlen(str));
		debug_printf("gjab_send_raw: %s\n", str);
	}
}

static void gjab_reqroster(gjconn j)
{
	xmlnode x;

	x = jutil_iqnew(JPACKET__GET, NS_ROSTER);
	xmlnode_put_attrib(x, "id", gjab_getid(j));

	gjab_send(j, x);
	xmlnode_free(x);
}

static void gjab_reqauth(gjconn j)
{
	xmlnode x, y, z;
	char *user;

	if (!j)
		return;

	x = jutil_iqnew(JPACKET__GET, NS_AUTH);
	xmlnode_put_attrib(x, "id", IQID_AUTH);
	y = xmlnode_get_tag(x, "query");

	user = j->user->user;

	if (user) {
		z = xmlnode_insert_tag(y, "username");
		xmlnode_insert_cdata(z, user, -1);
	}

	gjab_send(j, x);
	xmlnode_free(x);
}

static void gjab_auth(gjconn j)
{
	xmlnode x, y, z;
	char *hash, *user;

	if (!j)
		return;

	x = jutil_iqnew(JPACKET__SET, NS_AUTH);
	xmlnode_put_attrib(x, "id", IQID_AUTH);
	y = xmlnode_get_tag(x, "query");

	user = j->user->user;

	if (user) {
		z = xmlnode_insert_tag(y, "username");
		xmlnode_insert_cdata(z, user, -1);
	}

	z = xmlnode_insert_tag(y, "resource");
	xmlnode_insert_cdata(z, j->user->resource, -1);

	if (j->sid) {
		debug_printf("digest authentication (sid %s)\n", j->sid);
		z = xmlnode_insert_tag(y, "digest");
		hash = pmalloc(x->p, strlen(j->sid) + strlen(j->pass) + 1);
		strcpy(hash, j->sid);
		strcat(hash, j->pass);
		hash = shahash(hash);
		xmlnode_insert_cdata(z, hash, 40);
	} else {
		z = xmlnode_insert_tag(y, "password");
		xmlnode_insert_cdata(z, j->pass, -1);
	}

	gjab_send(j, x);
	xmlnode_free(x);

	return;
}

static void gjab_recv(gjconn j)
{
	static char buf[4096];
	int len;

	if (!j || j->state == JCONN_STATE_OFF)
		return;

	if ((len = read(j->fd, buf, sizeof(buf) - 1))) {
		struct jabber_data *jd = GJ_GC(j)->proto_data;
		buf[len] = '\0';
		debug_printf("input (len %d): %s\n", len, buf);
		XML_Parse(j->parser, buf, len, 0);
		if (jd->die)
			signoff(GJ_GC(j));
	} else if (len <= 0) {
		STATE_EVT(JCONN_STATE_OFF)
	}
}

static void startElement(void *userdata, const char *name, const char **attribs)
{
	xmlnode x;
	gjconn j = (gjconn) userdata;

	if (j->current) {
		/* Append the node to the current one */
		x = xmlnode_insert_tag(j->current, name);
		xmlnode_put_expat_attribs(x, attribs);

		j->current = x;
	} else {
		x = xmlnode_new_tag(name);
		xmlnode_put_expat_attribs(x, attribs);
		if (strcmp(name, "stream:stream") == 0) {
			/* special case: name == stream:stream */
			/* id attrib of stream is stored for digest auth */
			j->sid = g_strdup(xmlnode_get_attrib(x, "id"));
			/* STATE_EVT(JCONN_STATE_AUTH) */
			xmlnode_free(x);
		} else {
			j->current = x;
		}
	}
}

static void endElement(void *userdata, const char *name)
{
	gjconn j = (gjconn) userdata;
	xmlnode x;
	jpacket p;

	if (j->current == NULL) {
		/* we got </stream:stream> */
		STATE_EVT(JCONN_STATE_OFF)
		    return;
	}

	x = xmlnode_get_parent(j->current);

	if (!x) {
		/* it is time to fire the event */
		p = jpacket_new(j->current);

		if (j->on_packet)
			(j->on_packet) (j, p);
		else
			xmlnode_free(j->current);
	}

	j->current = x;
}

static void jabber_callback(gpointer data, gint source, GaimInputCondition condition)
{
	struct gaim_connection *gc = (struct gaim_connection *)data;
	struct jabber_data *jd = (struct jabber_data *)gc->proto_data;

	gjab_recv(jd->jc);
}

static void charData(void *userdata, const char *s, int slen)
{
	gjconn j = (gjconn) userdata;

	if (j->current)
		xmlnode_insert_cdata(j->current, s, slen);
}

static void gjab_connected(gpointer data, gint source, GaimInputCondition cond)
{
	xmlnode x;
	char *t, *t2;
	struct gaim_connection *gc = data;
	struct jabber_data *jd;
	gjconn j;

	if (!g_slist_find(connections, gc)) {
		close(source);
		return;
	}

	jd = gc->proto_data;
	j = jd->jc;

	if (j->fd != source)
		j->fd = source;

	if (source == -1) {
		STATE_EVT(JCONN_STATE_OFF)
		return;
	}

	j->state = JCONN_STATE_CONNECTED;
	STATE_EVT(JCONN_STATE_CONNECTED)

	/* start stream */
	x = jutil_header(NS_CLIENT, j->user->server);
	t = xmlnode2str(x);
	/* this is ugly, we can create the string here instead of jutil_header */
	/* what do you think about it? -madcat */
	t2 = strstr(t, "/>");
	*t2++ = '>';
	*t2 = '\0';
	gjab_send_raw(j, "<?xml version='1.0'?>");
	gjab_send_raw(j, t);
	xmlnode_free(x);

	j->state = JCONN_STATE_ON;
	STATE_EVT(JCONN_STATE_ON);

	gc = GJ_GC(j);
	gc->inpa = gaim_input_add(j->fd, GAIM_INPUT_READ, jabber_callback, gc);
}

static void gjab_start(gjconn j)
{
	struct aim_user *user;
	int port;

	if (!j || j->state != JCONN_STATE_OFF)
		return;

	user = GJ_GC(j)->user;
	port = user->proto_opt[USEROPT_PORT][0] ? atoi(user->proto_opt[USEROPT_PORT]) : DEFAULT_PORT;

	j->parser = XML_ParserCreate(NULL);
	XML_SetUserData(j->parser, (void *)j);
	XML_SetElementHandler(j->parser, startElement, endElement);
	XML_SetCharacterDataHandler(j->parser, charData);

	j->fd = proxy_connect(j->user->server, port, gjab_connected, GJ_GC(j));
	if (!user->gc || (j->fd < 0)) {
		STATE_EVT(JCONN_STATE_OFF)
		return;
	}
}

static struct conversation *find_chat(struct gaim_connection *gc, char *name)
{
	GSList *bcs = gc->buddy_chats;
	struct conversation *b = NULL;
	char *chat = g_strdup(normalize(name));

	while (bcs) {
		b = bcs->data;
		if (!strcasecmp(normalize(b->name), chat))
			break;
		b = NULL;
		bcs = bcs->next;
	}

	g_free(chat);
	return b;
}

static struct jabber_chat *find_existing_chat(struct gaim_connection *gc, jid chat)
{
	GSList *bcs = ((struct jabber_data *)gc->proto_data)->existing_chats;
	struct jabber_chat *jc = NULL;

	while (bcs) {
		jc = bcs->data;
		if (!jid_cmpx(chat, jc->Jid, JID_USER | JID_SERVER))
			break;
		jc = NULL;
		bcs = bcs->next;
	}

	return jc;
}

static struct jabber_chat *find_pending_chat(struct gaim_connection *gc, jid chat)
{
	GSList *bcs = ((struct jabber_data *)gc->proto_data)->pending_chats;
	struct jabber_chat *jc = NULL;

	while (bcs) {
		jc = bcs->data;
		if (!jid_cmpx(chat, jc->Jid, JID_USER | JID_SERVER))
			break;
		jc = NULL;
		bcs = bcs->next;
	}

	return jc;
}

static gboolean find_chat_buddy(struct conversation *b, char *name)
{
	GList *m = b->in_room;

	while (m) {
		if (!strcmp(m->data, name))
			return TRUE;
		m = m->next;
	}

	return FALSE;
}

static void jabber_handlemessage(gjconn j, jpacket p)
{
	xmlnode y, xmlns, subj;

	char *from = NULL, *msg = NULL, *type = NULL, *topic = NULL;
	char m[BUF_LONG * 2];

	type = xmlnode_get_attrib(p->x, "type");

	if (!type || !strcasecmp(type, "normal") || !strcasecmp(type, "chat")) {

		/* XXX namespaces could be handled better. (mid) */
		if ((xmlns = xmlnode_get_tag(p->x, "x")))
			type = xmlnode_get_attrib(xmlns, "xmlns");

		from = jid_full(p->from);
		/*
		if ((y = xmlnode_get_tag(p->x, "html"))) {
			msg = xmlnode_get_data(y);
		} else
		*/
		if ((y = xmlnode_get_tag(p->x, "body"))) {
			msg = xmlnode_get_data(y);
		}

		msg = utf8_to_str(msg);

		if (!from)
			return;

		if (type && !strcasecmp(type, "jabber:x:conference")) {
			char *room;
			GList *m = NULL;
			char **data;

			room = xmlnode_get_attrib(xmlns, "jid");
			data = g_strsplit(room, "@", 2);
			m = g_list_append(m, g_strdup(data[0]));
			m = g_list_append(m, g_strdup(data[1]));
			m = g_list_append(m, g_strdup(j->user->user));
			g_strfreev(data);

			serv_got_chat_invite(GJ_GC(j), room, from, msg, m);
		} else if (msg) { /* whisper */
			struct jabber_chat *jc;
			g_snprintf(m, sizeof(m), "%s", msg);
			if (((jc = find_existing_chat(GJ_GC(j), p->from)) != NULL) && jc->b)
				serv_got_chat_in(GJ_GC(j), jc->b->id, p->from->resource, 1, m, time(NULL));
			else {
				int flags = 0;
				if (xmlnode_get_tag(p->x, "gaim"))
					flags = IM_FLAG_GAIMUSER;
				if (find_conversation(jid_full(p->from)))
					serv_got_im(GJ_GC(j), jid_full(p->from), m, flags, time(NULL), -1);
				else {
					from = g_strdup_printf("%s@%s", p->from->user, p->from->server);
					serv_got_im(GJ_GC(j), from, m, flags, time(NULL), -1);
					g_free(from);
				}
			}
		}

		if (msg)
			g_free(msg);

	} else if (!strcasecmp(type, "error")) {
		if ((y = xmlnode_get_tag(p->x, "error"))) {
			type = xmlnode_get_attrib(y, "code");
			msg = xmlnode_get_data(y);
		}

		if (msg) {
			from = g_strdup_printf("Error %s", type ? type : "");
			do_error_dialog(msg, from);
			g_free(from);
		}
	} else if (!strcasecmp(type, "groupchat")) {
		struct jabber_chat *jc;
		static int i = 0;

		/*
		if ((y = xmlnode_get_tag(p->x, "html"))) {
			msg = xmlnode_get_data(y);
		} else
		*/
		if ((y = xmlnode_get_tag(p->x, "body"))) {
			msg = xmlnode_get_data(y);
		}

		msg = utf8_to_str(msg);
		
		if ((subj = xmlnode_get_tag(p->x, "subject"))) {
		   	topic = xmlnode_get_data(subj);
		} 
		topic = utf8_to_str(topic);

		jc = find_existing_chat(GJ_GC(j), p->from);
		if (!jc) {
			/* we're not in this chat. are we supposed to be? */
			struct jabber_data *jd = GJ_GC(j)->proto_data;
			if ((jc = find_pending_chat(GJ_GC(j), p->from)) != NULL) {
				/* yes, we're supposed to be. so now we are. */
				jc->b = serv_got_joined_chat(GJ_GC(j), i++, p->from->user);
				jc->id = jc->b->id;
				jd->existing_chats = g_slist_append(jd->existing_chats, jc);
				jd->pending_chats = g_slist_remove(jd->pending_chats, jc);
			} else {
				/* no, we're not supposed to be. */
				g_free(msg);
				return;
			}
		}
		if (p->from->resource) {
			if (!y) {
				if (!find_chat_buddy(jc->b, p->from->resource))
					add_chat_buddy(jc->b, p->from->resource);
				else if ((y = xmlnode_get_tag(p->x, "status"))) {
					char buf[8192];
					msg = xmlnode_get_data(y);
					g_snprintf(buf, sizeof(buf), "%s now has status: %s",
							p->from->resource, msg);
					write_to_conv(jc->b, buf, WFLAG_SYSTEM, NULL, time(NULL), -1);
				}
			} else if (jc->b && msg) {
				char buf[8192];

				if (topic) {
					char tbuf[8192];
					g_snprintf(tbuf, sizeof(tbuf), "%s", topic);
					chat_set_topic(jc->b, p->from->resource, tbuf);
				}
				

				g_snprintf(buf, sizeof(buf), "%s", msg);
				serv_got_chat_in(GJ_GC(j), jc->b->id, p->from->resource, 0, buf, time(NULL));
			}
		} else { /* message from the server */
		   	if(jc->b && topic) {
			   	char tbuf[8192];
				g_snprintf(tbuf, sizeof(tbuf), "%s", topic);
				chat_set_topic(jc->b, "", tbuf);
			}
		}

		g_free(msg);
		g_free(topic);

	} else {
		debug_printf("unhandled message %s\n", type);
	}
}

static void jabber_handlepresence(gjconn j, jpacket p)
{
	char *to, *from, *type;
	struct buddy *b = NULL;
	jid who;
	char *buddy;
	xmlnode y;
	char *show;
	int state = 0;
	GSList *resources;
	char *res;
	struct conversation *cnv = NULL;
	struct jabber_chat *jc = NULL;

	to = xmlnode_get_attrib(p->x, "to");
	from = xmlnode_get_attrib(p->x, "from");
	type = xmlnode_get_attrib(p->x, "type");

	if ((y = xmlnode_get_tag(p->x, "show"))) {
		show = xmlnode_get_data(y);
		if (!show) {
			state = 0;
		} else if (!strcasecmp(show, "away")) {
			state = UC_AWAY;
		} else if (!strcasecmp(show, "chat")) {
			state = UC_CHAT;
		} else if (!strcasecmp(show, "xa")) {
			state = UC_XA;
		} else if (!strcasecmp(show, "dnd")) {
			state = UC_DND;
		}
	} else {
		state = 0;
	}

	who = jid_new(j->p, from);
	if (who->user == NULL) {
		/* FIXME: transport */
		return;
	}

	buddy = g_strdup_printf("%s@%s", who->user, who->server);

	/* um. we're going to check if it's a chat. if it isn't, and there are pending
	 * chats, create the chat. if there aren't pending chats, add the buddy. */
	if ((cnv = find_chat(GJ_GC(j), who->user)) == NULL) {
		static int i = 0x70;
		struct jabber_data *jd = GJ_GC(j)->proto_data;
		if ((jc = find_pending_chat(GJ_GC(j), who)) != NULL) {
			jc->b = cnv = serv_got_joined_chat(GJ_GC(j), i++, who->user);
			jc->id = jc->b->id;
			jd->existing_chats = g_slist_append(jd->existing_chats, jc);
			jd->pending_chats = g_slist_remove(jd->pending_chats, jc);
		} else if (!(b = find_buddy(GJ_GC(j), buddy))) {
			b = add_buddy(GJ_GC(j), "Buddies", buddy, buddy);
			do_export(GJ_GC(j));
		}
	}

	if (!cnv) {
		resources = b->proto_data;
		res = who->resource;
		if (res)
			while (resources) {
				if (!strcmp(res, resources->data))
					break;
				resources = resources->next;
			}

		if (type && (strcasecmp(type, "unavailable") == 0)) {
			if (resources) {
				g_free(resources->data);
				b->proto_data = g_slist_remove(b->proto_data, resources->data);
			}
			if (!b->proto_data) {
				serv_got_update(GJ_GC(j), buddy, 0, 0, 0, 0, 0, 0);
			}
		} else {
			/* keep track of away msg same as yahoo plugin */
			struct jabber_data *jd = GJ_GC(j)->proto_data;
			gpointer val = g_hash_table_lookup(jd->hash, normalize(b->name));
			if (val) {
			   	g_free(val);
				g_hash_table_insert(jd->hash, normalize(b->name),
						g_strdup(xmlnode_get_tag_data(p->x, "status")));
			} else
				g_hash_table_insert(jd->hash, g_strdup(normalize(b->name)),
						g_strdup(xmlnode_get_tag_data(p->x, "status")));


			if (!resources) {
				b->proto_data = g_slist_append(b->proto_data, g_strdup(res));
			}

			serv_got_update(GJ_GC(j), buddy, 1, 0, 0, 0, state, 0);

		}
	} else {
		if (who->resource) {
			if (type && !strcasecmp(type, "unavailable")) {
				struct jabber_data *jd;
				if (!jc && !(jc = find_existing_chat(GJ_GC(j), who))) {
					g_free(buddy);
					return;
				}
				jd = jc->gc->proto_data;
				if (strcmp(who->resource, jc->Jid->resource) && jc->b) {
					remove_chat_buddy(jc->b, who->resource, NULL);
					return;
				}

				jd->existing_chats = g_slist_remove(jd->existing_chats, jc);
				serv_got_chat_left(GJ_GC(j), jc->id);
				g_free(jc);
			} else {
				if ((!jc && !(jc = find_existing_chat(GJ_GC(j), who))) || !jc->b) {
					g_free(buddy);
					return;
				}
				if (!find_chat_buddy(jc->b, who->resource))
					add_chat_buddy(jc->b, who->resource);
				else if ((y = xmlnode_get_tag(p->x, "status"))) {
					char buf[8192];
					char *msg = xmlnode_get_data(y);
					g_snprintf(buf, sizeof(buf), "%s now has status: %s",
							p->from->resource, msg);
					write_to_conv(jc->b, buf, WFLAG_SYSTEM, NULL, time(NULL), -1);
				}
			}
		}
	}

	g_free(buddy);

	return;
}

static void jabber_handles10n(gjconn j, jpacket p)
{
	xmlnode g;
	char *Jid = xmlnode_get_attrib(p->x, "from");
	char *ask = xmlnode_get_attrib(p->x, "type");

	g = xmlnode_new_tag("presence");
	xmlnode_put_attrib(g, "to", Jid);
	if (!strcmp(ask, "subscribe"))
		xmlnode_put_attrib(g, "type", "subscribed");
	else if (!strcmp(ask, "unsubscribe"))
		xmlnode_put_attrib(g, "type", "unsubscribed");
	else
		return;

	gjab_send(j, g);
}

static void jabber_handleroster(gjconn j, xmlnode querynode)
{
	xmlnode x;

	x = xmlnode_get_firstchild(querynode);
	while (x) {
		xmlnode g;
		char *Jid, *name, *sub, *ask;
		jid who;

		Jid = xmlnode_get_attrib(x, "jid");
		name = xmlnode_get_attrib(x, "name");
		sub = xmlnode_get_attrib(x, "subscription");
		ask = xmlnode_get_attrib(x, "ask");
		who = jid_new(j->p, Jid);

		if ((g = xmlnode_get_firstchild(x))) {
			while (g) {
				if (xmlnode_get_name(g) &&
						g_strncasecmp(xmlnode_get_name(g), "group", 5) == 0) {
					struct buddy *b = NULL;
					char *groupname, *buddyname;

					if (!who || !who->user) {
						/* FIXME: transport */
						g = xmlnode_get_nextsibling(g);
						continue;
					}
					buddyname = g_strdup_printf("%s@%s", who->user, who->server);
					groupname = xmlnode_get_data(xmlnode_get_firstchild(g));
					if (groupname == NULL)
						groupname = "Buddies";
					if (strcasecmp(sub, "from") && strcasecmp(sub, "none") &&
							!(b = find_buddy(GJ_GC(j), buddyname))) {
						debug_printf("adding buddy: %s\n", buddyname);
						b = add_buddy(GJ_GC(j), groupname, buddyname,
							      name ? name : buddyname);
						do_export(GJ_GC(j));
					/*
					} else if (b) {
						debug_printf("updating buddy: %s/%s\n", buddyname, name);
						g_snprintf(b->name, sizeof(b->name), "%s", buddyname);
						g_snprintf(b->show, sizeof(b->show), "%s",
							   name ? name : buddyname);
					*/
					}
					g_free(buddyname);
				}
				g = xmlnode_get_nextsibling(g);
			}
		} else {
			struct buddy *b;
			char *buddyname;

			if (!who || !who->user) {
				/* FIXME: transport */
				x = xmlnode_get_nextsibling(x);
				continue;
			}
			buddyname = g_strdup_printf("%s@%s", who->user, who->server);
			if (strcasecmp(sub, "from") && strcasecmp(sub, "none") &&
					!(b = find_buddy(GJ_GC(j), buddyname))) {
				debug_printf("adding buddy: %s\n", buddyname);
				b = add_buddy(GJ_GC(j), "Buddies", buddyname, name ? name : Jid);
				do_export(GJ_GC(j));
			}
			g_free(buddyname);
		}

		x = xmlnode_get_nextsibling(x);
	}

	x = jutil_presnew(0, NULL, "Online");
	gjab_send(j, x);
	xmlnode_free(x);
}

static void jabber_handlevcard(gjconn j, xmlnode querynode, char *from)
{
	struct gaim_connection *gc = GJ_GC(j);
	char buf[1024];
	char *fn, *url, *email, *nickname, *status, *desc;
	jid who;
	char *buddy;
	struct jabber_data *jd = GJ_GC(j)->proto_data;
	int at = 0;

	who = jid_new(j->p, from);
	buddy = g_strdup_printf("%s@%s", who->user, who->server);
	
	fn = xmlnode_get_tag_data(querynode, "FN");
	url = xmlnode_get_tag_data(querynode, "URL");
	email = xmlnode_get_tag_data(querynode, "EMAIL");
	nickname = xmlnode_get_tag_data(querynode, "NICKNAME");
	desc = xmlnode_get_tag_data(querynode, "DESC");
	status = g_hash_table_lookup(jd->hash, buddy);
	if (!status)
		status = "Online";

	at = g_snprintf(buf, sizeof buf, "<B>Jabber ID:</B> %s<BR>", buddy);
	if (fn)
		at += g_snprintf(buf + at, sizeof(buf) - at, "<B>Full Name:</B> %s<BR>", fn);
	if (nickname)
		at += g_snprintf(buf + at, sizeof(buf) - at, "<B>Nickname:</B> %s<BR>", nickname);
	if (url)
		at += g_snprintf(buf + at, sizeof(buf) - at, "<B>URL:</B> <A HREF=\"%s\">%s</A><BR>",
				url, url);
	if (email)
		at += g_snprintf(buf + at, sizeof(buf) - at,
				"<B>Email:</B> <A HREF=\"mailto:%s\">%s</A><BR>", email, email);
	at += g_snprintf(buf + at, sizeof(buf) - at, "<B>Status:</B> %s\n", status);
	if (desc)
		at += g_snprintf(buf + at, sizeof(buf) - at, "<HR>%s<br>\n", desc);

	g_show_info_text(gc, buddy, 2, buf, NULL);
	g_free(buddy);
}

static void jabber_handleauthresp(gjconn j, jpacket p)
{
	if (jpacket_subtype(p) == JPACKET__RESULT) {
		if (xmlnode_has_children(p->x)) {
			xmlnode query = xmlnode_get_tag(p->x, "query");
			set_login_progress(GJ_GC(j), 4, "Authenticating");
			if (!xmlnode_get_tag(query, "digest")) {
				g_free(j->sid);
				j->sid = NULL;
			}
			gjab_auth(j);
		} else {
			debug_printf("auth success\n");

			account_online(GJ_GC(j));
			serv_finish_login(GJ_GC(j));

			if (bud_list_cache_exists(GJ_GC(j)))
				do_import(GJ_GC(j), NULL);

			((struct jabber_data *)GJ_GC(j)->proto_data)->did_import = TRUE;

			gjab_reqroster(j);
		}
	} else {
		xmlnode xerr;
		char *errmsg = NULL;
		int errcode = 0;
		struct jabber_data *jd = GJ_GC(j)->proto_data;

		debug_printf("auth failed\n");
		xerr = xmlnode_get_tag(p->x, "error");
		if (xerr) {
			char msg[BUF_LONG];
			errmsg = xmlnode_get_data(xerr);
			if (xmlnode_get_attrib(xerr, "code")) {
				errcode = atoi(xmlnode_get_attrib(xerr, "code"));
				g_snprintf(msg, sizeof(msg), "Error %d: %s", errcode, errmsg);
			} else
				g_snprintf(msg, sizeof(msg), "%s", errmsg);
			hide_login_progress(GJ_GC(j), msg);
		} else {
			hide_login_progress(GJ_GC(j), "Unknown login error");
		}

		jd->die = TRUE;
	}
}

static void jabber_handleversion(gjconn j, xmlnode iqnode) {
	xmlnode querynode, x;
	char *id, *from;
	char os[1024];
	struct utsname osinfo;

	uname(&osinfo);
	g_snprintf(os, sizeof os, "%s %s %s", osinfo.sysname, osinfo.release, osinfo.machine);

	id = xmlnode_get_attrib(iqnode, "id");
	from = xmlnode_get_attrib(iqnode, "from");

	x = jutil_iqnew(JPACKET__RESULT, NS_VERSION);

	xmlnode_put_attrib(x, "to", from);
	xmlnode_put_attrib(x, "id", id);
	querynode = xmlnode_get_tag(x, "query");
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "name"), PACKAGE, -1);
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "version"), VERSION, -1);
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "os"), os, -1);

	gjab_send(j, x);

	xmlnode_free(x);
}

static void jabber_handletime(gjconn j, xmlnode iqnode) {
	xmlnode querynode, x;
	char *id, *from;
	time_t now_t; 
	struct tm *now;
	char buf[1024];

	time(&now_t);
	now = localtime(&now_t);

	id = xmlnode_get_attrib(iqnode, "id");
	from = xmlnode_get_attrib(iqnode, "from");

	x = jutil_iqnew(JPACKET__RESULT, NS_TIME);

	xmlnode_put_attrib(x, "to", from);
	xmlnode_put_attrib(x, "id", id);
	querynode = xmlnode_get_tag(x, "query");

	strftime(buf, 1024, "%Y%m%dT%T", now);
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "utc"), buf, -1);
	strftime(buf, 1024, "%Z", now);
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "tz"), buf, -1);
	strftime(buf, 1024, "%d %b %Y %T", now);
	xmlnode_insert_cdata(xmlnode_insert_tag(querynode, "display"), buf, -1);
	
	gjab_send(j, x);

	xmlnode_free(x);
}

static void jabber_handlelast(gjconn j, xmlnode iqnode) {
   	xmlnode x, querytag;
	char *id, *from;
	struct jabber_data *jd = GJ_GC(j)->proto_data;
	char idle_time[32];
	
	id = xmlnode_get_attrib(iqnode, "id");
	from = xmlnode_get_attrib(iqnode, "from");

	x = jutil_iqnew(JPACKET__RESULT, "jabber:iq:last");

	xmlnode_put_attrib(x, "to", from);
	xmlnode_put_attrib(x, "id", id);
	querytag = xmlnode_get_tag(x, "query");
	g_snprintf(idle_time, sizeof idle_time, "%ld", jd->idle ? time(NULL) - jd->idle : 0);
	xmlnode_put_attrib(querytag, "seconds", idle_time);

	gjab_send(j, x);
	xmlnode_free(x);
}

static void jabber_handlepacket(gjconn j, jpacket p)
{
	char *id;
	switch (p->type) {
	case JPACKET_MESSAGE:
		jabber_handlemessage(j, p);
		break;
	case JPACKET_PRESENCE:
		jabber_handlepresence(j, p);
		break;
	case JPACKET_IQ:
		debug_printf("jpacket_subtype: %d\n", jpacket_subtype(p));

		if (((id = xmlnode_get_attrib(p->x, "id")) != NULL) && !strcmp(id, IQID_AUTH)) {
			jabber_handleauthresp(j, p);
			break;
		}

		if (jpacket_subtype(p) == JPACKET__SET) {
		} else if (jpacket_subtype(p) == JPACKET__GET) {
		   	xmlnode querynode;
			querynode = xmlnode_get_tag(p->x, "query");
		   	if (NSCHECK(querynode, NS_VERSION)) {
			   	jabber_handleversion(j, p->x);
			} else if (NSCHECK(querynode, NS_TIME)) {
			   	jabber_handletime(j, p->x);
			} else if (NSCHECK(querynode, "jabber:iq:last")) {
			   	jabber_handlelast(j, p->x);
			}
		} else if (jpacket_subtype(p) == JPACKET__RESULT) {
			xmlnode querynode, vcard;
			char *xmlns, *from;

			from = xmlnode_get_attrib(p->x, "from");
			querynode = xmlnode_get_tag(p->x, "query");
			xmlns = xmlnode_get_attrib(querynode, "xmlns");
			vcard = xmlnode_get_tag(p->x, "vCard");
			if (!vcard)
				vcard = xmlnode_get_tag(p->x, "VCARD");

			if (NSCHECK(querynode, NS_ROSTER)) {
				jabber_handleroster(j, querynode);
			} else if (NSCHECK(querynode, NS_VCARD)) {
			   	jabber_handlevcard(j, querynode, from);
			} else if (vcard) {
				jabber_handlevcard(j, vcard, from);
			} else {
				/* debug_printf("jabber:iq:query: %s\n", xmlns); */
			}

		} else if (jpacket_subtype(p) == JPACKET__ERROR) {
			xmlnode xerr;
			char *from, *errmsg = NULL;
			int errcode = 0;

			from = xmlnode_get_attrib(p->x, "from");
			xerr = xmlnode_get_tag(p->x, "error");
			if (xerr) {
				errmsg = xmlnode_get_data(xerr);
				if (xmlnode_get_attrib(xerr, "code"))
					errcode = atoi(xmlnode_get_attrib(xerr, "code"));
			}

			from = g_strdup_printf("Error %d (%s)", errcode, from);
			do_error_dialog(errmsg, from);
			g_free(from);

		}

		break;

	case JPACKET_S10N:
		jabber_handles10n(j, p);
		break;
	default:
		debug_printf("jabber: packet type %d (%s)\n", p->type, xmlnode2str(p->x));
	}

	xmlnode_free(p->x);

	return;
}

static void jabber_handlestate(gjconn j, int state)
{
	switch (state) {
	case JCONN_STATE_OFF:
		hide_login_progress(GJ_GC(j), "Unable to connect");
		signoff(GJ_GC(j));
		break;
	case JCONN_STATE_CONNECTED:
		set_login_progress(GJ_GC(j), 2, "Connected");
		break;
	case JCONN_STATE_ON:
		set_login_progress(GJ_GC(j), 3, "Requesting Authentication Method");
		gjab_reqauth(j);
		break;
	default:
		debug_printf("state change: %d\n", state);
	}
	return;
}

static void jabber_login(struct aim_user *user)
{
	struct gaim_connection *gc = new_gaim_conn(user);
	struct jabber_data *jd = gc->proto_data = g_new0(struct jabber_data, 1);
	char *loginname = create_valid_jid(user->username, DEFAULT_SERVER, "GAIM");

	jd->hash = g_hash_table_new(g_str_hash, g_str_equal);

	set_login_progress(gc, 1, "Connecting");

	if (!(jd->jc = gjab_new(loginname, user->password, gc))) {
		g_free(loginname);
		debug_printf("jabber: unable to connect (jab_new failed)\n");
		hide_login_progress(gc, "Unable to connect");
		signoff(gc);
		return;
	}

	g_free(loginname);
	gjab_state_handler(jd->jc, jabber_handlestate);
	gjab_packet_handler(jd->jc, jabber_handlepacket);
	gjab_start(jd->jc);
}

static gboolean jabber_destroy_hash(gpointer key, gpointer val, gpointer data) {
   	g_free(key);
	g_free(val);
	return TRUE;
}

static gboolean jabber_free(gpointer data)
{
	gjab_delete(data);
	return FALSE;
}

static void jabber_close(struct gaim_connection *gc)
{
	struct jabber_data *jd = gc->proto_data;
	g_hash_table_foreach_remove(jd->hash, jabber_destroy_hash, NULL);
	g_hash_table_destroy(jd->hash);
	if (gc->inpa)
		gaim_input_remove(gc->inpa);
	close(jd->jc->fd);
	g_timeout_add(50, jabber_free, jd->jc);
	xmlnode_free(jd->jc->current);
	g_free(jd->jc->sid);
	jd->jc = NULL;
	g_free(jd);
	gc->proto_data = NULL;
}

static int jabber_send_im(struct gaim_connection *gc, char *who, char *message, int flags)
{
	xmlnode x, y;
	char *realwho;
	gjconn j = ((struct jabber_data *)gc->proto_data)->jc;

	if (!who || !message)
		return 0;

	x = xmlnode_new_tag("message");
	if (!strchr(who, '@'))
		realwho = g_strdup_printf("%s@%s", who, j->user->server);
	else
		realwho = g_strdup(who);
	xmlnode_put_attrib(x, "to", realwho);
	g_free(realwho);

	xmlnode_insert_tag(x, "gaim");
	xmlnode_put_attrib(x, "type", "chat");

	if (message && strlen(message)) {
		char *utf8 = str_to_utf8(message);
		y = xmlnode_insert_tag(x, "body");
		xmlnode_insert_cdata(y, utf8, -1);
		g_free(utf8);
	}

	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);
	xmlnode_free(x);
	return 1;
}

static void jabber_add_buddy(struct gaim_connection *gc, char *name)
{
	xmlnode x, y;
	char *realwho;
	gjconn j = ((struct jabber_data *)gc->proto_data)->jc;

	if (!((struct jabber_data *)gc->proto_data)->did_import)
		return;

	if (!name)
		return;

	if (!strcmp(gc->username, name))
		return;

	if (!strchr(name, '@'))
		realwho = g_strdup_printf("%s@%s", name, j->user->server);
	else {
		jid who = jid_new(j->p, name);
		if (who->user == NULL) {
			/* FIXME: transport */
			return;
		}
		realwho = g_strdup_printf("%s@%s", who->user, who->server);
	}

	x = jutil_iqnew(JPACKET__SET, NS_ROSTER);
	y = xmlnode_insert_tag(xmlnode_get_tag(x, "query"), "item");
	xmlnode_put_attrib(y, "jid", realwho);
	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);
	xmlnode_free(x);

	x = xmlnode_new_tag("presence");
	xmlnode_put_attrib(x, "to", realwho);
	xmlnode_put_attrib(x, "type", "subscribe");
	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);

	g_free(realwho);
}

static void jabber_remove_buddy(struct gaim_connection *gc, char *name, char *group)
{
	xmlnode x, y;
	char *realwho;
	gjconn j = ((struct jabber_data *)gc->proto_data)->jc;

	if (!name)
		return;

	if (!strchr(name, '@'))
		realwho = g_strdup_printf("%s@%s", name, j->user->server);
	else
		realwho = g_strdup(name);

	x = jutil_iqnew(JPACKET__SET, NS_ROSTER);
	y = xmlnode_insert_tag(xmlnode_get_tag(x, "query"), "item");
	xmlnode_put_attrib(y, "jid", realwho);
	xmlnode_put_attrib(y, "subscription", "remove");
	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);

	g_free(realwho);
	xmlnode_free(x);
}

static char **jabber_list_icon(int uc)
{
	switch (uc) {
	case UC_AWAY:
		return available_away_xpm;
	case UC_CHAT:
		return available_chat_xpm;
	case UC_XA:
		return available_xa_xpm;
	case UC_DND:
		return available_dnd_xpm;
	default:
		return available_xpm;
	}
}

static GList *jabber_chat_info(struct gaim_connection *gc)
{
	gjconn j = ((struct jabber_data *)gc->proto_data)->jc;

	GList *m = NULL;
	struct proto_chat_entry *pce;

	pce = g_new0(struct proto_chat_entry, 1);
	pce->label = _("Room:");
	m = g_list_append(m, pce);

	pce = g_new0(struct proto_chat_entry, 1);
	pce->label = _("Server:");
	pce->def = DEFAULT_GROUPCHAT;
	m = g_list_append(m, pce);

	pce = g_new0(struct proto_chat_entry, 1);
	pce->label = _("Handle:");
	pce->def = j->user->user;
	m = g_list_append(m, pce);

	return m;
}

static void jabber_join_chat(struct gaim_connection *gc, GList *data)
{
	xmlnode x;
	char *realwho;
	gjconn j = ((struct jabber_data *)gc->proto_data)->jc;
	GSList *pc = ((struct jabber_data *)gc->proto_data)->pending_chats;
	struct jabber_chat *jc;

	if (!data || !data->next || !data->next->next)
		return;

	jc = g_new0(struct jabber_chat, 1);
	realwho = create_valid_jid(data->data, data->next->data,
			data->next->next->data);
	jc->Jid = jid_new(j->p, realwho);
	jc->gc = gc;
	debug_printf("%s\n", realwho);

	x = jutil_presnew(0, realwho, NULL);
	gjab_send(j, x);
	xmlnode_free(x);
	g_free(realwho);

	((struct jabber_data *)gc->proto_data)->pending_chats = g_slist_append(pc, jc);
}

static void jabber_chat_invite(struct gaim_connection *gc, int id, char *message, char *name)
{
	xmlnode x, y;
	GSList *bcs = gc->buddy_chats;
	struct conversation *b = NULL;
	struct jabber_data *jd = gc->proto_data;
	gjconn j = jd->jc;
	struct jabber_chat *jc = NULL;
	char *realwho, *subject;

	if (!name)
		return;

	/* find which chat we're inviting to */
	while (bcs) {
		b = bcs->data;
		if (id == b->id)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	bcs = jd->existing_chats;
	while (bcs) {
		jc = bcs->data;
		if (jc->b == b)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	x = xmlnode_new_tag("message");
	if (!strchr(name, '@'))
		realwho = g_strdup_printf("%s@%s", name, j->user->server);
	else
		realwho = g_strdup(name);
	xmlnode_put_attrib(x, "to", realwho);
	g_free(realwho);

	y = xmlnode_insert_tag(x, "x");
	xmlnode_put_attrib(y, "xmlns", "jabber:x:conference");
	subject = g_strdup_printf("%s@%s", jc->Jid->user, jc->Jid->server);
	xmlnode_put_attrib(y, "jid", subject);
	g_free(subject);

	if (message && strlen(message)) {
		char *utf8 = str_to_utf8(message);
		y = xmlnode_insert_tag(x, "body");
		xmlnode_insert_cdata(y, utf8, -1);
		g_free(utf8);
	}

	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);
	xmlnode_free(x);
}

static void jabber_chat_leave(struct gaim_connection *gc, int id)
{
	GSList *bcs = gc->buddy_chats;
	struct conversation *b = NULL;
	struct jabber_data *jd = gc->proto_data;
	gjconn j = jd->jc;
	struct jabber_chat *jc = NULL;
	char *realwho;
	xmlnode x;

	while (bcs) {
		b = bcs->data;
		if (id == b->id)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	bcs = jd->existing_chats;
	while (bcs) {
		jc = bcs->data;
		if (jc->b == b)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	realwho = g_strdup_printf("%s@%s", jc->Jid->user, jc->Jid->server);
	x = jutil_presnew(0, realwho, NULL);
	g_free(realwho);
	xmlnode_put_attrib(x, "type", "unavailable");
	gjab_send(j, x);
	xmlnode_free(x);
	jc->b = NULL;
}

static int jabber_chat_send(struct gaim_connection *gc, int id, char *message)
{
	GSList *bcs = gc->buddy_chats;
	struct conversation *b = NULL;
	struct jabber_data *jd = gc->proto_data;
	xmlnode x, y;
	struct jabber_chat *jc = NULL;
	char *chatname;

	while (bcs) {
		b = bcs->data;
		if (id == b->id)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return -EINVAL;

	bcs = jd->existing_chats;
	while (bcs) {
		jc = bcs->data;
		if (jc->b == b)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return -EINVAL;

	x = xmlnode_new_tag("message");
	xmlnode_put_attrib(x, "from", jid_full(jc->Jid));
	chatname = g_strdup_printf("%s@%s", jc->Jid->user, jc->Jid->server);
	xmlnode_put_attrib(x, "to", chatname);
	g_free(chatname);
	xmlnode_put_attrib(x, "type", "groupchat");

	if (message && strlen(message) > strlen("/topic ") &&
			!g_strncasecmp(message, "/topic ", strlen("/topic "))) {
		char buf[8192];
		char *utf8 = str_to_utf8(message + strlen("/topic "));
		y = xmlnode_insert_tag(x, "subject");
		xmlnode_insert_cdata(y, utf8, -1);
		y = xmlnode_insert_tag(x, "body");
		g_snprintf(buf, sizeof(buf), "/me has changed the subject to: %s", utf8);
		xmlnode_insert_cdata(y, buf, -1);
		g_free(utf8);
	} else if (message && strlen(message)) {
		char *utf8 = str_to_utf8(message);
		y = xmlnode_insert_tag(x, "body");
		xmlnode_insert_cdata(y, utf8, -1);
		g_free(utf8);
	}

	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);
	xmlnode_free(x);
	return 0;
}

static void jabber_chat_whisper(struct gaim_connection *gc, int id, char *who, char *message)
{
	GSList *bcs = gc->buddy_chats;
	struct conversation *b = NULL;
	struct jabber_data *jd = gc->proto_data;
	xmlnode x, y;
	struct jabber_chat *jc = NULL;
	char *chatname;

	while (bcs) {
		b = bcs->data;
		if (id == b->id)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	bcs = jd->existing_chats;
	while (bcs) {
		jc = bcs->data;
		if (jc->b == b)
			break;
		bcs = bcs->next;
	}
	if (!bcs)
		return;

	x = xmlnode_new_tag("message");
	xmlnode_put_attrib(x, "from", jid_full(jc->Jid));
	chatname = g_strdup_printf("%s@%s/%s", jc->Jid->user, jc->Jid->server, who);
	xmlnode_put_attrib(x, "to", chatname);
	g_free(chatname);
	xmlnode_put_attrib(x, "type", "normal");

	if (message && strlen(message)) {
		char *utf8 = str_to_utf8(message);
		y = xmlnode_insert_tag(x, "body");
		xmlnode_insert_cdata(y, utf8, -1);
		g_free(utf8);
	}

	gjab_send(((struct jabber_data *)gc->proto_data)->jc, x);
	xmlnode_free(x);
}

static char *jabber_normalize(const char *s)
{
	static char buf[BUF_LEN];
	char *t, *u;
	int x = 0;

	g_return_val_if_fail((s != NULL), NULL);

	u = t = g_strdup(s);

	g_strdown(t);

	while (*t && (x < BUF_LEN - 1)) {
		if (*t != ' ')
			buf[x++] = *t;
		t++;
	}
	buf[x] = '\0';
	g_free(u);

	if (!strchr(buf, '@')) {
		strcat(buf, "@jabber.org"); /* this isn't always right, but eh */
	} else if ((u = strchr(strchr(buf, '@'), '/')) != NULL) {
		*u = '\0';
	}

	return buf;
}

static void jabber_get_info(struct gaim_connection *gc, char *who) {
	xmlnode x;
	char *id;
	struct jabber_data *jd = gc->proto_data;
	gjconn j = jd->jc;

	x = jutil_iqnew(JPACKET__GET, NS_VCARD);
	xmlnode_put_attrib(x, "to", who);
	id = gjab_getid(j);
	xmlnode_put_attrib(x, "id", id);

	gjab_send(j, x);

	xmlnode_free(x);
	
}

static GList *jabber_buddy_menu(struct gaim_connection *gc, char *who) {
	GList *m = NULL;
	struct proto_buddy_menu *pbm;

	pbm = g_new0(struct proto_buddy_menu, 1);
	pbm->label = _("Get Info");
	pbm->callback = jabber_get_info;
	pbm->gc = gc;
	m = g_list_append(m, pbm);

	return m;
}

static GList *jabber_away_states(struct gaim_connection *gc) {
	GList *m = NULL;

	m = g_list_append(m, "Online");
	m = g_list_append(m, "Chatty");
	m = g_list_append(m, "Away");
	m = g_list_append(m, "Extended Away");
	m = g_list_append(m, "Do Not Disturb");

	return m;
}

static void jabber_set_away(struct gaim_connection *gc, char *state, char *message)
{
	xmlnode x, y;
	struct jabber_data *jd = gc->proto_data;
	gjconn j = jd->jc;

	gc->away = NULL; /* never send an auto-response */

	x = xmlnode_new_tag("presence");

	if (!strcmp(state, GAIM_AWAY_CUSTOM)) {
		/* oh goody. Gaim is telling us what to do. */
		if (message) {
			/* Gaim wants us to be away */
			y = xmlnode_insert_tag(x, "show");
			xmlnode_insert_cdata(y, "away", -1);
			y = xmlnode_insert_tag(x, "status");
			xmlnode_insert_cdata(y, message, -1);
			gc->away = "";
		} else {
			/* Gaim wants us to not be away */
			/* but for Jabber, we can just send presence with no other information. */
		}
	} else {
		/* state is one of our own strings. it won't be NULL. */
		if (!strcmp(state, "Online")) {
			/* once again, we don't have to put anything here */
		} else if (!strcmp(state, "Chatty")) {
			y = xmlnode_insert_tag(x, "show");
			xmlnode_insert_cdata(y, "chat", -1);
		} else if (!strcmp(state, "Away")) {
			y = xmlnode_insert_tag(x, "show");
			xmlnode_insert_cdata(y, "away", -1);
			gc->away = "";
		} else if (!strcmp(state, "Extended Away")) {
			y = xmlnode_insert_tag(x, "show");
			xmlnode_insert_cdata(y, "xa", -1);
			gc->away = "";
		} else if (!strcmp(state, "Do Not Disturb")) {
			y = xmlnode_insert_tag(x, "show");
			xmlnode_insert_cdata(y, "dnd", -1);
			gc->away = "";
		}
	}

	gjab_send(j, x);
	xmlnode_free(x);
}

static void jabber_set_idle(struct gaim_connection *gc, int idle) {
	struct jabber_data *jd = (struct jabber_data *)gc->proto_data;
	debug_printf("jabber_set_idle: setting idle %i\n", idle);
   	jd->idle = idle ? time(NULL) - idle : idle;
}

static void jabber_keepalive(struct gaim_connection *gc) {
	struct jabber_data *jd = (struct jabber_data *)gc->proto_data;
	gjab_send_raw(jd->jc, "  \t  ");
}

static GList *jabber_user_opts()
{
	GList *m = NULL;
	struct proto_user_opt *puo;

	puo = g_new0(struct proto_user_opt, 1);
	puo->label = "Port:";
	puo->def = "5222";
	puo->pos = USEROPT_PORT;
	m = g_list_append(m, puo);

	return m;
}

static void jabber_buddy_free(struct buddy *b)
{
	while (b->proto_data) {
		g_free(((GSList *)b->proto_data)->data);
		b->proto_data = g_slist_remove(b->proto_data, ((GSList *)b->proto_data)->data);
	}
}

static struct prpl *my_protocol = NULL;

void jabber_init(struct prpl *ret)
{
	/* the NULL's aren't required but they're nice to have */
	ret->protocol = PROTO_JABBER;
	ret->options = OPT_PROTO_UNIQUE_CHATNAME | OPT_PROTO_CHAT_TOPIC;
	ret->name = jabber_name;
	ret->list_icon = jabber_list_icon;
	ret->away_states = jabber_away_states;
	ret->buddy_menu = jabber_buddy_menu;
	ret->user_opts = jabber_user_opts;
	ret->login = jabber_login;
	ret->close = jabber_close;
	ret->send_im = jabber_send_im;
	ret->set_info = NULL;
	ret->get_info = jabber_get_info;
	ret->set_away = jabber_set_away;
	ret->set_dir = NULL;
	ret->get_dir = NULL;
	ret->dir_search = NULL;
	ret->set_idle = jabber_set_idle;
	ret->change_passwd = NULL;
	ret->add_buddy = jabber_add_buddy;
	ret->add_buddies = NULL;
	ret->remove_buddy = jabber_remove_buddy;
	ret->add_permit = NULL;
	ret->add_deny = NULL;
	ret->rem_permit = NULL;
	ret->rem_deny = NULL;
	ret->set_permit_deny = NULL;
	ret->warn = NULL;
	ret->chat_info = jabber_chat_info;
	ret->join_chat = jabber_join_chat;
	ret->chat_invite = jabber_chat_invite;
	ret->chat_leave = jabber_chat_leave;
	ret->chat_whisper = jabber_chat_whisper;
	ret->chat_send = jabber_chat_send;
	ret->keepalive = jabber_keepalive;
	ret->normalize = jabber_normalize;
	ret->buddy_free = jabber_buddy_free;

	my_protocol = ret;
}

#ifndef STATIC

char *gaim_plugin_init(GModule *handle)
{
	load_protocol(jabber_init, sizeof(struct prpl));
	return NULL;
}

void gaim_plugin_remove()
{
	struct prpl *p = find_prpl(PROTO_JABBER);
	if (p == my_protocol)
		unload_protocol(p);
}

char *name()
{
	return "Jabber";
}

char *description()
{
	return PRPL_DESC("Jabber");
}

#endif