avatar.c
author Myhailo Danylenko <isbear@ukrpost.net>
Thu, 30 Jun 2011 16:09:04 +0300
changeset 36 32424a111e97
parent 35 d0bb20377181
child 37 62e97e4f562c
permissions -rw-r--r--
remove api.h, offending 0.10.0

/*
 * avatar.c             -- Pep avatar events
 *
 * Copyrigth (C) 2009      Myhailo Danylenko <isbear@ukrpost.net>
 *
 * 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
 */

#define PNG_USER_MEM_SUPPORTED

#include <glib.h>
#include <loudmouth/loudmouth.h>
#include <aalib.h>
#include <png.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>

#include <mcabber/compl.h>
#include <mcabber/commands.h>
#include <mcabber/utils.h>
#include <mcabber/xmpp.h>
#include <mcabber/screen.h>
#include <mcabber/hbuf.h>
#include <mcabber/hooks.h>
#include <mcabber/settings.h>
#include <mcabber/modules.h>
#include <mcabber/xmpp_helper.h>

#include <mcabber/pep.h>

#include "config.h"

// module description

void avatar_init   (void);
void avatar_uninit (void);

#define DESCRIPTION ( \
	"PEP avatars handling\n" \
	"Recognizes options avatar_directory, avatar_font_height, avatar_font_width, avatar_max_height, avatar_max_width and avatar_background\n" \
	"Provides command /avatar" )
static const gchar *deps[] = { "pep", NULL };

static module_info_t info_avatar_dev = {
	.branch      = "dev",
	.api         = 20,
	.version     = PROJECT_VERSION,
	.description = DESCRIPTION,
	.requires    = deps,
	.init        = avatar_init,
	.uninit      = avatar_uninit,
	.next        = NULL,
};

module_info_t info_avatar_0_10_0 = {
	.branch      = "0.10.0",
	.api         = 1,
	.version     = PROJECT_VERSION,
	.description = DESCRIPTION,
	.requires    = deps,
	.init        = avatar_init,
	.uninit      = avatar_uninit,
	.next        = &info_avatar_dev,
};

module_info_t info_avatar = {
	.branch      = "0.10.1",
	.api         = 1,
	.version     = PROJECT_VERSION,
	.description = DESCRIPTION,
	.requires    = deps,
	.init        = avatar_init,
	.uninit      = avatar_uninit,
	.next        = &info_avatar_0_10_0,
};

// globals

#define NS_AVATAR_DATA            ( "urn:xmpp:avatar:data"            )
#define NS_AVATAR_METADATA        ( "urn:xmpp:avatar:metadata"        )
#define NS_AVATAR_METADATA_NOTIFY ( "urn:xmpp:avatar:metadata+notify" )

#ifdef MCABBER_API_HAVE_CMD_ID
static gpointer avatar_cmid = NULL;
static gboolean avatar_set_safe = FALSE;
#endif

static guint avatar_hid_connect    = 0;
static guint avatar_hid_disconnect = 0;

static GSList *reply_handlers = NULL;

static LmMessageHandler *avatar_metadata_reply_handler = NULL;

static gboolean  publish_delayed = FALSE;
static gsize     publish_len     = 0;
static guchar   *publish_data    = NULL;

// predeclarations

static LmHandlerResult avatar_retrieve_data_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer userdata);

// png stuff

static png_voidp png_glib_malloc (png_structp ignore, png_size_t size)
{
	return g_malloc (size);
}

static void png_glib_free (png_structp ignore, png_voidp chunk)
{
	g_free (chunk);
}

