view libpurple/protocols/mxit/markup.c @ 29815:3175b89c5156

Add support for recieving and sending MSN file transfer previews.
author Elliott Sales de Andrade <qulogic@pidgin.im>
date Sun, 28 Feb 2010 04:14:15 +0000
parents 259bbfb423d4
children d7325448badb
line wrap: on
line source

/*
 *					MXit Protocol libPurple Plugin
 *
 *			-- convert between MXit and libPurple markup --
 *
 *				Pieter Loubser	<libpurple@mxit.com>
 *
 *			(C) Copyright 2009	MXit Lifestyle (Pty) Ltd.
 *				<http://www.mxitlifestyle.com>
 *
 * 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	"purple.h"

#include	"protocol.h"
#include	"mxit.h"
#include	"markup.h"
#include	"chunk.h"
#include	"formcmds.h"
#include	"roster.h"


/* define this to enable emoticon (markup) debugging */
#undef		MXIT_DEBUG_EMO
/* define this to enable markup conversion debugging */
#undef		MXIT_DEBUG_MARKUP


#define		MXIT_FRAME_MAGIC		"MXF\x01"			/* mxit emoticon magic number */
#define		MXIT_MAX_EMO_ID			16					/* maximum emoticon ID length */
#define		COLORCODE_LEN			6					/* colour code ID length */


/* HTML tag types */
#define		MXIT_TAG_COLOR			0x01				/* font color tag */
#define		MXIT_TAG_SIZE			0x02				/* font size tag */
#define		MXIT_MAX_MSG_TAGS		90					/* maximum tags per message (pigdin hack work around) */

/*
 * a HTML tag object
 */
struct tag {
	char	type;
	char*	value;
};


#define		MXIT_VIBE_MSG_COLOR		"#9933FF"

/* vibes */
static const char*	vibes[] = {
	/* 0 */		N_( "Cool Vibrations" ),
	/* 1 */		N_( "Purple Rain" ),
	/* 2 */		N_( "Polite" ),
	/* 3 */		N_( "Rock n Roll" ),
	/* 4 */		N_( "Summer Slumber" ),
	/* 5 */		N_( "Electric Razor" ),
	/* 6 */		N_( "S.O.S" ),
	/* 7 */		N_( "Jack Hammer" ),
	/* 8 */		N_( "Bumble Bee" ),
	/* 9 */		N_( "Ripple" )
};



#ifdef	MXIT_DEBUG_EMO
/*------------------------------------------------------------------------
 * Dump a byte buffer as hexadecimal to the console for debugging purposes.
 *
 *  @param buf				The data to dump
 *  @param len				The length of the data
 */
static void hex_dump( const char* buf, int len )
{
	char		msg[256];
	int			pos;
	int			i;

	purple_debug_info( MXIT_PLUGIN_ID, "Dumping data (%i bytes)\n", len );

	memset( msg, 0x00, sizeof( msg ) );
	pos = 0;

	for ( i = 0; i < len; i++ ) {

		if ( pos == 0 )
			pos += sprintf( &msg[pos], "%04i:  ", i );

		pos += sprintf( &msg[pos], "0x%02X ", (unsigned char) buf[i] );

		if ( i % 16 == 15 ) {
			pos += sprintf( &msg[pos], "\n" );
			purple_debug_info( MXIT_PLUGIN_ID, msg );
			pos = 0;
		}
		else if ( i % 16 == 7 )
			pos += sprintf( &msg[pos], " " );
	}

	if ( pos > 0 ) {
		pos += sprintf( &msg[pos], "\n" );
		purple_debug_info( MXIT_PLUGIN_ID, msg );
		pos = 0;
	}
}
#endif


/*------------------------------------------------------------------------
 * Adds a link to a message
 *
 *  @param mx				The Markup message object
 *	@param linkname			This is the what will be returned when the link gets clicked
 *	@param displayname		This is the name for the link which will be displayed in the UI
 */
void mxit_add_html_link( struct RXMsgData* mx, const char* linkname, const char* displayname )
{
#ifdef	MXIT_LINK_CLICK
	char	retstr[256];
	gchar*	retstr64;
	char	link[256];
	int		len;

	len = g_snprintf( retstr, sizeof( retstr ), "%s|%s|%s|%s|%s", MXIT_LINK_KEY, purple_account_get_username( mx->session->acc ),
											purple_account_get_protocol_id( mx->session->acc ), mx->from, linkname );
	retstr64 = purple_base64_encode( (const unsigned char*) retstr, len );
	g_snprintf( link, sizeof( link ), "%s%s", MXIT_LINK_PREFIX, retstr64 );
	g_free( retstr64 );

	g_string_append_printf( mx->msg, "<a href=\"%s\">%s</a>", link, displayname );
#else
	g_string_append_printf( mx->msg, "<b>%s</b>", linkname );
#endif
}


