view plugins/log_reader.c @ 13608:6c34fbb75bbd

[gaim-migrate @ 15994] Disconnect the account with a decent error message when there are problems with an oscar FLAP connection. This needed to be added after I removed it when rewriting things for non-blocking I/O. committer: Tailor Script <tailor@pidgin.im>
author Mark Doliner <mark@kingant.net>
date Mon, 10 Apr 2006 03:37:37 +0000
parents 2f0d4179ec05
children 5c8565315d3e
line wrap: on
line source

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <stdio.h>

#ifndef GAIM_PLUGINS
# define GAIM_PLUGINS
#endif

#include "internal.h"

#include "debug.h"
#include "log.h"
#include "plugin.h"
#include "pluginpref.h"
#include "prefs.h"
#include "stringref.h"
#include "util.h"
#include "version.h"
#include "xmlnode.h"

/* This must be the last Gaim header included. */
#ifdef _WIN32
#include "win32dep.h"
#endif

/* Where is the Windows partition mounted? */
#ifndef GAIM_LOG_READER_WINDOWS_MOUNT_POINT
#define GAIM_LOG_READER_WINDOWS_MOUNT_POINT "/mnt/windows"
#endif

enum name_guesses {
	NAME_GUESS_UNKNOWN,
	NAME_GUESS_ME,
	NAME_GUESS_THEM
};


/*****************************************************************************
 * Adium Logger                                                              *
 *****************************************************************************/

/* The adium logger doesn't write logs, only reads them.  This is to include
 * Adium logs in the log viewer transparently.
 */

static GaimLogLogger *adium_logger;

enum adium_log_type {
	ADIUM_HTML,
	ADIUM_TEXT,
};

struct adium_logger_data {
	char *path;
	enum adium_log_type type;
};

static GList *adium_logger_list(GaimLogType type, const char *sn, GaimAccount *account)
{
	GList *list = NULL;
	const char *logdir;
	GaimPlugin *plugin;
	GaimPluginProtocolInfo *prpl_info;
	char *prpl_name;
	char *temp;
	char *path;
	GDir *dir;

	g_return_val_if_fail(sn != NULL, list);
	g_return_val_if_fail(account != NULL, list);

	logdir = gaim_prefs_get_string("/plugins/core/log_reader/adium/log_directory");

	/* By clearing the log directory path, this logger can be (effectively) disabled. */
	if (!*logdir)
		return list;

	plugin = gaim_find_prpl(gaim_account_get_protocol_id(account));
	if (!plugin)
		return NULL;

	prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(plugin);
	if (!prpl_info->list_icon)
		return NULL;

	prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1);

	temp = g_strdup_printf("%s.%s", prpl_name, account->username);
	path = g_build_filename(logdir, temp, sn, NULL);
	g_free(temp);

	dir = g_dir_open(path, 0, NULL);
	if (dir) {
		const gchar *file;

		while ((file = g_dir_read_name(dir))) {
			if (!gaim_str_has_prefix(file, sn))
				continue;
			if (gaim_str_has_suffix(file, ".html") || gaim_str_has_suffix(file, ".AdiumHTMLLog")) {
				struct tm tm;
				const char *date = file;

				date += strlen(sn) + 2;
				if (sscanf(date, "%u|%u|%u",
						&tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) {

					gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse",
							"Filename timestamp parsing error\n");
				} else {
					char *filename = g_build_filename(path, file, NULL);
					FILE *handle = g_fopen(filename, "rb");
					char *contents;
					char *contents2;
					struct adium_logger_data *data;
					GaimLog *log;

					if (!handle) {
						g_free(filename);
						continue;
					}

					/* XXX: This is really inflexible. */
					contents = g_malloc(57);
					fread(contents, 56, 1, handle);
					fclose(handle);
					contents[56] = '\0';

					/* XXX: This is fairly inflexible. */
					contents2 = contents;
					while (*contents2 && *contents2 != '>')
						contents2++;
					if (*contents2)
						contents2++;
					while (*contents2 && *contents2 != '>')
						contents2++;
					if (*contents2)
						contents2++;

					if (sscanf(contents2, "%u.%u.%u",
							&tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) {

						gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse",
								"Contents timestamp parsing error\n");
						g_free(contents);
						g_free(filename);
						continue;
					}
					g_free(contents);

					data = g_new0(struct adium_logger_data, 1);
					data->path = filename;
					data->type = ADIUM_HTML;

					tm.tm_year -= 1900;
					tm.tm_mon  -= 1;

					/* XXX: Look into this later... Should we pass in a struct tm? */
					log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
					log->logger = adium_logger;
					log->logger_data = data;

					list = g_list_append(list, log);
				}
			} else if (gaim_str_has_suffix(file, ".adiumLog")) {
				struct tm tm;
				const char *date = file;

				date += strlen(sn) + 2;
				if (sscanf(date, "%u|%u|%u",
						&tm.tm_year, &tm.tm_mon, &tm.tm_mday) != 3) {

					gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse",
							"Filename timestamp parsing error\n");
				} else {
					char *filename = g_build_filename(path, file, NULL);
					FILE *handle = g_fopen(filename, "rb");
					char *contents;
					char *contents2;
					struct adium_logger_data *data;
					GaimLog *log;

					if (!handle) {
						g_free(filename);
						continue;
					}

					/* XXX: This is really inflexible. */
					contents = g_malloc(14);
					fread(contents, 13, 1, handle);
					fclose(handle);
					contents[13] = '\0';

					contents2 = contents;
					while (*contents2 && *contents2 != '(')
						contents2++;
					if (*contents2)
						contents2++;

					if (sscanf(contents2, "%u.%u.%u",
							&tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 3) {

						gaim_debug(GAIM_DEBUG_ERROR, "Adium log parse",
								"Contents timestamp parsing error\n");
						g_free(contents);
						g_free(filename);
						continue;
					}

					g_free(contents);

					tm.tm_year -= 1900;
					tm.tm_mon  -= 1;

					data = g_new0(struct adium_logger_data, 1);
					data->path = filename;
					data->type = ADIUM_TEXT;

					/* XXX: Look into this later... Should we pass in a struct tm? */
					log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, mktime(&tm), NULL);
					log->logger = adium_logger;
					log->logger_data = data;

					list = g_list_append(list, log);
				}
			}
		}
		g_dir_close(dir);
	}

	g_free(prpl_name);
	g_free(path);

	return list;
}