// reads file and returns array of pointers to image rows in
// grayscale 8-bit without alpha-channel format (i.e. one byte per pixel)
// after use just do g_free of this pointer - all data are in one memory chunk
static png_bytep *png_read_file (const char *file, int *rheight, int *rwidth, png_uint_16 background)
{
	FILE        *fd       = fopen (file, "rb");
	png_infop    info_ptr;
	png_structp  png_ptr;
	int          rowbytes;
	png_bytep   *row_pointers;
	int          width;
	int          height;

	if (!fd)
		return NULL;

	{ // check signature
		png_byte header[8];

		fread (header, 1, 8, fd);

		if (png_sig_cmp (header, 0, 8)) {
			fclose (fd);
			return NULL;
		}
	}

	// initialize reader
	png_ptr = png_create_read_struct_2 (PNG_LIBPNG_VER_STRING, NULL, NULL, NULL, NULL, png_glib_malloc, png_glib_free);

	if (!png_ptr) {
		fclose (fd);
		return NULL;
	}

	info_ptr = png_create_info_struct (png_ptr);
	if (!info_ptr) {
		png_destroy_read_struct (&png_ptr, NULL, NULL);
		fclose (fd);
		return NULL;
	}

	if (setjmp (png_jmpbuf (png_ptr))) {
		png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
		fclose (fd);
		return NULL;
	}

	png_init_io (png_ptr, fd);
	png_set_sig_bytes (png_ptr, 8);

	// get information
	png_read_info (png_ptr, info_ptr);

	{ // set up transformations
		png_byte color_type = info_ptr->color_type;
		png_byte bit_depth  = info_ptr->bit_depth;

		// trying to convert anything to grayscale 8-bit without alpha channel
		if (color_type == PNG_COLOR_TYPE_PALETTE)
			png_set_palette_to_rgb (png_ptr);
		if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
			png_set_expand_gray_1_2_4_to_8 (png_ptr);
		if (bit_depth == 16)
			png_set_strip_16 (png_ptr);
		if (png_get_valid(png_ptr, info_ptr, PNG_INFO_tRNS))
			png_set_tRNS_to_alpha (png_ptr);
		if (color_type & PNG_COLOR_MASK_COLOR)
			png_set_rgb_to_gray_fixed (png_ptr, 1, -1, -1);
		if (color_type & PNG_COLOR_MASK_ALPHA) {
			png_color_16 my_background = {
				.red   = background,
				.green = background,
				.blue  = background,
				.gray  = background,
			};
			png_set_background (png_ptr, &my_background, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1.0);
		}

		// renew information in info structure
		png_read_update_info (png_ptr, info_ptr);
	}

	width    = info_ptr->width;
	height   = info_ptr->height;
	rowbytes = info_ptr->rowbytes;

	{ // allocate buffer for image data
		png_bytep chunk = g_malloc ((sizeof(png_bytep) + rowbytes) * height);
		png_bytep start = chunk + sizeof(png_bytep) * height;
		int       y;

		row_pointers = (png_bytep *) chunk;

		for (y = 0; y < height; ++y)
			row_pointers[y] = start + y * rowbytes;
	}

	// read image
	if (setjmp (png_jmpbuf (png_ptr))) {
		g_free (row_pointers);
		png_destroy_read_struct (&png_ptr, &info_ptr, NULL);
		fclose (fd);
		return NULL;
	}

	png_read_image (png_ptr, row_pointers);
	png_read_end (png_ptr, NULL);

	// free resources
	png_destroy_read_struct (&png_ptr, &info_ptr, NULL); 
	fclose (fd);

	*rheight = height;
	*rwidth  = width;

	return row_pointers;
}

// aa stuff

