view libpurple/smiley.c @ 22317:2ab289f312d0

Hide PurpleSmiley internals.
author Sadrul Habib Chowdhury <imadil@gmail.com>
date Mon, 25 Feb 2008 02:43:06 +0000
parents 76fa93017695
children d1bcb54920f8
line wrap: on
line source

/**
 * @file smiley.c Simley API
 * @ingroup core
 */

/* purple
 *
 * Purple 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., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 */

#include "internal.h"
#include "xmlnode.h"
#include "debug.h"
#include "imgstore.h"
#include "smiley.h"
#include "util.h"


/**************************************************************************/
/* Main structures, members and constants                                 */
/**************************************************************************/

struct _PurpleSmiley
{
        PurpleStoredImage *img;        /**< The id of the stored image with the
                                        the smiley data.        */
        char *shortcut;                /**< Shortcut associated with the custom
                                        smiley. This field will work as a
                                        unique key by this API. */
        char *checksum;                /**< The smiley checksum.        */
};

static GHashTable *smiley_data_index = NULL;
static GHashTable *smiley_shortcut_index = NULL;
static GHashTable *smiley_checksum_index = NULL;

static guint save_timer = 0;
static gboolean smileys_loaded = FALSE;
static char *smileys_dir = NULL;

#define SMILEYS_DEFAULT_FOLDER			"custom_smiley"
#define SMILEYS_LOG_ID				"smileys"

#define XML_FILE_NAME				"smileys.xml"

#define XML_ROOT_TAG				"smileys"
#define XML_PROFILE_TAG			"profile"
#define XML_PROFILE_NAME_ATTRIB_TAG		"name"
#define XML_ACCOUNT_TAG			"account"
#define XML_ACCOUNT_USERID_ATTRIB_TAG		"userid"
#define XML_SMILEY_SET_TAG			"smiley_set"
#define XML_SMILEY_TAG				"smiley"
#define XML_SHORTCUT_ATTRIB_TAG		"shortcut"
#define XML_CHECKSUM_ATRIB_TAG			"checksum"
#define XML_FILENAME_ATRIB_TAG			"filename"


/******************************************************************************
 * XML descriptor file layout                                                 *
 ******************************************************************************
 *
 * Althought we are creating the profile XML structure here, now we
 * won't handle it.
 * So, we just add one profile named "default" that has no associated
 * account elements, and have only the smiley_set that will contain
 * all existent custom smiley.
 *
 * It's our "Highlander Profile" :-)
 *
 ******************************************************************************
 *
 * <smileys>
 *   <profile name="john.doe">
 *     <account userid="john.doe@jabber.org">
 *     <account userid="john.doe@gmail.com">
 *     <smiley_set>
 *       <smiley shortcut="aaa" checksum="xxxxxxxx" filename="file_name1.gif"/>
 *       <smiley shortcut="bbb" checksum="yyyyyyy" filename="file_name2.gif"/>
 *     </smiley_set>
 *   </profile>
 * </smiley>
 *
 *****************************************************************************/


/*********************************************************************
 * Forward declarations                                              *
 *********************************************************************/

static gboolean read_smiley_file(const char *path, guchar **data, size_t *len);

static char *get_file_full_path(const char *filename);

static PurpleSmiley *purple_smiley_create(const char *shortcut);

static PurpleSmiley *purple_smiley_load_file(const char *shortcut, const char *checksum,
		const char *filename);

static void
purple_smiley_set_data_impl(PurpleSmiley *smiley, guchar *smiley_data,
		size_t smiley_data_len, const char *filename);


/*********************************************************************
 * Writing to disk                                                   *
 *********************************************************************/

static xmlnode *
smiley_to_xmlnode(PurpleSmiley *smiley)
{
	xmlnode *smiley_node = NULL;

	smiley_node = xmlnode_new(XML_SMILEY_TAG);

	if (!smiley_node)
		return NULL;

	xmlnode_set_attrib(smiley_node, XML_SHORTCUT_ATTRIB_TAG,
			smiley->shortcut);

	xmlnode_set_attrib(smiley_node, XML_CHECKSUM_ATRIB_TAG,
			smiley->checksum);

	xmlnode_set_attrib(smiley_node, XML_FILENAME_ATRIB_TAG,
			purple_imgstore_get_filename(smiley->img));

	return smiley_node;
}

