view src/conversation.c @ 4819:815afc71c8e4

[gaim-migrate @ 5144] On the one hand, rabbits not laying eggs is a conscious decision--it's not that they can't, it's that they choose not to. But on the other hand, the first hand makes no sense. This should make reading ICQ info for people with non-ascii cruft not crash Gaim and/or output ugly pango utf8 messages. I also make the ICQ info strings internationalizationable. committer: Tailor Script <tailor@pidgin.im>
author Mark Doliner <mark@kingant.net>
date Tue, 18 Mar 2003 05:52:22 +0000
parents 1c371e4244d6
children 9567b13d0e98
line wrap: on
line source

/*
 * gaim
 *
 * Copyright (C) 2002-2003, Christian Hammond <chipx86@gnupdate.org>
 * 
 * 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 <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <errno.h>
#include <ctype.h>
#include "conversation.h"
#include "gaim.h"
#include "prpl.h"

#ifdef _WIN32
#include "win32dep.h"
#endif

struct ConvPlacementData
{
	char *name;
	gaim_conv_placement_fnc fnc;
};

#define SEND_TYPED_TIMEOUT 5000

static struct gaim_window_ui_ops *win_ui_ops = NULL;

static GList *conversations = NULL;
static GList *ims = NULL;
static GList *chats = NULL;
static GList *windows = NULL;
static GList *conv_placement_fncs = NULL;
static gaim_conv_placement_fnc place_conv = NULL;
static int place_conv_index = -1;

static gint
insertname_compare(gconstpointer one, gconstpointer two)
{
	const char *a = (const char *)one;
	const char *b = (const char *)two;

	if (*a == '@') {
		if (*b != '@') return -1;

		return strcasecmp(a + 1, b + 1);

	} else if (*a == '%') {
		if (*b != '%') return -1;

		return strcasecmp(a + 1, b + 1);

	} else if (*a == '+') {
		if (*b == '@') return  1;
		if (*b != '+') return -1;

		return strcasecmp(a + 1, b + 1);

	} else if (*b == '@' || *b == '%' || *b == '+')
		return 1;

	return strcasecmp(a, b);
}

static gboolean
find_nick(struct gaim_connection *gc, const char *message)
{
	char *msg, *who, *p;
	int n;

	msg = g_utf8_strdown(message, -1);

	who = g_utf8_strdown(gc->username, -1);
	n = strlen(who);

	if ((p = strstr(msg, who)) != NULL) {
		if ((p == msg || !isalnum(*(p - 1))) && !isalnum(*(p + n))) {
			g_free(who);
			g_free(msg);

			return TRUE;
		}
	}

	g_free(who);

	if (!gaim_utf8_strcasecmp(gc->username, gc->displayname)) {
		g_free(msg);

		return FALSE;
	}

	who = g_utf8_strdown(gc->displayname, -1);
	n = strlen(who);

	if (n > 0 && (p = strstr(msg, who)) != NULL) {
		if ((p == msg || !isalnum(*(p - 1))) && !isalnum(*(p + n))) {
			g_free(who);
			g_free(msg);

			return TRUE;
		}
	}

	g_free(who);
	g_free(msg);

	return FALSE;
}

static gboolean
reset_typing(gpointer data)
{
	char *name = (char *)data;
	struct gaim_conversation *c = gaim_find_conversation(name);
	struct gaim_im *im;

	if (!c)
		return FALSE;

	im = GAIM_IM(c);

	gaim_im_set_typing_state(im, NOT_TYPING);
	gaim_im_update_typing(im);
	gaim_im_stop_typing_timeout(im);

	return FALSE;
}

static gboolean
send_typed(gpointer data)
{
	struct gaim_conversation *conv = (struct gaim_conversation *)data;
	struct gaim_connection *gc;
	const char *name;

	gc   = gaim_conversation_get_gc(conv);
	name = gaim_conversation_get_name(conv);

	if (conv != NULL && gc != NULL && name != NULL) {
		gaim_im_set_type_again(GAIM_IM(conv), TRUE);

		/* XXX Somebody add const stuff! */
		serv_send_typing(gc, (char *)name, TYPED);

		debug_printf("typed...\n");
	}

	return FALSE;
}