/*------------------------------------------------------------------------
 * Extract an ASN.1 formatted length field from the data.
 *
 *  @param data				The source data
 *  @param size				The extracted length
 *  @return					The number of bytes extracted
 */
static unsigned int asn_getlength( const char* data, int* size )
{
	unsigned int	len		= 0;
	unsigned char	bytes;
	unsigned char	byte;
	int				i;

	/* first byte specifies the number of bytes in the length */
	bytes = ( data[0] & ~0x80 );
	if ( bytes > sizeof( unsigned int ) ) {
		/* file too big! */
		return -1;
	}
	data++;

	/* parse out the actual length */
	for ( i = 0; i < bytes; i++ ) {
		byte = data[i];
		len <<= 8;
		len += byte;
	}

	*size = len;
	return bytes + 1;
}


/*------------------------------------------------------------------------
 * Extract an ASN.1 formatted UTF-8 string field from the data.
 *
 *  @param data				The source data
 *  @param type				Expected type of string
 *  @param utf8				The extracted string.  Must be deallocated by caller.
 *  @return					The number of bytes extracted
 */
static int asn_getUtf8( const char* data, char type, char** utf8 )
{
	int		len;

	/* validate the field type [1 byte] */
	if ( data[0] != type ) {
		/* this is not a utf-8 string! */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid UTF-8 encoded string in ASN data (0x%02X)\n", (unsigned char) data[0] );
		return -1;
	}

	len = data[1];						/* length field [1 bytes] */
	*utf8 = g_malloc( len + 1 );
	memcpy( *utf8, &data[2], len );		/* data field */
	(*utf8)[len] = '\0';

	return ( len + 2 );
}


/*------------------------------------------------------------------------
 * Free data associated with a Markup message object.
 *
 *  @param mx				The Markup message object
 */
static void free_markupdata( struct RXMsgData* mx )
{
	if ( mx ) {
		if ( mx->msg )
			g_string_free( mx->msg, TRUE );
		if ( mx->from )
			g_free( mx->from );
		g_free( mx );
	}
}


/*------------------------------------------------------------------------
 * Split the message into smaller messages and send them one at a time
 * to pidgin to be displayed on the UI
 *
 *  @param mx				The received message object
 */