static char *adium_logger_read (GaimLog *log, GaimLogReadFlags *flags)
{
	struct adium_logger_data *data;
	GError *error = NULL;
	gchar *read = NULL;
	gsize length;

	g_return_val_if_fail(log != NULL, g_strdup(""));

	data = log->logger_data;

	g_return_val_if_fail(data->path != NULL, g_strdup(""));

	gaim_debug(GAIM_DEBUG_INFO, "Adium log read",
				"Reading %s\n", data->path);
	if (!g_file_get_contents(data->path, &read, &length, &error)) {
		gaim_debug(GAIM_DEBUG_ERROR, "Adium log read",
				"Error reading log\n");
		if (error)
			g_error_free(error);
		return g_strdup("");
	}

	if (data->type != ADIUM_HTML) {
		char *escaped = g_markup_escape_text(read, -1);
		g_free(read);
		read = escaped;
	}

#ifdef WIN32
	/* This problem only seems to show up on Windows.
	 * The BOM is displaying as a space at the beginning of the log.
	 */
	if (gaim_str_has_prefix(read, "\xef\xbb\xbf"))
	{
		/* FIXME: This feels so wrong... */
		char *temp = g_strdup(&(read[3]));
		g_free(read);
		read = temp;
	}
#endif

	/* TODO: Apply formatting.
	 * Replace the above hack with something better, since we'll
	 * be looping over the entire log file contents anyway.
	 */

	return read;
}

static int adium_logger_size (GaimLog *log)
{
	struct adium_logger_data *data;
	char *text;
	size_t size;

	g_return_val_if_fail(log != NULL, 0);

	data = log->logger_data;

	if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
		struct stat st;

		if (!data->path || stat(data->path, &st))
			st.st_size = 0;
	
		return st.st_size;
	}

	text = adium_logger_read(log, NULL);
	size = strlen(text);
	g_free(text);

	return size;
}

static void adium_logger_finalize(GaimLog *log)
{
	struct adium_logger_data *data;

	g_return_if_fail(log != NULL);

	data = log->logger_data;

	g_free(data->path);
}


/*****************************************************************************
 * Fire Logger                                                               *
 *****************************************************************************/

/* The fire logger doesn't write logs, only reads them.  This is to include
 * Fire logs in the log viewer transparently.
 */

static GaimLogLogger *fire_logger;

struct fire_logger_data {
};

static GList *fire_logger_list(GaimLogType type, const char *sn, GaimAccount *account)
{
	/* TODO: Do something here. */
	return NULL;
}

static char * fire_logger_read (GaimLog *log, GaimLogReadFlags *flags)
{
	struct fire_logger_data *data;

	g_return_val_if_fail(log != NULL, g_strdup(""));

	data = log->logger_data;

	/* TODO: Do something here. */
	return g_strdup("");
}

static int fire_logger_size (GaimLog *log)
{
	g_return_val_if_fail(log != NULL, 0);

	if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
		return 0;

	/* TODO: Do something here. */
	return 0;
}

static void fire_logger_finalize(GaimLog *log)
{
	g_return_if_fail(log != NULL);

	/* TODO: Do something here. */
}


/*****************************************************************************
 * Messenger Plus! Logger                                                    *
 *****************************************************************************/

/* The messenger_plus logger doesn't write logs, only reads them.  This is to include
 * Messenger Plus! logs in the log viewer transparently.
 */

static GaimLogLogger *messenger_plus_logger;

struct messenger_plus_logger_data {
};

static GList *messenger_plus_logger_list(GaimLogType type, const char *sn, GaimAccount *account)
{
	/* TODO: Do something here. */
	return NULL;
}

static char * messenger_plus_logger_read (GaimLog *log, GaimLogReadFlags *flags)
{
	struct messenger_plus_logger_data *data = log->logger_data;

	g_return_val_if_fail(log != NULL, g_strdup(""));

	data = log->logger_data;
	
	/* TODO: Do something here. */
	return g_strdup("");
}

static int messenger_plus_logger_size (GaimLog *log)
{
	g_return_val_if_fail(log != NULL, 0);

	if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
		return 0;

	/* TODO: Do something here. */
	return 0;
}

static void messenger_plus_logger_finalize(GaimLog *log)
{
	g_return_if_fail(log != NULL);

	/* TODO: Do something here. */
}


/*****************************************************************************
 * MSN Messenger Logger                                                      *
 *****************************************************************************/

/* The msn logger doesn't write logs, only reads them.  This is to include
 * MSN Messenger message histories in the log viewer transparently.
 */

static GaimLogLogger *msn_logger;

struct msn_logger_data {
	xmlnode *root;
	xmlnode *message;
	const char *session_id;
	int last_log;
	GString *text;
};

static time_t msn_logger_parse_timestamp(xmlnode *message)
{
	const char *date;
	const char *time;
	struct tm tm;
	char am_pm;

	g_return_val_if_fail(message != NULL, (time_t)0);

	date = xmlnode_get_attrib(message, "Date");
	if (!(date && *date)) {
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse",
			"Attribute missing: %s\n", "Date");
		return (time_t)0;
	}

	time = xmlnode_get_attrib(message, "Time");
	if (!(time && *time)) {
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse",
			"Attribute missing: %s\n", "Time");
		return (time_t)0;
	}

	if (sscanf(date, "%u/%u/%u", &tm.tm_mon, &tm.tm_mday, &tm.tm_year) != 3)
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse",
			"%s parsing error\n", "Date");

	if (sscanf(time, "%u:%u:%u %c", &tm.tm_hour, &tm.tm_min, &tm.tm_sec, &am_pm) != 4)
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log timestamp parse",
			"%s parsing error\n", "Time");

	tm.tm_year -= 1900;
	tm.tm_mon  -= 1;
	if (am_pm == 'P') {
		tm.tm_hour += 12;
	} else if (tm.tm_hour == 12) {
		/* 12 AM = 00 hr */
		tm.tm_hour = 0;
	}
	/* Let the C library deal with daylight savings time. */
	tm.tm_isdst = -1;

	return mktime(&tm);
}