static void
add_smiley_to_main_node(gpointer key, gpointer value, gpointer user_data)
{
	xmlnode *child_node;

	child_node = smiley_to_xmlnode(value);
	xmlnode_insert_child((xmlnode*)user_data, child_node);
}

static xmlnode *
smileys_to_xmlnode()
{
	xmlnode *root_node, *profile_node, *smileyset_node;

	root_node = xmlnode_new(XML_ROOT_TAG);
	xmlnode_set_attrib(root_node, "version", "1.0");

	/* See the top comment's above to understand why initial tag elements
	 * are not being considered by now. */
	profile_node = xmlnode_new(XML_PROFILE_TAG);
	if (profile_node) {
		xmlnode_set_attrib(profile_node, XML_PROFILE_NAME_ATTRIB_TAG, "Default");
		xmlnode_insert_child(root_node, profile_node);

		smileyset_node = xmlnode_new(XML_SMILEY_SET_TAG);
		if (smileyset_node) {
			xmlnode_insert_child(profile_node, smileyset_node);
			g_hash_table_foreach(smiley_data_index, add_smiley_to_main_node, smileyset_node);
		}
	}

	return root_node;
}

static void
sync_smileys()
{
	xmlnode *root_node;
	char *data;

	if (!smileys_loaded) {
		purple_debug_error(SMILEYS_LOG_ID, "Attempted to save smileys before it "
						 "was read!\n");
		return;
	}

	root_node = smileys_to_xmlnode();
	data = xmlnode_to_formatted_str(root_node, NULL);
	purple_util_write_data_to_file(XML_FILE_NAME, data, -1);

	g_free(data);
	xmlnode_free(root_node);
}

static gboolean
save_smileys_cb(gpointer data)
{
	sync_smileys();
	save_timer = 0;
	return FALSE;
}

static void
purple_smileys_save()
{
	if (save_timer == 0)
		save_timer = purple_timeout_add_seconds(5, save_smileys_cb, NULL);
}


/*********************************************************************
 * Reading from disk                                                 *
 *********************************************************************/

static PurpleSmiley *
parse_smiley(xmlnode *smiley_node)
{
	PurpleSmiley *smiley;
	const char *shortcut = NULL;
	const char *checksum = NULL;
	const char *filename = NULL;

	shortcut = xmlnode_get_attrib(smiley_node, XML_SHORTCUT_ATTRIB_TAG);
	checksum = xmlnode_get_attrib(smiley_node, XML_CHECKSUM_ATRIB_TAG);
	filename = xmlnode_get_attrib(smiley_node, XML_FILENAME_ATRIB_TAG);

	if ((shortcut == NULL) || (checksum == NULL) || (filename == NULL))
		return NULL;

	smiley = purple_smiley_load_file(shortcut, checksum, filename);

	return smiley;
}

static void
purple_smileys_load()
{
	xmlnode *root_node, *profile_node;
	xmlnode *smileyset_node = NULL;
	xmlnode *smiley_node;

	smileys_loaded = TRUE;

	root_node = purple_util_read_xml_from_file(XML_FILE_NAME,
			_(SMILEYS_LOG_ID));

	if (root_node == NULL)
		return;

	/* See the top comment's above to understand why initial tag elements
	 * are not being considered by now. */
	profile_node = xmlnode_get_child(root_node, XML_PROFILE_TAG);
	if (profile_node)
		smileyset_node = xmlnode_get_child(profile_node, XML_SMILEY_SET_TAG);

	if (smileyset_node) {
		smiley_node = xmlnode_get_child(smileyset_node, XML_SMILEY_TAG);
		for (; smiley_node != NULL;
				smiley_node = xmlnode_get_next_twin(smiley_node)) {
			PurpleSmiley *smiley;

			smiley = parse_smiley(smiley_node);

			purple_smileys_add(smiley);
		}
	}

	xmlnode_free(root_node);
}


/*********************************************************************
 * Stuff                                                             *
 *********************************************************************/