static void mxit_show_split_message( struct RXMsgData* mx )
{
	const char*		cont	= "<font color=\"#999999\">continuing...</font>\n";
	GString*		msg		= NULL;
	char*			ch		= NULL;
	int				pos		= 0;
	int				start	= 0;
	int				l_nl	= 0;
	int				l_sp	= 0;
	int				l_gt	= 0;
	int				stop	= 0;
	int				tags	= 0;
	int				segs	= 0;
	gboolean		intag	= FALSE;

	/*
	 * awful hack to work around the awful hack in pidgin to work around GtkIMHtml's
	 * inefficient rendering of messages with lots of formatting changes.
	 * (reference: see the function pidgin_conv_write_conv() in gtkconv.c) the issue
	 * is that when you have more than 100 '<' characters in the message passed to
	 * pidgin, none of the markup (including links) are rendered and thus just dump
	 * all the text as is to the conversation window. this message dump is very
	 * confusing and makes it totally unusable. to work around this we will count
	 * the amount of tags and if its more than the pidgin threshold, we will just
	 * break the message up into smaller parts and send them seperately to pidgin.
	 * to the user it will look like multiple messages, but at least he will be able
	 * to use and understand it.
	 */

	ch = mx->msg->str;
	pos = start;
	while ( ch[pos] ) {

		if ( ch[pos] == '<' ) {
			tags++;
			intag = TRUE;
		}
		else if ( ch[pos] == '\n' ) {
			l_nl = pos;
		}
		else if ( ch[pos] == '>' ) {
			l_gt = pos;
			intag = FALSE;
		}
		else if ( ch[pos] == ' ' ) {
			/* ignore spaces inside tags */
			if ( !intag )
				l_sp = pos;
		}
		else if ( ( ch[pos] == 'w' ) && ( pos + 4 < mx->msg->len ) && ( memcmp( &ch[pos], "www.", 4 ) == 0 ) ) {
			tags += 2;
		}
		else if ( ( ch[pos] == 'h' ) && ( pos + 8 < mx->msg->len ) && ( memcmp( &ch[pos], "http://", 7 ) == 0 ) ) {
			tags += 2;
		}

		if ( tags > MXIT_MAX_MSG_TAGS ) {
			/* we have reached the maximum amount of tags pidgin (gtk) can handle per message.
			   so its time to send what we have and then start building a new message */

			/* now find the right place to break the message */
			if ( l_nl > start ) {
				/* break at last '\n' char */
				stop = l_nl;
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = '\n';
			}
			else if ( l_sp > start ) {
				/* break at last ' ' char */
				stop = l_sp;
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = ' ';
			}
			else {
				/* break at the last '>' char */
				char t;
				stop = l_gt + 1;
				t = ch[stop];
				ch[stop] = '\0';
				msg = g_string_new( &ch[start] );
				ch[stop] = t;
				stop--;
			}

			/* build the string */
			if ( segs )
				g_string_prepend( msg, cont );

			/* push message to pidgin */
			serv_got_im( mx->session->con, mx->from, msg->str, mx->flags, mx->timestamp );
			g_string_free( msg, TRUE );
			msg = NULL;

			tags = 0;
			segs++;
			start = stop + 1;
		}

		pos++;
	}

	if ( start != pos ) {
		/* send the last part of the message */

		/* build the string */
		ch[pos] = '\0';
		msg = g_string_new( &ch[start] );
		ch[pos] = '\n';
		if ( segs )
			g_string_prepend( msg, cont );

		/* push message to pidgin */
		serv_got_im( mx->session->con, mx->from, msg->str, mx->flags, mx->timestamp );
		g_string_free( msg, TRUE );
		msg = NULL;
	}
}


/*------------------------------------------------------------------------
 * Insert custom emoticons and inline images into the message (if there
 * are any), then give the message to the UI to display to the user.
 *
 *  @param mx				The received message object
 */
void mxit_show_message( struct RXMsgData* mx )
{
	char*				pos;
	int					start;
	unsigned int		end;
	int					emo_ofs;
	char				ii[128];
	char				tag[64];
	int*				img_id;

	if ( mx->got_img ) {
		/* search and replace all emoticon tags with proper image tags */

		while ( ( pos = strstr( mx->msg->str, MXIT_II_TAG ) ) != NULL ) {
			start = pos - mx->msg->str;					/* offset at which MXIT_II_TAG starts */
			emo_ofs = start + strlen( MXIT_II_TAG );	/* offset at which EMO's ID starts */
			end = emo_ofs + 1;							/* offset at which MXIT_II_TAG ends */

			while ( ( end < mx->msg->len ) && ( mx->msg->str[end] != '>' ) )
				end++;

			if ( end == mx->msg->len )			/* end of emoticon tag not found */
				break;

			memset( ii, 0x00, sizeof( ii ) );
			memcpy( ii, &mx->msg->str[emo_ofs], end - emo_ofs );

			/* remove inline image tag */
			g_string_erase( mx->msg, start, ( end - start ) + 1 );

			/* find the image entry */
			img_id = (int*) g_hash_table_lookup( mx->session->iimages, ii );
			if ( !img_id ) {
				/* inline image not found, so we will just skip it */
				purple_debug_error( MXIT_PLUGIN_ID, "inline image NOT found (%s)\n", ii );
			}
			else {
				/* insert img tag */
				g_snprintf( tag, sizeof( tag ), "<img id=\"%i\">", *img_id );
				g_string_insert( mx->msg, start, tag );
			}
		}
	}

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup RX (converted): '%s'\n", mx->msg->str );
#endif

	if ( mx->processed ) {
		/* this message has already been taken care of, so just ignore it here */
	}
	else if ( mx->chatid < 0 ) {
		/* normal chat message */
		mxit_show_split_message( mx );
	}
	else {
		/* this is a multimx message */
		serv_got_chat_in( mx->session->con, mx->chatid, mx->from, mx->flags, mx->msg->str, mx->timestamp);
	}

	/* freeup resource */
	free_markupdata( mx );
}


/*------------------------------------------------------------------------
 * Extract the custom emoticon ID from the message.
 *
 *  @param message			The input data
 *  @param emid				The extracted emoticon ID
 */