// converts image in format, returned by png_read_file into ascii,
// scaling it to fit specified max values. If font parameters are
// set properly, should result in image on screen with original png
// aspect ratio.
static gchar *aa_convert_image (png_bytep *row_pointers, int height, int width, int maxcharh, int maxcharw, int fonth, int fontw)
{
	gchar                     *result;
	struct aa_hardware_params  hwparams = {
		.font      = NULL,
		.supported = AA_NORMAL_MASK,
		.width     = 0,
		.height    = 0,
	};

	// params for zooming
	int finalcharh;
	int finalcharw;
	int finalh;
	int finalw;
	int starty;
	int startx;
	int finalratioh;
	int finalratiow;

	// process aa parameters from environment variables
	aa_parseoptions (&hwparams, NULL, NULL, NULL);

	{ // calculate parameters for image zooming into aa surface
	  // this always makes me a hard time, thus such a mess :/
	  // I hope, compiler will optimize out all these variables...

		// font size in font pixels (real?)
		//int fonth = 16;
		//int fontw = 8;
		//float fontaspect = (float) fontw / fonth;
		// aa pixels per character
		const int aappch = 2;
		const int aappcw = 2;
		//const float aaaspect = (float) aappcw / aappch;
		// font pixels per aa pixel
		int aappph = (float) fonth / aappch;
		int aapppw = (float) fontw / aappcw;
		float aapixelaspect = (float) aapppw / aappph;
		// original image size in real pixels
		int originh = height;
		int originw = width;
		float originaspect = (float) originw / originh;
		// maximal resulting image size in chars
		//int maxcharh = res_height;
		//int maxcharw = res_width;
		// maximal resulting image size in aa pixels
		int maxh = maxcharh * aappch;
		int maxw = maxcharw * aappcw;
		// maximal resulting image size in real pixels
		int maxh_rp;
		int maxw_rp;
		if (aapixelaspect > 1) {
			maxh_rp = maxh;
			maxw_rp = (float) maxw * aapixelaspect;
		} else {
			maxh_rp = (float) maxh / aapixelaspect;
			maxw_rp = maxw;
		}
		float maxaspect_rp = (float) maxw_rp / maxh_rp;
		// resulting image size in real pixels
		float ratio_rp;
		int resh_rp;
		int resw_rp;
		if (originh > maxh_rp || originw > maxw_rp) {
			// boundaries crossed, will zoom
			if (originaspect > maxaspect_rp) {
				// width is bigger
				ratio_rp = (float) maxw_rp / originw;
				resh_rp = (float) originh * ratio_rp;
				resw_rp = maxw_rp;
			} else {
				// height is bigger
				ratio_rp = (float) maxh_rp / originh;
				resh_rp = maxh_rp;
				resw_rp = (float) originw * ratio_rp;
			}
		} else {
			// image fits, no zooming
			ratio_rp = 1;
			resh_rp = originh;
			resw_rp = originw;
		}
		// resulting image size in aa pixels
		int resh;
		int resw;
		if (aapixelaspect > 1) {
			resh = resh_rp;
			resw = (float) resw_rp / aapixelaspect;
		} else {
			resh = (float) resh_rp * aapixelaspect;
			resw = resw_rp;
		}
		// number of original image pixels per one resulting image pixel
		//int ratioh = (float) originh / resh;
		//int ratiow = (float) originw / resw;
		// resulting image size in chars
		int rescharh = (float) resh / aappch;
		int rescharw = (float) resw / aappcw;

		// re-calculate parameters, as they may change due to rounding (?)
		finalcharh = rescharh;
		finalcharw = rescharw;
		finalh = rescharh * aappch;
		finalw = rescharw * aappcw;
#if 0
		// I do not know, why I added this here, and it needs more digging
		// to know, if this is supposed to be used somewhere, so, for now
		// let it be this way
		int finalh_rp;
		int finalw_rp;
		if (aapixelaspect > 1) {
			finalh_rp = finalh;
			finalw_rp = (float) finalw * aapixelaspect;
		} else {
			finalh_rp = (float) finalh / aapixelaspect;
			finalw_rp = finalw;
		}
#endif
		finalratioh = (float) originh / finalh;
		finalratiow = (float) originw / finalw;

		// center image part actually used
		int areah = (float) finalh * finalratioh;
		int areaw = (float) finalw * finalratiow;
		int losth = originh - areah;
		int lostw = originw - areaw;
		starty = losth / 2;
		startx = lostw / 2;
	}

	// create aa image
	hwparams.height = finalcharh;
	hwparams.width  = finalcharw;

	aa_context *context = aa_init (&mem_d, &hwparams, NULL);

	if (context == NULL)
		return NULL;

	{ // fill aa image from png buffer with scaling
		int height, width;
		int y;

		height = aa_imgheight (context);
		width  = aa_imgwidth (context);
		
		if (height > finalh)
			height = finalh;
		if (width > finalw)
			width = finalw;

		for (y = 0;  y < height; ++y) {
			int x;

			for (x = 0; x < width; ++x) {
				png_bytep    *row_pointer = row_pointers + starty + y * finalratioh;
				unsigned int  color = 0;
				unsigned int  cy;

				// just arithmetic average
				for (cy = 0; cy < finalratioh; ++cy) {
					png_bytep row = row_pointer[cy] + startx + x * finalratiow;
					int       cx;

					for (cx = 0; cx < finalratiow; ++cx)
						color += row[cx];
				}

				color /= finalratioh * finalratiow;

				aa_putpixel (context, x, y, color);
			}
		}
	}

	// render ascii image
	aa_render (context, &aa_defrenderparams, 0, 0, aa_scrwidth (context), aa_scrheight (context));
	aa_flush (context);

	{ // format the result
		char    *text   = (char *) aa_text (context);
		int      width  = aa_scrwidth (context);
		int      height = aa_scrheight (context);
		GString *avatar = g_string_new (NULL);
		char    *end    = text + width * height;

		while (text < end) {
			g_string_append_len (avatar, text, width);
			g_string_append_c (avatar, '\n');
			text += width;
		}

		// strip last '\n'
		g_string_truncate (avatar, avatar->len - 1);

		result = g_string_free (avatar, FALSE);

	}

	// free resources
	aa_close (context);

	return result;
}