static void
common_send(struct gaim_conversation *conv, const char *message)
{
	GaimConversationType type;
	struct gaim_connection *gc;
	struct gaim_conversation_ui_ops *ops;
	char *buf, *buf2, *buffy;
	gulong length = 0;
	gboolean binary = FALSE;
	int plugin_return;
	int limit;
	int err = 0;
	GList *first;

	if ((gc = gaim_conversation_get_gc(conv)) == NULL)
		return;

	type = gaim_conversation_get_type(conv);
	ops  = gaim_conversation_get_ui_ops(conv);

	limit = 32 * 1024; /* You shouldn't be sending more than 32K in your
						  messages. That's a book. */

	buf = g_malloc(limit);
	strncpy(buf, message, limit);

	if (strlen(buf) == 0) {
		g_free(buf);

		return;
	}

	first = g_list_first(conv->send_history);

	if (first->data)
		g_free(first->data);

	first->data = g_strdup(buf);

	conv->send_history = g_list_prepend(first, NULL);

	buf2 = g_malloc(limit);

	if (gc->flags & OPT_CONN_HTML) {
		if (convo_options & OPT_CONVO_SEND_LINKS)
			linkify_text(buf);
	}

	buffy = g_strdup(buf);
	plugin_return = plugin_event(
			(type == GAIM_CONV_IM ? event_im_send : event_chat_send),
			gc,
			(type == GAIM_CONV_IM
			 ? gaim_conversation_get_name(conv)
			 : (void *)gaim_chat_get_id(GAIM_CHAT(conv))),
			&buffy);

	if (buffy == NULL) {
		g_free(buf2);
		g_free(buf);
		return;
	}

	if (plugin_return) {
		g_free(buffy);
		g_free(buf2);
		g_free(buf);
		return;
	}

	strncpy(buf, buffy, limit);
	g_free(buffy);

	if (type == GAIM_CONV_IM) {
		struct gaim_im *im = GAIM_IM(conv);

		buffy = g_strdup(buf);
		plugin_event(event_im_displayed_sent, gc,
					 gaim_conversation_get_name(conv), &buffy);

		if (buffy != NULL) {
			int imflags = 0;

			if (conv->u.im->images != NULL) {
				int id = 1, offset = 0;
				char *bigbuf = NULL;
				GSList *tmplist;

				for (tmplist = conv->u.im->images;
					 tmplist != NULL;
					 tmplist = tmplist->next) {

					char *img_filename = (char *)tmplist->data;
					FILE *imgfile;
					char *filename, *c;
					struct stat st;
					char imgtag[1024];

					if (stat(img_filename, &st) != 0) {
						debug_printf("Could not stat %s\n",
									 (char *)img_filename);
						continue;
					}

					/*
					 * Here we check to make sure the user still wants to send
					 * the image. He may have deleted the <img> tag in which
					 * case we don't want to send the binary data.
					 */
					filename = img_filename;

					while ((c = strchr(filename, '/')) != NULL)
						filename = c + 1;
					
					g_snprintf(imgtag, sizeof(imgtag),
							   "<IMG SRC=\"file://%s\" ID=\"%d\" "
							   "DATASIZE=\"%d\">",
							   filename, id, (int)st.st_size);

					if (strstr(buffy, imgtag) == 0) {
						debug_printf("Not sending image: %s\n", img_filename);
						continue;
					}

					if (!binary) {
						length = strlen(buffy) + strlen("<BINARY></BINARY>");
						bigbuf = g_malloc(length + 1);
						g_snprintf(bigbuf,
								   strlen(buffy) + strlen("<BINARY> ") + 1,
								   "%s<BINARY>", buffy);

						offset = strlen(buffy) + strlen("<BINARY>");
						binary = TRUE;
					}

					g_snprintf(imgtag, sizeof(imgtag),
							   "<DATA ID=\"%d\" SIZE=\"%d\">",
							   id, (int)st.st_size);

					length += strlen(imgtag) + st.st_size + strlen("</DATA>");

					bigbuf = g_realloc(bigbuf, length + 1);

					if ((imgfile = fopen(img_filename, "r")) == NULL) {
						debug_printf("Could not open %s\n", img_filename);
						continue;
					}

					strncpy(bigbuf + offset, imgtag, strlen(imgtag) + 1);

					offset += strlen(imgtag);
					offset += fread(bigbuf + offset, 1, st.st_size, imgfile);

					fclose(imgfile);

					strncpy(bigbuf + offset, "</DATA>",
							strlen("</DATA>") + 1);

					offset += strlen("</DATA>");
					id++;
				}

				if (binary) {
					strncpy(bigbuf + offset, "</BINARY>",
							strlen("<BINARY>") + 1);

					err = serv_send_im(gc,
						(char *)gaim_conversation_get_name(conv),
						bigbuf, length, imflags);
				}
				else
					err = serv_send_im(gc,
						(char *)gaim_conversation_get_name(conv),
						buffy, -1, imflags);

				if (err > 0) {
					GSList *tempy;

					for (tempy = conv->u.im->images;
						 tempy != NULL;
						 tempy = tempy->next) {

						g_free(tempy->data);
					}

					g_slist_free(tempy);
					conv->u.im->images = NULL;

					if (binary)
						gaim_im_write(im, NULL, bigbuf, length,
									  WFLAG_SEND, time(NULL));
					else
						gaim_im_write(im, NULL, buffy, -1, WFLAG_SEND,
									  time(NULL));

					if (im_options & OPT_IM_POPDOWN)
						gaim_window_hide(gaim_conversation_get_window(conv));
				}

				if (binary)
					g_free(bigbuf);
			}
			else {
				err = serv_send_im(gc, (char *)gaim_conversation_get_name(conv),
								   buffy, -1, imflags);

				if (err > 0) {
					gaim_im_write(im, NULL, buf, -1, WFLAG_SEND, time(NULL));

					if (im_options & OPT_IM_POPDOWN)
						gaim_window_hide(gaim_conversation_get_window(conv));
				}
			}

			g_free(buffy);
		}
	}
	else {
		err = serv_chat_send(gc, gaim_chat_get_id(GAIM_CHAT(conv)), buf);
	}

	g_free(buf2);
	g_free(buf);

	if (err < 0) {
		if (err == -E2BIG)
			do_error_dialog(_("Unable to send message. "
							  "The message is too large."), NULL,
							GAIM_ERROR);
		else if (err == -ENOTCONN)
			debug_printf("Not yet connected.\n");
		else
			do_error_dialog(_("Unable to send message."), NULL, GAIM_ERROR);
	}
	else {
		if (err > 0 && (away_options & OPT_AWAY_BACK_ON_IM)) {
			if (awaymessage != NULL) {
				do_im_back();
			}
			else if (gc->away) {
				serv_set_away(gc, GAIM_AWAY_CUSTOM, NULL);
			}
		}
	}

}

static void
update_conv_indexes(struct gaim_window *win)
{
	GList *l;
	int i;

	for (l = gaim_window_get_conversations(win), i = 0;
		 l != NULL;
		 l = l->next, i++) {

		struct gaim_conversation *conv = (struct gaim_conversation *)l->data;

		conv->conversation_pos = i;
	}
}

struct gaim_window *
gaim_window_new(void)
{
	struct gaim_window *win;

	win = g_malloc0(sizeof(struct gaim_window));

	win->ui_ops = gaim_get_win_ui_ops();

	if (win->ui_ops != NULL && win->ui_ops->new_window != NULL)
		win->ui_ops->new_window(win);

	windows = g_list_append(windows, win);

	return win;
}

void
gaim_window_destroy(struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;
	GList *node;

	if (win == NULL)
		return;

	ops = gaim_window_get_ui_ops(win);

	/*
	 * If there are any conversations in this, destroy them all. The last
	 * conversation will call gaim_window_destroy(), but this time, this
	 * check will fail and the window will actually be destroyed.
	 *
	 * This is needed because chats may not close right away. They may
	 * wait for notification first. When they get that, the window is
	 * already destroyed, and gaim either crashes or spits out gtk warnings.
	 * The problem is fixed with this check.
	 */
	if (gaim_window_get_conversation_count(win) > 0) {

		node = g_list_first(gaim_window_get_conversations(win));
		while(node != NULL)
		{
			struct gaim_conversation *conv = node->data;

			node = g_list_next(node);

			gaim_conversation_destroy(conv);
		}
	}
	else
	{
		if (ops != NULL && ops->destroy_window != NULL)
			ops->destroy_window(win);

		g_list_free(gaim_window_get_conversations(win));

		windows = g_list_remove(windows, win);

		g_free(win);
	}
}

void
gaim_window_show(struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL)
		return;

	ops = gaim_window_get_ui_ops(win);

	if (ops == NULL || ops->show == NULL)
		return;

	ops->show(win);
}

void
gaim_window_hide(struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL)
		return;

	ops = gaim_window_get_ui_ops(win);

	if (ops == NULL || ops->hide == NULL)
		return;

	ops->hide(win);
}

void
gaim_window_raise(struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL)
		return;

	ops = gaim_window_get_ui_ops(win);

	if (ops == NULL || ops->raise == NULL)
		return;

	ops->raise(win);
}

void
gaim_window_flash(struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL)
		return;

	ops = gaim_window_get_ui_ops(win);

	if (ops == NULL || ops->flash == NULL)
		return;

	ops->flash(win);
}

void
gaim_window_set_ui_ops(struct gaim_window *win, struct gaim_window_ui_ops *ops)
{
	struct gaim_conversation_ui_ops *conv_ops = NULL;
	GList *l;

	if (win == NULL || win->ui_ops == ops)
		return;

	if (ops != NULL) {
		if (ops->get_conversation_ui_ops != NULL)
			conv_ops = ops->get_conversation_ui_ops();
	}

	if (win->ui_ops != NULL) {
		if (win->ui_ops->destroy_window != NULL)
			win->ui_ops->destroy_window(win);
	}

	win->ui_ops = ops;

	if (win->ui_ops != NULL) {
		if (win->ui_ops->new_window != NULL)
			win->ui_ops->new_window(win);
	}

	for (l = gaim_window_get_conversations(win);
		 l != NULL;
		 l = l->next) {

		struct gaim_conversation *conv = (struct gaim_conversation *)l;

		gaim_conversation_set_ui_ops(conv, conv_ops);

		if (win->ui_ops != NULL && win->ui_ops->add_conversation != NULL)
			win->ui_ops->add_conversation(win, conv);
	}
}