static GList *msn_logger_list(GaimLogType type, const char *sn, GaimAccount *account)
{
	GList *list = NULL;
	char *username;
	GaimBuddy *buddy;
	const char *logdir;
	const char *savedfilename = NULL;
	char *logfile;
	char *path;
	GError *error = NULL;
	gchar *contents = NULL;
	gsize length;
	xmlnode *root;
	xmlnode *message;
	const char *old_session_id = "";
	struct msn_logger_data *data = NULL;

	g_return_val_if_fail(sn != NULL, list);
	g_return_val_if_fail(account != NULL, list);

	if (strcmp(account->protocol_id, "prpl-msn"))
		return list;

	logdir = gaim_prefs_get_string("/plugins/core/log_reader/msn/log_directory");

	/* By clearing the log directory path, this logger can be (effectively) disabled. */
	if (!*logdir)
		return list;

	buddy = gaim_find_buddy(account, sn);

	if ((username = g_strdup(gaim_account_get_string(
			account, "log_reader_msn_log_folder", NULL)))) {
		/* As a special case, we allow the null string to kill the parsing
		 * straight away. This would allow the user to deal with the case
		 * when two account have the same username at different domains and
		 * only one has logs stored.
		 */
		if (!*username) {
			g_free(username);
			return list;
		}
	} else {
		username = g_strdup(gaim_normalize(account, account->username));
	}

	if (buddy)
		savedfilename = gaim_blist_node_get_string(&buddy->node, "log_reader_msn_log_filename");

	if (savedfilename) {
		/* As a special case, we allow the null string to kill the parsing
		 * straight away. This would allow the user to deal with the case
		 * when two buddies have the same username at different domains and
		 * only one has logs stored.
		 */
		if (!*savedfilename) {
			g_free(username);
			return list;
		}

		logfile = g_strdup(savedfilename);
	} else {
		logfile = g_strdup_printf("%s.xml", gaim_normalize(account, sn));
	}

	path = g_build_filename(logdir, username, "History", logfile, NULL);

	if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
		gboolean found = FALSE;
		char *at_sign;
		GDir *dir;

		g_free(path);

		if (savedfilename) {
			/* We had a saved filename, but it doesn't exist.
			 * Returning now is the right course of action because we don't
			 * want to detect another file incorrectly.
			 */
			g_free(username);
			g_free(logfile);
			return list;
		}

		/* Perhaps we're using a new version of MSN with the weird numbered folders.
		 * I don't know how the numbers are calculated, so I'm going to attempt to
		 * find logs by pattern matching...
		 */

		at_sign = g_strrstr(username, "@");
		if (at_sign)
			*at_sign = '\0';

		dir = g_dir_open(logdir, 0, NULL);
		if (dir) {
			const gchar *name;

			while ((name = g_dir_read_name(dir))) {
				const char *c = name;

				if (!gaim_str_has_prefix(c, username))
					continue;

				c += strlen(username);
				while (*c) {
					if (!g_ascii_isdigit(*c))
						break;

					c++;
				}

				path = g_build_filename(logdir, name, NULL);
				/* The !c makes sure we got to the end of the while loop above. */
				if (!*c && g_file_test(path, G_FILE_TEST_IS_DIR)) {
					char *history_path = g_build_filename(
						path,  "History", NULL);
					if (g_file_test(history_path, G_FILE_TEST_IS_DIR)) {
						gaim_account_set_string(account,
							"log_reader_msn_log_folder", name);
						g_free(path);
						path = history_path;
						found = TRUE;
						break;
					}
					g_free(path);
					g_free(history_path);
				}
				else
					g_free(path);
			}
			g_dir_close(dir);
		}
		g_free(username);

		if (!found) {
			g_free(logfile);
			return list;
		}

		/* If we've reached this point, we've found a History folder. */

		username = g_strdup(gaim_normalize(account, sn));
		at_sign = g_strrstr(username, "@");
		if (at_sign)
			*at_sign = '\0';

		found = FALSE;
		dir = g_dir_open(path, 0, NULL);
		if (dir) {
			const gchar *name;

			while ((name = g_dir_read_name(dir))) {
				const char *c = name;

				if (!gaim_str_has_prefix(c, username))
					continue;

				c += strlen(username);
				while (*c) {
					if (!g_ascii_isdigit(*c))
						break;

					c++;
				}

				path = g_build_filename(path, name, NULL);
				if (!strcmp(c, ".xml") &&
				    g_file_test(path, G_FILE_TEST_EXISTS)) {
					found = TRUE;
					g_free(logfile);
					logfile = g_strdup(name);
					break;
				}
				else
					g_free(path);
			}
			g_dir_close(dir);
		}
		g_free(username);

		if (!found) {
			g_free(logfile);
			return list;
		}
	} else {
		g_free(username);
		g_free(logfile);
		logfile = NULL; /* No sense saving the obvious buddy@domain.com. */
	}

	gaim_debug(GAIM_DEBUG_INFO, "MSN log read",
				"Reading %s\n", path);
	if (!g_file_get_contents(path, &contents, &length, &error)) {
		g_free(path);
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log read",
				"Error reading log\n");
		if (error)
			g_error_free(error);
		return list;
	}
	g_free(path);

	/* Reading the file was successful...
	 * Save its name if it involves the crazy numbers. The idea here is that you could
	 * then tweak the blist.xml file by hand if need be. This would be the case if two
	 * buddies have the same username at different domains. One set of logs would get
	 * detected for both buddies.
	 */
	if (buddy && logfile) {
		gaim_blist_node_set_string(&buddy->node, "log_reader_msn_log_filename", logfile);
		g_free(logfile);
	}

	root = xmlnode_from_str(contents, length);
	g_free(contents);
	if (!root)
		return list;

	for (message = xmlnode_get_child(root, "Message"); message;
			message = xmlnode_get_next_twin(message)) {
		const char *session_id;

		session_id = xmlnode_get_attrib(message, "SessionID");
		if (!session_id) {
			gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse",
					"Error parsing message: %s\n", "SessionID missing");
			continue;
		}

		if (strcmp(session_id, old_session_id)) {
			/*
			 * The session ID differs from the last message.
			 * Thus, this is the start of a new conversation.
			 */
			GaimLog *log;

			data = g_new0(struct msn_logger_data, 1);
			data->root = root;
			data->message = message;
			data->session_id = session_id;
			data->text = NULL;
			data->last_log = FALSE;

			/* XXX: Look into this later... Should we pass in a struct tm? */
			log = gaim_log_new(GAIM_LOG_IM, sn, account, NULL, msn_logger_parse_timestamp(message), NULL);
			log->logger = msn_logger;
			log->logger_data = data;

			list = g_list_append(list, log);
		}
		old_session_id = session_id;
	}

	if (data)
		data->last_log = TRUE;

	return list;
}