static char *get_file_full_path(const char *filename)
{
	char *path;

	path = g_build_filename(purple_smileys_get_storing_dir(), filename, NULL);

	if (!g_file_test(path, G_FILE_TEST_EXISTS)) {
		g_free(path);
		return NULL;
	}

	return path;
}

static PurpleSmiley *
purple_smiley_load_file(const char *shortcut, const char *checksum, const char *filename)
{
	PurpleSmiley *smiley = NULL;
	guchar *smiley_data;
	size_t smiley_data_len;
	char *fullpath = NULL;

	g_return_val_if_fail(shortcut  != NULL, NULL);
	g_return_val_if_fail(checksum  != NULL, NULL);
	g_return_val_if_fail(filename != NULL, NULL);

	fullpath = get_file_full_path(filename);
	if (!fullpath)
		return NULL;

	smiley = purple_smiley_create(shortcut);
	if (!smiley) {
		g_free(fullpath);
		return NULL;
	}

	smiley->checksum = g_strdup(checksum);

	if (read_smiley_file(fullpath, &smiley_data, &smiley_data_len))
		purple_smiley_set_data_impl(smiley, smiley_data,
				smiley_data_len, filename);
	else
		purple_smiley_delete(smiley);

	g_free(fullpath);

	return smiley;
}

static void
purple_smiley_data_store(PurpleStoredImage *stored_img)
{
	const char *dirname;
	char *path;
	FILE *file = NULL;

	g_return_if_fail(stored_img != NULL);

	if (!smileys_loaded)
		return;

	dirname  = purple_smileys_get_storing_dir();
	path = g_build_filename(dirname, purple_imgstore_get_filename(stored_img), NULL);

	if (!g_file_test(dirname, G_FILE_TEST_IS_DIR)) {
		purple_debug_info(SMILEYS_LOG_ID, "Creating smileys directory.\n");

		if (g_mkdir(dirname, S_IRUSR | S_IWUSR | S_IXUSR) < 0) {
			purple_debug_error(SMILEYS_LOG_ID,
			                   "Unable to create directory %s: %s\n",
			                   dirname, g_strerror(errno));
		}
	}

	if ((file = g_fopen(path, "wb")) != NULL) {
		if (!fwrite(purple_imgstore_get_data(stored_img),
				purple_imgstore_get_size(stored_img), 1, file)) {
			purple_debug_error(SMILEYS_LOG_ID, "Error writing %s: %s\n",
			                   path, g_strerror(errno));
		} else {
			purple_debug_info(SMILEYS_LOG_ID, "Wrote cache file: %s\n", path);
		}

		fclose(file);
	} else {
		purple_debug_error(SMILEYS_LOG_ID, "Unable to create file %s: %s\n",
		                   path, g_strerror(errno));
		g_free(path);

		return;
	}

	g_free(path);
}

static void
purple_smiley_data_unstore(const char *filename)
{
	const char *dirname;
	char *path;

	g_return_if_fail(filename != NULL);

	dirname  = purple_smileys_get_storing_dir();
	path = g_build_filename(dirname, filename, NULL);

	if (g_file_test(path, G_FILE_TEST_EXISTS)) {
		if (g_unlink(path))
			purple_debug_error(SMILEYS_LOG_ID, "Failed to delete %s: %s\n",
			                   path, g_strerror(errno));
		else
			purple_debug_info(SMILEYS_LOG_ID, "Deleted cache file: %s\n", path);
	}

	g_free(path);
}

static gboolean
read_smiley_file(const char *path, guchar **data, size_t *len)
{
	GError *err = NULL;

	if (!g_file_get_contents(path, (gchar **)data, len, &err)) {
		purple_debug_error(SMILEYS_LOG_ID, "Error reading %s: %s\n",
				path, err->message);
		g_error_free(err);

		return FALSE;
	}

	return TRUE;
}

static PurpleStoredImage *
purple_smiley_data_new(guchar *smiley_data, size_t smiley_data_len)
{
	char *filename;
	PurpleStoredImage *stored_img;

	g_return_val_if_fail(smiley_data != NULL,   NULL);
	g_return_val_if_fail(smiley_data_len  > 0,  NULL);

	filename = purple_util_get_image_filename(smiley_data, smiley_data_len);

	if (filename == NULL) {
		g_free(smiley_data);
		return NULL;
	}

	stored_img = purple_imgstore_add(smiley_data, smiley_data_len, filename);

	g_free(filename);

	return stored_img;
}

