diff libgaim/savedstatuses.c @ 14192:60b1bc8dbf37

[gaim-migrate @ 16863] Renamed 'core' to 'libgaim' committer: Tailor Script <tailor@pidgin.im>
author Evan Schoenberg <evan.s@dreskin.net>
date Sat, 19 Aug 2006 01:50:10 +0000
parents
children 0c3957362c69
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libgaim/savedstatuses.c	Sat Aug 19 01:50:10 2006 +0000
@@ -0,0 +1,1166 @@
+/**
+ * @file savedstatuses.c Saved Status API
+ * @ingroup core
+ *
+ * gaim
+ *
+ * Gaim is the legal property of its developers, whose names are too numerous
+ * to list here.  Please refer to the COPYRIGHT file distributed with this
+ * source distribution.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+#include "internal.h"
+
+#include "debug.h"
+#include "idle.h"
+#include "notify.h"
+#include "savedstatuses.h"
+#include "dbus-maybe.h"
+#include "status.h"
+#include "util.h"
+#include "xmlnode.h"
+
+/**
+ * The maximum number of transient statuses to save.  This
+ * is used during the shutdown process to clean out old
+ * transient statuses.
+ */
+#define MAX_TRANSIENTS 5
+
+/**
+ * The default message to use when the user becomes auto-away.
+ */
+#define DEFAULT_AUTOAWAY_MESSAGE _("I'm not here right now")
+
+/**
+ * The information stores a snap-shot of the statuses of all
+ * your accounts.  Basically these are your saved away messages.
+ * There is an overall status and message that applies to
+ * all your accounts, and then each individual account can
+ * optionally have a different custom status and message.
+ *
+ * The changes to status.xml caused by the new status API
+ * are fully backward compatible.  The new status API just
+ * adds the optional sub-statuses to the XML file.
+ */
+struct _GaimSavedStatus
+{
+	char *title;
+	GaimStatusPrimitive type;
+	char *message;
+
+	/** The timestamp when this saved status was created. This must be unique. */
+	time_t creation_time;
+
+	time_t lastused;
+
+	unsigned int usage_count;
+
+	GList *substatuses;      /**< A list of GaimSavedStatusSub's. */
+};
+
+/*
+ * TODO: If a GaimStatusType is deleted, need to also delete any
+ *       associated GaimSavedStatusSub's?
+ */
+struct _GaimSavedStatusSub
+{
+	GaimAccount *account;
+	const GaimStatusType *type;
+	char *message;
+};
+
+static GList      *saved_statuses = NULL;
+static guint       save_timer = 0;
+static gboolean    statuses_loaded = FALSE;
+
+/*
+ * This hash table keeps track of which timestamps we've
+ * used so that we don't have two saved statuses with the
+ * same 'creation_time' timestamp.  The 'created' timestamp
+ * is used as a unique identifier.
+ *
+ * So the key in this hash table is the creation_time and
+ * the value is a pointer to the GaimSavedStatus.
+ */
+static GHashTable *creation_times;
+
+static void schedule_save(void);
+
+/*********************************************************************
+ * Private utility functions                                         *
+ *********************************************************************/
+
+static void
+free_saved_status_sub(GaimSavedStatusSub *substatus)
+{
+	g_return_if_fail(substatus != NULL);
+
+	g_free(substatus->message);
+	GAIM_DBUS_UNREGISTER_POINTER(substatus);
+	g_free(substatus);
+}
+
+static void
+free_saved_status(GaimSavedStatus *status)
+{
+	g_return_if_fail(status != NULL);
+
+	g_free(status->title);
+	g_free(status->message);
+
+	while (status->substatuses != NULL)
+	{
+		GaimSavedStatusSub *substatus = status->substatuses->data;
+		status->substatuses = g_list_remove(status->substatuses, substatus);
+		free_saved_status_sub(substatus);
+	}
+
+	GAIM_DBUS_UNREGISTER_POINTER(status);
+	g_free(status);
+}
+
+/*
+ * Set the timestamp for when this saved status was created, and
+ * make sure it is unique.
+ */
+static void
+set_creation_time(GaimSavedStatus *status, time_t creation_time)
+{
+	g_return_if_fail(status != NULL);
+
+	/* Avoid using 0 because it's an invalid hash key */
+	status->creation_time = creation_time != 0 ? creation_time : 1;
+
+	while (g_hash_table_lookup(creation_times, &status->creation_time) != NULL)
+		status->creation_time++;
+
+	g_hash_table_insert(creation_times,
+						&status->creation_time,
+						status);
+}
+
+/**
+ * A magic number is calcuated for each status, and then the
+ * statuses are ordered by the magic number.  The magic number
+ * is the date the status was last used offset by one day for
+ * each time the status has been used (but only by 10 days at
+ * the most).
+ *
+ * The goal is to have recently used statuses at the top of
+ * the list, but to also keep frequently used statuses near
+ * the top.
+ */
+static gint
+saved_statuses_sort_func(gconstpointer a, gconstpointer b)
+{
+	const GaimSavedStatus *saved_status_a = a;
+	const GaimSavedStatus *saved_status_b = b;
+	time_t time_a = saved_status_a->lastused +
+						(MIN(saved_status_a->usage_count, 10) * 86400);
+	time_t time_b = saved_status_b->lastused +
+						(MIN(saved_status_b->usage_count, 10) * 86400);
+	if (time_a > time_b)
+		return -1;
+	if (time_a < time_b)
+		return 1;
+	return 0;
+}
+
+/**
+ * Transient statuses are added and removed automatically by
+ * Gaim.  If they're not used for a certain length of time then
+ * they'll expire and be automatically removed.  This function
+ * does the expiration.
+ */
+static void
+remove_old_transient_statuses()
+{
+	GList *l, *next;
+	GaimSavedStatus *saved_status, *current_status;
+	int count;
+	time_t creation_time;
+
+	current_status = gaim_savedstatus_get_current();
+
+	/*
+	 * Iterate through the list of saved statuses.  Delete all
+	 * transient statuses except for the first MAX_TRANSIENTS
+	 * (remember, the saved statuses are already sorted by popularity).
+	 */
+	count = 0;
+	for (l = saved_statuses; l != NULL; l = next)
+	{
+		next = l->next;
+		saved_status = l->data;
+		if (gaim_savedstatus_is_transient(saved_status))
+		{
+			if (count == MAX_TRANSIENTS)
+			{
+				if (saved_status != current_status)
+				{
+					saved_statuses = g_list_remove(saved_statuses, saved_status);
+					creation_time = gaim_savedstatus_get_creation_time(saved_status);
+					g_hash_table_remove(creation_times, &creation_time);
+					free_saved_status(saved_status);
+				}
+			}
+			else
+				count++;
+		}
+	}
+
+	if (count == MAX_TRANSIENTS)
+		schedule_save();
+}
+
+/*********************************************************************
+ * Writing to disk                                                   *
+ *********************************************************************/
+
+static xmlnode *
+substatus_to_xmlnode(GaimSavedStatusSub *substatus)
+{
+	xmlnode *node, *child;
+
+	node = xmlnode_new("substatus");
+
+	child = xmlnode_new_child(node, "account");
+	xmlnode_set_attrib(child, "protocol", gaim_account_get_protocol_id(substatus->account));
+	xmlnode_insert_data(child, gaim_account_get_username(substatus->account), -1);
+
+	child = xmlnode_new_child(node, "state");
+	xmlnode_insert_data(child, gaim_status_type_get_id(substatus->type), -1);
+
+	if (substatus->message != NULL)
+	{
+		child = xmlnode_new_child(node, "message");
+		xmlnode_insert_data(child, substatus->message, -1);
+	}
+
+	return node;
+}
+
+static xmlnode *
+status_to_xmlnode(GaimSavedStatus *status)
+{
+	xmlnode *node, *child;
+	char buf[21];
+	GList *cur;
+
+	node = xmlnode_new("status");
+	if (status->title != NULL)
+	{
+		xmlnode_set_attrib(node, "name", status->title);
+	}
+	else
+	{
+		/*
+		 * Gaim 1.5.0 and earlier require a name to be set, so we
+		 * do this little hack to maintain backward compatability
+		 * in the status.xml file.  Eventually this should be removed
+		 * and we should determine if a status is transient by
+		 * whether the "name" attribute is set to something or if
+		 * it does not exist at all.
+		 */
+		xmlnode_set_attrib(node, "name", "Auto-Cached");
+		xmlnode_set_attrib(node, "transient", "true");
+	}
+
+	snprintf(buf, sizeof(buf), "%lu", status->creation_time);
+	xmlnode_set_attrib(node, "created", buf);
+
+	snprintf(buf, sizeof(buf), "%lu", status->lastused);
+	xmlnode_set_attrib(node, "lastused", buf);
+
+	snprintf(buf, sizeof(buf), "%u", status->usage_count);
+	xmlnode_set_attrib(node, "usage_count", buf);
+
+	child = xmlnode_new_child(node, "state");
+	xmlnode_insert_data(child, gaim_primitive_get_id_from_type(status->type), -1);
+
+	if (status->message != NULL)
+	{
+		child = xmlnode_new_child(node, "message");
+		xmlnode_insert_data(child, status->message, -1);
+	}
+
+	for (cur = status->substatuses; cur != NULL; cur = cur->next)
+	{
+		child = substatus_to_xmlnode(cur->data);
+		xmlnode_insert_child(node, child);
+	}
+
+	return node;
+}
+
+static xmlnode *
+statuses_to_xmlnode(void)
+{
+	xmlnode *node, *child;
+	GList *cur;
+
+	node = xmlnode_new("statuses");
+	xmlnode_set_attrib(node, "version", "1.0");
+
+	for (cur = saved_statuses; cur != NULL; cur = cur->next)
+	{
+		child = status_to_xmlnode(cur->data);
+		xmlnode_insert_child(node, child);
+	}
+
+	return node;
+}
+
+static void
+sync_statuses(void)
+{
+	xmlnode *node;
+	char *data;
+
+	if (!statuses_loaded)
+	{
+		gaim_debug_error("status", "Attempted to save statuses before they "
+						 "were read!\n");
+		return;
+	}
+
+	node = statuses_to_xmlnode();
+	data = xmlnode_to_formatted_str(node, NULL);
+	gaim_util_write_data_to_file("status.xml", data, -1);
+	g_free(data);
+	xmlnode_free(node);
+}
+
+static gboolean
+save_cb(gpointer data)
+{
+	sync_statuses();
+	save_timer = 0;
+	return FALSE;
+}
+
+static void
+schedule_save(void)
+{
+	if (save_timer == 0)
+		save_timer = gaim_timeout_add(5000, save_cb, NULL);
+}
+
+
+/*********************************************************************
+ * Reading from disk                                                 *
+ *********************************************************************/
+
+static GaimSavedStatusSub *
+parse_substatus(xmlnode *substatus)
+{
+	GaimSavedStatusSub *ret;
+	xmlnode *node;
+	char *data;
+
+	ret = g_new0(GaimSavedStatusSub, 1);
+	GAIM_DBUS_REGISTER_POINTER(ret, GaimSavedStatusSub);
+
+	/* Read the account */
+	node = xmlnode_get_child(substatus, "account");
+	if (node != NULL)
+	{
+		char *acct_name;
+		const char *protocol;
+		acct_name = xmlnode_get_data(node);
+		protocol = xmlnode_get_attrib(node, "protocol");
+		if ((acct_name != NULL) && (protocol != NULL))
+			ret->account = gaim_accounts_find(acct_name, protocol);
+		g_free(acct_name);
+	}
+
+	if (ret->account == NULL)
+	{
+		g_free(ret);
+		return NULL;
+	}
+
+	/* Read the state */
+	node = xmlnode_get_child(substatus, "state");
+	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
+	{
+		ret->type = gaim_status_type_find_with_id(
+							ret->account->status_types, data);
+		g_free(data);
+	}
+
+	/* Read the message */
+	node = xmlnode_get_child(substatus, "message");
+	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
+	{
+		ret->message = data;
+	}
+
+	return ret;
+}
+
+/**
+ * Parse a saved status and add it to the saved_statuses linked list.
+ *
+ * Here's an example of the XML for a saved status:
+ *   <status name="Girls">
+ *       <state>away</state>
+ *       <message>I like the way that they walk
+ *   And it's chill to hear them talk
+ *   And I can always make them smile
+ *   From White Castle to the Nile</message>
+ *       <substatus>
+ *           <account protocol='prpl-oscar'>markdoliner</account>
+ *           <state>available</state>
+ *           <message>The ladies man is here to answer your queries.</message>
+ *       </substatus>
+ *       <substatus>
+ *           <account protocol='prpl-oscar'>giantgraypanda</account>
+ *           <state>away</state>
+ *           <message>A.C. ain't in charge no more.</message>
+ *       </substatus>
+ *   </status>
+ *
+ * I know.  Moving, huh?
+ */
+static GaimSavedStatus *
+parse_status(xmlnode *status)
+{
+	GaimSavedStatus *ret;
+	xmlnode *node;
+	const char *attrib;
+	char *data;
+	int i;
+
+	ret = g_new0(GaimSavedStatus, 1);
+	GAIM_DBUS_REGISTER_POINTER(ret, GaimSavedStatus);
+
+	attrib = xmlnode_get_attrib(status, "transient");
+	if ((attrib == NULL) || (strcmp(attrib, "true")))
+	{
+		/* Read the title */
+		attrib = xmlnode_get_attrib(status, "name");
+		ret->title = g_strdup(attrib);
+	}
+
+	if (ret->title != NULL)
+	{
+		/* Ensure the title is unique */
+		i = 2;
+		while (gaim_savedstatus_find(ret->title) != NULL)
+		{
+			g_free(ret->title);
+			ret->title = g_strdup_printf("%s %d", attrib, i);
+			i++;
+		}
+	}
+
+	/* Read the creation time */
+	attrib = xmlnode_get_attrib(status, "created");
+	set_creation_time(ret, (attrib != NULL ? atol(attrib) : 0));
+
+	/* Read the last used time */
+	attrib = xmlnode_get_attrib(status, "lastused");
+	ret->lastused = (attrib != NULL ? atol(attrib) : 0);
+
+	/* Read the usage count */
+	attrib = xmlnode_get_attrib(status, "usage_count");
+	ret->usage_count = (attrib != NULL ? atol(attrib) : 0);
+
+	/* Read the primitive status type */
+	node = xmlnode_get_child(status, "state");
+	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
+	{
+		ret->type = gaim_primitive_get_type_from_id(data);
+		g_free(data);
+	}
+
+	/* Read the message */
+	node = xmlnode_get_child(status, "message");
+	if ((node != NULL) && ((data = xmlnode_get_data(node)) != NULL))
+	{
+		ret->message = data;
+	}
+
+	/* Read substatuses */
+	for (node = xmlnode_get_child(status, "substatus"); node != NULL;
+			node = xmlnode_get_next_twin(node))
+	{
+		GaimSavedStatusSub *new;
+		new = parse_substatus(node);
+		if (new != NULL)
+			ret->substatuses = g_list_prepend(ret->substatuses, new);
+	}
+
+	return ret;
+}
+
+/**
+ * Read the saved statuses from a file in the Gaim user dir.
+ *
+ * @return TRUE on success, FALSE on failure (if the file can not
+ *         be opened, or if it contains invalid XML).
+ */
+static void
+load_statuses(void)
+{
+	xmlnode *statuses, *status;
+
+	statuses_loaded = TRUE;
+
+	statuses = gaim_util_read_xml_from_file("status.xml", _("saved statuses"));
+
+	if (statuses == NULL)
+		return;
+
+	for (status = xmlnode_get_child(statuses, "status"); status != NULL;
+			status = xmlnode_get_next_twin(status))
+	{
+		GaimSavedStatus *new;
+		new = parse_status(status);
+		saved_statuses = g_list_prepend(saved_statuses, new);
+	}
+	saved_statuses = g_list_sort(saved_statuses, saved_statuses_sort_func);
+
+	xmlnode_free(statuses);
+}
+
+
+/**************************************************************************
+* Saved status API
+**************************************************************************/
+GaimSavedStatus *
+gaim_savedstatus_new(const char *title, GaimStatusPrimitive type)
+{
+	GaimSavedStatus *status;
+
+	/* Make sure we don't already have a saved status with this title. */
+	if (title != NULL)
+		g_return_val_if_fail(gaim_savedstatus_find(title) == NULL, NULL);
+
+	status = g_new0(GaimSavedStatus, 1);
+	GAIM_DBUS_REGISTER_POINTER(status, GaimSavedStatus);
+	status->title = g_strdup(title);
+	status->type = type;
+	set_creation_time(status, time(NULL));
+
+	saved_statuses = g_list_insert_sorted(saved_statuses, status, saved_statuses_sort_func);
+
+	schedule_save();
+
+	return status;
+}
+
+void
+gaim_savedstatus_set_title(GaimSavedStatus *status, const char *title)
+{
+	g_return_if_fail(status != NULL);
+
+	/* Make sure we don't already have a saved status with this title. */
+	g_return_if_fail(gaim_savedstatus_find(title) == NULL);
+
+	g_free(status->title);
+	status->title = g_strdup(title);
+
+	schedule_save();
+}
+
+void
+gaim_savedstatus_set_type(GaimSavedStatus *status, GaimStatusPrimitive type)
+{
+	g_return_if_fail(status != NULL);
+
+	status->type = type;
+
+	schedule_save();
+}
+
+void
+gaim_savedstatus_set_message(GaimSavedStatus *status, const char *message)
+{
+	g_return_if_fail(status != NULL);
+
+	g_free(status->message);
+	if ((message != NULL) && (*message == '\0'))
+		status->message = NULL;
+	else
+		status->message = g_strdup(message);
+
+	schedule_save();
+}
+
+void
+gaim_savedstatus_set_substatus(GaimSavedStatus *saved_status,
+							   const GaimAccount *account,
+							   const GaimStatusType *type,
+							   const char *message)
+{
+	GaimSavedStatusSub *substatus;
+
+	g_return_if_fail(saved_status != NULL);
+	g_return_if_fail(account      != NULL);
+	g_return_if_fail(type         != NULL);
+
+	/* Find an existing substatus or create a new one */
+	substatus = gaim_savedstatus_get_substatus(saved_status, account);
+	if (substatus == NULL)
+	{
+		substatus = g_new0(GaimSavedStatusSub, 1);
+		GAIM_DBUS_REGISTER_POINTER(substatus, GaimSavedStatusSub);
+		substatus->account = (GaimAccount *)account;
+		saved_status->substatuses = g_list_prepend(saved_status->substatuses, substatus);
+	}
+
+	substatus->type = type;
+	g_free(substatus->message);
+	substatus->message = g_strdup(message);
+
+	schedule_save();
+}
+
+void
+gaim_savedstatus_unset_substatus(GaimSavedStatus *saved_status,
+								 const GaimAccount *account)
+{
+	GList *iter;
+	GaimSavedStatusSub *substatus;
+
+	g_return_if_fail(saved_status != NULL);
+	g_return_if_fail(account      != NULL);
+
+	for (iter = saved_status->substatuses; iter != NULL; iter = iter->next)
+	{
+		substatus = iter->data;
+		if (substatus->account == account)
+		{
+			saved_status->substatuses = g_list_delete_link(saved_status->substatuses, iter);
+			g_free(substatus->message);
+			g_free(substatus);
+			return;
+		}
+	}
+}
+
+gboolean
+gaim_savedstatus_delete(const char *title)
+{
+	GaimSavedStatus *status;
+	time_t creation_time, current, idleaway;
+
+	status = gaim_savedstatus_find(title);
+
+	if (status == NULL)
+		return FALSE;
+
+	saved_statuses = g_list_remove(saved_statuses, status);
+	creation_time = gaim_savedstatus_get_creation_time(status);
+	g_hash_table_remove(creation_times, &creation_time);
+	free_saved_status(status);
+
+	schedule_save();
+
+	/*
+	 * If we just deleted our current status or our idleaway status,
+	 * then set the appropriate pref back to 0.
+	 */
+	current = gaim_prefs_get_int("/core/savedstatus/default");
+	if (current == creation_time)
+		gaim_prefs_set_int("/core/savedstatus/default", 0);
+
+	idleaway = gaim_prefs_get_int("/core/savedstatus/idleaway");
+	if (idleaway == creation_time)
+		gaim_prefs_set_int("/core/savedstatus/idleaway", 0);
+
+	return TRUE;
+}
+
+const GList *
+gaim_savedstatuses_get_all(void)
+{
+	return saved_statuses;
+}
+
+GList *
+gaim_savedstatuses_get_popular(unsigned int how_many)
+{
+	GList *popular = NULL;
+	GList *cur;
+	int i;
+	GaimSavedStatus *current, *next;
+
+	/* We don't want the current status to be in the GList */
+	current = gaim_savedstatus_get_current();
+
+	/* Copy 'how_many' elements to a new list */
+	i = 0;
+	cur = saved_statuses;
+	while ((i < how_many) && (cur != NULL))
+	{
+		next = cur->data;
+		if ((next != current) && (!gaim_savedstatus_is_transient(next)
+			|| gaim_savedstatus_get_message(next) != NULL))
+		{
+			popular = g_list_prepend(popular, cur->data);
+			i++;
+		}
+		cur = cur->next;
+	}
+
+	popular = g_list_reverse(popular);
+
+	return popular;
+}
+
+GaimSavedStatus *
+gaim_savedstatus_get_current(void)
+{
+	if (gaim_savedstatus_is_idleaway())
+		return gaim_savedstatus_get_idleaway();
+	else
+		return gaim_savedstatus_get_default();
+}
+
+GaimSavedStatus *
+gaim_savedstatus_get_default()
+{
+	int creation_time;
+	GaimSavedStatus *saved_status = NULL;
+
+	creation_time = gaim_prefs_get_int("/core/savedstatus/default");
+
+	if (creation_time != 0)
+		saved_status = g_hash_table_lookup(creation_times, &creation_time);
+
+	if (saved_status == NULL)
+	{
+		/*
+		 * We don't have a current saved status!  This is either a new
+		 * Gaim user or someone upgrading from Gaim 1.5.0 or older, or
+		 * possibly someone who deleted the status they were currently
+		 * using?  In any case, add a default status.
+		 */
+		saved_status = gaim_savedstatus_new(NULL, GAIM_STATUS_AVAILABLE);
+		gaim_prefs_set_int("/core/savedstatus/default",
+						   gaim_savedstatus_get_creation_time(saved_status));
+	}
+
+	return saved_status;
+}
+
+GaimSavedStatus *
+gaim_savedstatus_get_idleaway()
+{
+	int creation_time;
+	GaimSavedStatus *saved_status = NULL;
+
+	creation_time = gaim_prefs_get_int("/core/savedstatus/idleaway");
+
+	if (creation_time != 0)
+		saved_status = g_hash_table_lookup(creation_times, &creation_time);
+
+	if (saved_status == NULL)
+	{
+		/* We don't have a specified "idle" status!  Weird. */
+		saved_status = gaim_savedstatus_find_transient_by_type_and_message(
+				GAIM_STATUS_AWAY, DEFAULT_AUTOAWAY_MESSAGE);
+
+		if (saved_status == NULL)
+		{
+			saved_status = gaim_savedstatus_new(NULL, GAIM_STATUS_AWAY);
+			gaim_savedstatus_set_message(saved_status, DEFAULT_AUTOAWAY_MESSAGE);
+			gaim_prefs_set_int("/core/savedstatus/idleaway",
+							   gaim_savedstatus_get_creation_time(saved_status));
+		}
+	}
+
+	return saved_status;
+}
+
+gboolean
+gaim_savedstatus_is_idleaway()
+{
+	return gaim_prefs_get_bool("/core/savedstatus/isidleaway");
+}
+
+void
+gaim_savedstatus_set_idleaway(gboolean idleaway)
+{
+	GList *accounts, *node;
+	GaimSavedStatus *old, *saved_status;
+
+	if (gaim_savedstatus_is_idleaway() == idleaway)
+		/* Don't need to do anything */
+		return;
+
+	/* Changing our status makes us un-idle */
+	if (!idleaway)
+		gaim_idle_touch();
+
+	old = gaim_savedstatus_get_current();
+	gaim_prefs_set_bool("/core/savedstatus/isidleaway", idleaway);
+	saved_status = gaim_savedstatus_get_current();
+
+	accounts = gaim_accounts_get_all_active();
+	for (node = accounts; node != NULL; node = node->next)
+	{
+		GaimAccount *account;
+		GaimPresence *presence;
+		GaimStatus *status;
+
+		account = node->data;
+		presence = gaim_account_get_presence(account);
+		status = gaim_presence_get_active_status(presence);
+
+		if (!idleaway || gaim_status_is_available(status))
+			gaim_savedstatus_activate_for_account(saved_status, account);
+	}
+
+	g_list_free(accounts);
+
+	gaim_signal_emit(gaim_savedstatuses_get_handle(), "savedstatus-changed",
+					 saved_status, old);
+}
+
+GaimSavedStatus *
+gaim_savedstatus_get_startup()
+{
+	int creation_time;
+	GaimSavedStatus *saved_status = NULL;
+
+	creation_time = gaim_prefs_get_int("/core/savedstatus/startup");
+
+	if (creation_time != 0)
+		saved_status = g_hash_table_lookup(creation_times, &creation_time);
+
+	if (saved_status == NULL)
+	{
+		/*
+		 * We don't have a status to apply.
+		 * This may be the first login, or the user wants to
+		 * restore the "current" status.
+		 */
+		saved_status = gaim_savedstatus_get_current();
+	}
+
+	return saved_status;
+}
+
+
+GaimSavedStatus *
+gaim_savedstatus_find(const char *title)
+{
+	GList *iter;
+	GaimSavedStatus *status;
+
+	g_return_val_if_fail(title != NULL, NULL);
+
+	for (iter = saved_statuses; iter != NULL; iter = iter->next)
+	{
+		status = (GaimSavedStatus *)iter->data;
+		if ((status->title != NULL) && !strcmp(status->title, title))
+			return status;
+	}
+
+	return NULL;
+}
+
+GaimSavedStatus *
+gaim_savedstatus_find_by_creation_time(time_t creation_time)
+{
+	GList *iter;
+	GaimSavedStatus *status;
+
+	for (iter = saved_statuses; iter != NULL; iter = iter->next)
+	{
+		status = (GaimSavedStatus *)iter->data;
+		if (status->creation_time == creation_time)
+			return status;
+	}
+
+	return NULL;
+}
+
+GaimSavedStatus *
+gaim_savedstatus_find_transient_by_type_and_message(GaimStatusPrimitive type,
+													const char *message)
+{
+	GList *iter;
+	GaimSavedStatus *status;
+
+	for (iter = saved_statuses; iter != NULL; iter = iter->next)
+	{
+		status = (GaimSavedStatus *)iter->data;
+		if ((status->type == type) && gaim_savedstatus_is_transient(status) &&
+			(((status->message == NULL) && (message == NULL)) ||
+			((status->message != NULL) && (message != NULL) && !strcmp(status->message, message))))
+		{
+			return status;
+		}
+	}
+
+	return NULL;
+}
+
+gboolean
+gaim_savedstatus_is_transient(const GaimSavedStatus *saved_status)
+{
+	g_return_val_if_fail(saved_status != NULL, TRUE);
+
+	return (saved_status->title == NULL);
+}
+
+const char *
+gaim_savedstatus_get_title(const GaimSavedStatus *saved_status)
+{
+	const char *message;
+
+	g_return_val_if_fail(saved_status != NULL, NULL);
+
+	/* If we have a title then return it */
+	if (saved_status->title != NULL)
+		return saved_status->title;
+
+	/* Otherwise, this is a transient status and we make up a title on the fly */
+	message = gaim_savedstatus_get_message(saved_status);
+
+	if ((message == NULL) || (*message == '\0'))
+	{
+		GaimStatusPrimitive primitive;
+		primitive = gaim_savedstatus_get_type(saved_status);
+		return gaim_primitive_get_name_from_type(primitive);
+	}
+	else
+	{
+		char *stripped;
+		static char buf[64];
+		stripped = gaim_markup_strip_html(message);
+		gaim_util_chrreplace(stripped, '\n', ' ');
+		strncpy(buf, stripped, sizeof(buf));
+		buf[sizeof(buf) - 1] = '\0';
+		if ((strlen(stripped) + 1) > sizeof(buf))
+		{
+			/* Truncate and ellipsize */
+			char *tmp = g_utf8_find_prev_char(buf, &buf[sizeof(buf) - 4]);
+			strcpy(tmp, "...");
+		}
+		g_free(stripped);
+		return buf;
+	}
+}
+
+GaimStatusPrimitive
+gaim_savedstatus_get_type(const GaimSavedStatus *saved_status)
+{
+	g_return_val_if_fail(saved_status != NULL, GAIM_STATUS_OFFLINE);
+
+	return saved_status->type;
+}
+
+const char *
+gaim_savedstatus_get_message(const GaimSavedStatus *saved_status)
+{
+	g_return_val_if_fail(saved_status != NULL, NULL);
+
+	return saved_status->message;
+}
+
+time_t
+gaim_savedstatus_get_creation_time(const GaimSavedStatus *saved_status)
+{
+	g_return_val_if_fail(saved_status != NULL, 0);
+
+	return saved_status->creation_time;
+}
+
+gboolean
+gaim_savedstatus_has_substatuses(const GaimSavedStatus *saved_status)
+{
+	g_return_val_if_fail(saved_status != NULL, FALSE);
+
+	return (saved_status->substatuses != NULL);
+}
+
+GaimSavedStatusSub *
+gaim_savedstatus_get_substatus(const GaimSavedStatus *saved_status,
+							   const GaimAccount *account)
+{
+	GList *iter;
+	GaimSavedStatusSub *substatus;
+
+	g_return_val_if_fail(saved_status != NULL, NULL);
+	g_return_val_if_fail(account      != NULL, NULL);
+
+	for (iter = saved_status->substatuses; iter != NULL; iter = iter->next)
+	{
+		substatus = iter->data;
+		if (substatus->account == account)
+			return substatus;
+	}
+
+	return NULL;
+}
+
+const GaimStatusType *
+gaim_savedstatus_substatus_get_type(const GaimSavedStatusSub *substatus)
+{
+	g_return_val_if_fail(substatus != NULL, NULL);
+
+	return substatus->type;
+}
+
+const char *
+gaim_savedstatus_substatus_get_message(const GaimSavedStatusSub *substatus)
+{
+	g_return_val_if_fail(substatus != NULL, NULL);
+
+	return substatus->message;
+}
+
+void
+gaim_savedstatus_activate(GaimSavedStatus *saved_status)
+{
+	GList *accounts, *node;
+	GaimSavedStatus *old = gaim_savedstatus_get_current();
+
+	g_return_if_fail(saved_status != NULL);
+
+	/* Make sure our list of saved statuses remains sorted */
+	saved_status->lastused = time(NULL);
+	saved_status->usage_count++;
+	saved_statuses = g_list_remove(saved_statuses, saved_status);
+	saved_statuses = g_list_insert_sorted(saved_statuses, saved_status, saved_statuses_sort_func);
+
+	accounts = gaim_accounts_get_all_active();
+	for (node = accounts; node != NULL; node = node->next)
+	{
+		GaimAccount *account;
+
+		account = node->data;
+
+		gaim_savedstatus_activate_for_account(saved_status, account);
+	}
+
+	g_list_free(accounts);
+
+	gaim_prefs_set_int("/core/savedstatus/default",
+					   gaim_savedstatus_get_creation_time(saved_status));
+	gaim_savedstatus_set_idleaway(FALSE);
+
+	gaim_signal_emit(gaim_savedstatuses_get_handle(), "savedstatus-changed",
+					 saved_status, old);
+}
+
+void
+gaim_savedstatus_activate_for_account(const GaimSavedStatus *saved_status,
+									  GaimAccount *account)
+{
+	const GaimStatusType *status_type;
+	const GaimSavedStatusSub *substatus;
+	const char *message = NULL;
+
+	g_return_if_fail(saved_status != NULL);
+	g_return_if_fail(account != NULL);
+
+	substatus = gaim_savedstatus_get_substatus(saved_status, account);
+	if (substatus != NULL)
+	{
+		status_type = substatus->type;
+		message = substatus->message;
+	}
+	else
+	{
+		status_type = gaim_account_get_status_type_with_primitive(account, saved_status->type);
+		if (status_type == NULL)
+			return;
+		message = saved_status->message;
+	}
+
+	if ((message != NULL) &&
+		(gaim_status_type_get_attr(status_type, "message")))
+	{
+		gaim_account_set_status(account, gaim_status_type_get_id(status_type),
+								TRUE, "message", message, NULL);
+	}
+	else
+	{
+		gaim_account_set_status(account, gaim_status_type_get_id(status_type),
+								TRUE, NULL);
+	}
+}
+
+void *
+gaim_savedstatuses_get_handle(void)
+{
+	static int handle;
+
+	return &handle;
+}
+
+void
+gaim_savedstatuses_init(void)
+{
+	void *handle = gaim_savedstatuses_get_handle();
+
+	creation_times = g_hash_table_new(g_int_hash, g_int_equal);
+
+	/*
+	 * Using 0 as the creation_time is a special case.
+	 * If someone calls gaim_savedstatus_get_current() or
+	 * gaim_savedstatus_get_idleaway() and either of those functions
+	 * sees a creation_time of 0, then it will create a default
+	 * saved status and return that to the user.
+	 */
+	gaim_prefs_add_none("/core/savedstatus");
+	gaim_prefs_add_int("/core/savedstatus/default", 0);
+	gaim_prefs_add_int("/core/savedstatus/startup", 0);
+	gaim_prefs_add_bool("/core/savedstatus/startup_current_status", TRUE);
+	gaim_prefs_add_int("/core/savedstatus/idleaway", 0);
+	gaim_prefs_add_bool("/core/savedstatus/isidleaway", FALSE);
+
+	load_statuses();
+
+	gaim_signal_register(handle, "savedstatus-changed",
+					 gaim_marshal_VOID__POINTER_POINTER, NULL, 2,
+					 gaim_value_new(GAIM_TYPE_SUBTYPE,
+									GAIM_SUBTYPE_SAVEDSTATUS),
+					 gaim_value_new(GAIM_TYPE_SUBTYPE,
+									GAIM_SUBTYPE_SAVEDSTATUS));
+}
+
+void
+gaim_savedstatuses_uninit(void)
+{
+	remove_old_transient_statuses();
+
+	if (save_timer != 0)
+	{
+		gaim_timeout_remove(save_timer);
+		save_timer = 0;
+		sync_statuses();
+	}
+
+	while (saved_statuses != NULL) {
+		GaimSavedStatus *saved_status = saved_statuses->data;
+		saved_statuses = g_list_remove(saved_statuses, saved_status);
+		free_saved_status(saved_status);
+	}
+
+	g_hash_table_destroy(creation_times);
+
+	gaim_signals_unregister_by_instance(gaim_savedstatuses_get_handle());
+}
+