static char * msn_logger_read (GaimLog *log, GaimLogReadFlags *flags)
{
	struct msn_logger_data *data;
	GString *text = NULL;
	xmlnode *message;

	g_return_val_if_fail(log != NULL, g_strdup(""));

	data = log->logger_data;

	if (data->text) {
		/* The GTK code which displays the logs g_free()s whatever is
		 * returned from this function. Thus, we can't reuse the str
		 * part of the GString. The only solution is to free it and
		 * start over.
		 */
		g_string_free(data->text, FALSE);
	}

	text = g_string_new("");

	if (!data->root || !data->message || !data->session_id) {
		/* Something isn't allocated correctly. */
		gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse",
				"Error parsing message: %s\n", "Internal variables inconsistent");
		data->text = text;

		return text->str;
	}

	for (message = data->message; message;
			message = xmlnode_get_next_twin(message)) {

		const char *new_session_id;
		xmlnode *text_node;
		const char *from_name = NULL;
		const char *to_name = NULL;
		xmlnode *from;
		xmlnode *to;
		enum name_guesses name_guessed = NAME_GUESS_UNKNOWN;
		const char *their_name;
		time_t time_unix;
		struct tm *tm_new;
		char *timestamp;
		const char *style;

		new_session_id = xmlnode_get_attrib(message, "SessionID");

		/* If this triggers, something is wrong with the XML. */
		if (!new_session_id) {
			gaim_debug(GAIM_DEBUG_ERROR, "MSN log parse",
					"Error parsing message: %s\n", "New SessionID missing");
			break;
		}

		if (strcmp(new_session_id, data->session_id)) {
			/* The session ID differs from the first message.
			 * Thus, this is the start of a new conversation.
			 */
			break;
		}

		text_node = xmlnode_get_child(message, "Text");
		if (!text_node)
			continue;

		from = xmlnode_get_child(message, "From");
		if (from) {
			xmlnode *user = xmlnode_get_child(from, "User");

			if (user) {
				from_name = xmlnode_get_attrib(user, "FriendlyName");

				/* This saves a check later. */
				if (!*from_name)
					from_name = NULL;
			}
		}

		to = xmlnode_get_child(message, "To");
		if (to) {
			xmlnode *user = xmlnode_get_child(to, "User");
			if (user) {
				to_name = xmlnode_get_attrib(user, "FriendlyName");

				/* This saves a check later. */
				if (!*to_name)
					to_name = NULL;
			}
		}

		their_name = from_name;
		if (from_name && gaim_prefs_get_bool("/plugins/core/log_reader/use_name_heuristics")) {
			const char *friendly_name = gaim_connection_get_display_name(log->account->gc);

			if (friendly_name != NULL) {
				int friendly_name_length = strlen(friendly_name);
				int alias_length         = strlen(log->account->alias);
				GaimBuddy *buddy = gaim_find_buddy(log->account, log->name);
				gboolean from_name_matches;
				gboolean to_name_matches;

				if (buddy && buddy->alias)
					their_name = buddy->alias;

				/* Try to guess which user is me.
				 * The first step is to determine if either of the names matches either my
				 * friendly name or alias. For this test, "match" is defined as:
				 * ^(friendly_name|alias)([^a-zA-Z0-9].*)?$
				 */
				from_name_matches = from_name != NULL && (
				                     (gaim_str_has_prefix(from_name, friendly_name) &&
				                      !isalnum(*(from_name + friendly_name_length))) ||
				                     (gaim_str_has_prefix(from_name, log->account->alias) &&
				                      !isalnum(*(from_name + alias_length))));

				to_name_matches = to_name != NULL && (
				                   (gaim_str_has_prefix(to_name, friendly_name) &&
				                    !isalnum(*(to_name + friendly_name_length))) ||
				                   (gaim_str_has_prefix(to_name, log->account->alias) &&
				                    !isalnum(*(to_name + alias_length))));

				if (from_name_matches) {
					if (!to_name_matches) {
						name_guessed = NAME_GUESS_ME;
					}
				} else if (to_name_matches) {
					name_guessed = NAME_GUESS_THEM;
				} else {
					if (buddy && buddy->alias) {
						char *alias = g_strdup(buddy->alias);

						/* "Truncate" the string at the first non-alphanumeric
						 * character. The idea is to relax the comparison.
						 */
						char *temp;
						for (temp = alias; *temp ; temp++) {
							if (!isalnum(*temp)) {
								*temp = '\0';
								break;
							}
						}
						alias_length = strlen(alias);

						/* Try to guess which user is them.
						 * The first step is to determine if either of the names
						 * matches their alias. For this test, "match" is
						 * defined as: ^alias([^a-zA-Z0-9].*)?$
						 */
						from_name_matches = (gaim_str_has_prefix(
								from_name, alias) &&
								!isalnum(*(from_name +
								alias_length)));

						to_name_matches = (gaim_str_has_prefix(
								to_name, alias) &&
								!isalnum(*(to_name +
								alias_length)));

						g_free(alias);

						if (from_name_matches) {
							if (!to_name_matches) {
								name_guessed = NAME_GUESS_THEM;
							}
						} else if (to_name_matches) {
							name_guessed = NAME_GUESS_ME;
						} else if (buddy->server_alias) {
							friendly_name_length =
								strlen(buddy->server_alias);

							/* Try to guess which user is them.
							 * The first step is to determine if either of
							 * the names matches their friendly name. For
							 * this test, "match" is defined as:
							 * ^friendly_name([^a-zA-Z0-9].*)?$
							 */
							from_name_matches = (gaim_str_has_prefix(
									from_name,
									buddy->server_alias) &&
									!isalnum(*(from_name +
									friendly_name_length)));

							to_name_matches = (gaim_str_has_prefix(
									to_name, buddy->server_alias) &&
									!isalnum(*(to_name +
									friendly_name_length)));

							if (from_name_matches) {
								if (!to_name_matches) {
									name_guessed = NAME_GUESS_THEM;
								}
							} else if (to_name_matches) {
								name_guessed = NAME_GUESS_ME;
							}
						}
					}
				}
			}
		}

		if (name_guessed != NAME_GUESS_UNKNOWN) {
			text = g_string_append(text, "<span style=\"color: #");
			if (name_guessed == NAME_GUESS_ME)
				text = g_string_append(text, "16569E");
			else
				text = g_string_append(text, "A82F2F");
			text = g_string_append(text, ";\">");
		}

		time_unix = msn_logger_parse_timestamp(message);
		tm_new = localtime(&time_unix);

		timestamp = g_strdup_printf("<font size=\"2\">(%02u:%02u:%02u)</font> ",
				tm_new->tm_hour, tm_new->tm_min, tm_new->tm_sec);
		text = g_string_append(text, timestamp);
		g_free(timestamp);

		if (from_name) {
			text = g_string_append(text, "<b>");

			if (name_guessed == NAME_GUESS_ME)
				text = g_string_append(text, log->account->alias);
			else if (name_guessed == NAME_GUESS_THEM)
				text = g_string_append(text, their_name);
			else
				text = g_string_append(text, from_name);

			text = g_string_append(text, ":</b> ");
		}

		if (name_guessed != NAME_GUESS_UNKNOWN)
			text = g_string_append(text, "</span>");

		style     = xmlnode_get_attrib(text_node, "Style");

		if (style && *style) {
			text = g_string_append(text, "<span style=\"");
			text = g_string_append(text, style);
			text = g_string_append(text, "\">");
			text = g_string_append(text, xmlnode_get_data(text_node));
			text = g_string_append(text, "</span>\n");
		} else {
			text = g_string_append(text, xmlnode_get_data(text_node));
			text = g_string_append(text, "\n");
		}
	}

	data->text = text;

	return text->str;
}

static int msn_logger_size (GaimLog *log)
{
	char *text;
	size_t size;

	g_return_val_if_fail(log != NULL, 0);

	if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes"))
		return 0;

	text = msn_logger_read(log, NULL);
	size = strlen(text);
	g_free(text);

	return size;
}