static void
purple_smiley_set_data_impl(PurpleSmiley *smiley, guchar *smiley_data,
				size_t smiley_data_len, const char *filename)
{
	PurpleStoredImage *old_img;
	const char *old_filename = NULL;
	const char *new_filename = NULL;

	g_return_if_fail(smiley     != NULL);
	g_return_if_fail(smiley_data != NULL);
	g_return_if_fail(smiley_data_len > 0);

	old_img = smiley->img;
	smiley->img = NULL;

	if (filename)
		smiley->img = purple_imgstore_add(smiley_data,
				smiley_data_len, filename);
	else
		smiley->img = purple_smiley_data_new(smiley_data, smiley_data_len);

	g_free(smiley->checksum);
	smiley->checksum = purple_util_get_image_checksum(
			smiley_data, smiley_data_len);

	if (!old_img)
		return;

	/* If the old and new image files have different names we need
	 * to unstore old image file. */
	old_filename = purple_imgstore_get_filename(old_img);
	new_filename = purple_imgstore_get_filename(smiley->img);

	if (g_ascii_strcasecmp(old_filename, new_filename)) {
		purple_smiley_data_unstore(old_filename);
		purple_imgstore_unref(old_img);
	}
}


/*****************************************************************************
 * Public API functions                                                      *
 *****************************************************************************/

static PurpleSmiley *
purple_smiley_create(const char *shortcut)
{
	PurpleSmiley *smiley;

	smiley = g_slice_new0(PurpleSmiley);
	if (!smiley)
		return NULL;

	smiley->shortcut = g_strdup(shortcut);

	return smiley;
}

static void
purple_smiley_destroy(PurpleSmiley *smiley)
{
	g_return_if_fail(smiley != NULL);

	g_free(smiley->shortcut);
	g_free(smiley->checksum);
	purple_imgstore_unref(smiley->img);

	g_slice_free(PurpleSmiley, smiley);
}

PurpleSmiley *
purple_smiley_new(PurpleStoredImage *img, const char *shortcut)
{
	PurpleSmiley *smiley = NULL;

	g_return_val_if_fail(shortcut  != NULL, NULL);
	g_return_val_if_fail(img       != NULL, NULL);

	smiley = purple_smileys_find_by_shortcut(shortcut);
	if (smiley)
		return smiley;

	smiley = purple_smiley_create(shortcut);
	if (!smiley)
		return NULL;

	smiley->checksum = purple_util_get_image_checksum(
				purple_imgstore_get_data(img),
				purple_imgstore_get_size(img));

	smiley->img = img;
	purple_smiley_data_store(img);

	return smiley;
}

PurpleSmiley *
purple_smiley_new_from_stream(const char *shortcut, guchar *smiley_data,
			size_t smiley_data_len, const char *filename)
{
	PurpleSmiley *smiley;

	g_return_val_if_fail(shortcut  != NULL,    NULL);
	g_return_val_if_fail(smiley_data != NULL,  NULL);
	g_return_val_if_fail(smiley_data_len  > 0, NULL);

	smiley = purple_smileys_find_by_shortcut(shortcut);
	if (smiley)
		return smiley;

	/* purple_smiley_create() sets shortcut */
	smiley = purple_smiley_create(shortcut);
	if (!smiley)
		return NULL;

	purple_smiley_set_data_impl(smiley, smiley_data, smiley_data_len, filename);

	purple_smiley_data_store(smiley->img);

	return smiley;
}

PurpleSmiley *
purple_smiley_new_from_file(const char *shortcut, const char *filepath, const char *filename)
{
	PurpleSmiley *smiley = NULL;
	guchar *smiley_data;
	size_t smiley_data_len;

	g_return_val_if_fail(shortcut  != NULL,  NULL);
	g_return_val_if_fail(filepath != NULL,  NULL);

	if (read_smiley_file(filepath, &smiley_data, &smiley_data_len))
		smiley = purple_smiley_new_from_stream(shortcut, smiley_data,
				smiley_data_len, filename);

	return smiley;
}