struct gaim_window_ui_ops *
gaim_window_get_ui_ops(const struct gaim_window *win)
{
	if (win == NULL)
		return NULL;

	return win->ui_ops;
}

int
gaim_window_add_conversation(struct gaim_window *win,
							 struct gaim_conversation *conv)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL || conv == NULL)
		return -1;

	if (gaim_conversation_get_window(conv) != NULL) {
		gaim_window_remove_conversation(
			gaim_conversation_get_window(conv),
			gaim_conversation_get_index(conv));
	}

	ops = gaim_window_get_ui_ops(win);

	win->conversations = g_list_append(win->conversations, conv);
	win->conversation_count++;

	conv->conversation_pos = win->conversation_count - 1;

	if (ops != NULL) {
		conv->window = win;

		if (ops->get_conversation_ui_ops != NULL)
			gaim_conversation_set_ui_ops(conv, ops->get_conversation_ui_ops());

		if (ops->add_conversation != NULL)
			ops->add_conversation(win, conv);
	}

	return win->conversation_count - 1;
}

struct gaim_conversation *
gaim_window_remove_conversation(struct gaim_window *win, unsigned int index)
{
	struct gaim_window_ui_ops *ops;
	struct gaim_conversation *conv;
	GList *node;

	if (win == NULL || index >= gaim_window_get_conversation_count(win))
		return NULL;

	ops = gaim_window_get_ui_ops(win);

	node = g_list_nth(gaim_window_get_conversations(win), index);
	conv = (struct gaim_conversation *)node->data;

	if (ops != NULL && ops->remove_conversation != NULL)
		ops->remove_conversation(win, conv);

	win->conversations = g_list_remove_link(win->conversations, node);

	g_list_free_1(node);

	win->conversation_count--;

	conv->window = NULL;

	if (gaim_window_get_conversation_count(win) == 0)
		gaim_window_destroy(win);
	else {
		/* Change all the indexes. */
		update_conv_indexes(win);
	}

	return conv;
}

void
gaim_window_move_conversation(struct gaim_window *win, unsigned int index,
							  unsigned int new_index)
{
	struct gaim_window_ui_ops *ops;
	struct gaim_conversation *conv;
	GList *l;

	if (win == NULL || index >= gaim_window_get_conversation_count(win) ||
		index == new_index)
		return;

	/* We can't move this past the last index. */
	if (new_index > gaim_window_get_conversation_count(win))
		new_index = gaim_window_get_conversation_count(win);

	/* Get the list item for this conversation at its current index. */
	l = g_list_nth(gaim_window_get_conversations(win), index);

	if (l == NULL) {
		/* Should never happen. */
		debug_printf("Misordered conversations list in window %p\n", win);

		return;
	}

	conv = (struct gaim_conversation *)l->data;

	/* Update the UI part of this. */
	ops = gaim_window_get_ui_ops(win);

	if (ops != NULL && ops->move_conversation != NULL)
		ops->move_conversation(win, conv, new_index);

	if (new_index > index)
		new_index--;

	/* Remove the old one. */
	win->conversations = g_list_delete_link(win->conversations, l);

	/* Insert it where it should go. */
	win->conversations = g_list_insert(win->conversations, conv, new_index);

	update_conv_indexes(win);
}

struct gaim_conversation *
gaim_window_get_conversation_at(const struct gaim_window *win,
								unsigned int index)
{
	if (win == NULL || index >= gaim_window_get_conversation_count(win))
		return NULL;

	return (struct gaim_conversation *)g_list_nth_data(
		gaim_window_get_conversations(win), index);
}

size_t
gaim_window_get_conversation_count(const struct gaim_window *win)
{
	if (win == NULL)
		return 0;

	return win->conversation_count;
}

void
gaim_window_switch_conversation(struct gaim_window *win, unsigned int index)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL || index < 0 ||
		index >= gaim_window_get_conversation_count(win))
		return;

	ops = gaim_window_get_ui_ops(win);

	if (ops != NULL && ops->switch_conversation != NULL)
		ops->switch_conversation(win, index);

	gaim_conversation_set_unseen(
		gaim_window_get_conversation_at(win, index), 0);
}

struct gaim_conversation *
gaim_window_get_active_conversation(const struct gaim_window *win)
{
	struct gaim_window_ui_ops *ops;

	if (win == NULL)
		return NULL;

	ops = gaim_window_get_ui_ops(win);

	if (ops != NULL && ops->get_active_index != NULL)
		return gaim_window_get_conversation_at(win, ops->get_active_index(win));

	return NULL;
}

GList *
gaim_window_get_conversations(const struct gaim_window *win)
{
	if (win == NULL)
		return NULL;

	return win->conversations;
}

GList *
gaim_get_windows(void)
{
	return windows;
}

struct gaim_window *
gaim_get_first_window_with_type(GaimConversationType type)
{
	GList *wins, *convs;
	struct gaim_window *win;
	struct gaim_conversation *conv;

	if (type == GAIM_CONV_UNKNOWN)
		return NULL;

	for (wins = gaim_get_windows(); wins != NULL; wins = wins->next) {
		win = (struct gaim_window *)wins->data;

		for (convs = gaim_window_get_conversations(win);
			 convs != NULL;
			 convs = convs->next) {

			conv = (struct gaim_conversation *)convs->data;

			if (gaim_conversation_get_type(conv) == type)
				return win;
		}
	}

	return NULL;
}

struct gaim_window *
gaim_get_last_window_with_type(GaimConversationType type)
{
	GList *wins, *convs;
	struct gaim_window *win;
	struct gaim_conversation *conv;

	if (type == GAIM_CONV_UNKNOWN)
		return NULL;

	for (wins = g_list_last(gaim_get_windows());
		 wins != NULL;
		 wins = wins->prev) {

		win = (struct gaim_window *)wins->data;

		for (convs = gaim_window_get_conversations(win);
			 convs != NULL;
			 convs = convs->next) {

			conv = (struct gaim_conversation *)convs->data;

			if (gaim_conversation_get_type(conv) == type)
				return win;
		}
	}

	return NULL;
}

/**************************************************************************
 * Conversation API
 **************************************************************************/