// avatar stuff

// returns filename to save this avatar. NULL on error. g_free result.
static gchar *avatar_id_filename (const gchar *id)
{
	gchar *escid = g_strdup (id);
	gchar *dir   = (gchar *) settings_opt_get ("avatar_directory");
	gchar *file;
	
	if (!dir)
		return NULL;
	
	{ // neutralize id
		const gchar *valid = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
	
		g_strcanon (escid, valid, '_');
	}
	
	dir = expand_filename (dir);
	
	file = g_strdup_printf ("%s/%s.png", dir, escid);
	
	g_free (dir);
	g_free (escid);

	return file;
}

// returns name of symlink to avatar image file for this jid
static gchar *avatar_jid_filename (const gchar *jid)
{
	gchar *dir  = (gchar *) settings_opt_get ("avatar_directory");
	gchar *file = NULL;
	gchar *bjid;

	if (!dir)
		return NULL;

	// if jid validity alone is enough for mcabber to use as
	// names for history logs, it's fine for us too.
	if (check_jid_syntax (jid))
		return NULL;

	bjid = jidtodisp (jid);
	mc_strtolower (bjid);
	dir  = expand_filename (dir);
	file = g_strdup_printf ("%s/%s", dir, bjid);

	g_free (bjid);
	return file;
}

// symlinks specified jid to specified file. performs jid
// checks, but not file checks.
static void set_jid_avatar (const char *jid, const char *file)
{
	gchar *jfile;

	if (!jid || !file)
		return;

	jfile = avatar_jid_filename (jid);

	if (!jfile)
		return;
	
	unlink (jfile);
	if (symlink (file, jfile) == -1)
		scr_log_print (LPRINT_LOGNORM, "avatar: Cannot symlink jid-file to avatar: %s.", strerror (errno));
}

// reply handler for metadata publish request
// just prints error message if unsuccessful
static LmHandlerResult avatar_publish_metadata_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer userdata)
{
	switch (lm_message_get_sub_type (message)) {
	case LM_MESSAGE_SUB_TYPE_RESULT:
		break;

	case LM_MESSAGE_SUB_TYPE_ERROR:

		{
			LmMessageNode *node   = lm_message_get_node (message);
			const gchar   *type;
			const gchar   *reason;
	
			node = lm_message_node_get_child (node, "error");
			type = lm_message_node_get_attribute (node, "type");
			if (node->children)
				reason = node->children->name;
			else
				reason = "undefined";
	
			scr_log_print (LPRINT_LOGNORM, "avatar: Metadata publishing failed: %s - %s.", type, reason);
		}

		break;

	default:
		return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
		break;
	}

	return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}

// reply handler for data publish request
// sends metadata update request or prints error message
static LmHandlerResult avatar_publish_data_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer userdata)
{
	LmMessage *request = (LmMessage *) userdata;

	reply_handlers = g_slist_remove (reply_handlers, handler);
	
	switch (lm_message_get_sub_type (message)) {
	case LM_MESSAGE_SUB_TYPE_RESULT:

		{ // send
			GError *error = NULL;

			lm_connection_send_with_reply (connection, request, avatar_metadata_reply_handler, &error);

			if (error) {
				scr_log_print (LPRINT_DEBUG, "avatar: Metadata publication error: %s.", error -> message);
				g_error_free (error);
			}

			lm_message_unref (request);
		}

		break;

	case LM_MESSAGE_SUB_TYPE_ERROR:

		{
			LmMessageNode *node   = lm_message_get_node (message);
			const gchar   *type;
			const gchar   *reason;

			node = lm_message_node_get_child (node, "error");
			type = lm_message_node_get_attribute (node, "type");
			if (node->children)
				reason = node->children->name;
			else
				reason = "undefined";

			scr_log_print (LPRINT_LOGNORM, "avatar: Data publishing failed: %s - %s.", type, reason);
		}

		break;

	default:
		return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
		break;
	}

	return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}