static void msn_logger_finalize(GaimLog *log)
{
	struct msn_logger_data *data;

	g_return_if_fail(log != NULL);

	data = log->logger_data;

	if (data->last_log)
		xmlnode_free(data->root);

	if (data->text)
		g_string_free(data->text, FALSE);
}


/*****************************************************************************
 * Trillian Logger                                                           *
 *****************************************************************************/

/* The trillian logger doesn't write logs, only reads them.  This is to include
 * Trillian logs in the log viewer transparently.
 */

static GaimLogLogger *trillian_logger;
static void trillian_logger_finalize(GaimLog *log);

struct trillian_logger_data {
	char *path; /* FIXME: Change this to use GaimStringref like log.c:old_logger_list */
	int offset;
	int length;
	char *their_nickname;
};

static GList *trillian_logger_list(GaimLogType type, const char *sn, GaimAccount *account)
{
	GList *list = NULL;
	const char *logdir;
	GaimPlugin *plugin;
	GaimPluginProtocolInfo *prpl_info;
	char *prpl_name;
	const char *buddy_name;
	char *filename;
	char *path;
	GError *error = NULL;
	gchar *contents = NULL;
	gsize length;
	gchar *line;
	gchar *c;

	g_return_val_if_fail(sn != NULL, list);
	g_return_val_if_fail(account != NULL, list);

	logdir = gaim_prefs_get_string("/plugins/core/log_reader/trillian/log_directory");

	/* By clearing the log directory path, this logger can be (effectively) disabled. */
	if (!*logdir)
		return list;

	plugin = gaim_find_prpl(gaim_account_get_protocol_id(account));
	if (!plugin)
		return NULL;

	prpl_info = GAIM_PLUGIN_PROTOCOL_INFO(plugin);
	if (!prpl_info->list_icon)
		return NULL;

	prpl_name = g_ascii_strup(prpl_info->list_icon(account, NULL), -1);

	buddy_name = gaim_normalize(account, sn);

	filename = g_strdup_printf("%s.log", buddy_name);
	path = g_build_filename(
		logdir, prpl_name, filename, NULL);

	gaim_debug(GAIM_DEBUG_INFO, "Trillian log list",
				"Reading %s\n", path);
	/* FIXME: There's really no need to read the entire file at once.
	 * See src/log.c:old_logger_list for a better approach.
	 */
	if (!g_file_get_contents(path, &contents, &length, &error)) {
		if (error) {
			g_error_free(error);
			error = NULL;
		}
		g_free(path);

		path = g_build_filename(
			logdir, prpl_name, "Query", filename, NULL);
		gaim_debug(GAIM_DEBUG_INFO, "Trillian log list",
					"Reading %s\n", path);
		if (!g_file_get_contents(path, &contents, &length, &error)) {
			if (error)
				g_error_free(error);
		}
	}
	g_free(filename);

	if (contents) {
		struct trillian_logger_data *data = NULL;
		int offset = 0;
		int last_line_offset = 0;

		line = contents;
		c = contents;
		while (*c) {
			offset++;

			if (*c != '\n') {
				c++;
				continue;
			}

			*c = '\0';
			if (gaim_str_has_prefix(line, "Session Close ")) {
				if (data && !data->length)
					data->length = last_line_offset - data->offset;
				if (!data->length) {
					/* This log had no data, so we remove it. */
					GList *last = g_list_last(list);

					gaim_debug(GAIM_DEBUG_INFO, "Trillian log list",
						"Empty log. Offset %i\n", data->offset);

					trillian_logger_finalize((GaimLog *)last->data);
					list = g_list_delete_link(list, last);
				}
			} else if (line[0] && line[1] && line [3] &&
					   gaim_str_has_prefix(&line[3], "sion Start ")) {

				char *their_nickname = line;
				char *timestamp;

				if (data && !data->length)
					data->length = last_line_offset - data->offset;

				while (*their_nickname && (*their_nickname != ':'))
					their_nickname++;
				their_nickname++;

				/* This code actually has nothing to do with
				 * the timestamp YET. I'm simply using this
				 * variable for now to NUL-terminate the
				 * their_nickname string.
				 */
				timestamp = their_nickname;
				while (*timestamp && *timestamp != ')')
					timestamp++;

				if (*timestamp == ')') {
					char *month;
					struct tm tm;

					*timestamp = '\0';
					if (line[0] && line[1] && line[2])
						timestamp += 3;

					/* Now we start dealing with the timestamp. */

					/* Skip over the day name. */
					while (*timestamp && (*timestamp != ' '))
						timestamp++;
					*timestamp = '\0';
					timestamp++;

					/* Parse out the month. */
					month = timestamp;
					while (*timestamp &&  (*timestamp != ' '))
						timestamp++;
					*timestamp = '\0';
					timestamp++;

					/* Parse the day, time, and year. */
					if (sscanf(timestamp, "%u %u:%u:%u %u",
							&tm.tm_mday, &tm.tm_hour,
							&tm.tm_min, &tm.tm_sec,
							&tm.tm_year) != 5) {

						gaim_debug(GAIM_DEBUG_ERROR,
							"Trillian log timestamp parse",
							"Session Start parsing error\n");
					} else {
						GaimLog *log;

						tm.tm_year -= 1900;

						/* Let the C library deal with
						 * daylight savings time.
						 */
						tm.tm_isdst = -1;

						/* Ugly hack, in case current locale
						 * is not English. This code is taken
						 * from log.c.
						 */
						if (strcmp(month, "Jan") == 0) {
							tm.tm_mon= 0;
						} else if (strcmp(month, "Feb") == 0) {
							tm.tm_mon = 1;
						} else if (strcmp(month, "Mar") == 0) {
							tm.tm_mon = 2;
						} else if (strcmp(month, "Apr") == 0) {
							tm.tm_mon = 3;
						} else if (strcmp(month, "May") == 0) {
							tm.tm_mon = 4;
						} else if (strcmp(month, "Jun") == 0) {
							tm.tm_mon = 5;
						} else if (strcmp(month, "Jul") == 0) {
							tm.tm_mon = 6;
						} else if (strcmp(month, "Aug") == 0) {
							tm.tm_mon = 7;
						} else if (strcmp(month, "Sep") == 0) {
							tm.tm_mon = 8;
						} else if (strcmp(month, "Oct") == 0) {
							tm.tm_mon = 9;
						} else if (strcmp(month, "Nov") == 0) {
							tm.tm_mon = 10;
						} else if (strcmp(month, "Dec") == 0) {
							tm.tm_mon = 11;
						}

						data = g_new0(
							struct trillian_logger_data, 1);
						data->path = g_strdup(path);
						data->offset = offset;
						data->length = 0;
						data->their_nickname =
							g_strdup(their_nickname);

						/* XXX: Look into this later... Should we pass in a struct tm? */
						log = gaim_log_new(GAIM_LOG_IM,
							sn, account, NULL, mktime(&tm), NULL);
						log->logger = trillian_logger;
						log->logger_data = data;

						list = g_list_append(list, log);
					}
				}
			}
			c++;
			line = c;
			last_line_offset = offset;
		}

		g_free(contents);
	}
	g_free(path);

	g_free(prpl_name);

	return list;
}