struct gaim_conversation *
gaim_conversation_new(GaimConversationType type, struct gaim_account *account,
					  const char *name)
{
	struct gaim_conversation *conv;

	if (type == GAIM_CONV_UNKNOWN)
		return NULL;

	/* Check if this conversation already exists. */
	if ((conv = gaim_find_conversation_with_account(name, account)) != NULL)
		return conv;

	conv = g_malloc0(sizeof(struct gaim_conversation));

	conv->type         = type;
	conv->account      = account;
	conv->name         = g_strdup(name);
	conv->title        = g_strdup(name);
	conv->send_history = g_list_append(NULL, NULL);
	conv->history      = g_string_new("");

	if (type == GAIM_CONV_IM)
	{
		conv->u.im = g_malloc0(sizeof(struct gaim_im));
		conv->u.im->conv = conv;

		ims = g_list_append(ims, conv);

		gaim_conversation_set_logging(conv,
									  (logging_options & OPT_LOG_CONVOS));
	}
	else if (type == GAIM_CONV_CHAT)
	{
		conv->u.chat = g_malloc0(sizeof(struct gaim_chat));
		conv->u.chat->conv = conv;

		chats = g_list_append(chats, conv);

		gaim_conversation_set_logging(conv, (logging_options & OPT_LOG_CHATS));
	}

	conversations = g_list_append(conversations, conv);

	/* Auto-set the title. */
	gaim_conversation_autoset_title(conv);

	/*
	 * Create a window if one does not exist. If it does, use the last
	 * created window.
	 */
	if (windows == NULL ||
			(type == GAIM_CONV_IM && !(im_options & OPT_IM_ONE_WINDOW)) ||
			(type == GAIM_CONV_CHAT && !(chat_options & OPT_CHAT_ONE_WINDOW))) {
		struct gaim_window *win;

		win = gaim_window_new();
		gaim_window_add_conversation(win, conv);

		/* Ensure the window is visible. */
		gaim_window_show(win);
	}
	else {
		if (place_conv == NULL)
			gaim_conv_placement_set_active(0);

		place_conv(conv);
	}

	plugin_event(event_new_conversation, name);

	return conv;
}

void
gaim_conversation_destroy(struct gaim_conversation *conv)
{
	struct gaim_window *win;
	struct gaim_conversation_ui_ops *ops;
	struct gaim_connection *gc;
	const char *name;
	GList *node;

	if (conv == NULL)
		return;

	win  = gaim_conversation_get_window(conv);
	ops  = gaim_conversation_get_ui_ops(conv);
	gc   = gaim_conversation_get_gc(conv);
	name = gaim_conversation_get_name(conv);

	if (gaim_conversation_get_type(conv) == GAIM_CONV_IM) {
		if (!(misc_options & OPT_MISC_STEALTH_TYPING))
			serv_send_typing(gc, (char *)name, NOT_TYPING);

		if (gc && gc->prpl->convo_closed != NULL)
			gc->prpl->convo_closed(gc, (char *)name);
	}
	else if (gaim_conversation_get_type(conv) == GAIM_CONV_CHAT) {
		/*
		 * This is unfortunately necessary, because calling serv_chat_leave()
		 * calls this gaim_conversation_destroy(), which leads to two calls
		 * here.. We can't just return after this, because then it'll return
		 * on the next pass. So, since serv_got_chat_left(), which is
		 * eventually called from the prpl that serv_chat_leave() calls,
		 * removes this conversation from the gc's buddy_chats list, we're
		 * going to check to see if this exists in the list. If so, we want
		 * to return after calling this, because it'll be called again. If not,
		 * fall through, because it'll have already been removed, and we'd
		 * be on the 2nd pass.
		 *
		 * Long paragraph. <-- Short sentence.
		 *
		 *   -- ChipX86
		 */

		if (gc && g_slist_find(gc->buddy_chats, conv) != NULL) {
			serv_chat_leave(gc, gaim_chat_get_id(GAIM_CHAT(conv)));

			return;
		}
	}

	plugin_event(event_del_conversation, conv);

	if (conv->name  != NULL) g_free(conv->name);
	if (conv->title != NULL) g_free(conv->title);

	for (node = g_list_first(conv->send_history);
		 node != NULL;
		 node = g_list_next(node)) {

		if (node->data != NULL)
			g_free(node->data);
	}

	g_list_free(g_list_first(conv->send_history));

	if (conv->history != NULL)
		g_string_free(conv->history, TRUE);

	conversations = g_list_remove(conversations, conv);

	if (conv->type == GAIM_CONV_IM) {
		GSList *snode;

		gaim_im_stop_typing_timeout(conv->u.im);
		gaim_im_stop_type_again_timeout(conv->u.im);

		for (snode = conv->u.im->images; snode != NULL; snode = snode->next) {
			if (snode->data != NULL)
				g_free(snode->data);
		}

		g_slist_free(conv->u.im->images);

		g_free(conv->u.im);

		ims = g_list_remove(ims, conv);
	}
	else if (conv->type == GAIM_CONV_CHAT) {

		for (node = conv->u.chat->in_room; node != NULL; node = node->next) {
			if (node->data != NULL)
				g_free(node->data);
		}

		for (node = conv->u.chat->ignored; node != NULL; node = node->next) {
			if (node->data != NULL)
				g_free(node->data);
		}

		g_list_free(conv->u.chat->in_room);
		g_list_free(conv->u.chat->ignored);

		if (conv->u.chat->who != NULL)
			g_free(conv->u.chat->who);

		if (conv->u.chat->topic != NULL)
			g_free(conv->u.chat->topic);

		g_free(conv->u.chat);

		chats = g_list_remove(chats, conv);
	}

	if (win != NULL) {
		gaim_window_remove_conversation(win,
			gaim_conversation_get_index(conv));
	}

	if (ops != NULL && ops->destroy_conversation != NULL)
		ops->destroy_conversation(conv);

	g_free(conv);
}

GaimConversationType
gaim_conversation_get_type(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return GAIM_CONV_UNKNOWN;

	return conv->type;
}

void
gaim_conversation_set_ui_ops(struct gaim_conversation *conv,
							 struct gaim_conversation_ui_ops *ops)
{
	if (conv == NULL || conv->ui_ops == ops)
		return;

	if (conv->ui_ops != NULL && conv->ui_ops->destroy_conversation != NULL)
		conv->ui_ops->destroy_conversation(conv);

	conv->ui_data = NULL;

	conv->ui_ops = ops;
}

struct gaim_conversation_ui_ops *
gaim_conversation_get_ui_ops(struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->ui_ops;
}

void
gaim_conversation_set_account(struct gaim_conversation *conv,
						   struct gaim_account *account)
{
	if (conv == NULL || account == gaim_conversation_get_account(conv))
		return;

	conv->account = account;

	gaim_conversation_update(conv, GAIM_CONV_UPDATE_ACCOUNT);
}

struct gaim_account *
gaim_conversation_get_account(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->account;
}

struct gaim_connection *
gaim_conversation_get_gc(const struct gaim_conversation *conv)
{
	struct gaim_account *account;

	if (conv == NULL)
		return NULL;

	account = gaim_conversation_get_account(conv);

	if (account == NULL)
		return NULL;

	return account->gc;
}

void
gaim_conversation_set_title(struct gaim_conversation *conv, const char *title)
{
	struct gaim_conversation_ui_ops *ops;

	if (conv == NULL || title == NULL)
		return;

	if (conv->title != NULL)
		g_free(conv->title);

	conv->title = g_strdup(title);

	ops = gaim_conversation_get_ui_ops(conv);

	if (ops != NULL && ops->set_title != NULL)
		ops->set_title(conv, conv->title);
}

const char *
gaim_conversation_get_title(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->title;
}

void
gaim_conversation_autoset_title(struct gaim_conversation *conv)
{
	struct gaim_account *account;
	struct buddy *b;
	const char *text, *name;

	if (conv == NULL)
		return;

	account = gaim_conversation_get_account(conv);
	name = gaim_conversation_get_name(conv);

	if (((im_options & OPT_IM_ALIAS_TAB) == OPT_IM_ALIAS_TAB) &&
		account != NULL && ((b = gaim_find_buddy(account, name)) != NULL)) {

		text = gaim_get_buddy_alias(b);
	}
	else
		text = name;

	gaim_conversation_set_title(conv, text);
}