void
purple_smiley_delete(PurpleSmiley *smiley)
{
	guchar *filename = NULL;

	g_return_if_fail(smiley != NULL);

	filename = g_hash_table_lookup(smiley_shortcut_index, smiley->shortcut);

	if (filename != NULL)
		purple_smileys_remove(smiley);

	if (smiley->img)
		purple_smiley_data_unstore(purple_imgstore_get_filename(smiley->img));

	purple_smiley_destroy(smiley);
}

gboolean
purple_smiley_set_shortcut(PurpleSmiley *smiley, const char *shortcut)
{
	char *filename = NULL;

	g_return_val_if_fail(smiley  != NULL, FALSE);
	g_return_val_if_fail(shortcut != NULL, FALSE);

	/* Check out whether the shortcut is already being used. */
	filename = g_hash_table_lookup(smiley_shortcut_index, shortcut);

	if (filename)
		return FALSE;

	/* Save filename associated with the current shortcut. */
	filename = g_strdup(g_hash_table_lookup(
			smiley_shortcut_index, smiley->shortcut));

	/* If the updating smiley was already inserted to internal indexes, the
	 * shortcut index will need update.
	 * So we remove the old element first, and add the new one right after.
	 */
	if (filename) {
		g_hash_table_remove(smiley_shortcut_index, smiley->shortcut);
		g_hash_table_insert(smiley_shortcut_index,
			g_strdup(shortcut), filename);
	}

	g_free(smiley->shortcut);
	smiley->shortcut = g_strdup(shortcut);

	purple_smileys_save();

	return TRUE;
}

void
purple_smiley_set_data(PurpleSmiley *smiley, guchar *smiley_data,
			   size_t smiley_data_len, gboolean keepfilename)
{
	gboolean updateindex = FALSE;
	const char *filename = NULL;

	g_return_if_fail(smiley     != NULL);
	g_return_if_fail(smiley_data != NULL);
	g_return_if_fail(smiley_data_len > 0);

	/* If the updating smiley was already inserted to internal indexes, the
	 * checksum index will need update. And, considering that the filename
	 * could be regenerated using the file's checksum, the filename index
	 * will be updated too.
	 * So we remove it here, and add it again after the information is
	 * updated by "purple_smiley_set_data_impl" method. */
	filename = g_hash_table_lookup(smiley_checksum_index, smiley->checksum);
	if (filename) {
		g_hash_table_remove(smiley_checksum_index, smiley->checksum);
		g_hash_table_remove(smiley_data_index, filename);

		updateindex = TRUE;
	}

	/* Updates the file data. */
	if ((keepfilename) && (smiley->img) &&
			(purple_imgstore_get_filename(smiley->img)))
		purple_smiley_set_data_impl(smiley, smiley_data,
				smiley_data_len,
				purple_imgstore_get_filename(smiley->img));
	else
		purple_smiley_set_data_impl(smiley, smiley_data,
				smiley_data_len, NULL);

	/* Reinserts the index item. */
	filename = purple_imgstore_get_filename(smiley->img);
	if ((updateindex) && (filename)) {
		g_hash_table_insert(smiley_checksum_index,
				g_strdup(smiley->checksum), g_strdup(filename));
		g_hash_table_insert(smiley_data_index, g_strdup(filename), smiley);
	}

	purple_smileys_save();
}

PurpleStoredImage *
purple_smiley_get_stored_image(const PurpleSmiley *smiley)
{
	return purple_imgstore_ref(smiley->img);
}

const char *purple_smiley_get_shortcut(const PurpleSmiley *smiley)
{
	g_return_val_if_fail(smiley != NULL, NULL);

	return smiley->shortcut;
}

const char *
purple_smiley_get_checksum(const PurpleSmiley *smiley)
{
	g_return_val_if_fail(smiley != NULL, NULL);

	return smiley->checksum;
}

gconstpointer
purple_smiley_get_data(const PurpleSmiley *smiley, size_t *len)
{
	g_return_val_if_fail(smiley != NULL, NULL);

	if (smiley->img) {
		if (len != NULL)
			*len = purple_imgstore_get_size(smiley->img);

		return purple_imgstore_get_data(smiley->img);
	}

	return NULL;
}