static char * trillian_logger_read (GaimLog *log, GaimLogReadFlags *flags)
{
	struct trillian_logger_data *data;
	char *read;
	FILE *file;
	GaimBuddy *buddy;
	char *escaped;
	GString *formatted;
	char *c;
	char *line;

	g_return_val_if_fail(log != NULL, g_strdup(""));

	data = log->logger_data;

	g_return_val_if_fail(data->path != NULL, g_strdup(""));
	g_return_val_if_fail(data->length > 0, g_strdup(""));
	g_return_val_if_fail(data->their_nickname != NULL, g_strdup(""));

	gaim_debug(GAIM_DEBUG_INFO, "Trillian log read",
				"Reading %s\n", data->path);

	read = g_malloc(data->length + 2);

	file = g_fopen(data->path, "rb");
	fseek(file, data->offset, SEEK_SET);
	fread(read, data->length, 1, file);
	fclose(file);

	if (read[data->length-1] == '\n') {
		read[data->length] = '\0';
	} else {
		read[data->length] = '\n';
		read[data->length+1] = '\0';
	}

	/* Load miscellaneous data. */
	buddy = gaim_find_buddy(log->account, log->name);

	escaped = g_markup_escape_text(read, -1);
	g_free(read);
	read = escaped;

	/* Apply formatting... */
	formatted = g_string_new("");
	c = read;
	line = read;
	while (*c)
	{
		if (*c == '\n')
		{
			char *link_temp_line;
			char *link;
			char *timestamp;
			char *footer = NULL;
			*c = '\0';

			/* Convert links.
			 *
			 * The format is (Link: URL)URL
			 * So, I want to find each occurance of "(Link: " and replace that chunk with:
			 * <a href="
			 * Then, replace the next ")" with:
			 * ">
			 * Then, replace the next " " (or add this if the end-of-line is reached) with:
			 * </a>
			 */
			link_temp_line = NULL;
			while ((link = g_strstr_len(line, strlen(line), "(Link: "))) {
				GString *temp;

				if (!*link)
					continue;

				*link = '\0';
				link++;

				temp = g_string_new(line);
				g_string_append(temp, "<a href=\"");

				if (strlen(link) >= 6) {
					link += (sizeof("(Link: ") - 1);

					while (*link && *link != ')') {
						g_string_append_c(temp, *link);
						link++;
					}
					if (link) {
						link++;

						g_string_append(temp, "\">");
						while (*link && *link != ' ') {
							g_string_append_c(temp, *link);
							link++;
						}
						g_string_append(temp, "</a>");
					}

					g_string_append(temp, link);

					/* Free the last round's line. */
					if (link_temp_line)
						g_free(line);

					line = temp->str;
					g_string_free(temp, FALSE);

					/* Save this memory location so we can free it later. */
					link_temp_line = line;
				}
			}

			timestamp = "";
			if (*line == '[') {
				timestamp = line;
				while (*timestamp && *timestamp != ']')
					timestamp++;
				if (*timestamp == ']') {
					*timestamp = '\0';
					line++;
					/* TODO: Parse the timestamp and convert it to Gaim's format. */
					g_string_append_printf(formatted,
						"<font size=\"2\">(%s)</font> ", line);
					line = timestamp;
					if (line[1] && line[2])
						line += 2;
				}

				if (gaim_str_has_prefix(line, "*** ")) {
					line += (sizeof("*** ") - 1);
					g_string_append(formatted, "<b>");
					footer = "</b>";
					if (gaim_str_has_prefix(line, "NOTE: This user is offline.")) {
						line = _("User is offline.");
					} else if (gaim_str_has_prefix(line,
							"NOTE: Your status is currently set to ")) {

						line += (sizeof("NOTE: ") - 1);
					} else if (gaim_str_has_prefix(line, "Auto-response sent to ")) {
						g_string_append(formatted, _("Auto-response sent:"));
						while (*line && *line != ':')
							line++;
						if (*line)
							line++;
						g_string_append(formatted, "</b>");
						footer = NULL;
					} else if (strstr(line, " signed off ")) {
						if (buddy != NULL && buddy->alias)
							g_string_append_printf(formatted,
								_("%s has signed off."), buddy->alias);
						else
							g_string_append_printf(formatted,
								_("%s has signed off."), log->name);
						line = "";
					} else if (strstr(line, " signed on ")) {
						if (buddy != NULL && buddy->alias)
							g_string_append(formatted, buddy->alias);
						else
							g_string_append(formatted, log->name);
						line = " logged in.";
					} else if (gaim_str_has_prefix(line,
						"One or more messages may have been undeliverable.")) {

						g_string_append(formatted,
							"<span style=\"color: #ff0000;\">");
						g_string_append(formatted,
							_("One or more messages may have been "
							  "undeliverable."));
						line = "";
						footer = "</span></b>";
					} else if (gaim_str_has_prefix(line,
							"You have been disconnected.")) {

						g_string_append(formatted,
							"<span style=\"color: #ff0000;\">");
						g_string_append(formatted,
							_("You were disconnected from the server."));
						line = "";
						footer = "</span></b>";
					} else if (gaim_str_has_prefix(line,
							"You are currently disconnected.")) {

						g_string_append(formatted,
							"<span style=\"color: #ff0000;\">");
						line = _("You are currently disconnected. Messages "
						         "will not be received unless you are "
						         "logged in.");
						footer = "</span></b>";
					} else if (gaim_str_has_prefix(line,
							"Your previous message has not been sent.")) {

						g_string_append(formatted,
							"<span style=\"color: #ff0000;\">");
						
						if (gaim_str_has_prefix(line,
							"Your previous message has not been sent.  "
							"Reason: Maximum length exceeded.")) {

							g_string_append(formatted,
								_("Message could not be sent because "
								  "the maximum length was exceeded."));
							line = "";
						} else {
							g_string_append(formatted,
								_("Message could not be sent."));
							line += (sizeof(
								"Your previous message "
								"has not been sent. ") - 1);
						}

						footer = "</span></b>";
					}
				} else if (gaim_str_has_prefix(line, data->their_nickname)) {
					if (buddy != NULL && buddy->alias) {
						line += strlen(data->their_nickname) + 2;
						g_string_append_printf(formatted,
							"<span style=\"color: #A82F2F;\">"
							"<b>%s</b></span>: ", buddy->alias);
					}
				} else {
					char *line2 = line;
					while (*line2 && *line2 != ':')
						line2++;
					if (*line2 == ':') {
						line2++;
						line = line2;
						g_string_append_printf(formatted,
							"<span style=\"color: #16569E;\">"
							"<b>%s</b></span>:", log->account->alias);
					}
				}
			}

			g_string_append(formatted, line);

			if (footer)
				g_string_append(formatted, footer);

			g_string_append_c(formatted, '\n');

			if (link_temp_line)
				g_free(link_temp_line);

			c++;
			line = c;
		} else
			c++;
	}

	g_free(read);
	read = formatted->str;
	g_string_free(formatted, FALSE);

	return read;
}