int
gaim_conversation_get_index(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return 0;

	return conv->conversation_pos;
}

void
gaim_conversation_set_unseen(struct gaim_conversation *conv,
							 GaimUnseenState state)
{
	if (conv == NULL)
		return;

	conv->unseen = state;

	gaim_conversation_update(conv, GAIM_CONV_UPDATE_UNSEEN);
}

void
gaim_conversation_foreach(void (*func)(struct gaim_conversation *conv))
{
	struct gaim_conversation *conv;
	GList *l;

	if (func == NULL)
		return;

	for (l = gaim_get_conversations(); l != NULL; l = l->next) {
		conv = (struct gaim_conversation *)l->data;

		func(conv);
	}
}

GaimUnseenState
gaim_conversation_get_unseen(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return 0;

	return conv->unseen;
}

const char *
gaim_conversation_get_name(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->name;
}

void
gaim_conversation_set_logging(struct gaim_conversation *conv, gboolean log)
{
	if (conv == NULL)
		return;

	conv->logging = log;

	gaim_conversation_update(conv, GAIM_CONV_UPDATE_LOGGING);
}

gboolean
gaim_conversation_is_logging(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return FALSE;

	return conv->logging;
}

GList *
gaim_conversation_get_send_history(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->send_history;
}

void
gaim_conversation_set_history(struct gaim_conversation *conv,
							  GString *history)
{
	if (conv == NULL)
		return;

	conv->history = history;
}

GString *
gaim_conversation_get_history(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->history;
}

struct gaim_window *
gaim_conversation_get_window(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	return conv->window;
}

struct gaim_im *
gaim_conversation_get_im_data(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	if (gaim_conversation_get_type(conv) != GAIM_CONV_IM)
		return NULL;

	return conv->u.im;
}

struct gaim_chat *
gaim_conversation_get_chat_data(const struct gaim_conversation *conv)
{
	if (conv == NULL)
		return NULL;

	if (gaim_conversation_get_type(conv) != GAIM_CONV_CHAT)
		return NULL;

	return conv->u.chat;
}

GList *
gaim_get_conversations(void)
{
	return conversations;
}

GList *
gaim_get_ims(void)
{
	return ims;
}

GList *
gaim_get_chats(void)
{
	return chats;
}

struct gaim_conversation *
gaim_find_conversation(const char *name)
{
	struct gaim_conversation *c = NULL;
	char *cuser;
	GList *cnv;

	if (name == NULL)
		return NULL;

	cuser = g_strdup(normalize(name));

	for (cnv = gaim_get_conversations(); cnv != NULL; cnv = cnv->next) {
		c = (struct gaim_conversation *)cnv->data;

		if (!gaim_utf8_strcasecmp(cuser, normalize(gaim_conversation_get_name(c))))
			break;

		c = NULL;
	}

	g_free(cuser);

	return c;
}

struct gaim_conversation *
gaim_find_conversation_with_account(const char *name, const struct gaim_account *account)
{
	struct gaim_conversation *c = NULL;
	char *cuser;
	GList *cnv;

	if (name == NULL)
		return NULL;

	cuser = g_strdup(normalize(name));

	for (cnv = gaim_get_conversations(); cnv != NULL; cnv = cnv->next) {
		c = (struct gaim_conversation *)cnv->data;

		if (!gaim_utf8_strcasecmp(cuser, normalize(gaim_conversation_get_name(c))) &&
			account == gaim_conversation_get_account(c)) {

			break;
		}

		c = NULL;
	}

	g_free(cuser);

	return c;
}

void
gaim_conversation_write(struct gaim_conversation *conv, const char *who,
						const char *message, size_t length, int flags,
						time_t mtime)
{
	struct gaim_account *account;
	struct gaim_conversation_ui_ops *ops;
	struct gaim_window *win;
	struct buddy *b;
	GaimUnseenState unseen;
	/* int logging_font_options = 0; */

	if (conv == NULL || message == NULL)
		return;

	ops = gaim_conversation_get_ui_ops(conv);

	if (ops == NULL || ops->write_conv == NULL)
		return;

	account = gaim_conversation_get_account(conv);

	if (gaim_conversation_get_type(conv) == GAIM_CONV_CHAT &&
		(account->gc == NULL || !g_slist_find(account->gc->buddy_chats, conv)))
		return;

	if (gaim_conversation_get_type(conv) == GAIM_CONV_IM &&
		!g_list_find(gaim_get_conversations(), conv))
		return;

	if (gaim_conversation_get_type(conv) == GAIM_CONV_IM ||
		!(account->gc->prpl->options & OPT_PROTO_UNIQUE_CHATNAME)) {

		if (who == NULL) {
			if (flags & WFLAG_SEND) {
				b = gaim_find_buddy(account, account->gc->username);
				if (b != NULL && strcmp(b->name, gaim_get_buddy_alias(b)))
					who = gaim_get_buddy_alias(b);
				else if (*account->alias)
					who = account->alias;
				else if (*account->gc->displayname)
					who = account->gc->displayname;
				else
					who = account->gc->username;
			}
			else {
				b = gaim_find_buddy(account, gaim_conversation_get_name(conv));

				if (b != NULL)
					who = gaim_get_buddy_alias(b);
				else
					who = gaim_conversation_get_name(conv);
			}
		}
		else {
			b = gaim_find_buddy(account, who);

			if (b != NULL)
				who = gaim_get_buddy_alias(b);
		}
	}

	ops->write_conv(conv, who, message, length, flags, mtime);

	win = gaim_conversation_get_window(conv);

	if (!(flags & WFLAG_NOLOG) &&
		((gaim_conversation_get_type(conv) == GAIM_CONV_CHAT &&
		  (chat_options & OPT_CHAT_POPUP)) ||
		 (gaim_conversation_get_type(conv) == GAIM_CONV_IM &&
		  (im_options & OPT_IM_POPUP)))) {

		gaim_window_show(win);
	}

	/* Tab highlighting */
	if (!(flags & WFLAG_RECV) && !(flags & WFLAG_SYSTEM))
		return;

	if (gaim_conversation_get_type(conv) == GAIM_CONV_IM) {
		if ((flags & WFLAG_RECV) == WFLAG_RECV)
			gaim_im_set_typing_state(GAIM_IM(conv), NOT_TYPING);
	}

	if (gaim_window_get_active_conversation(win) != conv) {
		if ((flags & WFLAG_NICK) == WFLAG_NICK ||
				gaim_conversation_get_unseen(conv) == GAIM_UNSEEN_NICK)
			unseen = GAIM_UNSEEN_NICK;
		else
			unseen = GAIM_UNSEEN_TEXT;
	}
	else
		unseen = GAIM_UNSEEN_NONE;

	gaim_conversation_set_unseen(conv, unseen);
}

void
gaim_conversation_update_progress(struct gaim_conversation *conv,
								  float percent)
{
	struct gaim_conversation_ui_ops *ops;

	if (conv == NULL)
		return;

	if (percent < 0)
		percent = 0;

	/*
	 * NOTE: A percent >= 1 indicates that the progress bar should be
	 *       closed.
	 */
	ops = gaim_conversation_get_ui_ops(conv);

	if (ops != NULL && ops->update_progress != NULL)
		ops->update_progress(conv, percent);
}