static void parse_emoticon_str( const char* message, char* emid )
{
	int		i;

	for ( i = 0; ( message[i] != '\0' && message[i] != '}' && i < MXIT_MAX_EMO_ID ); i++ ) {
		emid[i] = message[i];
	}

	if ( message[i] == '\0' ) {
		/* end of message reached, ignore the tag */
		emid[0] = '\0';
	}
	else if ( i == MXIT_MAX_EMO_ID ) {
		/* invalid tag length, ignore the tag */
		emid[0] = '\0';
	}
	else
		emid[i] = '\0';
}


/*------------------------------------------------------------------------
 * Callback function invoked when a custom emoticon request to the WAP site completes.
 *
 *  @param url_data
 *  @param user_data		The Markup message object
 *  @param url_text			The data returned from the WAP site
 *  @param len				The length of the data returned
 *  @param error_message	Descriptive error message
 */
static void emoticon_returned( PurpleUtilFetchUrlData* url_data, gpointer user_data, const gchar* url_text, gsize len, const gchar* error_message )
{
	struct RXMsgData*	mx			= (struct RXMsgData*) user_data;
	const char*			data		= url_text;
	unsigned int		pos			= 0;
	char				emo[16];
	int					id;
	char*				str;
	int					em_size		= 0;
	char*				em_data		= NULL;
	char*				em_id		= NULL;
	int*				intptr		= NULL;
	int					res;

#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "emoticon_returned\n" );
#endif

	if ( !url_text ) {
		/* no reply from the WAP site */
		purple_debug_error( MXIT_PLUGIN_ID, "Error contacting the MXit WAP site. Please try again later (emoticon).\n" );
		goto done;
	}

#ifdef	MXIT_DEBUG_EMO
	hex_dump( data, len );
#endif

	/* parse out the emoticon */
	pos = 0;

	/* validate the binary data received from the wapsite */
	if ( memcmp( MXIT_FRAME_MAGIC, &data[pos], strlen( MXIT_FRAME_MAGIC ) ) != 0 ) {
		/* bad data, magic constant is wrong */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad magic)\n" );
		goto done;
	}
	pos += strlen( MXIT_FRAME_MAGIC );

	/* validate the image frame desc byte */
	if ( data[pos] != '\x6F' ) {
		/* bad frame desc */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad frame desc)\n" );
		goto done;
	}
	pos++;

	/* get the data length */
	res = asn_getlength( &data[pos], &em_size );
	if ( res <= 0 ) {
		/* bad frame length */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad frame length)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the length '%i'\n", em_size );
#endif

	/* utf-8 (emoticon name) */
	res = asn_getUtf8( &data[pos], 0x0C, &str );
	if ( res <= 0 ) {
		/* bad utf-8 string */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad name string)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the string '%s'\n", str );
#endif
	g_free( str );
	str = NULL;

	/* utf-8 (emoticon shortcut) */
	res = asn_getUtf8( &data[pos], 0x81, &str );
	if ( res <= 0 ) {
		/* bad utf-8 string */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad shortcut string)\n" );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the string '%s'\n", str );
#endif
	em_id = str;

	/* validate the image data type */
	if ( data[pos] != '\x82' ) {
		/* bad frame desc */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad data type)\n" );
		g_free( em_id );
		goto done;
	}
	pos++;

	/* get the data length */
	res = asn_getlength( &data[pos], &em_size );
	if ( res <= 0 ) {
		/* bad frame length */
		purple_debug_error( MXIT_PLUGIN_ID, "Invalid emoticon received from wapsite (bad data length)\n" );
		g_free( em_id );
		goto done;
	}
	pos += res;
#ifdef	MXIT_DEBUG_EMO
	purple_debug_info( MXIT_PLUGIN_ID, "read the length '%i'\n", em_size );
#endif

	if ( g_hash_table_lookup( mx->session->iimages, em_id ) ) {
		/* emoticon found in the table, so ignore this one */
		goto done;
	}

	/* make a copy of the data */
	em_data = g_malloc( em_size );
	memcpy( em_data, &data[pos], em_size );

	/* strip the mxit markup tags from the emoticon id */
	if ( ( em_id[0] == '.' ) && ( em_id[1] == '{' ) ) {
		parse_emoticon_str( &em_id[2], emo );
		strcpy( em_id, emo );
	}

	/* we now have the emoticon, store it in the imagestore */
	id = purple_imgstore_add_with_id( em_data, em_size, NULL );

	/* map the mxit emoticon id to purple image id */
	intptr = g_malloc( sizeof( int ) );
	*intptr = id;
	g_hash_table_insert( mx->session->iimages, em_id, intptr );

	mx->flags |= PURPLE_MESSAGE_IMAGES;