// prints avatar (if available) for jid into jid's buffer
// id can be omitted
static gboolean print_avatar (const gchar *file, const gchar *jid, const gchar *id)
{
	png_bytep *row_pointers;
	gchar     *avatar;
	int        width;
	int        height;

	if (!file)
		return FALSE;

	{ // check if file exists to not trigger unnecessary error messages
		struct stat buf;

		if (stat (file, &buf) == -1) {
			gchar         *bjid;
			LmMessage     *request;
			LmMessageNode *node;

			if (!xmpp_is_online ()) {
				scr_log_print (LPRINT_NORMAL, "avatar: You are not online, request not sent.");
				return FALSE;
			}

			bjid = jidtodisp (jid);

			scr_write_incoming_message (bjid, "No avatar for this buddy yet, sending request.", 0, HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0); // NO conversion from utf-8

			// create data request
			request = lm_message_new_with_sub_type (bjid, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_GET);
			node = lm_message_get_node (request);
			lm_message_node_set_attribute (node, "from", lm_connection_get_jid (lconnection));

			node = lm_message_node_add_child (node, "pubsub", NULL);
			lm_message_node_set_attribute (node, "xmlns", NS_PUBSUB);

			node = lm_message_node_add_child (node, "items", NULL);
			lm_message_node_set_attribute (node, "node", NS_AVATAR_METADATA);

			{ // send, result will be handled by pep
				GError *error = NULL;

				lm_connection_send (lconnection, request, &error);

				if (error) {
					scr_log_print (LPRINT_DEBUG, "avatar: Request sending error: %s.", error -> message);
					g_error_free (error);
				}
			}

			lm_message_unref (request);
			g_free (bjid);
			return TRUE;
		}
	}

	{
		png_uint_16 background = settings_opt_get_int ("avatar_background");
	
		row_pointers = png_read_file (file, &width, &height, background);
	}

	if (!row_pointers) {
		scr_log_print (LPRINT_LOGNORM, "avatar: Cannot decode png data from file %s.", file);
		return FALSE;
	}

	{ // convert to ascii
		int maxcharh = settings_opt_get_int ("avatar_max_height");
		int maxcharw = settings_opt_get_int ("avatar_max_width");
		int fonth    = settings_opt_get_int ("avatar_font_height");
		int fontw    = settings_opt_get_int ("avatar_font_width");

		// if not set explicitly, calculate available space
		if (!maxcharh)
			maxcharh = scr_gettextheight() - 1; // -1 due to avatar header line

		if (!maxcharw)
			maxcharw = scr_gettextwidth();

		if (!fonth)
			fonth = 16;
		if (!fontw)
			fontw = 8;

		avatar = aa_convert_image (row_pointers, width, height, maxcharh, maxcharw, fonth, fontw);
	}

	g_free (row_pointers);

	if (!avatar) {
		scr_log_print (LPRINT_LOGNORM, "avatar: Error converting image to ascii.");
		return FALSE;
	}

	{ // print out avatar
		gchar *bjid = jidtodisp (jid);
		gchar *mesg;
		if (id)
			mesg = g_strdup_printf ("Avatar [%s]:\n%s", id, avatar);
		else
			mesg = g_strdup_printf ("Avatar:\n%s", avatar);

		scr_write_incoming_message (bjid, mesg, 0, HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0); // NO conversion from utf-8

		g_free (bjid);
		g_free (mesg);
	}

	g_free (avatar);

	return TRUE;
}

// reply handler for image/png data request
// saves image and prints it to buddy buffer
static LmHandlerResult avatar_retrieve_data_reply_handler (LmMessageHandler *handler, LmConnection *connection, LmMessage *message, gpointer userdata)
{
	gchar *from = (gchar *) userdata;

	reply_handlers = g_slist_remove (reply_handlers, handler);
	
	switch (lm_message_get_sub_type (message)) {
	case LM_MESSAGE_SUB_TYPE_RESULT:

		{
			LmMessageNode *node = lm_message_get_node (message);
			node = lm_message_node_get_child (node, "pubsub");
			if (!node)
				return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;

			node = lm_message_node_get_child (node, "items");
			if (!node)
				return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;

			{
				LmMessageNode *item;
				
				for (item = node->children; item; item = item->next) {
					if (!strcmp (item->name, "item")) {
						LmMessageNode *data = lm_message_node_get_child (item, "data");
						const gchar   *id   = lm_message_node_get_attribute (item, "id");

						if (!data)
							continue;

						{ // save to file and display in ascii
							const gchar *base64 = lm_message_node_get_value (data);
							guchar      *png;
							gsize        len;
							gchar       *file;

							if (!base64)
								continue;

							png = g_base64_decode (base64, &len);

							if (!png)
								continue;

							file = avatar_id_filename (id);

							if (!file) {
								g_free (png);
								continue;
							}
	
							{ // write image to file
								int fd = open (file, O_WRONLY|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH);

								if (fd == -1) {
									scr_log_print (LPRINT_LOGNORM, "avatar: Cannot create file: %s.", strerror (errno));
									g_free (png);
									g_free (file);
									continue;
								}

								write (fd, png, len);
								close (fd);
							}

							// create jid symlink to avatar image
							set_jid_avatar (from, file);

							g_free (png);

							// read it back :/
							print_avatar (file, from, id);

							g_free (file);
						}
					}
				}
			}
		}

		break;

	case LM_MESSAGE_SUB_TYPE_ERROR:

		{
			LmMessageNode *node   = lm_message_get_node (message);
			const gchar   *type;
			const gchar   *reason;

			node = lm_message_node_get_child (node, "error");
			type = lm_message_node_get_attribute (node, "type");
			if (node->children)
				reason = node->children->name;
			else
				reason = "undefined";

			scr_log_print (LPRINT_LOGNORM, "avatar: Obtaining avatar data for %s failed: %s - %s.", from, type, reason);
		}

		break;

	default:
		return LM_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
		break;
	}

	return LM_HANDLER_RESULT_REMOVE_MESSAGE;
}