void
gaim_conversation_update(struct gaim_conversation *conv,
						 GaimConvUpdateType type)
{
	struct gaim_conversation_ui_ops *ops;

	if (conv == NULL)
		return;

	ops = gaim_conversation_get_ui_ops(conv);

	if (ops != NULL && ops->updated != NULL)
		ops->updated(conv, type);
}

/**************************************************************************
 * IM Conversation API
 **************************************************************************/
struct gaim_conversation *
gaim_im_get_conversation(struct gaim_im *im)
{
	if (im == NULL)
		return NULL;

	return im->conv;
}

void
gaim_im_set_typing_state(struct gaim_im *im, int state)
{
	if (im == NULL)
		return;

	im->typing_state = state;
}

int
gaim_im_get_typing_state(const struct gaim_im *im)
{
	if (im == NULL)
		return 0;

	return im->typing_state;
}

void
gaim_im_start_typing_timeout(struct gaim_im *im, int timeout)
{
	struct gaim_conversation *conv;
	const char *name;

	if (im == NULL)
		return;

	if (im->typing_timeout > 0)
		gaim_im_stop_typing_timeout(im);

	conv = gaim_im_get_conversation(im);
	name = gaim_conversation_get_name(conv);

	im->typing_timeout = g_timeout_add_full(G_PRIORITY_DEFAULT_IDLE,
		timeout * 1000, reset_typing, g_strdup(name), g_free);
}

void
gaim_im_stop_typing_timeout(struct gaim_im *im)
{
	if (im == NULL)
		return;

	if (im->typing_timeout == 0)
		return;

	g_source_remove(im->typing_timeout);
	im->typing_timeout = 0;
}

guint
gaim_im_get_typing_timeout(const struct gaim_im *im)
{
	if (im == NULL)
		return 0;

	return im->typing_timeout;
}

void
gaim_im_set_type_again(struct gaim_im *im, time_t val)
{
	if (im == NULL)
		return;

	im->type_again = val;
}

time_t
gaim_im_get_type_again(const struct gaim_im *im)
{
	if (im == NULL)
		return 0;

	return im->type_again;
}

void
gaim_im_start_type_again_timeout(struct gaim_im *im)
{
	if (im == NULL)
		return;

	im->type_again_timeout = g_timeout_add(SEND_TYPED_TIMEOUT, send_typed,
										   gaim_im_get_conversation(im));
}

void
gaim_im_stop_type_again_timeout(struct gaim_im *im)
{
	if (im == NULL)
		return;

	if (im->type_again_timeout == 0)
		return;

	g_source_remove(im->type_again_timeout);
	im->type_again_timeout = 0;
}

guint
gaim_im_get_type_again_timeout(const struct gaim_im *im)
{
	if (im == NULL)
		return 0;

	return im->type_again_timeout;
}

void
gaim_im_update_typing(struct gaim_im *im)
{
	if (im == NULL)
		return;

	gaim_conversation_update(gaim_im_get_conversation(im),
							 GAIM_CONV_UPDATE_TYPING);
}

void
gaim_im_write(struct gaim_im *im, const char *who, const char *message,
			  size_t len, int flags, time_t mtime)
{
	struct gaim_conversation *c;

	if (im == NULL || message == NULL)
		return;

	c = gaim_im_get_conversation(im);

	/* Raise the window, if specified in prefs. */
	if (!(flags & WFLAG_NOLOG) & (im_options & OPT_IM_POPUP))
		gaim_window_raise(gaim_conversation_get_window(c));

	if (c->ui_ops != NULL && c->ui_ops->write_im != NULL)
		c->ui_ops->write_im(c, who, message, len, flags, mtime);
	else
		gaim_conversation_write(c, who, message, -1, flags, mtime);
}

void
gaim_im_send(struct gaim_im *im, const char *message)
{
	if (im == NULL || message == NULL)
		return;

	common_send(gaim_im_get_conversation(im), message);
}

/**************************************************************************
 * Chat Conversation API
 **************************************************************************/

struct gaim_conversation *
gaim_chat_get_conversation(struct gaim_chat *chat)
{
	if (chat == NULL)
		return NULL;

	return chat->conv;
}

GList *
gaim_chat_set_users(struct gaim_chat *chat, GList *users)
{
	if (chat == NULL)
		return NULL;

	chat->in_room = users;

	return users;
}

GList *
gaim_chat_get_users(const struct gaim_chat *chat)
{
	if (chat == NULL)
		return NULL;

	return chat->in_room;
}

void
gaim_chat_ignore(struct gaim_chat *chat, const char *name)
{
	if (chat == NULL || name == NULL)
		return;

	/* Make sure the user isn't already ignored. */
	if (gaim_chat_is_user_ignored(chat, name))
		return;

	gaim_chat_set_ignored(chat,
		g_list_append(gaim_chat_get_ignored(chat), g_strdup(name)));
}

void
gaim_chat_unignore(struct gaim_chat *chat, const char *name)
{
	GList *item;

	if (chat == NULL || name == NULL)
		return;

	/* Make sure the user is actually ignored. */
	if (!gaim_chat_is_user_ignored(chat, name))
		return;

	item = g_list_find(gaim_chat_get_ignored(chat),
					   gaim_chat_get_ignored_user(chat, name));

	gaim_chat_set_ignored(chat,
		g_list_remove_link(gaim_chat_get_ignored(chat), item));

	g_free(item->data);
	g_list_free_1(item);
}

GList *
gaim_chat_set_ignored(struct gaim_chat *chat, GList *ignored)
{
	if (chat == NULL)
		return NULL;

	chat->ignored = ignored;

	return ignored;
}

GList *
gaim_chat_get_ignored(const struct gaim_chat *chat)
{
	if (chat == NULL)
		return NULL;

	return chat->ignored;
}

const char *
gaim_chat_get_ignored_user(const struct gaim_chat *chat, const char *user)
{
	GList *ignored;

	if (chat == NULL || user == NULL)
		return NULL;

	for (ignored = gaim_chat_get_ignored(chat);
		 ignored != NULL;
		 ignored = ignored->next) {

		const char *ign = (const char *)ignored->data;

		if (!gaim_utf8_strcasecmp(user, ign) ||
			((*ign == '+' || *ign == '%') && !gaim_utf8_strcasecmp(user, ign + 1)))
			return ign;

		if (*ign == '@') {
			ign++;

			if ((*ign == '+' && !gaim_utf8_strcasecmp(user, ign + 1)) ||
				(*ign != '+' && !gaim_utf8_strcasecmp(user, ign)))
				return ign;
		}
	}

	return NULL;
}

gboolean
gaim_chat_is_user_ignored(const struct gaim_chat *chat, const char *user)
{
	if (chat == NULL || user == NULL)
		return FALSE;

	return (gaim_chat_get_ignored_user(chat, user) != NULL);
}