done:
	mx->img_count--;
	if ( ( mx->img_count == 0 ) && ( mx->converted ) ) {
		/*
		 * this was the last outstanding emoticon for this message,
		 * so we can now display it to the user.
		 */
		mxit_show_message( mx );
	}
}


/*------------------------------------------------------------------------
 * Send a request to the MXit WAP site to download the specified emoticon.
 *
 *  @param mx				The Markup message object
 *  @param id				The ID for the emoticon
 */
static void emoticon_request( struct RXMsgData* mx, const char* id )
{
	PurpleUtilFetchUrlData*	url_data;
	const char*				wapserver;
	char*					url;

	purple_debug_info( MXIT_PLUGIN_ID, "sending request for emoticon '%s'\n", id );

	wapserver = purple_account_get_string( mx->session->acc, MXIT_CONFIG_WAPSERVER, DEFAULT_WAPSITE );

	/* reference: "libpurple/util.h" */
	url = g_strdup_printf( "%s/res/?type=emo&mlh=%i&sc=%s&ts=%li", wapserver, MXIT_EMOTICON_SIZE, id, time( NULL ) );
	url_data = purple_util_fetch_url_request( url, TRUE, NULL, TRUE, NULL, FALSE, emoticon_returned, mx );
	g_free( url );
}


/*------------------------------------------------------------------------
 * Parse a Vibe command.
 *
 *  @param mx				The Markup message object
 *  @param message			The message text (which contains the vibe)
 *  @return id				The length of the message to skip
 */
static int mxit_parse_vibe( struct RXMsgData* mx, const char* message )
{
	int		vibeid;

	vibeid = message[2] - '0';

	purple_debug_info( MXIT_PLUGIN_ID, "Vibe received (%i)\n", vibeid );

	if ( vibeid > ( ARRAY_SIZE( vibes ) - 1 ) ) {
		purple_debug_warning( MXIT_PLUGIN_ID, "Unsupported vibe received (%i)\n", vibeid );
		/* unsupported vibe */
		return 0;
	}

	g_string_append_printf( mx->msg, "<font color=\"%s\"><i>%s Vibe...</i></font>", MXIT_VIBE_MSG_COLOR, _( vibes[vibeid] ) );
	return 2;
}


/*------------------------------------------------------------------------
 * Extract the nickname from a chatroom message and display it nicely in
 * libPurple-style (HTML) markup.
 *
 *  @param mx				The received message data object
 *  @param message			The message text
 *  @return					The length of the message to skip
 */
static int mxit_extract_chatroom_nick( struct RXMsgData* mx, char* message, int len )
{
	int		i;

	if ( message[0] == '<' ) {
		/*
		 * The message MIGHT contains an embedded nickname.  But we can't
		 * be sure unless we find the end-of-nickname sequence: (>\n)
		 * Search for it....
		 */
		gboolean	found	= FALSE;
		gchar*		nickname;

		for ( i = 1; i < len; i++ ) {
			if ( ( message[i] == '\n' ) && ( message[i-1] == '>' ) ) {
				found = TRUE;
				message[i-1] = '\0';	/* loose the '>' */
				i++;					/* and skip the new-line */
				break;
			}
		}

		if ( found ) {
			/*
			 * The message definitely had an embedded nickname - generate a marked-up
			 * message to be displayed.
			 */
			nickname = g_markup_escape_text( &message[1], -1 );

			/* add nickname within some BOLD markup to the new converted message */
			g_string_append_printf( mx->msg, "<b>%s:</b> ", nickname );

			/* free up the resources */
			g_free( nickname );

			return i;
		}
	}

	return 0;
}



/*------------------------------------------------------------------------
 * Convert a message containing MXit protocol markup to libPurple-style (HTML) markup.
 *
 *  @param mx				The received message data object
 *  @param message			The message text
 *  @param len				The length of the message
 */