// data is image/png data of length len
// call with len = 0 to publish empty avatar
static void avatar_publish (const guchar *data, gsize len)
{
	LmMessage     *datarequest;
	gchar         *id;
	LmMessage     *request;
	LmMessageNode *node;
	GError        *error       = NULL;

	if (!xmpp_is_online ()) {
		scr_log_print (LPRINT_DEBUG, "avatar: Not connected, delaying publish.");

		g_free (publish_data);

		publish_data    = (guchar *) g_strndup ((gchar *) data, len);
		publish_len     = len;
		publish_delayed = TRUE;

		return;
	}

	request = lm_message_new_with_sub_type (NULL, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_SET);
	node = lm_message_get_node (request);

	if (len) {
		// create data publish request
		lm_message_node_set_attribute (node, "from", lm_connection_get_jid (lconnection));
	
		node = lm_message_node_add_child (node, "pubsub", NULL);
		lm_message_node_set_attribute (node, "xmlns", NS_PUBSUB);
	
		node = lm_message_node_add_child (node, "publish", NULL);
		lm_message_node_set_attribute (node, "node", NS_AVATAR_DATA);
	
		node = lm_message_node_add_child (node, "item", NULL);
		id = g_compute_checksum_for_data (G_CHECKSUM_SHA1, data, len);
		lm_message_node_set_attribute (node, "id", id);
	
		{
			gchar *base64 = g_base64_encode (data, len);
	
			node = lm_message_node_add_child (node, "data", base64);
			lm_message_node_set_attribute (node, "xmlns", NS_AVATAR_DATA);
	
			g_free (base64);
		}

		// then create metadata publish request to be supplied to reply handler as userdata
		datarequest = lm_message_new_with_sub_type (NULL, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_SET);
		node = lm_message_get_node (datarequest);
	}

	lm_message_node_set_attribute (node, "from", lm_connection_get_jid (lconnection));

	node = lm_message_node_add_child (node, "pubsub", NULL);
	lm_message_node_set_attribute (node, "xmlns", NS_PUBSUB);

	node = lm_message_node_add_child (node, "publish", NULL);
	lm_message_node_set_attribute (node, "node", NS_AVATAR_METADATA);

	node = lm_message_node_add_child (node, "item", NULL);
	if (len)
		lm_message_node_set_attribute (node, "id", id);

	node = lm_message_node_add_child (node, "metadata", NULL);
	lm_message_node_set_attribute (node, "xmlns", NS_AVATAR_METADATA);

	if (len) {
		gchar *bytes = g_strdup_printf ("%u", (guint) len);

		node = lm_message_node_add_child (node, "info", NULL);
		lm_message_node_set_attributes (node, "bytes", bytes, "id", id, "type", "image/png", NULL);

		g_free (bytes);

		// create handler
		LmMessageHandler *handler = lm_message_handler_new (avatar_publish_data_reply_handler, (gpointer) datarequest, (GDestroyNotify) lm_message_unref /* custom, remove from queue? */);
		reply_handlers = g_slist_append (reply_handlers, handler);

		// send
		lm_connection_send_with_reply (lconnection, request, handler, &error);

		lm_message_handler_unref (handler);
	} else 
		// send
		lm_connection_send_with_reply (lconnection, request, avatar_metadata_reply_handler, &error);
	
	if (error) {
		scr_log_print (LPRINT_DEBUG, "avatar: Publish request sending error: %s.", error -> message);
		g_error_free (error);
	}

	lm_message_unref (request);
}