void
gaim_chat_set_topic(struct gaim_chat *chat, const char *who, const char *topic)
{
	if (chat == NULL)
		return;

	if (chat->who   != NULL) free(chat->who);
	if (chat->topic != NULL) free(chat->topic);

	chat->who   = (who   == NULL ? NULL : g_strdup(who));
	chat->topic = (topic == NULL ? NULL : g_strdup(topic));

	gaim_conversation_update(gaim_chat_get_conversation(chat),
							 GAIM_CONV_UPDATE_TOPIC);
}

const char *
gaim_chat_get_topic(const struct gaim_chat *chat)
{
	if (chat == NULL)
		return NULL;

	return chat->topic;
}

void
gaim_chat_set_id(struct gaim_chat *chat, int id)
{
	if (chat == NULL)
		return;

	chat->id = id;
}

int
gaim_chat_get_id(const struct gaim_chat *chat)
{
	if (chat == NULL)
		return -1;

	return chat->id;
}

void
gaim_chat_write(struct gaim_chat *chat, const char *who,
				const char *message, int flags, time_t mtime)
{
	struct gaim_conversation *conv;
	struct gaim_connection *gc;

	if (chat == NULL || who == NULL || message == NULL)
		return;

	conv = gaim_chat_get_conversation(chat);
	gc   = gaim_conversation_get_gc(conv);

	/* Don't display this if the person who wrote it is ignored. */
	if (gaim_chat_is_user_ignored(chat, who))
		return;

	/* Raise the window, if specified in prefs. */
	if (!(flags & WFLAG_NOLOG) & (chat_options & OPT_CHAT_POPUP))
		gaim_window_raise(gaim_conversation_get_window(conv));

	if (!(flags & WFLAG_WHISPER)) {
		char *str;

		str = g_strdup(normalize(who));

		if (!gaim_utf8_strcasecmp(str, normalize(gc->username)) ||
			!gaim_utf8_strcasecmp(str, normalize(gc->displayname))) {

			flags |= WFLAG_SEND;
		}
		else {
			flags |= WFLAG_RECV;

			if (find_nick(gc, message))
				flags |= WFLAG_NICK;
		}
		
		g_free(str);
	}

	/* Pass this on to either the ops structure or the default write func. */
	if (conv->ui_ops != NULL && conv->ui_ops->write_chat != NULL)
		conv->ui_ops->write_chat(conv, who, message, flags, mtime);
	else
		gaim_conversation_write(conv, who, message, -1, flags, mtime);
}

void
gaim_chat_send(struct gaim_chat *chat, const char *message)
{
	if (chat == NULL || message == NULL)
		return;

	common_send(gaim_chat_get_conversation(chat), message);
}

void
gaim_chat_add_user(struct gaim_chat *chat, const char *user,
				   const char *extra_msg)
{
	struct gaim_conversation *conv;
	struct gaim_conversation_ui_ops *ops;
	char tmp[BUF_LONG];

	if (chat == NULL || user == NULL)
		return;

	conv = gaim_chat_get_conversation(chat);
	ops  = gaim_conversation_get_ui_ops(conv);

	gaim_chat_set_users(chat,
		g_list_insert_sorted(gaim_chat_get_users(chat), g_strdup(user),
							 insertname_compare));

	plugin_event(event_chat_buddy_join,
				 gaim_conversation_get_gc(conv), gaim_chat_get_id(chat),
				 user);

	if (ops != NULL && ops->chat_add_user != NULL)
		ops->chat_add_user(conv, user);

	if (chat_options & OPT_CHAT_LOGON) {
		if (extra_msg == NULL)
			g_snprintf(tmp, sizeof(tmp), _("%s entered the room."), user);
		else
			g_snprintf(tmp, sizeof(tmp),
					   _("%s [<I>%s</I>] entered the room."),
					   user, extra_msg);

		gaim_conversation_write(conv, NULL, tmp, -1, WFLAG_SYSTEM, time(NULL));
	}
}

void
gaim_chat_rename_user(struct gaim_chat *chat, const char *old_user,
					  const char *new_user)
{
	struct gaim_conversation *conv;
	struct gaim_conversation_ui_ops *ops;
	char tmp[BUF_LONG];
	GList *names;

	if (chat == NULL || old_user == NULL || new_user == NULL)
		return;

	conv = gaim_chat_get_conversation(chat);
	ops  = gaim_conversation_get_ui_ops(conv);

	gaim_chat_set_users(chat,
		g_list_insert_sorted(gaim_chat_get_users(chat), g_strdup(new_user),
							 insertname_compare));

	if (ops != NULL && ops->chat_rename_user != NULL)
		ops->chat_rename_user(conv, old_user, new_user);

	for (names = gaim_chat_get_users(chat);
		 names != NULL;
		 names = names->next) {

		if (!gaim_utf8_strcasecmp((char *)names->data, old_user)) {
			gaim_chat_set_users(chat,
					g_list_remove(gaim_chat_get_users(chat), names->data));
			break;
		}
	}

	if (gaim_chat_is_user_ignored(chat, old_user)) {
		gaim_chat_unignore(chat, old_user);
		gaim_chat_ignore(chat, new_user);
	}
	else if (gaim_chat_is_user_ignored(chat, new_user))
		gaim_chat_unignore(chat, new_user);

	if (chat_options & OPT_CHAT_LOGON) {
		g_snprintf(tmp, sizeof(tmp),
				   _("%s is now known as %s"), old_user, new_user);

		gaim_conversation_write(conv, NULL, tmp, -1, WFLAG_SYSTEM, time(NULL));
	}
}

void
gaim_chat_remove_user(struct gaim_chat *chat, const char *user,
					  const char *reason)
{
	struct gaim_conversation *conv;
	struct gaim_conversation_ui_ops *ops;
	char tmp[BUF_LONG];
	GList *names;

	if (chat == NULL || user == NULL)
		return;

	conv = gaim_chat_get_conversation(chat);
	ops  = gaim_conversation_get_ui_ops(conv);

	plugin_event(event_chat_buddy_leave, gaim_conversation_get_gc(conv),
				 gaim_chat_get_id(chat), user);

	if (ops != NULL && ops->chat_remove_user != NULL)
		ops->chat_remove_user(conv, user);

	for (names = gaim_chat_get_users(chat);
		 names != NULL;
		 names = names->next) {

		if (!gaim_utf8_strcasecmp((char *)names->data, user)) {
			gaim_chat_set_users(chat,
					g_list_remove(gaim_chat_get_users(chat), names->data));
			break;
		}
	}

	/* NOTE: Don't remove them from ignored in case they re-enter. */

	if (chat_options & OPT_CHAT_LOGON) {
		if (reason != NULL && *reason != '\0')
			g_snprintf(tmp, sizeof(tmp),
					   _("%s left the room (%s)."), user, reason);
		else
			g_snprintf(tmp, sizeof(tmp), _("%s left the room."), user);

		gaim_conversation_write(conv, NULL, tmp, -1, WFLAG_SYSTEM, time(NULL));
	}
}

struct gaim_conversation *
gaim_find_chat(struct gaim_connection *gc, int id)
{
	GList *l;
	struct gaim_conversation *conv;

	for (l = gaim_get_chats(); l != NULL; l = l->next) {
		conv = (struct gaim_conversation *)l->data;

		if (gaim_chat_get_id(GAIM_CHAT(conv)) == id &&
			gaim_conversation_get_gc(conv) == gc)
			return conv;
	}

	return NULL;
}