void mxit_parse_markup( struct RXMsgData* mx, char* message, int len, short msgtype, int msgflags )
{
	char		tmpstr1[128];
	char*		ch;
	int			i			= 0;

	/* tags */
	gboolean	tag_bold	= FALSE;
	gboolean	tag_under	= FALSE;
	gboolean	tag_italic	= FALSE;

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup RX (original): '%s'\n", message );
#endif


	/*
	 * supported MXit markup:
	 * '*'			bold
	 * '_'			underline
	 * '/'			italics
	 * '$'			highlight text
	 * '.+' 		inc font size
	 * '.-'			dec font size
	 * '#XXXXXX'	foreground color
	 * '.{XX}'		custom emoticon
	 * '\'			escape the following character
	 * '::'			MXit commands
	 */


	if ( is_mxit_chatroom_contact( mx->session, mx->from ) ) {
		/* chatroom message, so we need to extract and skip the sender's nickname
		 * which is embedded inside the message */
		i = mxit_extract_chatroom_nick( mx, message, len );
	}

	/* run through the message and check for custom emoticons and markup */
	for ( ; i < len; i++ ) {
		switch ( message[i] ) {


			/* mxit markup parsing */
			case '*' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* bold markup */
					if ( !tag_bold )
						g_string_append( mx->msg, "<b>" );
					else
						g_string_append( mx->msg, "</b>" );
					tag_bold = !tag_bold;
					break;
			case '_' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* underscore markup */
					if ( !tag_under )
						g_string_append( mx->msg, "<u>" );
					else
						g_string_append( mx->msg, "</u>" );
					tag_under = !tag_under;
					break;
			case '/' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}

					/* italics markup */
					if ( !tag_italic )
						g_string_append( mx->msg, "<i>" );
					else
						g_string_append( mx->msg, "</i>" );
					tag_italic = !tag_italic;
					break;
			case '$' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + 1 >= len ) {
						/* message too short for complete link */
						g_string_append_c( mx->msg, '$' );
						break;
					}

					/* find the end tag */
					ch = strstr( &message[i + 1], "$" );
					if ( ch ) {
						/* end found */
						*ch = '\0';
						mxit_add_html_link( mx, &message[i + 1], &message[i + 1] );
						*ch = '$';
						i += ( ch - &message[i + 1] ) + 1;
					}
					else {
						g_string_append_c( mx->msg, message[i] );
					}
					/* highlight text */
					break;
			case '#' :
					if ( !( msgflags & CP_MSG_MARKUP ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + COLORCODE_LEN >= len ) {
						/* message too short for complete colour code */
						g_string_append_c( mx->msg, '#' );
						break;
					}

					/* foreground (text) color */
					memcpy( tmpstr1, &message[i + 1], COLORCODE_LEN );
					tmpstr1[ COLORCODE_LEN ] = '\0';			/* terminate string */
					if ( strcmp( tmpstr1, "??????" ) == 0 ) {
						/* need to reset the font */
						g_string_append( mx->msg, "</font>" );
						i += COLORCODE_LEN;
					}
					else if ( strspn( tmpstr1, "0123456789abcdefABCDEF") == COLORCODE_LEN ) {
						/* definitely a numeric colour code */
						g_string_append_printf( mx->msg, "<font color=\"#%s\">", tmpstr1 );
						i += COLORCODE_LEN;
					}
					else {
						/* not valid colour markup */
						g_string_append_c( mx->msg, '#' );
					}
					break;
			case '.' :
					if ( !( msgflags & CP_MSG_EMOTICON ) ) {
						g_string_append_c( mx->msg, message[i] );
						break;
					}
					else if ( i + 1 >= len ) {
						/* message too short */
						g_string_append_c( mx->msg, '.' );
						break;
					}

					switch ( message[i+1] ) {
						case '+' :
								/* increment text size */
								g_string_append( mx->msg, "<font size=\"+1\">" );
								i++;
								break;
						case '-' :
								/* decrement text size */
								g_string_append( mx->msg, "<font size=\"-1\">" );
								i++;
								break;
						case '{' :
								/* custom emoticon */
								if ( i + 2 >= len ) {
									/* message too short */
									g_string_append_c( mx->msg, '.' );
									break;
								}

								parse_emoticon_str( &message[i+2], tmpstr1 );
								if ( tmpstr1[0] != '\0' ) {
									mx->got_img = TRUE;

									if ( g_hash_table_lookup( mx->session->iimages, tmpstr1 ) ) {
										/* emoticon found in the cache, so we do not have to request it from the WAPsite */
									}
									else {
										/* request emoticon from the WAPsite */
										mx->img_count++;
										emoticon_request( mx, tmpstr1 );
									}

									g_string_append_printf( mx->msg, MXIT_II_TAG"%s>", tmpstr1 );
									i += strlen( tmpstr1 ) + 2;
								}
								else
									g_string_append_c( mx->msg, '.' );

								break;
						default :
								g_string_append_c( mx->msg, '.' );
								break;
					}
					break;
			case '\\' :
					if ( i + 1 >= len ) {
						/* message too short for an escaped character */
						g_string_append_c( mx->msg, '\\' );
					}
					else {
						/* ignore the next character, because its been escaped */
						g_string_append_c( mx->msg, message[i + 1] );
						i++;
					}
					break;


			/* command parsing */
			case ':' :
					if ( i + 1 >= len ) {
						/* message too short */
						g_string_append_c( mx->msg, ':' );
						break;
					}

					if ( message[i+1] == '@' ) {
						/* this is a vibe! */
						int		size;

						if ( i + 2 >= len ) {
							/* message too short */
							g_string_append_c( mx->msg, message[i] );
							break;
						}

						size = mxit_parse_vibe( mx, &message[i] );
						if ( size == 0 )
							g_string_append_c( mx->msg, message[i] );
						else
							i += size;
					}
					else if ( msgtype != CP_MSGTYPE_COMMAND ) {
						/* this is not a command message */
						g_string_append_c( mx->msg, message[i] );
					}
					else if ( message[i+1] == ':' ) {
						/* parse out the command */
						int		size;

						size = mxit_parse_command( mx, &message[i] );
						if ( size == 0 )
							g_string_append_c( mx->msg, ':' );
						else
							i += size;
					}
					else {
						g_string_append_c( mx->msg, ':' );
					}
					break;


			/* these aren't MXit markup, but are interpreted by libPurple */
			case '<' :
					g_string_append( mx->msg, "&lt;" );
					break;
			case '>' :
					g_string_append( mx->msg, "&gt;" );
					break;
			case '&' :
					g_string_append( mx->msg, "&amp;" );
					break;
			case '"' :
					g_string_append( mx->msg, "&quot;" );
					break;

			default :
					/* text */
					g_string_append_c( mx->msg, message[i] );
					break;
		}
	}
}


