--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/savedstatuses.c Sun Dec 26 00:46:26 2004 +0000 @@ -0,0 +1,569 @@ +/** + * @file savedstatus.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 "notify.h" +#include "savedstatuses.h" +#include "status.h" +#include "util.h" +#include "xmlnode.h" + +/** + * The information of 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 _GaimStatusSaved +{ + char *title; + GaimStatusPrimitive type; + char *message; + + GList *substatuses; /**< A list of GaimStatusSavedSub's. */ +}; + +/* + * TODO: If an account is deleted, need to also delete any associated + * GaimStatusSavedSub's. + * TODO: If a GaimStatusType is deleted, need to also delete any + * associated GaimStatusSavedSub's? + */ +struct _GaimStatusSavedSub +{ + GaimAccount *account; + const GaimStatusType *type; + char *message; +}; + +static GList *saved_statuses = NULL; +gboolean have_read_saved_statuses = FALSE; +static guint statuses_save_timer = 0; + +/************************************************************************** +* Helper functions +**************************************************************************/ + +/** + * Elements of this array correspond to the GaimStatusPrimitive + * enumeration. + */ +static const char *primitive_names[] = +{ + "unset", + "offline", + "available", + "unavailable", + "hidden", + "away", + "extended_away" +}; + +static GaimStatusPrimitive +gaim_primitive_get_type(const char *name) +{ + int i; + + g_return_val_if_fail(name != NULL, GAIM_STATUS_UNSET); + + for (i = 0; i < GAIM_STATUS_NUM_PRIMITIVES; i++) + { + if (!strcmp(name, primitive_names[i])) + return i; + } + + return GAIM_STATUS_UNSET; +} + +static void +free_statussavedsub(GaimStatusSavedSub *substatus) +{ + g_return_if_fail(substatus != NULL); + + g_free(substatus->message); + g_free(substatus); +} + +static void +free_statussaved(GaimStatusSaved *status) +{ + g_return_if_fail(status != NULL); + + g_free(status->title); + g_free(status->message); + + while (status->substatuses != NULL) + { + GaimStatusSavedSub *substatus = status->substatuses->data; + status->substatuses = g_list_remove(status->substatuses, substatus); + free_statussavedsub(substatus); + } + + g_free(status); +} + + +/************************************************************************** +* Saved status writting to disk +**************************************************************************/ + +static xmlnode * +substatus_to_xmlnode(GaimStatusSavedSub *substatus) +{ + xmlnode *node, *child; + + node = xmlnode_new("substatus"); + + child = xmlnode_new("account"); + xmlnode_set_attrib(node, "protocol", + gaim_account_get_protocol_id(substatus->account)); + xmlnode_insert_data(child, + gaim_account_get_username(substatus->account), -1); + xmlnode_insert_child(node, child); + + child = xmlnode_new("state"); + xmlnode_insert_data(child, gaim_status_type_get_id(substatus->type), -1); + xmlnode_insert_child(node, child); + + if (substatus->message != NULL) + { + child = xmlnode_new("message"); + xmlnode_insert_data(child, substatus->message, -1); + xmlnode_insert_child(node, child); + } + + return node; +} + +static xmlnode * +status_to_xmlnode(GaimStatusSaved *status) +{ + xmlnode *node, *child; + GList *cur; + + node = xmlnode_new("status"); + xmlnode_set_attrib(node, "name", status->title); + + child = xmlnode_new("state"); + xmlnode_insert_data(child, primitive_names[status->type], -1); + xmlnode_insert_child(node, child); + + child = xmlnode_new("message"); + xmlnode_insert_data(child, status->message, -1); + xmlnode_insert_child(node, child); + + 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"); + + 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 *statuses; + char *data; + + if (!have_read_saved_statuses) { + gaim_debug_error("status", "Attempted to save statuses before they " + "were read!\n"); + return; + } + + statuses = statuses_to_xmlnode(); + data = xmlnode_to_formatted_str(statuses, NULL); + gaim_util_write_data_to_file("status.xml", data, -1); + g_free(data); + xmlnode_free(statuses); +} + +static gboolean +save_callback(gpointer data) +{ + sync_statuses(); + statuses_save_timer = 0; + return FALSE; +} + +static void +schedule_save(void) +{ + if (statuses_save_timer != 0) + gaim_timeout_remove(statuses_save_timer); + statuses_save_timer = gaim_timeout_add(1000, save_callback, NULL); +} + + +/************************************************************************** +* Saved status reading from disk +**************************************************************************/ +static GaimStatusSavedSub * +parse_substatus(xmlnode *substatus) +{ + GaimStatusSavedSub *ret; + xmlnode *node; + char *data = NULL; + + ret = g_new0(GaimStatusSavedSub, 1); + + /* 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); + if (data != NULL) { + ret->type = gaim_status_type_find_with_id( + ret->account->status_types, data); + g_free(data); + data = NULL; + } + + /* Read the message */ + node = xmlnode_get_child(substatus, "message"); + if (node != NULL) + data = xmlnode_get_data(node); + if (data != 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 GaimStatusSaved * +parse_status(xmlnode *status) +{ + GaimStatusSaved *ret; + xmlnode *node; + const char *attrib; + char *data = NULL; + int i; + + ret = g_new0(GaimStatusSaved, 1); + + /* Read the title */ + attrib = xmlnode_get_attrib(status, "name"); + if (attrib == NULL) + attrib = "No Title"; + /* Ensure the title is unique */ + ret->title = g_strdup(attrib); + i = 2; + while (gaim_savedstatuses_find(ret->title) != NULL) + { + g_free(ret->title); + ret->title = g_strdup_printf("%s %d", attrib, i); + i++; + } + + /* Read the primitive status type */ + node = xmlnode_get_child(status, "state"); + if (node != NULL) + data = xmlnode_get_data(node); + if (data != NULL) { + ret->type = gaim_primitive_get_type(data); + g_free(data); + data = NULL; + } + + /* Read the message */ + node = xmlnode_get_child(status, "message"); + if (node != NULL) + data = xmlnode_get_data(node); + if (data != NULL) + ret->message = data; + + /* Read substatuses */ + for (node = xmlnode_get_child(status, "status"); node != NULL; + node = xmlnode_get_next_twin(node)) + { + GaimStatusSavedSub *new; + new = parse_substatus(node); + if (new != NULL) + ret->substatuses = g_list_append(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 gboolean +read_statuses(const char *filename) +{ + GError *error; + gchar *contents = NULL; + gsize length; + xmlnode *statuses, *status; + + gaim_debug_info("status", "Reading %s\n", filename); + + if (!g_file_get_contents(filename, &contents, &length, &error)) + { + gaim_debug_error("status", "Error reading statuses: %s\n", + error->message); + g_error_free(error); + return FALSE; + } + + statuses = xmlnode_from_str(contents, length); + + if (statuses == NULL) + { + FILE *backup; + gchar *name; + gaim_debug_error("status", "Error parsing statuses\n"); + name = g_strdup_printf("%s~", filename); + if ((backup = fopen(name, "w"))) + { + fwrite(contents, length, 1, backup); + fclose(backup); + chmod(name, S_IRUSR | S_IWUSR); + } + else + { + gaim_debug_error("status", "Unable to write backup %s\n", name); + } + g_free(name); + g_free(contents); + return FALSE; + } + + g_free(contents); + + for (status = xmlnode_get_child(statuses, "status"); status != NULL; + status = xmlnode_get_next_twin(status)) + { + GaimStatusSaved *new; + new = parse_status(status); + saved_statuses = g_list_append(saved_statuses, new); + } + + gaim_debug_info("status", "Finished reading statuses\n"); + + xmlnode_free(statuses); + + return TRUE; +} + +static void +load_statuses(void) +{ + const char *user_dir = gaim_user_dir(); + gchar *filename; + gchar *msg; + + g_return_if_fail(user_dir != NULL); + + have_read_saved_statuses = TRUE; + + filename = g_build_filename(user_dir, "status.xml", NULL); + + if (g_file_test(filename, G_FILE_TEST_EXISTS)) + { + if (!read_statuses(filename)) + { + msg = g_strdup_printf(_("An error was encountered parsing the " + "file containing your saved statuses (%s). They " + "have not been loaded, and the old file has been " + "renamed to status.xml~."), filename); + gaim_notify_error(NULL, NULL, _("Saved Statuses Error"), msg); + g_free(msg); + } + } + + g_free(filename); +} + + +/************************************************************************** +* Saved status API +**************************************************************************/ +GaimStatusSaved * +gaim_savedstatuses_new(const char *title, GaimStatusPrimitive type) +{ + GaimStatusSaved *status; + + status = g_new0(GaimStatusSaved, 1); + status->title = g_strdup(title); + status->type = type; + + saved_statuses = g_list_append(saved_statuses, status); + + schedule_save(); + + return status; +} + +gboolean +gaim_savedstatuses_delete(const char *title) +{ + GaimStatusSaved *status; + + status = gaim_savedstatuses_find(title); + + if (status == NULL) + return FALSE; + + saved_statuses = g_list_remove(saved_statuses, status); + free_statussaved(status); + + schedule_save(); + + return TRUE; +} + +const GList * +gaim_savedstatuses_get_all(void) +{ + return saved_statuses; +} + +GaimStatusSaved * +gaim_savedstatuses_find(const char *title) +{ + GList *l; + GaimStatusSaved *status; + + for (l = saved_statuses; l != NULL; l = g_list_next(l)) + { + status = (GaimStatusSaved *)l->data; + if (!strcmp(status->title, title)) + return status; + } + + return NULL; +} + +const char * +gaim_savedstatuses_get_title(const GaimStatusSaved *saved_status) +{ + return saved_status->title; +} + +GaimStatusPrimitive +gaim_savedstatuses_get_type(const GaimStatusSaved *saved_status) +{ + return saved_status->type; +} + +const char * +gaim_savedstatuses_get_message(const GaimStatusSaved *saved_status) +{ + return saved_status->message; +} + +void +gaim_savedstatuses_init(void) +{ + load_statuses(); +} + +void +gaim_savedstatuses_uninit(void) +{ + if (statuses_save_timer != 0) + { + gaim_timeout_remove(statuses_save_timer); + statuses_save_timer = 0; + sync_statuses(); + } + + while (saved_statuses != NULL) { + GaimStatusSaved *status = saved_statuses->data; + saved_statuses = g_list_remove(saved_statuses, status); + free_statussaved(status); + } +}