/**************************************************************************
 * Conversation placement functions
 **************************************************************************/
/* This one places conversations in the last made window. */
static void
conv_placement_last_created_win(struct gaim_conversation *conv)
{
	struct gaim_window *win;

	if (convo_options & OPT_CONVO_COMBINE)
		win = g_list_last(gaim_get_windows())->data;
	else
		win = gaim_get_last_window_with_type(gaim_conversation_get_type(conv));

	if (win == NULL) {
		win = gaim_window_new();

		gaim_window_add_conversation(win, conv);
		gaim_window_show(win);
	}
	else
		gaim_window_add_conversation(win, conv);
}

/* This one places each conversation in its own window. */
static void
conv_placement_new_window(struct gaim_conversation *conv)
{
	struct gaim_window *win;

	win = gaim_window_new();

	gaim_window_add_conversation(win, conv);

	gaim_window_show(win);
}

/*
 * This groups things by, well, group. Buddies from groups will always be
 * grouped together, and a buddy from a group not belonging to any currently
 * open windows will get a new window.
 */
static void
conv_placement_by_group(struct gaim_conversation *conv)
{
	struct gaim_window *win;
	GaimConversationType type;

	type = gaim_conversation_get_type(conv);

	if (type != GAIM_CONV_IM) {
		win = gaim_get_last_window_with_type(type);

		if (win == NULL)
			conv_placement_new_window(conv);
		else
			gaim_window_add_conversation(win, conv);
	}
	else {
		struct buddy *b;
		struct group *grp = NULL;
		GList *wins, *convs;

		b = gaim_find_buddy(gaim_conversation_get_account(conv),
					   gaim_conversation_get_name(conv));

		if (b != NULL)
			grp = gaim_find_buddys_group(b);

		/* Go through the list of IMs and find one with this group. */
		for (wins = gaim_get_windows(); wins != NULL; wins = wins->next) {
			struct gaim_window *win2;
			struct gaim_conversation *conv2;
			struct buddy *b2;
			struct group *g2 = NULL;

			win2 = (struct gaim_window *)wins->data;

			for (convs = gaim_window_get_conversations(win2);
				 convs != NULL;
				 convs = convs->next) {

				conv2 = (struct gaim_conversation *)convs->data;

				b2 = gaim_find_buddy(gaim_conversation_get_account(conv2),
								gaim_conversation_get_name(conv2));

				if (b2 != NULL)
					g2 = gaim_find_buddys_group(b2);

				if (grp == g2) {
					gaim_window_add_conversation(win2, conv);

					return;
				}
			}
		}

		/* Make a new window. */
		conv_placement_new_window(conv);
	}
}

/* This groups things by account.  Otherwise, the same semantics as above */
static void
conv_placement_by_account(struct gaim_conversation *conv)
{
	GaimConversationType type;
	GList *wins, *convs;
	struct gaim_account *account;


	account = gaim_conversation_get_account(conv);
	type = gaim_conversation_get_type(conv);


	/* Go through the list of IMs and find one with this group. */
	for (wins = gaim_get_windows(); wins != NULL; wins = wins->next) {
		struct gaim_window *win2;
		struct gaim_conversation *conv2;

		win2 = (struct gaim_window *)wins->data;

		for (convs = gaim_window_get_conversations(win2);
				convs != NULL;
				convs = convs->next) {

			conv2 = (struct gaim_conversation *)convs->data;

			if (((convo_options & OPT_CONVO_COMBINE) ||
						type == gaim_conversation_get_type(conv2)) &&
					account == gaim_conversation_get_account(conv2)) {
				gaim_window_add_conversation(win2, conv);
				return;
			}

		}
	}
	/* Make a new window. */
	conv_placement_new_window(conv);
}

static int
add_conv_placement_fnc(const char *name, gaim_conv_placement_fnc fnc)
{
	struct ConvPlacementData *data;

	data = g_malloc0(sizeof(struct ConvPlacementData));

	data->name = g_strdup(name);
	data->fnc  = fnc;

	conv_placement_fncs = g_list_append(conv_placement_fncs, data);

	return gaim_conv_placement_get_fnc_count() - 1;
}

static void
ensure_default_funcs(void)
{
	if (conv_placement_fncs == NULL) {
		add_conv_placement_fnc(_("Last created window"),
							   conv_placement_last_created_win);
		add_conv_placement_fnc(_("New window"),
							   conv_placement_new_window);
		add_conv_placement_fnc(_("By group"),
							   conv_placement_by_group);
		add_conv_placement_fnc(_("By account"),
							   conv_placement_by_account);
	}
}

int
gaim_conv_placement_add_fnc(const char *name, gaim_conv_placement_fnc fnc)
{
	if (name == NULL || fnc == NULL)
		return -1;

	if (conv_placement_fncs == NULL)
		ensure_default_funcs();

	return add_conv_placement_fnc(name, fnc);
}

void
gaim_conv_placement_remove_fnc(int index)
{
	struct ConvPlacementData *data;
	GList *node;

	if (index < 0 || index > g_list_length(conv_placement_fncs))
		return;

	node = g_list_nth(conv_placement_fncs, index);
	data = (struct ConvPlacementData *)node->data;

	g_free(data->name);
	g_free(data);

	conv_placement_fncs = g_list_remove_link(conv_placement_fncs, node);
	g_list_free_1(node);
}

int
gaim_conv_placement_get_fnc_count(void)
{
	ensure_default_funcs();

	return g_list_length(conv_placement_fncs);
}

const char *
gaim_conv_placement_get_name(int index)
{
	struct ConvPlacementData *data;

	ensure_default_funcs();

	if (index < 0 || index > g_list_length(conv_placement_fncs))
		return NULL;

	data = g_list_nth_data(conv_placement_fncs, index);

	if (data == NULL)
		return NULL;

	return data->name;
}

gaim_conv_placement_fnc
gaim_conv_placement_get_fnc(int index)
{
	struct ConvPlacementData *data;

	ensure_default_funcs();

	if (index < 0 || index > g_list_length(conv_placement_fncs))
		return NULL;

	data = g_list_nth_data(conv_placement_fncs, index);

	if (data == NULL)
		return NULL;

	return data->fnc;
}

int
gaim_conv_placement_get_fnc_index(gaim_conv_placement_fnc fnc)
{
	struct ConvPlacementData *data;
	GList *node;
	int i;

	ensure_default_funcs();

	for (node = conv_placement_fncs, i = 0;
		 node != NULL;
		 node = node->next, i++) {

		data = (struct ConvPlacementData *)node->data;

		if (data->fnc == fnc)
			return i;
	}

	return -1;
}

int
gaim_conv_placement_get_active(void)
{
	return place_conv_index;
}

void
gaim_conv_placement_set_active(int index)
{
	gaim_conv_placement_fnc fnc;

	ensure_default_funcs();

	fnc = gaim_conv_placement_get_fnc(index);

	if (fnc == NULL)
		return;

	place_conv = fnc;
	place_conv_index = index;
}

void
gaim_set_win_ui_ops(struct gaim_window_ui_ops *ops)
{
	win_ui_ops = ops;
}

struct gaim_window_ui_ops *
gaim_get_win_ui_ops(void)
{
	return win_ui_ops;
}