/*------------------------------------------------------------------------
 * Insert an inline image command.
 *
 *  @param mx				The message text as processed so far.
 *  @oaram id				The imgstore ID of the inline image.
 */
static void inline_image_add( GString* mx, int id )
{
	PurpleStoredImage *image;
	gconstpointer img_data;
	gsize img_size;	
	gchar* enc;

	image = purple_imgstore_find_by_id( id );
	if ( image == NULL )
		return;

	img_data = purple_imgstore_get_data( image );
	img_size = purple_imgstore_get_size( image );

	enc = purple_base64_encode( img_data, img_size );

	g_string_append( mx, "::op=img|dat=" );
	g_string_append( mx, enc );
	g_string_append_c( mx, ':' );

	g_free( enc );
}


/*------------------------------------------------------------------------
 * Convert libpurple (HTML) markup to MXit protocol markup (for sending to MXit).
 * Any MXit markup codes in the original message also need to be escaped.
 *
 *  @param message			The message text containing libPurple (HTML) markup
 *  @return					The message text containing MXit markup
 */
char* mxit_convert_markup_tx( const char* message, int* msgtype )
{
	GString*			mx;
	struct tag*			tag;
	GList*				entry;
	GList*				tagstack	= NULL;
	char*				reply;
	char				color[8];
	int					len			= strlen ( message );
	int					i;

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup TX (original): '%s'\n", message );
#endif

	/*
	 * libPurple uses the following HTML markup codes:
	 *   Bold:			<b>...</b>
	 *   Italics:		<i>...</i>
	 *   Underline:		<u>...</u>
	 *   Strikethrough:	<s>...</s>					(NO MXIT SUPPORT)
	 *   Font size:		<font size="">...</font>
	 *   Font type:		<font face="">...</font>	(NO MXIT SUPPORT)
	 *   Font colour:	<font color=#">...</font>
	 *   Links:			<a href="">...</a>
	 *   Newline:		<br>
	 *   Inline image:  <IMG ID="">
	 * The following characters are also encoded:
	 *   &amp;  &quot;  &lt;  &gt;
	 */

	/* new message data */
	mx = g_string_sized_new( len );

	/* run through the message and check for HTML markup */
	for ( i = 0; i < len; i++ ) {

		switch ( message[i] ) {
			case '<' :
				if ( purple_str_has_prefix( &message[i], "<b>" ) || purple_str_has_prefix( &message[i], "</b>" ) ) {
					/* bold */
					g_string_append_c( mx, '*' );
				}
				else if ( purple_str_has_prefix( &message[i], "<i>" ) || purple_str_has_prefix( &message[i], "</i>" ) ) {
					/* italics */
					g_string_append_c( mx, '/' );
				}
				else if ( purple_str_has_prefix( &message[i], "<u>" ) || purple_str_has_prefix( &message[i], "</u>" ) ) {
					/* underline */
					g_string_append_c( mx, '_' );
				}
				else if ( purple_str_has_prefix( &message[i], "<br>" ) ) {
					/* newline */
					g_string_append_c( mx, '\n' );
				}
				else if ( purple_str_has_prefix( &message[i], "<font size=" ) ) {
					/* font size */
					tag = g_new0( struct tag, 1 );
					tag->type = MXIT_TAG_SIZE;
					tagstack = g_list_prepend( tagstack, tag );
					// TODO: implement size control
				}
				else if ( purple_str_has_prefix( &message[i], "<font color=" ) ) {
					/* font colour */
					tag = g_new0( struct tag, 1 );
					tag->type = MXIT_TAG_COLOR;
					tagstack = g_list_append( tagstack, tag );
					memset( color, 0x00, sizeof( color ) );
					memcpy( color, &message[i + 13], 7 );
					g_string_append( mx, color );
				}
				else if ( purple_str_has_prefix( &message[i], "</font>" ) ) {
					/* end of font tag */
					entry = g_list_last( tagstack );
					if ( entry ) {
						tag = entry->data;
						if ( tag->type == MXIT_TAG_COLOR ) {
							/* font color reset */
							g_string_append( mx, "#??????" );
						}
						else if ( tag->type == MXIT_TAG_SIZE ) {
							/* font size */
							// TODO: implement size control
						}
						tagstack = g_list_remove( tagstack, tag );
						g_free( tag );
					}
				}
				else if ( purple_str_has_prefix( &message[i], "<IMG ID=" ) ) {
					/* inline image */
					int imgid;

					if ( sscanf( &message[i+9], "%i", &imgid ) ) {
						inline_image_add( mx, imgid );
						*msgtype = CP_MSGTYPE_COMMAND;		/* inline image must be sent as a MXit command */
					}
				}

				/* skip to end of tag ('>') */
				for ( i++; ( i < len ) && ( message[i] != '>' ) ; i++ );
			
				break;

			case '*' :	/* MXit bold */
			case '_' :	/* MXit underline */
			case '/' :	/* MXit italic */
			case '#' :	/* MXit font color */
			case '$' :	/* MXit highlight text */
			case '\\' :	/* MXit escape backslash */
				g_string_append( mx, "\\" );				/* escape character */
				g_string_append_c( mx, message[i] );		/* character to escape */
				break;

			default:
				g_string_append_c( mx, message[i] );
				break;
		}
	}

	/* unescape HTML entities to their literal characters (reference: "libpurple/utils.h") */
	reply = purple_unescape_html( mx->str );

	g_string_free( mx, TRUE );

#ifdef MXIT_DEBUG_MARKUP
	purple_debug_info( MXIT_PLUGIN_ID, "Markup TX (converted): '%s'\n", reply );
#endif

	return reply;
}


/*------------------------------------------------------------------------
 * Free an emoticon entry.
 *
 *  @param key				MXit emoticon ID
 *  @param value			Imagestore ID for emoticon
 *  @param user_data		NULL (unused)
 *  @return					TRUE
 */
static gboolean emoticon_entry_free( gpointer key, gpointer value, gpointer user_data )
{
	int* imgid = value;

	/* key is a string */
	g_free( key );

	/* value is 'id' in imagestore */
	purple_imgstore_unref_by_id( *imgid );
	g_free( value );

	return TRUE;
}


/*------------------------------------------------------------------------
 * Free all entries in the emoticon cache.
 *
 *  @param session			The MXit session object
 */
void mxit_free_emoticon_cache( struct MXitSession* session )
{
	g_hash_table_foreach_remove( session->iimages, emoticon_entry_free, NULL );
	g_hash_table_destroy ( session->iimages );
}