// pep node handler
// if avatar is not yet saved, sends data request, else updates jid's symlink
static void avatar_handler (const gchar *from, const gchar *enode, LmMessageNode *n, const gchar *id, gpointer ignore)
{
	LmMessageNode *node;

	// well, anyway we now do not store information about jid-avatar relationship,
	// thus "no avatar" publishes are meaningless to us
	for (node = n->children; node; node = node->next) {
		const gchar *name = node->name;

		if (!strcmp (name, "info")) {
			gchar       *jid    = jidtodisp (from);
			const gchar *url    = lm_message_node_get_attribute (node, "url");
			const gchar *type   = lm_message_node_get_attribute (node, "type");
			const gchar *id     = lm_message_node_get_attribute (node, "id");
			const gchar *bytes  = lm_message_node_get_attribute (node, "bytes");
			const gchar *height = lm_message_node_get_attribute (node, "height");
			const gchar *width  = lm_message_node_get_attribute (node, "width");

			{ // print to buddy's buffer
				GString *mesg = g_string_new ("Avatar:");
				gchar   *text;

				// [id] type w x h (# bytes) url
				g_string_append_printf (mesg, " [%s]", id ? id : "(none)");

				if (type) {
					g_string_append_c (mesg, ' ');
					g_string_append (mesg, type);
				}

				if (width && height)
					g_string_append_printf (mesg, " %s x %s", width, height);

				if (bytes)
					g_string_append_printf (mesg, " (%s bytes)", bytes);

				if (url) {
					g_string_append_c (mesg, ' ');
					g_string_append (mesg, url);
				}

				text = g_string_free (mesg, FALSE);

				scr_write_incoming_message (jid, text, 0, HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0); // NO conversion from utf-8

				g_free (text);
			}

			if (!url) {
				// it is server-published png, go ahead and request data
				LmMessage        *request;
				LmMessageNode    *node;
				LmMessageHandler *dhandler;

				{ // check, if file already exists
					gchar       *file = avatar_id_filename (id);
					struct stat  buf;

					if (!file) {
						scr_log_print (LPRINT_LOGNORM, "avatar: Cannot obtain filename to save file, probably avatar_directory is not set.");
						g_free (jid);
						continue;
					}
					
					if (stat (file, &buf) != -1) {
						scr_write_incoming_message (jid, "Avatar file exists, will not download.", 0, HBB_PREFIX_INFO|HBB_PREFIX_NOFLAG, 0);
						// link jid to this file
						set_jid_avatar (jid, file);
						g_free (file);
						g_free (jid);
						continue;
					}

					g_free (file);
				}

				// create data request
				request = lm_message_new_with_sub_type (jid, LM_MESSAGE_TYPE_IQ, LM_MESSAGE_SUB_TYPE_GET);
				node = lm_message_get_node (request);
				lm_message_node_set_attribute (node, "from", lm_connection_get_jid (lconnection));

				node = lm_message_node_add_child (node, "pubsub", NULL);
				lm_message_node_set_attribute (node, "xmlns", NS_PUBSUB);

				node = lm_message_node_add_child (node, "items", NULL);
				lm_message_node_set_attribute (node, "node", NS_AVATAR_DATA);

				node = lm_message_node_add_child (node, "item", NULL);
				lm_message_node_set_attribute (node, "id", id);

				// create handler
				dhandler = lm_message_handler_new (avatar_retrieve_data_reply_handler, jid, g_free);
				reply_handlers = g_slist_append (reply_handlers, dhandler);

				{ // send
					GError *error = NULL;

					lm_connection_send_with_reply (lconnection, request, dhandler, &error);

					if (error) {
						scr_log_print (LPRINT_DEBUG, "avatar: Data request sending error: %s.", error -> message);
						g_error_free (error);
					}
				}

				lm_message_handler_unref (dhandler);
				lm_message_unref (request);

				// NOT free jid here - it will be freed on handler destruction
			} else
				g_free (jid);
		}
	}
}