const char *
purple_smiley_get_extension(const PurpleSmiley *smiley)
{
	if (smiley->img != NULL)
		return purple_imgstore_get_extension(smiley->img);

	return NULL;
}

char *purple_smiley_get_full_path(PurpleSmiley *smiley)
{
	g_return_val_if_fail(smiley != NULL, NULL);

	if (smiley->img == NULL)
		return NULL;

	return get_file_full_path(purple_imgstore_get_filename(smiley->img));
}

static void add_smiley_to_list(gpointer key, gpointer value, gpointer user_data)
{
	GList** preturninglist = (GList**)user_data;
	GList *returninglist = *preturninglist;

	returninglist = g_list_append(returninglist, value);

	*preturninglist = returninglist;
}

GList *
purple_smileys_get_all(void)
{
	GList *returninglist = NULL;

	g_hash_table_foreach(smiley_data_index, add_smiley_to_list, &returninglist);

	return returninglist;
}

void
purple_smileys_add(PurpleSmiley *smiley)
{
	const char *filename = NULL;

	g_return_if_fail(smiley != NULL);

	if (!g_hash_table_lookup(smiley_shortcut_index, smiley->shortcut)) {
		filename = purple_imgstore_get_filename(smiley->img);

		g_hash_table_insert(smiley_data_index, g_strdup(filename), smiley);
		g_hash_table_insert(smiley_shortcut_index, g_strdup(smiley->shortcut), g_strdup(filename));
		g_hash_table_insert(smiley_checksum_index, g_strdup(smiley->checksum), g_strdup(filename));

		purple_smileys_save();
	}
}

void
purple_smileys_remove(PurpleSmiley *smiley)
{
	const char *filename = NULL;

	g_return_if_fail(smiley != NULL);

	filename = purple_imgstore_get_filename(smiley->img);

	if (g_hash_table_lookup(smiley_data_index, filename)) {
		g_hash_table_remove(smiley_data_index, filename);
		g_hash_table_remove(smiley_shortcut_index, smiley->shortcut);
		g_hash_table_remove(smiley_checksum_index, smiley->checksum);

		purple_smileys_save();
	}
}

PurpleSmiley *
purple_smileys_find_by_shortcut(const char *shortcut)
{
	PurpleSmiley *smiley = NULL;
	guchar *filename = NULL;

	g_return_val_if_fail(shortcut != NULL, NULL);

	filename = g_hash_table_lookup(smiley_shortcut_index, shortcut);

	if (filename == NULL)
		return NULL;

	smiley = g_hash_table_lookup(smiley_data_index, filename);

	if (!smiley)
		return NULL;

	return smiley;
}

PurpleSmiley *
purple_smileys_find_by_checksum(const char *checksum)
{
	PurpleSmiley *smiley = NULL;
	guchar *filename = NULL;

	g_return_val_if_fail(checksum != NULL, NULL);

	filename = g_hash_table_lookup(smiley_checksum_index, checksum);

	if (filename == NULL)
		return NULL;

	smiley = g_hash_table_lookup(smiley_data_index, filename);

	if (!smiley)
		return NULL;

	return smiley;
}

const char *
purple_smileys_get_storing_dir(void)
{
	return smileys_dir;
}

void
purple_smileys_init()
{
	smiley_data_index = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, NULL);
	smiley_shortcut_index = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, g_free);
	smiley_checksum_index = g_hash_table_new_full(g_str_hash, g_str_equal,
			g_free, g_free);

	smileys_dir = g_build_filename(purple_user_dir(), SMILEYS_DEFAULT_FOLDER, NULL);

	purple_smileys_load();
}

static gboolean
free_smiley_data(gpointer key, gpointer value, gpointer user_data)
{
	purple_smiley_destroy(value);

	return TRUE;
}

void
purple_smileys_uninit()
{
	if (save_timer != 0) {
		purple_timeout_remove(save_timer);
		save_timer = 0;
		sync_smileys();
	}

	g_hash_table_foreach_remove(smiley_data_index, free_smiley_data, NULL);
	g_hash_table_destroy(smiley_shortcut_index);
	g_hash_table_destroy(smiley_checksum_index);
	g_free(smileys_dir);
}