static int trillian_logger_size (GaimLog *log)
{
	struct trillian_logger_data *data;
	char *text;
	size_t size;

	g_return_val_if_fail(log != NULL, 0);

	data = log->logger_data;

	if (gaim_prefs_get_bool("/plugins/core/log_reader/fast_sizes")) {
		return data ? data->length : 0;
	}

	text = trillian_logger_read(log, NULL);
	size = strlen(text);
	g_free(text);

	return size;
}

static void trillian_logger_finalize(GaimLog *log)
{
	struct trillian_logger_data *data;

	g_return_if_fail(log != NULL);

	data = log->logger_data;

	g_free(data->path);
	g_free(data->their_nickname);

}


/*****************************************************************************
 * Plugin Code                                                               *
 *****************************************************************************/

static void
init_plugin(GaimPlugin *plugin)
{
	char *path;
#ifdef _WIN32
	char *folder;
#endif

	g_return_if_fail(plugin != NULL);

	gaim_prefs_add_none("/plugins/core/log_reader");


	/* Add general preferences. */

	gaim_prefs_add_bool("/plugins/core/log_reader/fast_sizes", FALSE);
	gaim_prefs_add_bool("/plugins/core/log_reader/use_name_heuristics", TRUE);


	/* Add Adium log directory preference. */
	gaim_prefs_add_none("/plugins/core/log_reader/adium");

	/* Calculate default Adium log directory. */
#ifdef _WIN32
		path = "";
#else
		path = g_build_filename(gaim_home_dir(), "Library", "Application Support",
			"Adium 2.0", "Users", "Default", "Logs", NULL);
#endif

	gaim_prefs_add_string("/plugins/core/log_reader/adium/log_directory", path);

#ifndef _WIN32
	g_free(path);
#endif


	/* Add Fire log directory preference. */
	gaim_prefs_add_none("/plugins/core/log_reader/fire");

	/* Calculate default Fire log directory. */
#ifdef _WIN32
		path = "";
#else
		path = g_build_filename(gaim_home_dir(), "Library", "Application Support",
			"Fire", "Sessions", NULL);
#endif

	gaim_prefs_add_string("/plugins/core/log_reader/fire/log_directory", path);

#ifndef _WIN32
	g_free(path);
#endif


	/* Add Messenger Plus! log directory preference. */
	gaim_prefs_add_none("/plugins/core/log_reader/messenger_plus");

	/* Calculate default Messenger Plus! log directory. */
#ifdef _WIN32
	folder = wgaim_get_special_folder(CSIDL_PERSONAL);
	if (folder) {
#endif
	path = g_build_filename(
#ifdef _WIN32
		folder,
#else
		GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Documents and Settings",
		g_get_user_name(), "My Documents",
#endif
		"My Chat Logs", NULL);
#ifdef _WIN32
	g_free(folder);
	} else /* !folder */
		path = g_strdup("");
#endif

	gaim_prefs_add_string("/plugins/core/log_reader/messenger_plus/log_directory", path);
	g_free(path);


	/* Add MSN Messenger log directory preference. */
	gaim_prefs_add_none("/plugins/core/log_reader/msn");

	/* Calculate default MSN message history directory. */
#ifdef _WIN32
	folder = wgaim_get_special_folder(CSIDL_PERSONAL);
	if (folder) {
#endif
	path = g_build_filename(
#ifdef _WIN32
		folder,
#else
		GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Documents and Settings",
		g_get_user_name(), "My Documents",
#endif
		"My Received Files", NULL);
#ifdef _WIN32
	g_free(folder);
	} else /* !folder */
		path = g_strdup("");
#endif

	gaim_prefs_add_string("/plugins/core/log_reader/msn/log_directory", path);
	g_free(path);


	/* Add Trillian log directory preference. */
	gaim_prefs_add_none("/plugins/core/log_reader/trillian");

#ifdef _WIN32
	/* XXX: While a major hack, this is the most reliable way I could
	 * think of to determine the Trillian installation directory.
	 */
	HKEY hKey;
	char buffer[1024] = "";
	DWORD size = (sizeof(buffer) - 1);
	DWORD type;

	path = NULL;
	/* TODO: Test this after removing the trailing "\\". */
	if(ERROR_SUCCESS == RegOpenKeyEx(HKEY_CLASSES_ROOT, "Trillian.SkinZip\\shell\\Add\\command\\", 
				0, KEY_QUERY_VALUE, &hKey)) {

		if(ERROR_SUCCESS == RegQueryValueEx(hKey, "", NULL, &type, (LPBYTE)buffer, &size)) {
			char *value = buffer;
			char *temp;

			/* Ensure the data is null terminated. */
			value[size] = '\0';

			/* Break apart buffer. */
			if (*value == '"') {
				value++;
				temp = value;
				while (*temp && *temp != '"')
					temp++;
			} else {
				temp = value;
				while (*temp && *temp != ' ')
					temp++;
			}
			*temp = '\0';

			/* Set path. */
			if (gaim_str_has_suffix(value, "trillian.exe"))
			{
				value[strlen(value) - (sizeof("trillian.exe") - 1)] = '\0';
				path = g_build_filename(value, "users", "default", "talk.ini", NULL);
			}
		}
		RegCloseKey(hKey);
	}

	if (!path) {
		char *folder = wgaim_get_special_folder(CSIDL_PROGRAM_FILES);
		if (folder)
			path = g_build_filename(folder, "Trillian",
					"users", "default", "talk.ini", NULL);
			g_free(folder);
		}
	}

	gboolean found = FALSE;

	if (path) {
		/* Read talk.ini file to find the log directory. */
		GError *error = NULL;

#if 0 && GTK_CHECK_VERSION(2,6,0) /* FIXME: Not tested yet. */
		GKeyFile *key_file;

		gaim_debug(GAIM_DEBUG_INFO, "Trillian talk.ini read",
				"Reading %s\n", path);
		if (!g_key_file_load_from_file(key_file, path, G_KEY_FILE_NONE, GError &error)) {
			gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read",
					"Error reading talk.ini\n");
			if (error)
				g_error_free(error);
		} else {
			char *logdir = g_key_file_get_string(key_file, "Logging", "Directory", &error);
			if (error) {
				gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read",
						"Error reading Directory value from Logging section\n");
				g_error_free(error);
			}

			if (logdir) {
				g_strchomp(logdir);
				gaim_prefs_add_string(
					"/plugins/core/log_reader/trillian/log_directory", logdir);
				found = TRUE;
			}

			g_key_file_free(key_file);
		}