// /AVATAR [filename|-]
static void do_avatar (char *arg)
{
	gchar  *fname;
	guchar *data;
	gsize   len;

	if (!*arg) { // print current byddy's avatar
		const gchar *jid = CURRENT_JID;

		if (!jid) {
			scr_log_print (LPRINT_NORMAL, "This item cannot have avatar.");
			return;
		}

		fname = avatar_jid_filename (jid);

		print_avatar (fname, jid, NULL);

		g_free (fname);
		return;
	}

	// publish
	if (arg[0] == '-' && arg[1] == '\0') {
		// no avatar
		avatar_publish (NULL, 0);
		return;
	}

	fname = expand_filename (arg);

	{ // read file
		char     buffer[1024];
		GString *datastring;
		int      fd           = open (fname, O_RDONLY);

		if (!fd) {
			scr_log_print (LPRINT_NORMAL, "Cannot open file '%s': %s.", fname, strerror (errno));
			g_free (fname);
			return;
		}

		datastring = g_string_new (NULL);

		{ // read data
			int ret;
			while ((ret = read (fd, buffer, 1024)) > 0)
				g_string_append_len (datastring, buffer, ret);
		}

		close (fd);

		len  = datastring->len;
		data = (guchar *) g_string_free (datastring, FALSE);
	}

	g_free (fname);

	avatar_publish (data, len);

	g_free (data);
}

static void avatar_free_reply_handlers (void)
{
	GSList *hel;

	for (hel = reply_handlers; hel; hel = hel->next) {
		LmMessageHandler *handler = (LmMessageHandler *) hel->data;
		lm_message_handler_invalidate (handler);
#ifdef HAVE_LM_CONNECTION_UNREGISTER_REPLY_HANDLER
		if (lconnection)
			lm_connection_unregister_reply_handler (lconnection, handler);
#endif
	}

	g_slist_free (reply_handlers);
	reply_handlers = NULL;

#ifdef HAVE_LM_CONNECTION_UNREGISTER_REPLY_HANDLERS
	if (lconnection)
		lm_connection_unregister_reply_handler (lconnection, avatar_metadata_reply_handler);
#endif
}

// release handlers before reconnect
static guint avatar_hdh (const gchar *hid, hk_arg_t *args, gpointer userdata)
{
	avatar_free_reply_handlers ();
	return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
}

static guint avatar_hch (const gchar *hid, hk_arg_t *args, gpointer userdata)
{
	if (publish_data) {
		guchar *tmp_data = publish_data;

		scr_log_print (LPRINT_DEBUG, "avatar: Publishing delayed data.");

		publish_data    = NULL;
		publish_delayed = FALSE;

		avatar_publish (publish_data, publish_len);

		g_free (tmp_data);
	}

	return HOOK_HANDLER_RESULT_ALLOW_MORE_HANDLERS;
}

void avatar_init (void)
{
	pep_register_xmlns_handler (NS_AVATAR_METADATA, avatar_handler, NULL, NULL);
	avatar_metadata_reply_handler = lm_message_handler_new (avatar_publish_metadata_reply_handler, NULL, NULL);

#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_add ("avatar", "", COMPL_FILENAME, 0, do_avatar, NULL);
#else
	avatar_cmid = cmd_add ("avatar", "", COMPL_FILENAME, 0, do_avatar, NULL);
	avatar_set_safe = cmd_set_safe ("avatar", TRUE);
#endif

	avatar_hid_connect    = hk_add_handler (avatar_hch, HOOK_POST_CONNECT, G_PRIORITY_DEFAULT, NULL);
	avatar_hid_disconnect = hk_add_handler (avatar_hdh, HOOK_PRE_DISCONNECT, G_PRIORITY_DEFAULT, NULL);

	xmpp_add_feature (NS_AVATAR_METADATA);
	xmpp_add_feature (NS_AVATAR_METADATA_NOTIFY);
}

void avatar_uninit (void)
{
	xmpp_del_feature (NS_AVATAR_METADATA);
	xmpp_del_feature (NS_AVATAR_METADATA_NOTIFY);

	hk_del_handler (HOOK_POST_CONNECT, avatar_hid_connect);
	hk_del_handler (HOOK_PRE_DISCONNECT, avatar_hid_disconnect);

#ifndef MCABBER_API_HAVE_CMD_ID
	cmd_del ("avatar");
#else
	if (avatar_cmid)
		cmd_del (avatar_cmid);
	if (avatar_set_safe)
		cmd_set_safe ("avatar", FALSE);
#endif

	pep_unregister_xmlns_handler (NS_AVATAR_METADATA);

	avatar_free_reply_handlers ();

	if (avatar_metadata_reply_handler) {
		lm_message_handler_invalidate (avatar_metadata_reply_handler);
		lm_message_handler_unref (avatar_metadata_reply_handler);
	}

	g_free (publish_data);
}

/* vim: se ts=4 sw=4: */