#else /* !GTK_CHECK_VERSION(2,6,0) */
		GError *error = NULL;
		gsize length;

		gaim_debug(GAIM_DEBUG_INFO, "Trillian talk.ini read",
					"Reading %s\n", path);
		if (!g_file_get_contents(path, &contents, &length, &error)) {
			gaim_debug(GAIM_DEBUG_ERROR, "Trillian talk.ini read",
					"Error reading talk.ini\n");
			if (error)
				g_error_free(error);
		} else {
			char *line = contents;
			while (*contents) {
				if (*contents == '\n') {
					*contents = '\0';

					/* XXX: This assumes the first Directory key is under [Logging]. */
					if (gaim_str_has_prefix(line, "Directory=")) {
						line += (sizeof("Directory=") - 1);
						g_strchomp(line);
						gaim_prefs_add_string(
							"/plugins/core/log_reader/trillian/log_directory",
							line);
						found = TRUE;
					}

					contents++;
					line = contents;
				} else
					contents++;
			}
			g_free(path);
			g_free(contents);
		}
#endif /* !GTK_CHECK_VERSION(2,6,0) */
	} /* path */

	if (!found) {
#endif /* defined(_WIN32) */

	/* Calculate default Trillian log directory. */
#ifdef _WIN32
	folder = wgaim_get_special_folder(CSIDL_PROGRAM_FILES);
	if (folder) {
#endif
	path = g_build_filename(
#ifdef _WIN32
		folder,
#else
		GAIM_LOG_READER_WINDOWS_MOUNT_POINT, "Program Files",
#endif
		"Trillian", "users", "default", "logs", NULL);
#ifdef _WIN32
	g_free(folder);
	} else /* !folder */
		path = g_strdup("");
#endif

	gaim_prefs_add_string("/plugins/core/log_reader/trillian/log_directory", path);
	g_free(path);

#ifdef _WIN32
	} /* !found */
#endif
}

static gboolean
plugin_load(GaimPlugin *plugin)
{
	g_return_val_if_fail(plugin != NULL, FALSE);

	adium_logger = gaim_log_logger_new("adium", _("Adium"), 6,
									   NULL,
									   NULL,
									   adium_logger_finalize,
									   adium_logger_list,
									   adium_logger_read,
									   adium_logger_size);
	gaim_log_logger_add(adium_logger);

	fire_logger = gaim_log_logger_new("fire", _("Fire"), 6,
									  NULL,
									  NULL,
									  fire_logger_finalize,
									  fire_logger_list,
									  fire_logger_read,
									  fire_logger_size);
	gaim_log_logger_add(fire_logger);

	messenger_plus_logger = gaim_log_logger_new("messenger_plus", _("Messenger Plus!"), 6,
												NULL,
												NULL,
												messenger_plus_logger_finalize,
												messenger_plus_logger_list,
												messenger_plus_logger_read,
												messenger_plus_logger_size);
	gaim_log_logger_add(messenger_plus_logger);

	msn_logger = gaim_log_logger_new("msn", _("MSN Messenger"), 6,
									 NULL,
									 NULL,
									 msn_logger_finalize,
									 msn_logger_list,
									 msn_logger_read,
									 msn_logger_size);
	gaim_log_logger_add(msn_logger);

	trillian_logger = gaim_log_logger_new("trillian", _("Trillian"), 6,
										  NULL,
										  NULL,
										  trillian_logger_finalize,
										  trillian_logger_list,
										  trillian_logger_read,
										  trillian_logger_size);
	gaim_log_logger_add(trillian_logger);

	return TRUE;
}

static gboolean
plugin_unload(GaimPlugin *plugin)
{
	g_return_val_if_fail(plugin != NULL, FALSE);

	gaim_log_logger_remove(adium_logger);
	gaim_log_logger_remove(fire_logger);
	gaim_log_logger_remove(messenger_plus_logger);
	gaim_log_logger_remove(msn_logger);
	gaim_log_logger_remove(trillian_logger);

	return TRUE;
}

static GaimPluginPrefFrame *
get_plugin_pref_frame(GaimPlugin *plugin)
{
	GaimPluginPrefFrame *frame;
	GaimPluginPref *ppref;

	g_return_val_if_fail(plugin != NULL, FALSE);

	frame = gaim_plugin_pref_frame_new();


	/* Add general preferences. */

	ppref = gaim_plugin_pref_new_with_label(_("General Log Reading Configuration"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/fast_sizes", _("Fast size calculations"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/use_name_heuristics", _("Use name heuristics"));
	gaim_plugin_pref_frame_add(frame, ppref);


	/* Add Log Directory preferences. */

	ppref = gaim_plugin_pref_new_with_label(_("Log Directory"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/adium/log_directory", _("Adium"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/fire/log_directory", _("Fire"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/messenger_plus/log_directory", _("Messenger Plus!"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/msn/log_directory", _("MSN Messenger"));
	gaim_plugin_pref_frame_add(frame, ppref);

	ppref = gaim_plugin_pref_new_with_name_and_label(
		"/plugins/core/log_reader/trillian/log_directory", _("Trillian"));
	gaim_plugin_pref_frame_add(frame, ppref);

	return frame;
}

static GaimPluginUiInfo prefs_info = {
	get_plugin_pref_frame,
	0,   /* page_num (reserved) */
	NULL /* frame (reserved) */
};

static GaimPluginInfo info =
{
	GAIM_PLUGIN_MAGIC,
	GAIM_MAJOR_VERSION,
	GAIM_MINOR_VERSION,
	GAIM_PLUGIN_STANDARD,                             /**< type           */
	NULL,                                             /**< ui_requirement */
	0,                                                /**< flags          */
	NULL,                                             /**< dependencies   */
	GAIM_PRIORITY_DEFAULT,                            /**< priority       */
	"core-log_reader",                                /**< id             */
	N_("Log Reader"),                                 /**< name           */
	VERSION,                                          /**< version        */

	/** summary */
	N_("Includes other IM clients' logs in the "
	   "log viewer."),

	/** description */
	N_("When viewing logs, this plugin will include "
	   "logs from other IM clients. Currently, this "
	   "includes Adium, Fire, Messenger Plus!, "
	   "MSN Messenger, and Trillian."),

	"Richard Laager <rlaager@users.sf.net>",          /**< author         */
	GAIM_WEBSITE,                                     /**< homepage       */
	plugin_load,                                      /**< load           */
	plugin_unload,                                    /**< unload         */
	NULL,                                             /**< destroy        */
	NULL,                                             /**< ui_info        */
	NULL,                                             /**< extra_info     */
	&prefs_info,                                      /**< prefs_info     */
	NULL                                              /**< actions        */
};

GAIM_INIT_PLUGIN(log_reader, init_plugin, info)