changeset 19242:f60192409e7f

merge of '0ae78c5d891099ade8e599dc2087ff9411b98d5f' and '615e30de07cd1a72f9af2a7949cd9b4ebdab63a0'
author Jeffrey Connelly <jaconnel@calpoly.edu>
date Tue, 14 Aug 2007 02:45:41 +0000
parents 0651af21ceee (diff) 6ea5602643ea (current diff)
children a242e014ab8c
files libpurple/protocols/myspace/myspace.c pidgin/sounds/Makefile.am pidgin/sounds/Makefile.mingw pidgin/sounds/alert.wav pidgin/sounds/login.wav pidgin/sounds/logout.wav pidgin/sounds/receive.wav pidgin/sounds/send.wav
diffstat 203 files changed, 7992 insertions(+), 4786 deletions(-) [+]
line wrap: on
line diff
--- a/COPYRIGHT	Tue Aug 14 02:45:06 2007 +0000
+++ b/COPYRIGHT	Tue Aug 14 02:45:41 2007 +0000
@@ -278,6 +278,7 @@
 Joao Luís Marques Pinto
 Aleksander Piotrowski
 Julien Pivotto
+Eric Polino <aluink@gmail.com>
 Ari Pollak
 Robey Pointer
 Eric Polino
--- a/ChangeLog	Tue Aug 14 02:45:06 2007 +0000
+++ b/ChangeLog	Tue Aug 14 02:45:41 2007 +0000
@@ -5,10 +5,18 @@
 	* Added an account action to open your inbox in the yahoo prpl.
 	* Added support for Unicode status messages in Yahoo.
 	* Server-stored aliases for Yahoo. (John Moody)
+	* Fixed support for Yahoo! doodling.
+	* Bonjour plugin uses native Avahi instead of Howl
+	* Bonjour plugin supports Buddy Icons
 
 	Pidgin:
 	* Show current outgoing conversation formatting on the font label on
 	  the toolbar
+	* Slim new redesign of conversation tabs to maximize number of
+	  conversations that can fit in a window
+
+	Finch:
+	* Sound support (Eric Polino)
 
 version 2.1.0 (07/28/2007):
 	libpurple:
--- a/ChangeLog.API	Tue Aug 14 02:45:06 2007 +0000
+++ b/ChangeLog.API	Tue Aug 14 02:45:41 2007 +0000
@@ -1,5 +1,15 @@
 Pidgin and Finch: The Pimpin' Penguin IM Clients That're Good for the Soul
 
+Version 2.1.1 (x/x/x):
+	libpurple:
+		Changed:
+		* PurpleAccountUiOps.request_authorize's authorize_cb and
+		  deny_cb parameters now correctly have type
+		  PurpleAccountRequestAuthorizationCb rather than GCallback.
+		  (You'll want to change your UI's implementation's signature
+		  to avoid warnings, and then remove some now-redundant casts
+		  back to the proper type.)
+
 version 2.1.0 (7/28/2007):
 	libpurple:
 		Added:
--- a/ChangeLog.win32	Tue Aug 14 02:45:06 2007 +0000
+++ b/ChangeLog.win32	Tue Aug 14 02:45:41 2007 +0000
@@ -9,6 +9,9 @@
 	  Apple Bonjour for Windows from:
 	  http://www.apple.com/support/downloads/bonjourforwindows.html
 
+version 2.0.1 (5/24/2007):
+	* No changes
+
 version 2.0.0 (5/3/2007):
 	* URI Handler support added via `pidgin.exe --protocolhandler=`
 	* Running a second instance will popup the Buddy List, if possible.
--- a/Doxyfile.in	Tue Aug 14 02:45:06 2007 +0000
+++ b/Doxyfile.in	Tue Aug 14 02:45:41 2007 +0000
@@ -300,7 +300,7 @@
 # by member name. If set to NO (the default) the members will appear in 
 # declaration order.
 
-SORT_BRIEF_DOCS        = YES
+SORT_BRIEF_DOCS        = NO
 
 # If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be 
 # sorted by fully-qualified names, including namespaces. If set to 
--- a/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -42,7 +42,7 @@
 GNT_DIR=finch
 endif
 
-SUBDIRS = libpurple doc $(GNT_DIR) $(GTK_DIR) m4macros po
+SUBDIRS = libpurple doc $(GNT_DIR) $(GTK_DIR) m4macros po share
 
 docs: Doxyfile
 if HAVE_DOXYGEN
--- a/Makefile.mingw	Tue Aug 14 02:45:06 2007 +0000
+++ b/Makefile.mingw	Tue Aug 14 02:45:41 2007 +0000
@@ -67,6 +67,7 @@
 	$(MAKE) -C $(PURPLE_TOP) -f $(MINGW_MAKEFILE) install
 	$(MAKE) -C $(PIDGIN_TOP) -f $(MINGW_MAKEFILE) install
 	$(MAKE) -C $(PURPLE_PO_TOP) -f $(MINGW_MAKEFILE) install
+	$(MAKE) -C share -f $(MINGW_MAKEFILE) install
 
 create_release_install_dir: install
 	rm -rf $(PIDGIN_INSTALL_DIR).release
--- a/configure.ac	Tue Aug 14 02:45:06 2007 +0000
+++ b/configure.ac	Tue Aug 14 02:45:41 2007 +0000
@@ -54,9 +54,9 @@
 
 m4_define([gnt_lt_current], [1])
 m4_define([gnt_major_version], [2])
-m4_define([gnt_minor_version], [0])
+m4_define([gnt_minor_version], [1])
 m4_define([gnt_micro_version], [0])
-m4_define([gnt_version_suffix], [])
+m4_define([gnt_version_suffix], [devel])
 m4_define([gnt_version],
           [gnt_major_version.gnt_minor_version.gnt_micro_version])
 m4_define([gnt_display_version], gnt_version[]m4_ifdef([gnt_version_suffix],[gnt_version_suffix]))
@@ -2090,6 +2090,8 @@
 	AC_DEFINE(DEBUG, 1, [Define if debugging is enabled.])
 fi
 
+AM_CONDITIONAL(PURPLE_AVAILABLE, true)
+
 AC_OUTPUT([Makefile
 		   Doxyfile
 		   doc/Makefile
@@ -2168,7 +2170,6 @@
 		   pidgin/plugins/perl/Makefile
 		   pidgin/plugins/perl/common/Makefile.PL
 		   pidgin/plugins/ticker/Makefile
-		   pidgin/sounds/Makefile
 		   libpurple/example/Makefile
 		   libpurple/gconf/Makefile
 		   libpurple/purple.pc
@@ -2202,6 +2203,8 @@
 		   libpurple/protocols/zephyr/Makefile
 		   libpurple/tests/Makefile
 		   libpurple/version.h
+		   share/Makefile
+		   share/sounds/Makefile
 		   finch/Makefile
 		   finch/libgnt/Makefile
 		   finch/libgnt/gnt.pc
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/ui-ops.dox	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,24 @@
+/** @page ui-ops UiOps structures
+
+  When implementing a UI for libpurple, you need to fill in various UiOps
+  structures:
+
+   - #PurpleAccountUiOps
+   - #PurpleBlistUiOps
+   - #PurpleConnectionUiOps
+   - #PurpleConversationUiOps
+   - #PurpleCoreUiOps
+   - #PurpleDebugUiOps
+   - #PurpleDnsQueryUiOps
+   - #PurpleEventLoopUiOps
+   - #PurpleIdleUiOps
+   - #PurpleNotifyUiOps
+   - #PurplePrivacyUiOps
+   - #PurpleRequestUiOps
+   - #PurpleRoomlistUiOps
+   - #PurpleSoundUiOps
+   - #PurpleWhiteboardUiOps
+   - #PurpleXferUiOps
+
+ */
+// vim: ft=c.doxygen
--- a/finch/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -60,6 +60,7 @@
 	$(GLIB_LIBS) \
 	$(LIBXML_LIBS) \
 	$(GNT_LIBS) \
+	$(GSTREAMER_LIBS) \
 	./libgnt/libgnt.la \
 	$(top_builddir)/libpurple/libpurple.la
 
@@ -77,4 +78,5 @@
 	$(GLIB_CFLAGS) \
 	$(DBUS_CFLAGS) \
 	$(LIBXML_CFLAGS) \
+	$(GSTREAMER_CFLAGS) \
 	$(GNT_CFLAGS)
--- a/finch/finch.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/finch.c	Tue Aug 14 02:45:41 2007 +0000
@@ -55,16 +55,10 @@
 	purple_debug_set_ui_ops(finch_debug_get_ui_ops());
 }
 
-/* XXX: this "leaks" a hashtable on shutdown.  I'll let
- * the finch guys decide if they want to go through the trouble
- * of properly freeing it, since their quit function doesn't
- * live in this file */
-
 static GHashTable *ui_info = NULL;
-
 static GHashTable *finch_ui_get_info()
 {
-	if(NULL == ui_info) {
+	if (ui_info == NULL) {
 		ui_info = g_hash_table_new(g_str_hash, g_str_equal);
 
 		g_hash_table_insert(ui_info, "name", (char*)_("Finch"));
@@ -74,12 +68,20 @@
 	return ui_info;
 }
 
+static void
+finch_quit(void)
+{
+	gnt_ui_uninit();
+	if (ui_info)
+		g_hash_table_destroy(ui_info);
+}
+
 static PurpleCoreUiOps core_ops =
 {
 	finch_prefs_init,
 	debug_init,
 	gnt_ui_init,
-	gnt_ui_uninit,
+	finch_quit,
 	finch_ui_get_info,
 
 	/* padding */
@@ -396,18 +398,29 @@
 	return 1;
 }
 
-int main(int argc, char **argv)
+static gboolean gnt_start(int *argc, char ***argv)
+{
+	/* Initialize the libpurple stuff */
+	if (!init_libpurple(*argc, *argv))
+		return FALSE;
+ 
+	purple_blist_show();
+	return TRUE;
+}
+
+int main(int argc, char *argv[])
 {
 	signal(SIGPIPE, SIG_IGN);
 
 	g_set_prgname("Finch");
+#if GLIB_CHECK_VERSION(2,2,0)
 	g_set_application_name(_("Finch"));
+#endif
 
-	/* Initialize the libpurple stuff */
-	if (!init_libpurple(argc, argv))
-		return 0;
- 
-	purple_blist_show();
+	gnt_init();
+
+	gnt_start(&argc, &argv);
+
 	gnt_main();
 
 #ifdef STANDALONE
--- a/finch/finch.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/finch.h	Tue Aug 14 02:45:41 2007 +0000
@@ -27,3 +27,4 @@
 
 #define FINCH_UI "gnt-purple"
 
+#define FINCH_PREFS_ROOT "/finch"
--- a/finch/gntaccount.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/gntaccount.c	Tue Aug 14 02:45:41 2007 +0000
@@ -912,9 +912,15 @@
 }
 
 static void *
-finch_request_authorize(PurpleAccount *account, const char *remote_user,
-					const char *id, const char *alias, const char *message, gboolean on_list,
-					GCallback auth_cb, GCallback deny_cb, void *user_data)
+finch_request_authorize(PurpleAccount *account,
+                        const char *remote_user,
+                        const char *id,
+                        const char *alias,
+                        const char *message,
+                        gboolean on_list,
+                        PurpleAccountRequestAuthorizationCb auth_cb,
+                        PurpleAccountRequestAuthorizationCb deny_cb,
+                        void *user_data)
 {
 	char *buffer;
 	PurpleConnection *gc;
@@ -941,8 +947,8 @@
 		GList *iter;
 		auth_and_add *aa = g_new(auth_and_add, 1);
 
-		aa->auth_cb = (PurpleAccountRequestAuthorizationCb)auth_cb;
-		aa->deny_cb = (PurpleAccountRequestAuthorizationCb)deny_cb;
+		aa->auth_cb = auth_cb;
+		aa->deny_cb = deny_cb;
 		aa->data = user_data;
 		aa->username = g_strdup(remote_user);
 		aa->alias = g_strdup(alias);
--- a/finch/gntconv.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/gntconv.h	Tue Aug 14 02:45:41 2007 +0000
@@ -30,6 +30,9 @@
 
 #include "conversation.h"
 
+/* Grabs the conv out of a PurpleConverstation */
+#define FINCH_CONV(conv) ((FinchConv *)(conv)->ui_data)
+
 /***************************************************************************
  * @name GNT Conversations API
  ***************************************************************************/
--- a/finch/gntsound.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/gntsound.c	Tue Aug 14 02:45:41 2007 +0000
@@ -23,28 +23,1052 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 #include "internal.h"
+#include "finch.h"
+
+#ifdef _WIN32
+#include <windows.h>
+#include <mmsystem.h>
+#endif
+
+#ifdef USE_GSTREAMER
+#include <gst/gst.h>
+#endif /* USE_GSTREAMER */
+
+#include "debug.h"
+#include "notify.h"
+#include "prefs.h"
+#include "sound.h"
+#include "util.h"
+
+#include "gntbox.h"
+#include "gntwindow.h"
+#include "gntcombobox.h"
+#include "gntlabel.h"
+#include "gntconv.h"
 #include "gntsound.h"
+#include "gntwidget.h"
+#include "gntentry.h"
+#include "gntcheckbox.h"
+#include "gntline.h"
+#include "gntslider.h"
+#include "gnttree.h"
+#include "gntfilesel.h"
+
+typedef struct {
+	PurpleSoundEventID id;
+	char *label;
+	char *pref;
+	char *def;
+	char *file;
+} FinchSoundEvent;
+
+typedef struct {
+	GntWidget *method;
+	GntWidget *command;
+	GntWidget *conv_focus;
+	GntWidget *while_status;
+	GntWidget *volume;
+	GntWidget *events;
+	GntWidget *window;
+	GntWidget *selector;
+
+	GntWidget *profiles;
+	GntWidget *new_profile;
+	gchar * original_profile;
+} SoundPrefDialog;
+
+#define DEFAULT_PROFILE "default"
+
+static SoundPrefDialog *pref_dialog;
+
+#define PLAY_SOUND_TIMEOUT 15000
+
+static guint mute_login_sounds_timeout = 0;
+static gboolean mute_login_sounds = FALSE;
+
+#ifdef USE_GSTREAMER
+static gboolean gst_init_failed;
+#endif /* USE_GSTREAMER */
+
+static FinchSoundEvent sounds[PURPLE_NUM_SOUNDS] = {
+	{PURPLE_SOUND_BUDDY_ARRIVE, N_("Buddy logs in"), "login", "login.wav", NULL},
+	{PURPLE_SOUND_BUDDY_LEAVE,  N_("Buddy logs out"), "logout", "logout.wav", NULL},
+	{PURPLE_SOUND_RECEIVE,      N_("Message received"), "im_recv", "receive.wav", NULL},
+	{PURPLE_SOUND_FIRST_RECEIVE, N_("Message received begins conversation"), "first_im_recv", "receive.wav", NULL},
+	{PURPLE_SOUND_SEND,         N_("Message sent"), "send_im", "send.wav", NULL},
+	{PURPLE_SOUND_CHAT_JOIN,    N_("Person enters chat"), "join_chat", "login.wav", NULL},
+	{PURPLE_SOUND_CHAT_LEAVE,   N_("Person leaves chat"), "left_chat", "logout.wav", NULL},
+	{PURPLE_SOUND_CHAT_YOU_SAY, N_("You talk in chat"), "send_chat_msg", "send.wav", NULL},
+	{PURPLE_SOUND_CHAT_SAY,     N_("Others talk in chat"), "chat_msg_recv", "receive.wav", NULL},
+	{PURPLE_SOUND_POUNCE_DEFAULT, NULL, "pounce_default", "alert.wav", NULL},
+	{PURPLE_SOUND_CHAT_NICK,    N_("Someone says your screen name in chat"), "nick_said", "alert.wav", NULL}
+};
+
+const char *
+finch_sound_get_active_profile()
+{
+	return purple_prefs_get_string(FINCH_PREFS_ROOT "/sound/actprofile");
+}
 
-const char *finch_sound_get_active_profile(void)
+/* This method creates a pref name based on the current active profile.
+ * So if "Home" is the current active profile the pref name
+ * [FINCH_PREFS_ROOT "/sound/profiles/Home/$NAME"] is created.
+ */
+static gchar *
+make_pref(const char *name)
+{
+	static char pref_string[512];
+	g_snprintf(pref_string, sizeof(pref_string),
+			FINCH_PREFS_ROOT "/sound/profiles/%s%s", finch_sound_get_active_profile(), name);
+	return pref_string;
+}
+
+
+static gboolean
+unmute_login_sounds_cb(gpointer data)
+{
+	mute_login_sounds = FALSE;
+	mute_login_sounds_timeout = 0;
+	return FALSE;
+}
+
+static gboolean
+chat_nick_matches_name(PurpleConversation *conv, const char *aname)
 {
-	return NULL;
+	PurpleConvChat *chat = NULL;
+	char *nick = NULL;
+	char *name = NULL;
+	gboolean ret = FALSE;
+	chat = purple_conversation_get_chat_data(conv);
+
+	if (chat == NULL)
+		return ret;
+
+	nick = g_strdup(purple_normalize(conv->account, chat->nick));
+	name = g_strdup(purple_normalize(conv->account, aname));
+
+	if (g_utf8_collate(nick, name) == 0)
+		ret = TRUE;
+
+	g_free(nick);
+	g_free(name);
+
+	return ret;
+}
+
+/*
+ * play a sound event for a conversation, honoring make_sound flag
+ * of conversation and checking for focus if conv_focus pref is set
+ */
+static void
+play_conv_event(PurpleConversation *conv, PurpleSoundEventID event)
+{
+	/* If we should not play the sound for some reason, then exit early */
+	if (conv != NULL)
+	{
+		FinchConv *gntconv;
+		gboolean has_focus;
+
+		gntconv = FINCH_CONV(conv);
+
+		has_focus = purple_conversation_has_focus(conv);
+
+		if (has_focus && !purple_prefs_get_bool(make_pref("/conv_focus")))
+		{
+			return;
+		}
+	}
+
+	purple_sound_play_event(event, conv ? purple_conversation_get_account(conv) : NULL);
+}
+
+static void
+buddy_state_cb(PurpleBuddy *buddy, PurpleSoundEventID event)
+{
+	purple_sound_play_event(event, purple_buddy_get_account(buddy));
+}
+
+static void
+im_msg_received_cb(PurpleAccount *account, char *sender,
+				   char *message, PurpleConversation *conv,
+				   PurpleMessageFlags flags, PurpleSoundEventID event)
+{
+	if (flags & PURPLE_MESSAGE_DELAYED)
+		return;
+
+	if (conv == NULL) {
+		purple_sound_play_event(PURPLE_SOUND_FIRST_RECEIVE, account);
+	} else {
+		play_conv_event(conv, event);
+	}
 }
 
-void finch_sound_set_active_profile(const char *name)
+static void
+im_msg_sent_cb(PurpleAccount *account, const char *receiver,
+			   const char *message, PurpleSoundEventID event)
+{
+	PurpleConversation *conv = purple_find_conversation_with_account(
+		PURPLE_CONV_TYPE_ANY, receiver, account);
+	play_conv_event(conv, event);
+}
+
+static void
+chat_buddy_join_cb(PurpleConversation *conv, const char *name,
+				   PurpleConvChatBuddyFlags flags, gboolean new_arrival,
+				   PurpleSoundEventID event)
+{
+	if (new_arrival && !chat_nick_matches_name(conv, name))
+		play_conv_event(conv, event);
+}
+
+static void
+chat_buddy_left_cb(PurpleConversation *conv, const char *name,
+				   const char *reason, PurpleSoundEventID event)
+{
+	if (!chat_nick_matches_name(conv, name))
+		play_conv_event(conv, event);
+}
+
+static void
+chat_msg_sent_cb(PurpleAccount *account, const char *message,
+				 int id, PurpleSoundEventID event)
+{
+	PurpleConnection *conn = purple_account_get_connection(account);
+	PurpleConversation *conv = NULL;
+
+	if (conn!=NULL)
+		conv = purple_find_chat(conn, id);
+
+	play_conv_event(conv, event);
+}
+
+static void
+chat_msg_received_cb(PurpleAccount *account, char *sender,
+					 char *message, PurpleConversation *conv,
+					 PurpleMessageFlags flags, PurpleSoundEventID event)
+{
+	PurpleConvChat *chat;
+
+	if (flags & PURPLE_MESSAGE_DELAYED)
+		return;
+
+	chat = purple_conversation_get_chat_data(conv);
+	g_return_if_fail(chat != NULL);
+
+	if (purple_conv_chat_is_user_ignored(chat, sender))
+		return;
+
+	if (chat_nick_matches_name(conv, sender))
+		return;
+
+	if (flags & PURPLE_MESSAGE_NICK || purple_utf8_has_word(message, chat->nick))
+		play_conv_event(conv, PURPLE_SOUND_CHAT_NICK);
+	else
+		play_conv_event(conv, event);
+}
+
+/*
+ * We mute sounds for the 10 seconds after you log in so that
+ * you don't get flooded with sounds when the blist shows all
+ * your buddies logging in.
+ */
+static void
+account_signon_cb(PurpleConnection *gc, gpointer data)
+{
+	if (mute_login_sounds_timeout != 0)
+		g_source_remove(mute_login_sounds_timeout);
+	mute_login_sounds = TRUE;
+	mute_login_sounds_timeout = purple_timeout_add_seconds(10, unmute_login_sounds_cb, NULL);
+}
+
+static void *
+finch_sound_get_handle()
+{
+	static int handle;
+
+	return &handle;
+}
+
+
+/* This gets called when the active profile changes */
+static void
+initialize_profile(const char *name, PurplePrefType type, gconstpointer val, gpointer null)
 {
+	if (purple_prefs_exists(make_pref("")))
+		return;
+
+	purple_prefs_add_none(make_pref(""));
+	purple_prefs_add_none(make_pref("/enabled"));
+	purple_prefs_add_none(make_pref("/file"));
+	purple_prefs_add_bool(make_pref("/enabled/login"), TRUE);
+	purple_prefs_add_path(make_pref("/file/login"), "");
+	purple_prefs_add_bool(make_pref("/enabled/logout"), TRUE);
+	purple_prefs_add_path(make_pref("/file/logout"), "");
+	purple_prefs_add_bool(make_pref("/enabled/im_recv"), TRUE);
+	purple_prefs_add_path(make_pref("/file/im_recv"), "");
+	purple_prefs_add_bool(make_pref("/enabled/first_im_recv"), FALSE);
+	purple_prefs_add_path(make_pref("/file/first_im_recv"), "");
+	purple_prefs_add_bool(make_pref("/enabled/send_im"), TRUE);
+	purple_prefs_add_path(make_pref("/file/send_im"), "");
+	purple_prefs_add_bool(make_pref("/enabled/join_chat"), FALSE);
+	purple_prefs_add_path(make_pref("/file/join_chat"), "");
+	purple_prefs_add_bool(make_pref("/enabled/left_chat"), FALSE);
+	purple_prefs_add_path(make_pref("/file/left_chat"), "");
+	purple_prefs_add_bool(make_pref("/enabled/send_chat_msg"), FALSE);
+	purple_prefs_add_path(make_pref("/file/send_chat_msg"), "");
+	purple_prefs_add_bool(make_pref("/enabled/chat_msg_recv"), FALSE);
+	purple_prefs_add_path(make_pref("/file/chat_msg_recv"), "");
+	purple_prefs_add_bool(make_pref("/enabled/nick_said"), FALSE);
+	purple_prefs_add_path(make_pref("/file/nick_said"), "");
+	purple_prefs_add_bool(make_pref("/enabled/pounce_default"), TRUE);
+	purple_prefs_add_path(make_pref("/file/pounce_default"), "");
+	purple_prefs_add_bool(make_pref("/conv_focus"), TRUE);
+	purple_prefs_add_bool(make_pref("/mute"), FALSE);
+	purple_prefs_add_path(make_pref("/command"), "");
+	purple_prefs_add_string(make_pref("/method"), "automatic");
+	purple_prefs_add_int(make_pref("/volume"), 50);
+}
+
+static void
+finch_sound_init(void)
+{
+	void *gnt_sound_handle = finch_sound_get_handle();
+	void *blist_handle = purple_blist_get_handle();
+	void *conv_handle = purple_conversations_get_handle();
+#ifdef USE_GSTREAMER
+	GError *error = NULL;
+#endif
+
+	purple_signal_connect(purple_connections_get_handle(), "signed-on",
+						gnt_sound_handle, PURPLE_CALLBACK(account_signon_cb),
+						NULL);
+
+	purple_prefs_add_none(FINCH_PREFS_ROOT "/sound");
+	purple_prefs_add_string(FINCH_PREFS_ROOT "/sound/actprofile", DEFAULT_PROFILE);
+	purple_prefs_add_none(FINCH_PREFS_ROOT "/sound/profiles");
+
+	purple_prefs_connect_callback(gnt_sound_handle, FINCH_PREFS_ROOT "/sound/actprofile", initialize_profile, NULL);
+	purple_prefs_trigger_callback(FINCH_PREFS_ROOT "/sound/actprofile");
+
+	
+#ifdef USE_GSTREAMER
+	purple_debug_info("sound", "Initializing sound output drivers.\n");
+	if ((gst_init_failed = !gst_init_check(NULL, NULL, &error))) {
+		purple_notify_error(NULL, _("GStreamer Failure"),
+					_("GStreamer failed to initialize."),
+					error ? error->message : "");
+		if (error) {
+			g_error_free(error);
+			error = NULL;
+		}
+	}
+#endif /* USE_GSTREAMER */
+
+	purple_signal_connect(blist_handle, "buddy-signed-on",
+						gnt_sound_handle, PURPLE_CALLBACK(buddy_state_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_BUDDY_ARRIVE));
+	purple_signal_connect(blist_handle, "buddy-signed-off",
+						gnt_sound_handle, PURPLE_CALLBACK(buddy_state_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_BUDDY_LEAVE));
+	purple_signal_connect(conv_handle, "received-im-msg",
+						gnt_sound_handle, PURPLE_CALLBACK(im_msg_received_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_RECEIVE));
+	purple_signal_connect(conv_handle, "sent-im-msg",
+						gnt_sound_handle, PURPLE_CALLBACK(im_msg_sent_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_SEND));
+	purple_signal_connect(conv_handle, "chat-buddy-joined",
+						gnt_sound_handle, PURPLE_CALLBACK(chat_buddy_join_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_CHAT_JOIN));
+	purple_signal_connect(conv_handle, "chat-buddy-left",
+						gnt_sound_handle, PURPLE_CALLBACK(chat_buddy_left_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_CHAT_LEAVE));
+	purple_signal_connect(conv_handle, "sent-chat-msg",
+						gnt_sound_handle, PURPLE_CALLBACK(chat_msg_sent_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_CHAT_YOU_SAY));
+	purple_signal_connect(conv_handle, "received-chat-msg",
+						gnt_sound_handle, PURPLE_CALLBACK(chat_msg_received_cb),
+						GINT_TO_POINTER(PURPLE_SOUND_CHAT_SAY));
+}
+
+static void
+finch_sound_uninit(void)
+{
+#ifdef USE_GSTREAMER
+	if (!gst_init_failed)
+		gst_deinit();
+#endif
+
+	purple_signals_disconnect_by_handle(finch_sound_get_handle());
 }
 
-GList *finch_sound_get_profiles(void)
+#ifdef USE_GSTREAMER
+static gboolean
+bus_call (GstBus *bus, GstMessage *msg, gpointer data)
+{
+	GstElement *play = data;
+	GError *err = NULL;
+
+	switch (GST_MESSAGE_TYPE (msg)) {
+	case GST_MESSAGE_EOS:
+		gst_element_set_state(play, GST_STATE_NULL);
+		gst_object_unref(GST_OBJECT(play));
+		break;
+	case GST_MESSAGE_ERROR:
+		gst_message_parse_error(msg, &err, NULL);
+		purple_debug_error("gstreamer", "%s\n", err->message);
+		g_error_free(err);
+		break;
+	case GST_MESSAGE_WARNING:
+		gst_message_parse_warning(msg, &err, NULL);
+		purple_debug_warning("gstreamer", "%s\n", err->message);
+		g_error_free(err);
+		break;
+	default:
+		break;
+	}
+	return TRUE;
+}
+#endif
+
+static void
+finch_sound_play_file(const char *filename)
+{
+	const char *method;
+#ifdef USE_GSTREAMER
+	float volume;
+	char *uri;
+	GstElement *sink = NULL;
+	GstElement *play = NULL;
+	GstBus *bus = NULL;
+#endif
+	if (purple_prefs_get_bool(make_pref("/mute")))
+		return;
+
+	method = purple_prefs_get_string(make_pref("/method"));
+
+	if (!strcmp(method, "none")) {
+		return;
+	} else if (!strcmp(method, "beep")) {
+		beep();
+		return;
+	}
+
+	if (!g_file_test(filename, G_FILE_TEST_EXISTS)) {
+		purple_debug_error("gntsound", "sound file (%s) does not exist.\n", filename);
+		return;
+	}
+
+#ifndef _WIN32
+	if (!strcmp(method, "custom")) {
+		const char *sound_cmd;
+		char *command;
+		char *esc_filename;
+		GError *error = NULL;
+
+		sound_cmd = purple_prefs_get_path(make_pref("/command"));
+
+		if (!sound_cmd || *sound_cmd == '\0') {
+			purple_debug_error("gntsound",
+					 "'Command' sound method has been chosen, "
+					 "but no command has been set.");
+			return;
+		}
+
+		esc_filename = g_shell_quote(filename);
+
+		if (strstr(sound_cmd, "%s"))
+			command = purple_strreplace(sound_cmd, "%s", esc_filename);
+		else
+			command = g_strdup_printf("%s %s", sound_cmd, esc_filename);
+
+		if (!g_spawn_command_line_async(command, &error)) {
+			purple_debug_error("gntsound", "sound command could not be launched: %s\n", error->message);
+			g_error_free(error);
+		}
+
+		g_free(esc_filename);
+		g_free(command);
+		return;
+	}
+#ifdef USE_GSTREAMER
+	if (gst_init_failed)  /* Perhaps do beep instead? */
+		return;
+	volume = (float)(CLAMP(purple_prefs_get_int(make_pref("/volume")), 0, 100)) / 50;
+	if (!strcmp(method, "automatic")) {
+		if (purple_running_gnome()) {
+			sink = gst_element_factory_make("gconfaudiosink", "sink");
+		}
+		if (!sink)
+			sink = gst_element_factory_make("autoaudiosink", "sink");
+		if (!sink) {
+			purple_debug_error("sound", "Unable to create GStreamer audiosink.\n");
+			return;
+		}
+	} else if (!strcmp(method, "esd")) {
+		sink = gst_element_factory_make("esdsink", "sink");
+		if (!sink) {
+			purple_debug_error("sound", "Unable to create GStreamer audiosink.\n");
+			return;
+		}
+	} else if (!strcmp(method, "alsa")) {
+		sink = gst_element_factory_make("alsasink", "sink");
+		if (!sink) {
+			purple_debug_error("sound", "Unable to create GStreamer audiosink.\n");
+			return;
+		}
+	} else {
+		purple_debug_error("sound", "Unknown sound method '%s'\n", method);
+		return;
+	}
+
+	play = gst_element_factory_make("playbin", "play");
+	
+	if (play == NULL) {
+		return;
+	}
+	
+	uri = g_strdup_printf("file://%s", filename);
+
+	g_object_set(G_OBJECT(play), "uri", uri,
+		                     "volume", volume,
+		                     "audio-sink", sink, NULL);
+
+	bus = gst_pipeline_get_bus(GST_PIPELINE(play));
+	gst_bus_add_watch(bus, bus_call, play);
+
+	gst_element_set_state(play, GST_STATE_PLAYING);
+
+	gst_object_unref(bus);
+	g_free(uri);
+
+#else /* USE_GSTREAMER */
+	beep();
+	return;
+#endif /* USE_GSTREAMER */
+#else /* _WIN32 */
+	purple_debug_info("sound", "Playing %s\n", filename);
+
+	if (G_WIN32_HAVE_WIDECHAR_API ()) {
+		wchar_t *wc_filename = g_utf8_to_utf16(filename,
+				-1, NULL, NULL, NULL);
+		if (!PlaySoundW(wc_filename, NULL, SND_ASYNC | SND_FILENAME))
+			purple_debug(PURPLE_DEBUG_ERROR, "sound", "Error playing sound.\n");
+		g_free(wc_filename);
+	} else {
+		char *l_filename = g_locale_from_utf8(filename,
+				-1, NULL, NULL, NULL);
+		if (!PlaySoundA(l_filename, NULL, SND_ASYNC | SND_FILENAME))
+			purple_debug(PURPLE_DEBUG_ERROR, "sound", "Error playing sound.\n");
+		g_free(l_filename);
+	}
+#endif /* _WIN32 */
+}
+
+static void
+finch_sound_play_event(PurpleSoundEventID event)
 {
-	return NULL;
+	char *enable_pref;
+	char *file_pref;
+	if ((event == PURPLE_SOUND_BUDDY_ARRIVE) && mute_login_sounds)
+		return;
+
+	if (event >= PURPLE_NUM_SOUNDS) {
+		purple_debug_error("sound", "got request for unknown sound: %d\n", event);
+		return;
+	}
+
+	enable_pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s",
+			finch_sound_get_active_profile(),
+			sounds[event].pref);
+	file_pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", finch_sound_get_active_profile(), sounds[event].pref);
+
+	/* check NULL for sounds that don't have an option, ie buddy pounce */
+	if (purple_prefs_get_bool(enable_pref)) {
+		char *filename = g_strdup(purple_prefs_get_path(file_pref));
+		if (!filename || !strlen(filename)) {
+			g_free(filename);
+			/* XXX Consider creating a constant for "sounds/purple" to be shared with Pidgin */
+			filename = g_build_filename(DATADIR, "sounds", "purple", sounds[event].def, NULL);
+		}
+
+		purple_sound_play_file(filename, NULL);
+		g_free(filename);
+	}
+
+	g_free(enable_pref);
+	g_free(file_pref);
+}
+
+GList *
+finch_sound_get_profiles()
+{
+	GList *list = NULL, *iter;
+	iter = purple_prefs_get_children_names(FINCH_PREFS_ROOT "/sound/profiles");
+	while (iter) {
+		list = g_list_append(list, g_strdup(strrchr(iter->data, '/') + 1));
+		g_free(iter->data);
+		iter = g_list_delete_link(iter, iter);
+	}
+	return list;
+}
+
+/* This will also create it if it doesn't exist */
+void
+finch_sound_set_active_profile(const char *name)
+{
+	purple_prefs_set_string(FINCH_PREFS_ROOT "/sound/actprofile", name);
+}
+
+static gboolean
+finch_sound_profile_exists(const char *name)
+{
+	gchar * tmp;
+	gboolean ret = purple_prefs_exists(tmp = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s", name));
+	g_free(tmp);
+	return ret;
+}
+
+static void
+save_cb(GntWidget *button, gpointer win)
+{
+	GList * itr;
+
+	purple_prefs_set_string(make_pref("/method"), gnt_combo_box_get_selected_data(GNT_COMBO_BOX(pref_dialog->method)));
+	purple_prefs_set_path(make_pref("/command"), gnt_entry_get_text(GNT_ENTRY(pref_dialog->command)));
+	purple_prefs_set_bool(make_pref("/conv_focus"), gnt_check_box_get_checked(GNT_CHECK_BOX(pref_dialog->conv_focus)));
+	purple_prefs_set_int("/purple/sound/while_status", GPOINTER_TO_INT(gnt_combo_box_get_selected_data(GNT_COMBO_BOX(pref_dialog->while_status))));
+	purple_prefs_set_int(make_pref("/volume"), gnt_slider_get_value(GNT_SLIDER(pref_dialog->volume)));
+
+	for (itr = gnt_tree_get_rows(GNT_TREE(pref_dialog->events)); itr; itr = itr->next) {
+		FinchSoundEvent * event = &sounds[GPOINTER_TO_INT(itr->data)];
+		char * filepref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", finch_sound_get_active_profile(), event->pref);
+		char * boolpref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", finch_sound_get_active_profile(), event->pref);
+		purple_prefs_set_bool(boolpref, gnt_tree_get_choice(GNT_TREE(pref_dialog->events), itr->data));
+		purple_prefs_set_path(filepref, event->file ? event->file : "");
+		g_free(filepref);
+		g_free(boolpref);
+	}
+	gnt_widget_destroy(GNT_WIDGET(win));
+}
+
+static void
+file_cb(GntFileSel *w, const char *path, const char *file, gpointer data)
+{
+	FinchSoundEvent *event = data;
+
+	g_free(event->file);
+	event->file = g_strdup(path);
+
+	gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(event->id), 1, file);
+	gnt_tree_set_choice(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(event->id), TRUE);
+	
+	gnt_widget_destroy(GNT_WIDGET(w));
+}
+
+static void
+test_cb(GntWidget *button, gpointer null)
+{
+	PurpleSoundEventID id = GPOINTER_TO_INT(gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events)));
+	FinchSoundEvent * event = &sounds[id];
+	char *enabled, *file, *tmpfile;
+	gboolean temp_value;
+
+	enabled = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s",
+			finch_sound_get_active_profile(), event->pref);
+	file = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s",
+			finch_sound_get_active_profile(), event->pref);
+
+	temp_value = purple_prefs_get_bool(enabled);
+	tmpfile = g_strdup(purple_prefs_get_string(file));
+
+	purple_prefs_set_string(file, event->file);
+	if (!temp_value) purple_prefs_set_bool(enabled, TRUE);
+
+	purple_sound_play_event(id, NULL);
+
+	if (!temp_value) purple_prefs_set_bool(enabled, FALSE);
+	purple_prefs_set_string(file, tmpfile);
+
+	g_free(enabled);
+	g_free(file);
+	g_free(tmpfile);
+}
+
+static void
+reset_cb(GntWidget *button, gpointer null)
+{
+	/* Don't dereference this pointer ! */
+	gpointer key = gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events)); 
+
+	FinchSoundEvent * event = &sounds[GPOINTER_TO_INT(key)];
+	g_free(event->file);
+	event->file = NULL;
+	gnt_tree_change_text(GNT_TREE(pref_dialog->events), key, 1, _("(default)"));
+}
+
+
+static void
+choose_cb(GntWidget *button, gpointer null)
+{
+	GntWidget *w = gnt_file_sel_new();
+	GntFileSel *sel = GNT_FILE_SEL(w);
+	PurpleSoundEventID id = GPOINTER_TO_INT(gnt_tree_get_selection_data(GNT_TREE(pref_dialog->events)));
+	FinchSoundEvent * event = &sounds[id];
+	char *path = NULL;
+
+	gnt_box_set_title(GNT_BOX(w), _("Select Sound File ..."));
+	gnt_file_sel_set_current_location(sel,
+			(event && event->file) ? (path = g_path_get_dirname(event->file))
+				: purple_home_dir());
+
+	g_signal_connect_swapped(G_OBJECT(sel->cancel), "activate", G_CALLBACK(gnt_widget_destroy), sel);
+	g_signal_connect(G_OBJECT(sel), "file_selected", G_CALLBACK(file_cb), event);
+	g_signal_connect_swapped(G_OBJECT(sel), "destroy", G_CALLBACK(g_nullify_pointer), &pref_dialog->selector);
+
+	/* If there's an already open file-selector, close that one. */
+	if (pref_dialog->selector)
+		gnt_widget_destroy(pref_dialog->selector);
+
+	pref_dialog->selector = w;
+
+	gnt_widget_show(w);
+	g_free(path);
 }
 
-PurpleSoundUiOps *finch_sound_get_ui_ops(void)
+static void
+release_pref_dialog(GntBindable *data, gpointer null)
+{
+	GList * itr;
+	for (itr = gnt_tree_get_rows(GNT_TREE(pref_dialog->events)); itr; itr = itr->next) {
+		PurpleSoundEventID id = GPOINTER_TO_INT(itr->data);
+		FinchSoundEvent * e = &sounds[id];
+		g_free(e->file);
+		e->file = NULL;
+	}
+	if (pref_dialog->selector)
+		gnt_widget_destroy(pref_dialog->selector);
+	g_free(pref_dialog);
+	pref_dialog = NULL;
+}
+
+static void
+load_pref_window(const char * profile)
 {
-	return NULL;
+	gint i;
+
+	finch_sound_set_active_profile(profile);
+
+	gnt_combo_box_set_selected(GNT_COMBO_BOX(pref_dialog->method), (gchar *)purple_prefs_get_string(make_pref("/method")));
+
+	gnt_entry_set_text(GNT_ENTRY(pref_dialog->command), purple_prefs_get_path(make_pref("/command")));
+
+	gnt_check_box_set_checked(GNT_CHECK_BOX(pref_dialog->conv_focus), purple_prefs_get_bool(make_pref("/conv_focus")));
+
+	gnt_combo_box_set_selected(GNT_COMBO_BOX(pref_dialog->while_status), GINT_TO_POINTER(purple_prefs_get_int("/purple" "/sound/while_status")));
+
+	gnt_slider_set_value(GNT_SLIDER(pref_dialog->volume), CLAMP(purple_prefs_get_int(make_pref("/volume")), 0, 100));
+
+	for (i = 0; i < PURPLE_NUM_SOUNDS; i++) {
+		FinchSoundEvent * event = &sounds[i];
+		gchar *boolpref;
+		gchar *filepref, *basename = NULL;
+		const char * profile = finch_sound_get_active_profile();
+
+		filepref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/file/%s", profile, event->pref);
+
+		g_free(event->file);
+		event->file = g_strdup(purple_prefs_get_path(filepref));
+
+		g_free(filepref);
+		if (event->label == NULL) {
+			continue;
+		}
+
+		boolpref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s/enabled/%s", profile, event->pref);
+
+		gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), 0, event->label);
+		gnt_tree_change_text(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), 1,
+				event->file[0] ? (basename = g_path_get_basename(event->file)) : _("(default)"));
+		g_free(basename);
+
+		gnt_tree_set_choice(GNT_TREE(pref_dialog->events), GINT_TO_POINTER(i), purple_prefs_get_bool(boolpref));
+
+		g_free(boolpref);
+	}
+
+	gnt_tree_set_selected(GNT_TREE(pref_dialog->profiles), (gchar *)finch_sound_get_active_profile());
+
+	gnt_widget_draw(pref_dialog->window);
+}
+
+static void
+reload_pref_window(const char *profile)
+{
+	if (!strcmp(profile, finch_sound_get_active_profile()))
+		return;
+	load_pref_window(profile);
+}
+
+static void
+prof_del_cb(GntWidget *button, gpointer null)
+{
+	const char * profile = gnt_entry_get_text(GNT_ENTRY(pref_dialog->new_profile));
+	gchar * pref;
+
+	if (!strcmp(profile, DEFAULT_PROFILE))
+		return;
+
+	pref = g_strdup_printf(FINCH_PREFS_ROOT "/sound/profiles/%s", profile);
+	purple_prefs_remove(pref);
+	g_free(pref);
+
+	if (!strcmp(pref_dialog->original_profile, profile)) {
+		g_free(pref_dialog->original_profile);
+		pref_dialog->original_profile = g_strdup(DEFAULT_PROFILE);
+	}
+
+	if(!strcmp(profile, finch_sound_get_active_profile()))
+		reload_pref_window(DEFAULT_PROFILE);
+
+	gnt_tree_remove(GNT_TREE(pref_dialog->profiles), (gchar *) profile);
+}
+
+static void
+prof_add_cb(GntButton *button, GntEntry * entry)
+{
+	const char * profile = gnt_entry_get_text(entry);
+	GntTreeRow * row;
+	if (!finch_sound_profile_exists(profile)) {
+		gpointer key = g_strdup(profile);
+		row = gnt_tree_create_row(GNT_TREE(pref_dialog->profiles), profile);
+		gnt_tree_add_row_after(GNT_TREE(pref_dialog->profiles), key,
+				row,
+				NULL, NULL);
+		gnt_entry_set_text(entry, "");
+		gnt_tree_set_selected(GNT_TREE(pref_dialog->profiles), key);
+		finch_sound_set_active_profile(key);
+	} else 
+		reload_pref_window(profile);
+}
+
+static void
+prof_load_cb(GntTree *tree, gpointer oldkey, gpointer newkey, gpointer null)
+{
+	reload_pref_window(newkey);
+}
+
+static void
+cancel_cb(GntButton *button, gpointer win)
+{
+	finch_sound_set_active_profile(pref_dialog->original_profile);
+	gnt_widget_destroy(GNT_WIDGET(win));
 }
 
-void finch_sounds_show_all(void)
+void
+finch_sounds_show_all(void)
 {
+	GntWidget *box, *tmpbox, *splitbox, *cmbox, *slider;
+	GntWidget *entry;
+	GntWidget *chkbox;
+	GntWidget *button;
+	GntWidget *label;
+	GntWidget *tree;
+	GntWidget *win;
+
+	gint i;
+	GList *itr, *list;
+
+	if (pref_dialog) {
+		gnt_window_present(pref_dialog->window);
+		return;
+	}
+
+	pref_dialog = g_new0(SoundPrefDialog, 1);
+
+	pref_dialog->original_profile = g_strdup(finch_sound_get_active_profile());
+
+	pref_dialog->window = win = gnt_window_box_new(FALSE, TRUE);
+	gnt_box_set_pad(GNT_BOX(win), 0);
+	gnt_box_set_toplevel(GNT_BOX(win), TRUE);
+	gnt_box_set_title(GNT_BOX(win), _("Sound Preferences"));
+	gnt_box_set_fill(GNT_BOX(win), TRUE);
+	gnt_box_set_alignment(GNT_BOX(win), GNT_ALIGN_MID);
+
+	/* Profiles */
+	splitbox = gnt_hbox_new(FALSE);
+	gnt_box_set_pad(GNT_BOX(splitbox), 0);
+	gnt_box_set_alignment(GNT_BOX(splitbox), GNT_ALIGN_TOP);
+
+	box = gnt_vbox_new(FALSE);
+	gnt_box_set_pad(GNT_BOX(box), 0);
+	gnt_box_add_widget(GNT_BOX(box), gnt_label_new_with_format(_("Profiles"), GNT_TEXT_FLAG_BOLD));
+	pref_dialog->profiles = tree = gnt_tree_new();
+	gnt_tree_set_hash_fns(GNT_TREE(tree), g_str_hash, g_str_equal, g_free);
+	gnt_tree_set_compare_func(GNT_TREE(tree), (GCompareFunc)g_ascii_strcasecmp);
+	g_signal_connect(G_OBJECT(tree), "selection-changed", G_CALLBACK(prof_load_cb), NULL);
+
+	itr = list = finch_sound_get_profiles();
+	for (; itr; itr = itr->next) {
+		/* Do not free itr->data. It's the stored as a key for the tree, and will
+		 * be freed when the tree is destroyed. */
+		gnt_tree_add_row_after(GNT_TREE(tree), itr->data,
+				gnt_tree_create_row(GNT_TREE(tree), itr->data), NULL, NULL);
+	}
+	g_list_free(list);
+
+	gnt_box_add_widget(GNT_BOX(box), tree);
+
+	pref_dialog->new_profile = entry = gnt_entry_new("");
+	gnt_box_add_widget(GNT_BOX(box), entry);
+
+	tmpbox = gnt_hbox_new(FALSE);
+	button = gnt_button_new("Add");
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(prof_add_cb), entry);
+	gnt_box_add_widget(GNT_BOX(tmpbox), button);
+	button = gnt_button_new("Delete");
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(prof_del_cb), NULL);
+	gnt_box_add_widget(GNT_BOX(tmpbox), button);
+	gnt_box_add_widget(GNT_BOX(box), tmpbox);
+	gnt_box_add_widget(GNT_BOX(splitbox), box);
+
+	gnt_box_add_widget(GNT_BOX(splitbox), gnt_vline_new());
+
+	/* Sound method */
+
+	box = gnt_vbox_new(FALSE);
+	gnt_box_set_pad(GNT_BOX(box), 0);
+
+	pref_dialog->method = cmbox = gnt_combo_box_new();
+	gnt_tree_set_hash_fns(GNT_TREE(GNT_COMBO_BOX(cmbox)->dropdown), g_str_hash, g_str_equal, NULL);
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "automatic", _("Automatic"));
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "alsa", "ALSA");
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "esd", "ESD");
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "beep", _("Console Beep"));
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "custom", _("Command"));
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), "nosound", _("No Sound"));
+
+	label = gnt_label_new_with_format(_("Sound Method"), GNT_TEXT_FLAG_BOLD);
+	gnt_box_add_widget(GNT_BOX(box), label); 
+	tmpbox = gnt_hbox_new(TRUE);
+	gnt_box_set_fill(GNT_BOX(tmpbox), FALSE);
+	gnt_box_set_pad(GNT_BOX(tmpbox), 0);
+	gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Method: ")));
+	gnt_box_add_widget(GNT_BOX(tmpbox), cmbox);
+	gnt_box_add_widget(GNT_BOX(box), tmpbox); 
+
+	tmpbox = gnt_hbox_new(TRUE);
+	gnt_box_set_pad(GNT_BOX(tmpbox), 0);
+	gnt_box_set_fill(GNT_BOX(tmpbox), FALSE);
+	gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Sound Command\n(%s for filename)")));
+	pref_dialog->command = entry = gnt_entry_new("");
+	gnt_box_add_widget(GNT_BOX(tmpbox), entry);
+	gnt_box_add_widget(GNT_BOX(box), tmpbox);
+
+	gnt_box_add_widget(GNT_BOX(box), gnt_line_new(FALSE));
+
+	/* Sound options */
+	gnt_box_add_widget(GNT_BOX(box), gnt_label_new_with_format(_("Sound Options"), GNT_TEXT_FLAG_BOLD)); 
+	pref_dialog->conv_focus = chkbox = gnt_check_box_new(_("Sounds when conversation has focus"));
+	gnt_box_add_widget(GNT_BOX(box), chkbox);
+
+	tmpbox = gnt_hbox_new(TRUE);
+	gnt_box_set_pad(GNT_BOX(tmpbox), 0);
+	gnt_box_set_fill(GNT_BOX(tmpbox), FALSE);
+	gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new("Enable Sounds:"));
+	pref_dialog->while_status = cmbox = gnt_combo_box_new();
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(3), _("Always"));
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(1), _("Only when available"));
+	gnt_combo_box_add_data(GNT_COMBO_BOX(cmbox), GINT_TO_POINTER(2), _("Only when not available"));
+	gnt_box_add_widget(GNT_BOX(tmpbox), cmbox);
+	gnt_box_add_widget(GNT_BOX(box), tmpbox);
+
+	tmpbox = gnt_hbox_new(TRUE);
+	gnt_box_set_pad(GNT_BOX(tmpbox), 0);
+	gnt_box_set_fill(GNT_BOX(tmpbox), FALSE);
+	gnt_box_add_widget(GNT_BOX(tmpbox), gnt_label_new(_("Volume(0-100):")));
+
+	pref_dialog->volume = slider = gnt_slider_new(FALSE, 100, 0);
+	gnt_slider_set_step(GNT_SLIDER(slider), 5);
+	label = gnt_label_new("");
+	gnt_slider_reflect_label(GNT_SLIDER(slider), GNT_LABEL(label));
+	gnt_box_set_pad(GNT_BOX(tmpbox), 1);
+	gnt_box_add_widget(GNT_BOX(tmpbox), slider);
+	gnt_box_add_widget(GNT_BOX(tmpbox), label);
+	gnt_box_add_widget(GNT_BOX(box), tmpbox);
+	gnt_box_add_widget(GNT_BOX(splitbox), box);
+
+	gnt_box_add_widget(GNT_BOX(win), splitbox);
+
+	gnt_box_add_widget(GNT_BOX(win), gnt_hline_new());
+
+	/* Sound events */
+	gnt_box_add_widget(GNT_BOX(win), gnt_label_new_with_format(_("Sound Events"), GNT_TEXT_FLAG_BOLD)); 
+	pref_dialog->events = tree = gnt_tree_new_with_columns(2);
+	gnt_tree_set_column_titles(GNT_TREE(tree), _("Event"), _("File"));
+	gnt_tree_set_show_title(GNT_TREE(tree), TRUE);
+
+	for (i = 0; i < PURPLE_NUM_SOUNDS; i++) {
+		FinchSoundEvent * event = &sounds[i];
+		
+		if (event->label == NULL) {
+			continue;
+		}
+
+		gnt_tree_add_choice(GNT_TREE(tree), GINT_TO_POINTER(i),
+			gnt_tree_create_row(GNT_TREE(tree), "", ""),
+			NULL, NULL);
+	}
+
+	gnt_tree_adjust_columns(GNT_TREE(tree));
+	gnt_box_add_widget(GNT_BOX(win), tree);
+
+	box = gnt_hbox_new(FALSE);
+	button = gnt_button_new(_("Test"));
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(test_cb), NULL);
+	gnt_box_add_widget(GNT_BOX(box), button);
+	button = gnt_button_new(_("Reset"));
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(reset_cb), NULL);
+	gnt_box_add_widget(GNT_BOX(box), button);
+	button = gnt_button_new(_("Choose..."));
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(choose_cb), NULL);
+	gnt_box_add_widget(GNT_BOX(box), button);
+	gnt_box_add_widget(GNT_BOX(win), box);
+
+	gnt_box_add_widget(GNT_BOX(win), gnt_line_new(FALSE));
+
+	/* Add new stuff before this */
+	box = gnt_hbox_new(FALSE);
+	gnt_box_set_fill(GNT_BOX(box), TRUE);
+	button = gnt_button_new(_("Save"));
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(save_cb), win);
+	gnt_box_add_widget(GNT_BOX(box), button);
+	button = gnt_button_new(_("Cancel"));
+	g_signal_connect(G_OBJECT(button), "activate", G_CALLBACK(cancel_cb), win);
+	gnt_box_add_widget(GNT_BOX(box), button);
+	gnt_box_add_widget(GNT_BOX(win), box);
+
+	g_signal_connect(G_OBJECT(win), "destroy", G_CALLBACK(release_pref_dialog), NULL);
+
+	load_pref_window(finch_sound_get_active_profile());
+
+	gnt_widget_show(win);
+}	
+
+static PurpleSoundUiOps sound_ui_ops =
+{
+	finch_sound_init,
+	finch_sound_uninit,
+	finch_sound_play_file,
+	finch_sound_play_event,
+	NULL,
+	NULL,
+	NULL,
+	NULL
+};
+
+PurpleSoundUiOps *
+finch_sound_get_ui_ops(void)
+{
+	return &sound_ui_ops;
 }
 
--- a/finch/gntui.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/gntui.c	Tue Aug 14 02:45:41 2007 +0000
@@ -35,6 +35,7 @@
 #include "gntprefs.h"
 #include "gntrequest.h"
 #include "gntstatus.h"
+#include "gntsound.h"
 
 #include <prefs.h>
 
@@ -58,6 +59,9 @@
 	finch_blist_init();
 	purple_blist_set_ui_ops(finch_blist_get_ui_ops());
 
+	/* Initialize sound */
+	purple_sound_set_ui_ops(finch_sound_get_ui_ops());
+
 	/* Now the conversations */
 	finch_conversation_init();
 	purple_conversations_set_ui_ops(finch_conv_get_ui_ops());
@@ -80,6 +84,7 @@
 	gnt_register_action(_("Debug Window"), finch_debug_window_show);
 	gnt_register_action(_("File Transfers"), finch_xfer_dialog_show);
 	gnt_register_action(_("Plugins"), finch_plugins_show_all);
+	gnt_register_action(_("Sounds"), finch_sounds_show_all);
 	gnt_register_action(_("Preferences"), finch_prefs_show_all);
 	gnt_register_action(_("Statuses"), finch_savedstatus_show_all);
 
--- a/finch/libgnt/configure.ac	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/configure.ac	Tue Aug 14 02:45:41 2007 +0000
@@ -26,7 +26,7 @@
 
 m4_define([gnt_lt_current], [1])
 m4_define([gnt_major_version], [2])
-m4_define([gnt_minor_version], [0])
+m4_define([gnt_minor_version], [1])
 m4_define([gnt_micro_version], [0])
 m4_define([gnt_version_suffix], [devel])
 m4_define([gnt_version],
@@ -36,6 +36,7 @@
 AC_INIT([libgnt], [gnt_display_version], [devel@pidgin.im])
 AC_CANONICAL_SYSTEM
 AM_CONFIG_HEADER(config.h)
+AC_CONFIG_AUX_DIR([.])
 AM_INIT_AUTOMAKE(AC_PACKAGE_NAME, AC_PACKAGE_VERSION)
 
 GNT_MAJOR_VERSION=gnt_major_version
@@ -314,6 +315,8 @@
 	AC_DEFINE(NO_LIBXML, 1, [Do not have libxml2.])
 fi
 
+AM_CONDITIONAL(PURPLE_AVAILABLE, false)
+
 AC_OUTPUT([Makefile
            gnt.pc
            wms/Makefile
--- a/finch/libgnt/gnt.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gnt.h	Tue Aug 14 02:45:41 2007 +0000
@@ -157,4 +157,3 @@
 		void (*callback)(int status, gpointer data), gpointer data);
 
 gboolean gnt_is_refugee(void);
-
--- a/finch/libgnt/gntbindable.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntbindable.c	Tue Aug 14 02:45:41 2007 +0000
@@ -20,13 +20,182 @@
  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
+#include <string.h>
+
 #include "gntbindable.h"
 #include "gntstyle.h"
 #include "gnt.h"
 #include "gntutils.h"
+#include "gnttextview.h"
+#include "gnttree.h"
+#include "gntbox.h"
+#include "gntbutton.h"
+#include "gntwindow.h"
+#include "gntlabel.h"
 
 static GObjectClass *parent_class = NULL;
 
+static struct
+{
+	char * okeys;         /* Old keystrokes */
+	char * keys;          /* New Keystrokes being bound to the action */
+	GntBindableClass * klass; /* Class of the object that's getting keys rebound */
+	char * name;          /* The name of the action */
+	GList * params;       /* The list of paramaters */
+} rebind_info;
+
+static void 
+gnt_bindable_free_rebind_info()
+{
+	g_free(rebind_info.name);
+	g_free(rebind_info.keys);
+	g_free(rebind_info.okeys);
+}
+
+static void
+gnt_bindable_rebinding_cancel(GntWidget *button, gpointer data)
+{
+	gnt_bindable_free_rebind_info();
+	gnt_widget_destroy(GNT_WIDGET(data));
+}
+
+static void
+gnt_bindable_rebinding_rebind(GntWidget *button, gpointer data)
+{
+	if (rebind_info.keys) {
+		gnt_bindable_register_binding(rebind_info.klass,
+				NULL,
+				rebind_info.okeys,
+				rebind_info.params);
+		gnt_bindable_register_binding(rebind_info.klass,
+				rebind_info.name,
+				rebind_info.keys,
+				rebind_info.params);
+	}
+	gnt_bindable_free_rebind_info();
+	gnt_widget_destroy(GNT_WIDGET(data));
+}
+
+static gboolean
+gnt_bindable_rebinding_grab_key(GntBindable *bindable, const char *text, gpointer data)
+{
+	GntTextView *textview = GNT_TEXT_VIEW(data);
+	char *new_text;
+	const char *tmp;
+
+	if (text && *text) {
+		/* Rebinding tab or enter for something is probably not that great an idea */
+		if (!strcmp(text, GNT_KEY_CTRL_I) || !strcmp(text, GNT_KEY_ENTER)) {
+			return FALSE;
+		}
+		
+		tmp = gnt_key_lookup(text);
+		new_text = g_strdup_printf("KEY: \"%s\"", tmp);
+		gnt_text_view_clear(textview);
+		gnt_text_view_append_text_with_flags(textview, new_text, GNT_TEXT_FLAG_NORMAL);
+		g_free(new_text);
+
+		g_free(rebind_info.keys);
+		rebind_info.keys = g_strdup(text);
+
+		return TRUE;
+	}
+	return FALSE;
+} 
+static void
+gnt_bindable_rebinding_activate(GntBindable *data, gpointer bindable)
+{
+	const char *widget_name = g_type_name(G_OBJECT_TYPE(bindable));
+	char *keys;
+	GntWidget *key_textview;
+	GntWidget *label;
+	GntWidget *bind_button, *cancel_button;
+	GntWidget *button_box;
+	GList *current_row_data;
+	char *tmp;
+	GntWidget *win = gnt_window_new();
+	GntTree *tree = GNT_TREE(data);
+	GntWidget *vbox = gnt_box_new(FALSE, TRUE);
+
+	rebind_info.klass = GNT_BINDABLE_GET_CLASS(bindable);
+
+	current_row_data = gnt_tree_get_selection_text_list(tree);
+	rebind_info.name = g_strdup(g_list_nth_data(current_row_data, 1));
+
+	keys = gnt_tree_get_selection_data(tree);
+	rebind_info.okeys = g_strdup(gnt_key_translate(keys));
+
+	rebind_info.params = NULL;
+
+	g_list_foreach(current_row_data, (GFunc)g_free, NULL);
+	g_list_free(current_row_data);
+
+	gnt_box_set_alignment(GNT_BOX(vbox), GNT_ALIGN_MID);
+
+	gnt_box_set_title(GNT_BOX(win), "Key Capture");
+
+	tmp = g_strdup_printf("Type the new bindings for %s in a %s.", rebind_info.name, widget_name);
+	label = gnt_label_new(tmp);
+	g_free(tmp);
+	gnt_box_add_widget(GNT_BOX(vbox), label);
+
+	tmp = g_strdup_printf("KEY: \"%s\"", keys);
+	key_textview = gnt_text_view_new();
+	gnt_widget_set_size(key_textview, key_textview->priv.x, 2);
+	gnt_text_view_append_text_with_flags(GNT_TEXT_VIEW(key_textview), tmp, GNT_TEXT_FLAG_NORMAL);
+	g_free(tmp);
+	gnt_widget_set_name(key_textview, "keystroke");
+	gnt_box_add_widget(GNT_BOX(vbox), key_textview);
+
+	g_signal_connect(G_OBJECT(win), "key_pressed", G_CALLBACK(gnt_bindable_rebinding_grab_key), key_textview);
+
+	button_box = gnt_box_new(FALSE, FALSE);
+	
+	bind_button = gnt_button_new("BIND");
+	gnt_widget_set_name(bind_button, "bind");
+	gnt_box_add_widget(GNT_BOX(button_box), bind_button);
+	
+	cancel_button = gnt_button_new("Cancel");
+	gnt_widget_set_name(cancel_button, "cancel");
+	gnt_box_add_widget(GNT_BOX(button_box), cancel_button);
+	
+	g_signal_connect(G_OBJECT(bind_button), "activate", G_CALLBACK(gnt_bindable_rebinding_rebind), win);
+	g_signal_connect(G_OBJECT(cancel_button), "activate", G_CALLBACK(gnt_bindable_rebinding_cancel), win);
+	
+	gnt_box_add_widget(GNT_BOX(vbox), button_box);
+
+	gnt_box_add_widget(GNT_BOX(win), vbox);
+	gnt_widget_show(win);
+}
+
+typedef struct
+{
+	GHashTable *hash;
+	GntTree *tree;
+} BindingView;
+
+static void
+add_binding(gpointer key, gpointer value, gpointer data)
+{
+	BindingView *bv = data;
+	GntBindableActionParam *act = value;
+	const char *name = g_hash_table_lookup(bv->hash, act->action);
+	if (name && *name) {
+		const char *k = gnt_key_lookup(key);
+		if (!k)
+			k = key;
+		gnt_tree_add_row_after(bv->tree, (gpointer)k,
+				gnt_tree_create_row(bv->tree, k, name), NULL, NULL);
+	}
+}
+
+static void
+add_action(gpointer key, gpointer value, gpointer data)
+{
+	BindingView *bv = data;
+	g_hash_table_insert(bv->hash, value, key);
+}
+
 static void
 gnt_bindable_class_init(GntBindableClass *klass)
 {
@@ -88,7 +257,7 @@
 {
 	static GType type = 0;
 
-	if(type == 0) {
+	if (type == 0) {
 		static const GTypeInfo info = {
 			sizeof(GntBindableClass),
 			(GBaseInitFunc)duplicate_hashes,	/* base_init		*/
@@ -184,7 +353,7 @@
 
 	action = g_hash_table_lookup(klass->actions, name);
 	if (!action) {
-		g_printerr("GntWidget: Invalid action name %s for %s\n",
+		g_printerr("GntBindable: Invalid action name %s for %s\n",
 				name, g_type_name(G_OBJECT_CLASS_TYPE(klass)));
 		if (list)
 			g_list_free(list);
@@ -251,4 +420,55 @@
 	g_free(param);
 }
 
+GntBindable * gnt_bindable_bindings_view(GntBindable *bind)
+{
+	GntBindable *tree = GNT_BINDABLE(gnt_tree_new_with_columns(2));
+	GntBindableClass *klass = GNT_BINDABLE_CLASS(GNT_BINDABLE_GET_CLASS(bind));
+	GHashTable *hash = g_hash_table_new(g_direct_hash, g_direct_equal);
+	BindingView bv = {hash, GNT_TREE(tree)};
 
+	gnt_tree_set_compare_func(bv.tree, (GCompareFunc)g_utf8_collate);
+	g_hash_table_foreach(klass->actions, add_action, &bv);
+	g_hash_table_foreach(klass->bindings, add_binding, &bv);
+	if (GNT_TREE(tree)->list == NULL) {
+		gnt_widget_destroy(GNT_WIDGET(tree));
+		tree = NULL;
+	} else
+		gnt_tree_adjust_columns(bv.tree);
+	g_hash_table_destroy(hash);
+
+	return tree;
+}
+
+static void
+reset_binding_window(GntBindableClass *window, gpointer k)
+{
+	GntBindableClass *klass = GNT_BINDABLE_CLASS(k);
+	klass->help_window = NULL;
+}
+
+gboolean
+gnt_bindable_build_help_window(GntBindable *bindable)
+{
+	GntWidget *tree;
+	GntBindableClass *klass = GNT_BINDABLE_GET_CLASS(bindable);
+	char *title;
+
+	tree = GNT_WIDGET(gnt_bindable_bindings_view(bindable));
+
+	klass->help_window = GNT_BINDABLE(gnt_window_new());
+	title = g_strdup_printf("Bindings for %s", g_type_name(G_OBJECT_TYPE(bindable)));
+	gnt_box_set_title(GNT_BOX(klass->help_window), title);
+	if (tree) {
+		g_signal_connect(G_OBJECT(tree), "activate", G_CALLBACK(gnt_bindable_rebinding_activate), bindable);
+		gnt_box_add_widget(GNT_BOX(klass->help_window), tree);
+	} else
+		gnt_box_add_widget(GNT_BOX(klass->help_window), gnt_label_new("This widget has no customizable bindings."));
+
+	g_signal_connect(G_OBJECT(klass->help_window), "destroy", G_CALLBACK(reset_binding_window), klass);
+	gnt_widget_show(GNT_WIDGET(klass->help_window));
+	g_free(title);
+
+	return TRUE;
+}
+
--- a/finch/libgnt/gntbindable.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntbindable.h	Tue Aug 14 02:45:41 2007 +0000
@@ -57,7 +57,8 @@
 	GHashTable *actions;  /* name -> Action */
 	GHashTable *bindings; /* key -> ActionParam */
 
-	void (*gnt_reserved1)(void);
+	GntBindable * help_window;
+
 	void (*gnt_reserved2)(void);
 	void (*gnt_reserved3)(void);
 	void (*gnt_reserved4)(void);
@@ -150,6 +151,31 @@
  */
 gboolean gnt_bindable_perform_action_named(GntBindable *bindable, const char *name, ...);
 
+/**
+* Returns a GntTree populated with "key" -> "binding" for the widget.
+*/
+/**
+* 
+* @param widget
+*
+* @return
+*/
+GntBindable * gnt_bindable_bindings_view(GntBindable *bind);
+
+/**
+ *
+ * Builds a window that list the key bindings for a GntBindable object.  From this window a user can select a listing to rebind a new key for the given action.
+ *
+ */
+/**
+ * 
+ * @param bindable
+ *	
+ * @return
+ */
+
+gboolean gnt_bindable_build_help_window(GntBindable *bindable);
+
 G_END_DECLS
 
 #endif /* GNT_BINDABLE_H */
--- a/finch/libgnt/gntcombobox.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntcombobox.h	Tue Aug 14 02:45:41 2007 +0000
@@ -69,7 +69,8 @@
 G_BEGIN_DECLS
 
 /**
- * 
+ *
+ * Get the GType for GntComboBox
  *
  * @return
  */
@@ -77,44 +78,55 @@
 
 /**
  * 
+ * Create a new GntComboBox
  *
- * @return
+ * @return A new GntComboBox
  */
 GntWidget * gnt_combo_box_new(void);
 
 /**
  * 
- * @param box
- * @param key
- * @param text
+ * Add an entry
+ *
+ * @param box The GntComboBox
+ * @param key The data
+ * @param text The text to display
  */
 void gnt_combo_box_add_data(GntComboBox *box, gpointer key, const char *text);
 
 /**
+ *
+ * Remove an entry
  * 
- * @param box
- * @param key
+ * @param box The GntComboBox
+ * @param key The data to be removed
  */
 void gnt_combo_box_remove(GntComboBox *box, gpointer key);
 
 /**
  * 
- * @param box
+ * Remove all entries
+ *
+ * @param box The GntComboBox
  */
 void gnt_combo_box_remove_all(GntComboBox *box);
 
 /**
  * 
- * @param box
+ * Get the data that is currently selected
  *
- * @return
+ * @param box The GntComboBox
+ *
+ * @return The data of the currently selected entry
  */
 gpointer gnt_combo_box_get_selected_data(GntComboBox *box);
 
 /**
  * 
- * @param box
- * @param key
+ * Set the current selection to a specific entry
+ *
+ * @param box The GntComboBox
+ * @param key The data to be set to
  */
 void gnt_combo_box_set_selected(GntComboBox *box, gpointer key);
 
--- a/finch/libgnt/gntfilesel.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntfilesel.c	Tue Aug 14 02:45:41 2007 +0000
@@ -355,6 +355,7 @@
 		} else if (strcmp(str, "..") == 0) {
 			gnt_tree_set_selected(tree, dir);
 		}
+		gnt_bindable_perform_action_named(GNT_BINDABLE(tree), "end-search", NULL);
 		g_free(dir);
 		g_free(str);
 		g_free(path);
@@ -495,10 +496,11 @@
 	if (!sel->multiselect)
 		return FALSE;
 	tree = sel->dirsonly ? sel->dirs : sel->files;
-	if (!gnt_widget_has_focus(tree))
+	if (!gnt_widget_has_focus(tree) ||
+			gnt_tree_is_searching(GNT_TREE(tree)))
 		return FALSE;
 
-	file = gnt_tree_get_selection_data(sel->dirsonly ? GNT_TREE(sel->dirs) : GNT_TREE(sel->files));
+	file = gnt_tree_get_selection_data(GNT_TREE(tree));
 
 	str = gnt_file_sel_get_selected_file(sel);
 	if ((find = g_list_find_custom(sel->tags, str, (GCompareFunc)g_utf8_collate)) != NULL) {
@@ -526,7 +528,8 @@
 	if (!sel->multiselect)
 		return FALSE;
 	tree = sel->dirsonly ? sel->dirs : sel->files;
-	if (!gnt_widget_has_focus(tree))
+	if (!gnt_widget_has_focus(tree) ||
+			gnt_tree_is_searching(GNT_TREE(tree)))
 		return FALSE;
 
 	g_list_foreach(sel->tags, (GFunc)g_free, NULL);
@@ -547,6 +550,9 @@
 	if (!gnt_widget_has_focus(sel->dirs) &&
 			!gnt_widget_has_focus(sel->files))
 		return FALSE;
+	if (gnt_tree_is_searching(GNT_TREE(sel->dirs)) ||
+			gnt_tree_is_searching(GNT_TREE(sel->files)))
+		return FALSE;
 
 	path = g_build_filename(sel->current, "..", NULL);
 	dir = g_path_get_basename(sel->current);
--- a/finch/libgnt/gntline.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntline.c	Tue Aug 14 02:45:41 2007 +0000
@@ -24,6 +24,12 @@
 
 enum
 {
+	PROP_0,
+	PROP_VERTICAL
+};
+
+enum
+{
 	SIGS = 1,
 };
 
@@ -65,14 +71,57 @@
 }
 
 static void
+gnt_line_set_property(GObject *obj, guint prop_id, const GValue *value,
+		GParamSpec *spec)
+{
+	GntLine *line = GNT_LINE(obj);
+	switch (prop_id) {
+		case PROP_VERTICAL:
+			line->vertical = g_value_get_boolean(value);
+			if (line->vertical) {
+				GNT_WIDGET_SET_FLAGS(line, GNT_WIDGET_GROW_Y);
+			} else {
+				GNT_WIDGET_SET_FLAGS(line, GNT_WIDGET_GROW_X);
+			}
+			break;
+		default:
+			break;
+	}
+}
+
+static void
+gnt_line_get_property(GObject *obj, guint prop_id, GValue *value,
+		GParamSpec *spec)
+{
+	GntLine *line = GNT_LINE(obj);
+	switch (prop_id) {
+		case PROP_VERTICAL:
+			g_value_set_boolean(value, line->vertical);
+			break;
+		default:
+			break;
+	}
+}
+
+static void
 gnt_line_class_init(GntLineClass *klass)
 {
+	GObjectClass *gclass = G_OBJECT_CLASS(klass);
 	parent_class = GNT_WIDGET_CLASS(klass);
 	parent_class->draw = gnt_line_draw;
 	parent_class->map = gnt_line_map;
 	parent_class->size_request = gnt_line_size_request;
 
-	GNTDEBUG;
+	gclass->set_property = gnt_line_set_property;
+	gclass->get_property = gnt_line_get_property;
+	g_object_class_install_property(gclass,
+			PROP_VERTICAL,
+			g_param_spec_boolean("vertical", "Vertical",
+				"Whether it's a vertical line or a horizontal one.",
+				TRUE,
+				G_PARAM_READWRITE|G_PARAM_STATIC_NAME|G_PARAM_STATIC_NICK|G_PARAM_STATIC_BLURB
+			)
+		);
 }
 
 static void
@@ -118,20 +167,7 @@
 
 GntWidget *gnt_line_new(gboolean vertical)
 {
-	GntWidget *widget = g_object_new(GNT_TYPE_LINE, NULL);
-	GntLine *line = GNT_LINE(widget);
-
-	line->vertical = vertical;
-
-	if (vertical)
-	{
-		GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_Y);
-	}
-	else
-	{
-		GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X);
-	}
-
+	GntWidget *widget = g_object_new(GNT_TYPE_LINE, "vertical", vertical, NULL);
 	return widget;
 }
 
--- a/finch/libgnt/gntmain.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntmain.c	Tue Aug 14 02:45:41 2007 +0000
@@ -252,6 +252,18 @@
 	if (HOLDING_ESCAPE)
 		keys[0] = '\033';
 	k = keys;
+
+#if 0
+	/* I am not sure what's happening here. If this actually does something,
+	 * then this needs to go in gnt_keys_refine. */
+	if (*k < 0) { /* Alt not sending ESC* */
+		*(k + 1) = 128 - *k;
+		*k = 27;
+		*(k + 2) = 0;
+		rd++;
+	}
+#endif
+
 	while (rd) {
 		char back;
 		int p;
@@ -517,7 +529,8 @@
 
 void gnt_screen_release(GntWidget *widget)
 {
-	gnt_wm_window_close(wm, widget);
+	if (wm)
+		gnt_wm_window_close(wm, widget);
 }
 
 void gnt_screen_update(GntWidget *widget)
@@ -564,7 +577,9 @@
 
 void gnt_quit()
 {
-	g_hash_table_destroy(wm->nodes); /* XXX: */
+	g_object_unref(G_OBJECT(wm));
+	wm = NULL;
+
 	update_panels();
 	doupdate();
 	gnt_uninit_colors();
--- a/finch/libgnt/gntmenu.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntmenu.c	Tue Aug 14 02:45:41 2007 +0000
@@ -247,11 +247,14 @@
 	int current = menu->selected;
 
 	if (menu->submenu) {
-		do menu = menu->submenu; while (menu->submenu);
-		return (gnt_widget_key_pressed(GNT_WIDGET(menu), text));
+		GntMenu *sub = menu;
+		do sub = sub->submenu; while (sub->submenu);
+		if (gnt_widget_key_pressed(GNT_WIDGET(sub), text))
+			return TRUE;
 	}
 
-	if (text[0] == 27 && text[1] == 0) {
+	if ((text[0] == 27 && text[1] == 0) ||
+			(menu->type != GNT_MENU_TOPLEVEL && strcmp(text, GNT_KEY_LEFT) == 0)) {
 		/* Escape closes menu */
 		GntMenu *par = menu->parentmenu;
 		if (par != NULL) {
@@ -271,11 +274,17 @@
 			menu->selected++;
 			if (menu->selected >= g_list_length(menu->list))
 				menu->selected = 0;
-		} else if (strcmp(text, GNT_KEY_ENTER) == 0) {
+		} else if (strcmp(text, GNT_KEY_ENTER) == 0 ||
+				strcmp(text, GNT_KEY_DOWN) == 0) {
 			gnt_widget_activate(widget);
 		}
 
 		if (current != menu->selected) {
+			GntMenu *sub = menu->submenu;
+			while (sub) {
+				gnt_widget_hide(GNT_WIDGET(sub));
+				sub = sub->submenu;
+			}
 			gnt_widget_draw(widget);
 			return TRUE;
 		}
@@ -283,6 +292,12 @@
 		if (text[1] == '\0') {
 			if (check_for_trigger(menu, text[0]))
 				return TRUE;
+		} else if (strcmp(text, GNT_KEY_RIGHT) == 0) {
+			GntMenuItem *item = gnt_tree_get_selection_data(GNT_TREE(menu));
+			if (item && item->submenu) {
+				menuitem_activate(menu, item);
+				return TRUE;
+			}
 		}
 		return org_key_pressed(widget, text);
 	}
--- a/finch/libgnt/gnttextview.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gnttextview.c	Tue Aug 14 02:45:41 2007 +0000
@@ -68,17 +68,31 @@
 	int i = 0;
 	GList *lines;
 	int rows, scrcol;
+	int comp = 0;          /* Used for top-aligned text */
 	gboolean has_scroll = !(view->flags & GNT_TEXT_VIEW_NO_SCROLL);
 
 	wbkgd(widget->window, COLOR_PAIR(GNT_COLOR_NORMAL));
 	werase(widget->window);
 
+	if ((view->flags & GNT_TEXT_VIEW_TOP_ALIGN) &&
+			g_list_length(view->list) < widget->priv.height) {
+		GList *now = view->list;
+		comp = widget->priv.height - g_list_length(view->list);
+		view->list = g_list_nth_prev(view->list, comp);
+		if (!view->list) {
+			view->list = g_list_first(now);
+			comp = widget->priv.height - g_list_length(view->list);
+		} else {
+			comp = 0;
+		}
+	}
+
 	for (i = 0, lines = view->list; i < widget->priv.height && lines; i++, lines = lines->next)
 	{
 		GList *iter;
 		GntTextLine *line = lines->data;
 
-		wmove(widget->window, widget->priv.height - 1 - i, 0);
+		wmove(widget->window, widget->priv.height - 1 - i - comp, 0);
 
 		for (iter = line->segments; iter; iter = iter->next)
 		{
@@ -398,7 +412,7 @@
 static void
 gnt_text_view_size_changed(GntWidget *widget, int w, int h)
 {
-	if (w != widget->priv.width) {
+	if (w != widget->priv.width && GNT_WIDGET_IS_FLAG_SET(widget, GNT_WIDGET_MAPPED)) {
 		gnt_text_view_reflow(GNT_TEXT_VIEW(widget));
 	}
 }
@@ -422,11 +436,16 @@
 gnt_text_view_init(GTypeInstance *instance, gpointer class)
 {
 	GntWidget *widget = GNT_WIDGET(instance);
-	
-	GNT_WIDGET_SET_FLAGS(GNT_WIDGET(instance), GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X);
+	GntTextView *view = GNT_TEXT_VIEW(widget);
+	GntTextLine *line = g_new0(GntTextLine, 1);
 
+	GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW | 
+            GNT_WIDGET_GROW_Y | GNT_WIDGET_GROW_X);
 	widget->priv.minw = 5;
 	widget->priv.minh = 2;
+	view->string = g_string_new(NULL);
+	view->list = g_list_append(view->list, line);
+
 	GNTDEBUG;
 }
 
@@ -464,13 +483,6 @@
 GntWidget *gnt_text_view_new()
 {
 	GntWidget *widget = g_object_new(GNT_TYPE_TEXT_VIEW, NULL);
-	GntTextView *view = GNT_TEXT_VIEW(widget);
-	GntTextLine *line = g_new0(GntTextLine, 1);
-
-	GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_BORDER | GNT_WIDGET_NO_SHADOW);
-
-	view->string = g_string_new(NULL);
-	view->list = g_list_append(view->list, line);
 
 	return widget;
 }
--- a/finch/libgnt/gnttextview.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gnttextview.h	Tue Aug 14 02:45:41 2007 +0000
@@ -47,9 +47,11 @@
 typedef struct _GntTextViewPriv		GntTextViewPriv;
 typedef struct _GntTextViewClass		GntTextViewClass;
 
-typedef enum _GntTextViewFlag {
+typedef enum
+{
 	GNT_TEXT_VIEW_NO_SCROLL     = 1 << 0,
 	GNT_TEXT_VIEW_WRAP_CHAR     = 1 << 1,
+	GNT_TEXT_VIEW_TOP_ALIGN     = 1 << 2,
 } GntTextViewFlag;
 
 struct _GntTextView
--- a/finch/libgnt/gnttree.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gnttree.c	Tue Aug 14 02:45:41 2007 +0000
@@ -450,7 +450,7 @@
 			if (COLUMN_INVISIBLE(tree, i)) {
 				continue;
 			}
-			mvwaddstr(widget->window, pos, x + 1, tree->columns[i].title);
+			mvwaddnstr(widget->window, pos, x + (x != pos), tree->columns[i].title, tree->columns[i].width);
 			NEXT_X;
 		}
 		if (pos)
@@ -768,6 +768,7 @@
 		g_string_free(tree->priv->search, TRUE);
 		tree->priv->search = NULL;
 		tree->priv->search_timeout = 0;
+		GNT_WIDGET_UNSET_FLAGS(GNT_WIDGET(tree), GNT_WIDGET_DISABLE_ACTIONS);
 	}
 }
 
@@ -1053,7 +1054,9 @@
 	GntTree *tree = GNT_TREE(widget);
 	tree->show_separator = TRUE;
 	tree->priv = g_new0(GntTreePriv, 1);
-	GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y | GNT_WIDGET_CAN_TAKE_FOCUS);
+	GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_GROW_X | GNT_WIDGET_GROW_Y |
+			GNT_WIDGET_CAN_TAKE_FOCUS | GNT_WIDGET_NO_SHADOW);
+	gnt_widget_set_take_focus(widget, TRUE);
 	widget->priv.minw = 4;
 	widget->priv.minh = 1;
 	GNTDEBUG;
@@ -1605,9 +1608,6 @@
 			"columns", col,
 			NULL);
 
-	GNT_WIDGET_SET_FLAGS(widget, GNT_WIDGET_NO_SHADOW);
-	gnt_widget_set_take_focus(widget, TRUE);
-
 	return widget;
 }
 
--- a/finch/libgnt/gntutils.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntutils.h	Tue Aug 14 02:45:41 2007 +0000
@@ -40,6 +40,7 @@
  */
 void gnt_util_get_text_bound(const char *text, int *width, int *height);
 
+/* excluding *end */
 /**
  * Get the onscreen width of a string, or a substring.
  *
--- a/finch/libgnt/gntwm.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/gntwm.c	Tue Aug 14 02:45:41 2007 +0000
@@ -39,6 +39,8 @@
 #include "gntmarshal.h"
 #include "gnt.h"
 #include "gntbox.h"
+#include "gntbutton.h"
+#include "gntentry.h"
 #include "gntlabel.h"
 #include "gntmenu.h"
 #include "gnttextview.h"
@@ -84,6 +86,7 @@
 static time_t last_active_time;
 static gboolean idle_update;
 static GList *act = NULL; /* list of WS with unseen activitiy */
+static gboolean ignore_keys = FALSE;
 
 static GList *
 g_list_bring_to_front(GList *list, gpointer data)
@@ -485,35 +488,6 @@
 	return TRUE;
 }
 
-static gboolean
-help_for_widget(GntBindable *bindable, GList *null)
-{
-	GntWM *wm = GNT_WM(bindable);
-	GntWidget *widget, *tree, *win, *active;
-	char *title;
-
-	if (!wm->cws->ordered)
-		return TRUE;
-
-	widget = wm->cws->ordered->data;
-	if (!GNT_IS_BOX(widget))
-		return TRUE;
-	active = GNT_BOX(widget)->active;
-
-	tree = gnt_widget_bindings_view(active);
-	win = gnt_window_new();
-	title = g_strdup_printf("Bindings for %s", g_type_name(G_OBJECT_TYPE(active)));
-	gnt_box_set_title(GNT_BOX(win), title);
-	if (tree)
-		gnt_box_add_widget(GNT_BOX(win), tree);
-	else
-		gnt_box_add_widget(GNT_BOX(win), gnt_label_new("This widget has no customizable bindings."));
-
-	gnt_widget_show(win);
-
-	return TRUE;
-}
-
 static void
 destroy__list(GntWidget *widget, GntWM *wm)
 {
@@ -684,6 +658,8 @@
 		{'j', "&#x2518;"},
 		{'a', "&#x2592;"},
 		{'n', "&#x253c;"},
+		{'w', "&#x252c;"},
+		{'v', "&#x2534;"},
 		{'\0', NULL}
 	};
 
@@ -841,6 +817,7 @@
 shift_right(GntBindable *bindable, GList *null)
 {
 	GntWM *wm = GNT_WM(bindable);
+	
 	if (wm->_list.window)
 		return TRUE;
 
@@ -1136,10 +1113,106 @@
 	return TRUE;
 }
 
+static gboolean
+ignore_keys_start(GntBindable *bindable, GList *n)
+{
+	GntWM *wm = GNT_WM(bindable);
+
+	if(!wm->menu && !wm->_list.window && wm->mode == GNT_KP_MODE_NORMAL){
+		ignore_keys = TRUE;
+		return TRUE;
+	}
+	return FALSE;
+}
+
+static gboolean
+ignore_keys_end(GntBindable *bindable, GList *n)
+{
+	return ignore_keys ? !(ignore_keys = FALSE) : FALSE;
+}
+
+static gboolean
+help_for_bindable(GntWM *wm, GntBindable *bindable)
+{
+	gboolean ret = TRUE;
+	GntBindableClass *klass = GNT_BINDABLE_GET_CLASS(bindable);
+ 
+	if (klass->help_window) {
+		gnt_wm_raise_window(wm, GNT_WIDGET(klass->help_window));
+	} else {
+		ret =  gnt_bindable_build_help_window(bindable);
+	}
+	return ret;
+}
+
+static gboolean
+help_for_wm(GntBindable *bindable, GList *null)
+{
+	return help_for_bindable(GNT_WM(bindable),bindable);
+}
+
+static gboolean
+help_for_window(GntBindable *bindable, GList *null)
+{
+	GntWM *wm = GNT_WM(bindable);
+	GntWidget *widget;
+
+	if(!wm->cws->ordered)
+		return FALSE;
+
+	widget = wm->cws->ordered->data;
+
+	return help_for_bindable(wm,GNT_BINDABLE(widget));
+}
+
+static gboolean
+help_for_widget(GntBindable *bindable, GList *null)
+{
+	GntWM *wm = GNT_WM(bindable);
+	GntWidget *widget;
+
+	if (!wm->cws->ordered)
+		return TRUE;
+
+	widget = wm->cws->ordered->data;
+	if (!GNT_IS_BOX(widget))
+		return TRUE;
+
+	return help_for_bindable(wm, GNT_BINDABLE(GNT_BOX(widget)->active));
+}
+
+static void
+accumulate_windows(gpointer window, gpointer node, gpointer p)
+{
+	GList *list = *(GList**)p;
+	list = g_list_prepend(list, window);
+	*(GList**)p = list;
+}
+
+static void
+gnt_wm_destroy(GObject *obj)
+{
+	GntWM *wm = GNT_WM(obj);
+	GList *list = NULL;
+	g_hash_table_foreach(wm->nodes, accumulate_windows, &list);
+	g_list_foreach(list, (GFunc)gnt_widget_destroy, NULL);
+	g_list_free(list);
+	g_hash_table_destroy(wm->nodes);
+	wm->nodes = NULL;
+
+	while (wm->workspaces) {
+		g_object_unref(wm->workspaces->data);
+		wm->workspaces = g_list_delete_link(wm->workspaces, wm->workspaces);
+	}
+}
+
 static void
 gnt_wm_class_init(GntWMClass *klass)
 {
 	int i;
+	GObjectClass *gclass = G_OBJECT_CLASS(klass);
+
+	gclass->dispose = gnt_wm_destroy;
 
 	klass->new_window = gnt_wm_new_window_real;
 	klass->decorate_window = NULL;
@@ -1291,8 +1364,16 @@
 				"\033" "T", NULL);
 	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "workspace-list", workspace_list,
 				"\033" "s", NULL);
-	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "toggle-clipboard",
-				toggle_clipboard, "\033" "C", NULL);
+	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "toggle-clipboard", toggle_clipboard,
+				"\033" "C", NULL);
+	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "help-for-wm", help_for_wm,
+				"\033" "\\", NULL);
+	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "help-for-window", help_for_window,
+				"\033" "|", NULL);
+	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "ignore-keys-start", ignore_keys_start, 
+				GNT_KEY_CTRL_G, NULL);
+	gnt_bindable_class_register_action(GNT_BINDABLE_CLASS(klass), "ignore-keys-end", ignore_keys_end, 
+				"\033" GNT_KEY_CTRL_G, NULL);
 
 	gnt_style_read_actions(G_OBJECT_CLASS_TYPE(klass), GNT_BINDABLE_CLASS(klass));
 
@@ -1679,6 +1760,14 @@
 	keys = gnt_bindable_remap_keys(GNT_BINDABLE(wm), keys);
 
 	idle_update = TRUE;
+	if(ignore_keys){
+		if(keys && !strcmp(keys, "\033" GNT_KEY_CTRL_G)){
+			if(gnt_bindable_perform_action_key(GNT_BINDABLE(wm), keys)){
+				return TRUE;
+			}
+		}
+		return wm->cws->ordered ? gnt_widget_key_pressed(GNT_WIDGET(wm->cws->ordered->data), keys) : FALSE;
+	}
 
 	if (gnt_bindable_perform_action_key(GNT_BINDABLE(wm), keys)) {
 		return TRUE;
@@ -1897,7 +1986,7 @@
 
 	if (!node)
 		return;
-	
+
 	if (widget != wm->_list.window && !GNT_IS_MENU(widget) &&
 				wm->cws->ordered->data != widget) {
 		GntWidget *w = wm->cws->ordered->data;
@@ -1978,3 +2067,4 @@
 	wm->event_stack = set;
 }
 
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,40 @@
+EXTRA_DIST = gendef.sh
+
+pg_LTLIBRARIES = gnt.la
+
+pgdir = $(libdir)
+
+sources = \
+	gnt.def \
+	gnt.override \
+	gntbox.override \
+	gntfilesel.override \
+	gnttree.override \
+	gntwidget.override
+
+gnt_la_SOURCES = gnt.c common.c common.h gntmodule.c
+
+gnt_la_LDFLAGS = -module -avoid-version \
+	`pkg-config --libs pygobject-2.0`
+
+gnt_la_LIBADD = \
+	$(GLIB_LIBS) \
+	../libgnt.la
+
+AM_CPPFLAGS = \
+	-I../ \
+	$(GLIB_CFLAGS) \
+	$(GNT_CFLAGS)  \
+	-I/usr/include/python2.4 \
+	`pkg-config --cflags pygobject-2.0`
+
+CLEANFILES = gnt.def gnt.c gnt.defe
+
+gnt.def: $(srcdir)/../*.h
+	$(srcdir)/gendef.sh
+
+gnt.c: $(sources)
+	pygtk-codegen-2.0 --prefix gnt \
+	--override gnt.override \
+	gnt.def > $@
+
--- a/finch/libgnt/pygnt/Makefile.make	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/Makefile.make	Tue Aug 14 02:45:41 2007 +0000
@@ -10,5 +10,7 @@
 	--override gnt.override \
 	gnt.def > $@
 
+#python codegen/codegen.py --prefix gnt \
+
 clean:
 	@rm *.so *.o gnt.c
--- a/finch/libgnt/pygnt/dbus-gnt	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/dbus-gnt	Tue Aug 14 02:45:41 2007 +0000
@@ -17,13 +17,14 @@
 
 convwins = {}
 
-def buddysignedon():
+def buddysignedon(buddy):
     pass
 
 def conv_closed(conv):
     key = get_dict_key(conv)
     stuff = convwins[key]
     stuff[0].destroy()
+    # if a conv window is closed, then reopened, this thing crashes
     convwins[key] = None
 
 def wrote_msg(account, who, msg, conv, flags):
@@ -31,10 +32,13 @@
     tv = stuff[1]
     tv.append_text_with_flags("\n", 0)
     tv.append_text_with_flags(strftime("(%X) "), 8)
-    tv.append_text_with_flags(who + ": ", 1)
-    tv.append_text_with_flags(msg, 0)
+    if flags & 3:
+        tv.append_text_with_flags(who + ": ", 1)
+        tv.append_text_with_flags(msg, 0)
+        stuff[0].set_urgent()
+    else:
+        tv.append_text_with_flags(msg, 8)
     tv.scroll(0)
-    stuff[0].set_urgent()
 
 bus = dbus.SessionBus()
 obj = bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
@@ -72,6 +76,9 @@
             purple.PurpleConvChatSend(chatdata, entry.get_text())
         entry.clear()
 
+def conv_window_destroyed(win, key):
+    del convwins[key]
+
 def show_conversation(conv):
     key = get_dict_key(conv)
     if key in convwins:
@@ -91,27 +98,28 @@
     tv.clear()
     win.show()
     convwins[key] = [win, tv, entry]
+    win.connect("destroy", conv_window_destroyed, key)
     return convwins[key]
 
 def show_buddylist():
-	win = gnt.Window()
-	tree = gnt.Tree()
-	tree.set_property("columns", 1)
-	win.add_widget(tree)
-	node = purple.PurpleBlistGetRoot()
-	while node:
-		if purple.PurpleBlistNodeIsGroup(node):
-			sys.stderr.write(str(node) + "\n")
-			tree.add_row_after(str(node), ["asd", ""], None, None)
-			#tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None)
-			#tree.add_row_after(node, ["aasd", ""], None, None)
-		elif purple.PurpleBlistNodeIsContact(node):
-			buddy = purple.PurpleContactGetPriorityBuddy(node)
-			group = purple.PurpleBuddyGetGroup(buddy)
-			#tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None)
+    win = gnt.Window()
+    tree = gnt.Tree()
+    tree.set_property("columns", 1)
+    win.add_widget(tree)
+    node = purple.PurpleBlistGetRoot()
+    while node:
+        if purple.PurpleBlistNodeIsGroup(node):
+            sys.stderr.write(str(node) + "\n")
+            tree.add_row_after(str(node), ["asd", ""], None, None)
+            #tree.add_row_after(node, [str(purple.PurpleGroupGetName(node)), ""], None, None)
+            #tree.add_row_after(node, ["aasd", ""], None, None)
+        elif purple.PurpleBlistNodeIsContact(node):
+            buddy = purple.PurpleContactGetPriorityBuddy(node)
+            group = purple.PurpleBuddyGetGroup(buddy)
+            #tree.add_row_after(node, [str(purple.PurpleBuddyGetName(buddy)), ""], group, None)
 
-		node = purple.PurpleBlistNodeNext(node, False)
-	win.show()
+        node = purple.PurpleBlistNodeNext(node, False)
+    win.show()
 
 gnt.gnt_init()
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/example/rss/gnthtml.py	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+
+"""
+gr - An RSS-reader built using libgnt and feedparser.
+
+Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im>
+
+This application is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This application 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this application; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+"""
+
+"""
+This file defines GParser, which is a simple HTML parser to display HTML
+in a GntTextView nicely.
+"""
+
+import sgmllib
+import gnt
+
+class GParser(sgmllib.SGMLParser):
+    def __init__(self, view):
+        sgmllib.SGMLParser.__init__(self, False)
+        self.link = None
+        self.view = view
+        self.flag = gnt.TEXT_FLAG_NORMAL
+
+    def parse(self, s):
+        self.feed(s)
+        self.close()
+
+    def unknown_starttag(self, tag, attrs):
+        if tag in ["b", "i", "blockquote", "strong"]:
+            self.flag = self.flag | gnt.TEXT_FLAG_BOLD
+        elif tag in ["p", "hr", "br"]:
+            self.view.append_text_with_flags("\n", self.flag)
+        else:
+            print tag
+
+    def unknown_endtag(self, tag):
+        if tag in ["b", "i", "blockquote", "strong"]:
+            self.flag = self.flag & ~gnt.TEXT_FLAG_BOLD
+        elif tag in ["p", "hr", "br"]:
+            self.view.append_text_with_flags("\n", self.flag)
+        else:
+            print tag
+
+    def start_u(self, attrs):
+        self.flag = self.flag | gnt.TEXT_FLAG_UNDERLINE
+
+    def end_u(self):
+        self.flag = self.flag & ~gnt.TEXT_FLAG_UNDERLINE
+
+    def start_a(self, attributes):
+        for name, value in attributes:
+            if name == "href":
+                self.link = value
+
+    def do_img(self, attrs):
+        for name, value in attrs:
+            if name == 'src':
+                self.view.append_text_with_flags("[img:" + value + "]", self.flag)
+
+    def end_a(self):
+        if not self.link:
+            return
+        self.view.append_text_with_flags(" (", self.flag)
+        self.view.append_text_with_flags(self.link, self.flag | gnt.TEXT_FLAG_UNDERLINE)
+        self.view.append_text_with_flags(")", self.flag)
+        self.link = None
+
+    def handle_data(self, data):
+        if len(data.strip()) == 0:
+            return
+        self.view.append_text_with_flags(data, self.flag)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/example/rss/gntrss-ui.py	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,400 @@
+#!/usr/bin/env python
+
+"""
+gr - An RSS-reader built using libgnt and feedparser.
+
+Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im>
+
+This application is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This application 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this application; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+"""
+
+"""
+This file deals with the UI part (gnt) of the application
+
+TODO:
+    - Allow showing feeds of only selected 'category' and/or 'priority'. A different
+      window should be used to change such filtering.
+    - Display details of each item in its own window.
+    - Add search capability, and allow searching only in title/body. Also allow
+      filtering in the search results.
+    - Show the data and time for feed items (probably in a separate column .. perhaps not)
+    - Have a simple way to add a feed.
+    - Allow renaming a feed.
+"""
+
+import gntrss
+import gnthtml
+import gnt
+import gobject
+import sys
+
+__version__ = "0.0.1alpha"
+__author__ = "Sadrul Habib Chowdhury (sadrul@pidgin.im)"
+__copyright__ = "Copyright 2007, Sadrul Habib Chowdhury"
+__license__ = "GPL" # see full license statement above
+
+gnt.gnt_init()
+
+class RssTree(gnt.Tree):
+    __gsignals__ = {
+        'active_changed' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,))
+    }
+
+    __gntbindings__ = {
+        'jump-next-unread' : ('jump_next_unread', 'J')
+    }
+
+    def jump_next_unread(self, null):
+        first = None
+        next = None
+        all = self.get_rows()
+        for item in all:
+            if item.unread:
+                if next:
+                    first = item
+                    break
+                elif not first and self.active != item:
+                    first = item
+            if self.active == item:
+                next = item
+        if first:
+            self.set_active(first)
+            self.set_selected(first)
+        return True
+
+    def __init__(self):
+        self.active = None
+        gnt.Tree.__init__(self)
+        gnt.set_flag(self, 8)    # remove borders
+        self.connect('key_pressed', self.do_key_pressed)
+
+    def set_active(self, active):
+        if self.active == active:
+            return
+        if self.active:
+            flag = gnt.TEXT_FLAG_NORMAL
+            if self.active.unread:
+                flag = flag | gnt.TEXT_FLAG_BOLD
+            self.set_row_flags(self.active, flag)
+        old = self.active
+        self.active = active
+        flag = gnt.TEXT_FLAG_UNDERLINE
+        if self.active.unread:
+            flag = flag | gnt.TEXT_FLAG_BOLD
+        self.set_row_flags(self.active, flag)
+        self.emit('active_changed', old)
+
+    def do_key_pressed(self, null, text):
+        if text == '\r':
+            now = self.get_selection_data()
+            self.set_active(now)
+            return True
+        return False
+
+gobject.type_register(RssTree)
+gnt.register_bindings(RssTree)
+
+win = gnt.Box(homo = False, vert = True)
+win.set_toplevel(True)
+win.set_title("GntRss")
+win.set_pad(0)
+
+#
+# [[[ Generic feed/item callbacks
+#
+def feed_item_added(feed, item):
+    add_feed_item(item)
+
+def add_feed(feed):
+    if not feed.get_data('gntrss-connected'):
+        feed.connect('added', feed_item_added)
+        feed.connect('notify', update_feed_title)
+        feed.set_data('gntrss-connected', True)
+    feeds.add_row_after(feed, [feed.title, str(feed.unread)], None, None)
+
+def remove_item(item, feed):
+    items.remove(item)
+
+def update_feed_item(item, property):
+    if property.name == 'unread':
+        if feeds.active == item.parent:
+            flag = 0
+            if item == items.active:
+                flag = gnt.TEXT_FLAG_UNDERLINE
+            if item.unread:
+                flag = flag | gnt.TEXT_FLAG_BOLD
+            else:
+                flag = flag | gnt.TEXT_FLAG_NORMAL
+            items.set_row_flags(item, flag)
+
+        unread = item.parent.unread
+        if item.unread:
+            unread = unread + 1
+        else:
+            unread = unread - 1
+        item.parent.set_property('unread', unread)
+
+def add_feed_item(item):
+    currentfeed = feeds.active
+    if item.parent != currentfeed:
+        return
+    months = ["", "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+    dt = str(item.date_parsed[2]) + "." + months[item.date_parsed[1]] + "." + str(item.date_parsed[0])
+    items.add_row_after(item, [str(item.title), dt], None, None)
+    if item.unread:
+        items.set_row_flags(item, gnt.TEXT_FLAG_BOLD)
+    if not item.get_data('gntrss-connected'):
+        item.set_data('gntrss-connected', True)
+        # this needs to happen *without* having to add the item in the tree
+        item.connect('notify', update_feed_item)
+        item.connect('delete', remove_item)
+
+#
+# ]]] Generic feed/item callbacks
+#
+
+
+####
+# [[[ The list of feeds
+###
+
+# 'Add Feed' dialog
+add_feed_win = None
+def add_feed_win_closed(win):
+    global add_feed_win
+    add_feed_win = None
+
+def add_new_feed():
+    global add_feed_win
+
+    if add_feed_win:
+        gnt.gnt_window_present(add_feed_win)
+        return
+    win = gnt.Window()
+    win.set_title("New Feed")
+
+    box = gnt.Box(False, False)
+    label = gnt.Label("Link")
+    box.add_widget(label)
+    entry = gnt.Entry("")
+    entry.set_size(40, 1)
+    box.add_widget(entry)
+
+    win.add_widget(box)
+    win.show()
+    add_feed_win = win
+    add_feed_win.connect("destroy", add_feed_win_closed)
+
+#
+# The active row in the feed-list has changed. Update the feed-item table.
+def feed_active_changed(tree, old):
+    items.remove_all()
+    if not tree.active:
+        return
+    update_items_title()
+    for item in tree.active.items:
+        add_feed_item(item)
+    win.give_focus_to_child(items)
+
+#
+# Check for the action keys and decide how to deal with them.
+def feed_key_pressed(tree, text):
+    if tree.is_searching():
+        return
+    if text == 'r':
+        feed = tree.get_selection_data()
+        tree.perform_action_key('j')
+        #tree.perform_action('move-down')
+        feed.refresh()
+    elif text == 'R':
+        feeds = tree.get_rows()
+        for feed in feeds:
+            feed.refresh()
+    elif text == 'm':
+        feed = tree.get_selection_data()
+        if feed:
+            feed.mark_read()
+            feed.set_property('unread', 0)
+    elif text == 'a':
+        add_new_feed()
+    else:
+        return False
+    return True
+
+feeds = RssTree()
+feeds.set_property('columns', 2)
+feeds.set_col_width(0, 20)
+feeds.set_col_width(1, 6)
+feeds.set_column_resizable(0, False)
+feeds.set_column_resizable(1, False)
+feeds.set_column_is_right_aligned(1, True)
+feeds.set_show_separator(False)
+feeds.set_column_title(0, "Feeds")
+feeds.set_show_title(True)
+
+feeds.connect('active_changed', feed_active_changed)
+feeds.connect('key_pressed', feed_key_pressed)
+gnt.unset_flag(feeds, 256)   # Fix the width
+
+####
+# ]]] The list of feeds
+###
+
+####
+# [[[ The list of items in the feed
+####
+
+#
+# The active item in the feed-item list has changed. Update the
+# summary content.
+def item_active_changed(tree, old):
+    details.clear()
+    if not tree.active:
+        return
+    item = tree.active
+    details.append_text_with_flags(str(item.title) + "\n", gnt.TEXT_FLAG_BOLD)
+    details.append_text_with_flags("Link: ", gnt.TEXT_FLAG_BOLD)
+    details.append_text_with_flags(str(item.link) + "\n", gnt.TEXT_FLAG_UNDERLINE)
+    details.append_text_with_flags("Date: ", gnt.TEXT_FLAG_BOLD)
+    details.append_text_with_flags(str(item.date) + "\n", gnt.TEXT_FLAG_NORMAL)
+    details.append_text_with_flags("\n", gnt.TEXT_FLAG_NORMAL)
+    parser = gnthtml.GParser(details)
+    parser.parse(str(item.summary))
+    item.mark_unread(False)
+
+    if old and old.unread:   # If the last selected item is marked 'unread', then make sure it's bold
+        items.set_row_flags(old, gnt.TEXT_FLAG_BOLD)
+
+#
+# Look for action keys in the feed-item list.
+def item_key_pressed(tree, text):
+    if tree.is_searching():
+        return
+    current = tree.get_selection_data()
+    if text == 'M':     # Mark all of the items 'read'
+        feed = feeds.active
+        if feed:
+            feed.mark_read()
+    elif text == 'm':     # Mark the current item 'read'
+        current.mark_unread(False)
+        tree.perform_action_key('j')
+    elif text == 'U':     # Mark the current item 'unread'
+        current.mark_unread(True)
+    elif text == 'd':
+        current.remove()
+        tree.perform_action_key('j')
+    else:
+        return False
+    return True
+
+items = RssTree()
+items.set_property('columns', 2)
+items.set_col_width(0, 40)
+items.set_col_width(1, 11)
+items.set_column_resizable(1, False)
+items.set_column_title(0, "Items")
+items.set_column_title(1, "Date")
+items.set_show_title(True)
+items.connect('key_pressed', item_key_pressed)
+items.connect('active_changed', item_active_changed)
+
+####
+# ]]] The list of items in the feed
+####
+
+#
+# Update the title of the items list depending on the selection in the feed list
+def update_items_title():
+    feed = feeds.active
+    if feed:
+        items.set_column_title(0, str(feed.title) + ": " + str(feed.unread) + "(" + str(len(feed.items)) + ")")
+    else:
+        items.set_column_title(0, "Items")
+    items.draw()
+
+# The container on the top
+line = gnt.Line(vertical = False)
+
+# The textview to show the details of a feed
+details = gnt.TextView()
+details.set_take_focus(True)
+details.set_flag(gnt.TEXT_VIEW_TOP_ALIGN)
+details.attach_scroll_widget(details)
+
+# Make it look nice
+s = feeds.get_size()
+size = gnt.screen_size()
+size[0] = size[0] - s[0]
+items.set_size(size[0], size[1] / 2)
+details.set_size(size[0], size[1] / 2)
+
+# Category tree
+cat = gnt.Tree()
+cat.set_property('columns', 1)
+cat.set_column_title(0, 'Category')
+cat.set_show_title(True)
+gnt.set_flag(cat, 8)    # remove borders
+
+box = gnt.Box(homo = False, vert = False)
+box.set_pad(0)
+
+vbox = gnt.Box(homo = False, vert = True)
+vbox.set_pad(0)
+vbox.add_widget(feeds)
+vbox.add_widget(gnt.Line(False))
+vbox.add_widget(cat)
+box.add_widget(vbox)
+
+box.add_widget(gnt.Line(True))
+
+vbox = gnt.Box(homo = False, vert = True)
+vbox.set_pad(0)
+vbox.add_widget(items)
+vbox.add_widget(gnt.Line(False))
+vbox.add_widget(details)
+box.add_widget(vbox)
+
+win.add_widget(box)
+win.show()
+
+def update_feed_title(feed, property):
+    if property.name == 'title':
+        if feed.customtitle:
+            title = feed.customtitle
+        else:
+            title = feed.title
+        feeds.change_text(feed, 0, title)
+    elif property.name == 'unread':
+        feeds.change_text(feed, 1, str(feed.unread) + "(" + str(len(feed.items)) + ")")
+        flag = 0
+        if feeds.active == feed:
+            flag = gnt.TEXT_FLAG_UNDERLINE
+            update_items_title()
+        if feed.unread > 0:
+            flag = flag | gnt.TEXT_FLAG_BOLD
+        feeds.set_row_flags(feed, flag)
+
+# populate everything
+for feed in gntrss.feeds:
+    feed.refresh()
+    feed.set_auto_refresh(True)
+    add_feed(feed)
+
+gnt.gnt_register_action("Stuff", add_new_feed)
+gnt.gnt_main()
+
+gnt.gnt_quit()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/example/rss/gntrss.py	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,250 @@
+#!/usr/bin/env python
+
+"""
+gr - An RSS-reader built using libgnt and feedparser.
+
+Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im>
+
+This application is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This application 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this application; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+USA
+"""
+
+"""
+This file deals with the rss parsing part (feedparser) of the application
+"""
+
+import os
+import tempfile, urllib2
+import feedparser
+import gobject
+import sys
+import time
+
+##
+# The FeedItem class. It will update emit 'delete' signal when it's
+# destroyed.
+##
+class FeedItem(gobject.GObject):
+    __gproperties__ = {
+        'unread' : (gobject.TYPE_BOOLEAN, 'read',
+            'The unread state of the item.',
+            False, gobject.PARAM_READWRITE)
+    }
+    __gsignals__ = {
+        'delete' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,))
+    }
+    def __init__(self, item, parent):
+        self.__gobject_init__()
+        try:
+            "Apparently some feed items don't have any dates in them"
+            self.date = item['date']
+            self.date_parsed = item['date_parsed']
+        except:
+            item['date'] = self.date = time.ctime()
+            self.date_parsed = feedparser._parse_date(self.date)
+
+        self.title = item['title'].encode('utf8')
+        self.summary = item['summary'].encode('utf8')
+        self.link = item['link']
+        self.parent = parent
+        self.unread = True
+
+    def remove(self):
+        self.emit('delete', self.parent)
+        if self.unread:
+            self.parent.set_property('unread', self.parent.unread - 1)
+
+    def do_set_property(self, property, value):
+        if property.name == 'unread':
+            self.unread = value
+
+    def mark_unread(self, unread):
+        if self.unread == unread:
+            return
+        self.set_property('unread', unread)
+
+gobject.type_register(FeedItem)
+
+def item_hash(item):
+    return str(item['title'])
+
+"""
+The Feed class. It will update the 'link', 'title', 'desc' and 'items'
+attributes if/when they are updated (triggering 'notify::<attr>' signal)
+
+TODO:
+    - Add a 'count' attribute
+    - Each feed will have a 'uidata', which will be its display window
+    - Look into 'category'. Is it something that feed defines, or the user?
+    - Have separate refresh times for each feed.
+    - Have 'priority' for each feed. (somewhat like category, perhaps?)
+"""
+class Feed(gobject.GObject):
+    __gproperties__ = {
+        'link' : (gobject.TYPE_STRING, 'link',
+            'The web page this feed is associated with.',
+            '...', gobject.PARAM_READWRITE),
+        'title' : (gobject.TYPE_STRING, 'title',
+            'The title of the feed.',
+            '...', gobject.PARAM_READWRITE),
+        'desc' : (gobject.TYPE_STRING, 'description',
+            'The description for the feed.',
+            '...', gobject.PARAM_READWRITE),
+        'items' : (gobject.TYPE_POINTER, 'items',
+            'The items in the feed.', gobject.PARAM_READWRITE),
+        'unread' : (gobject.TYPE_INT, 'unread',
+            'Number of unread items in the feed.', 0, 10000, 0, gobject.PARAM_READWRITE)
+    }
+    __gsignals__ = {
+        'added' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_OBJECT,))
+    }
+
+    def __init__(self, feed):
+        self.__gobject_init__()
+        url = feed['link']
+        name = feed['name']
+        self.url = url           # The url of the feed itself
+        self.link = url          # The web page associated with the feed
+        self.desc = url
+        self.title = (name, url)[not name]
+        self.customtitle = name
+        self.unread = 0
+        self.items = []
+        self.hash = {}
+        self.pending = False
+        self._refresh = {'time' : 30, 'id' : 0}
+
+    def do_set_property(self, property, value):
+        if property.name == 'link':
+            self.link = value
+        elif property.name == 'desc':
+            self.desc = value
+        elif property.name == 'title':
+            self.title = value
+        elif property.name == 'unread':
+            self.unread = value
+        pass
+
+    def set_result(self, result):
+        # XXX Look at result['bozo'] first, and emit some signal that the UI can use
+        # to indicate (dim the row?) that the feed has invalid XML format or something
+
+        try:
+            channel = result['channel']
+            self.set_property('link', channel['link'])
+            self.set_property('desc', channel['description'])
+            self.set_property('title', channel['title'])
+            items = result['items']
+        except:
+            items = ()
+
+        tmp = {}
+        for item in self.items:
+            tmp[hash(item)] = item
+
+        unread = self.unread
+        for item in items:
+            try:
+                exist = self.hash[item_hash(item)]
+                del tmp[hash(exist)]
+            except:
+                itm = FeedItem(item, self)
+                self.items.append(itm)
+                self.emit('added', itm)
+                self.hash[item_hash(item)] = itm
+                unread = unread + 1
+
+        if unread != self.unread:
+            self.set_property('unread', unread)
+
+        for hv in tmp:
+            self.items.remove(tmp[hv])
+            tmp[hv].remove()
+            "Also notify the UI about the count change"
+
+        self.pending = False
+        return False
+
+    def refresh(self):
+        if self.pending:
+            return
+        self.pending = True
+        FeedReader(self).run()
+        return True
+
+    def mark_read(self):
+        for item in self.items:
+            item.mark_unread(False)
+
+    def set_auto_refresh(self, auto):
+        if auto:
+            if self._refresh['id']:
+                return
+            if self._refresh['time'] < 1:
+                self._refresh['time'] = 1
+            self.id = gobject.timeout_add(self._refresh['time'] * 1000 * 60, self.refresh)
+        else:
+            if not self._refresh['id']:
+                return
+            gobject.source_remove(self._refresh['id'])
+            self._refresh['id'] = 0
+
+gobject.type_register(Feed)
+
+"""
+The FeedReader updates a Feed. It fork()s off a child to avoid blocking.
+"""
+class FeedReader:
+    def __init__(self, feed):
+        self.feed = feed
+
+    def reap_child(self, pid, status):
+        result = feedparser.parse(self.tmpfile.name)
+        self.tmpfile.close()
+        self.feed.set_result(result)
+
+    def run(self):
+        self.tmpfile = tempfile.NamedTemporaryFile()
+        self.pid = os.fork()
+        if self.pid == 0:
+            tmp = urllib2.urlopen(self.feed.url)
+            content = tmp.read()
+            tmp.close()
+            self.tmpfile.write(content)
+            self.tmpfile.flush()
+            # Do NOT close tmpfile here
+            os._exit(os.EX_OK)
+        gobject.child_watch_add(self.pid, self.reap_child)
+
+feeds = []
+urls = (
+    {'name': '/.',
+     'link': "http://rss.slashdot.org/Slashdot/slashdot"},
+    {'name': 'KernelTrap',
+     'link': "http://kerneltrap.org/node/feed"},
+    {'name': None,
+     'link': "http://pidgin.im/rss.php"},
+    {'name': "F1",
+     'link': "http://www.formula1.com/rss/news/latest.rss"},
+    {'name': "Freshmeat",
+     'link': "http://www.pheedo.com/f/freshmeatnet_announcements_unix"},
+    {'name': "Cricinfo",
+     'link': "http://www.cricinfo.com/rss/livescores.xml"}
+)
+
+for url in urls:
+    feed = Feed(url)
+    feeds.append(feed)
+
--- a/finch/libgnt/pygnt/gendef.sh	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/gendef.sh	Tue Aug 14 02:45:41 2007 +0000
@@ -28,7 +28,7 @@
 	gnt.h"
 
 # Generate the def file
-rm gnt.def
+rm -f gnt.def
 for file in $FILES
 do
 	python /usr/share/pygtk/2.0/codegen/h2def.py ../$file >> gnt.def
@@ -43,6 +43,7 @@
 GNT_TYPE_KEY_PRESS_MODE
 GNT_TYPE_ENTRY_FLAG
 GNT_TYPE_TEXT_FORMAT_FLAGS
+GNT_TYPE_TEXT_VIEW_FLAG
 "
 
 for enum in $ENUMS
--- a/finch/libgnt/pygnt/gnt.override	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/gnt.override	Tue Aug 14 02:45:41 2007 +0000
@@ -29,8 +29,10 @@
 #include "common.h"
 %%
 include
+ gntbox.override
  gntfilesel.override
  gnttree.override
+ gntwidget.override
 %%
 modulename gnt
 %%
@@ -39,3 +41,154 @@
 ignore-glob
 	*_get_gtype
 %%
+define set_flag
+static PyObject *
+_wrap_set_flag(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {"flags", NULL};
+	PyGObject *widget;
+	int flags;
+
+	if (!PyArg_ParseTuple(args, "O!i:gnt.set_flag", &PyGntWidget_Type, &widget,
+				&flags)) {
+		return NULL;
+	}
+
+	GNT_WIDGET_SET_FLAGS(widget->obj, flags);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+define unset_flag
+static PyObject *
+_wrap_unset_flag(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {"flags", NULL};
+	PyGObject *widget;
+	int flags;
+
+	if (!PyArg_ParseTuple(args, "O!i:gnt.unset_flag", &PyGntWidget_Type, &widget,
+				&flags)) {
+		return NULL;
+	}
+
+	GNT_WIDGET_UNSET_FLAGS(widget->obj, flags);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+define screen_size noargs
+static PyObject *
+_wrap_screen_size(PyObject *self)
+{
+	PyObject *list = PyList_New(0);
+
+	if (list == NULL)
+		return NULL;
+
+	PyList_Append(list, PyInt_FromLong((long)getmaxx(stdscr)));
+	PyList_Append(list, PyInt_FromLong((long)getmaxy(stdscr)));
+
+	return list;
+}
+%%
+override gnt_register_action
+static GHashTable *actions;
+
+
+
+static PyObject *
+_wrap_gnt_register_action(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = {"name", "callback", NULL};
+	PyGObject *callback;
+	GClosure *closure;
+	char *name;
+
+	if (!PyArg_ParseTuple(args, "sO:gnt.gnt_register_action", &name, &callback)) {
+		return NULL;
+	}
+
+	if (!PyCallable_Check(callback)) {
+		PyErr_SetString(PyExc_TypeError, "the callback must be callable ... doh!");
+		return NULL;
+	}
+
+	gnt_register_action(name, callback->obj);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+define register_bindings
+
+static gboolean
+pygnt_binding_callback(GntBindable *bindable, GList *list)
+{
+	PyObject *wrapper = pygobject_new(G_OBJECT(bindable));
+	PyObject_CallMethod(wrapper, list->data, "O", Py_None);
+	Py_DECREF(wrapper);
+	return TRUE;
+}
+
+static PyObject *
+_wrap_register_bindings(PyObject *self, PyObject *args)
+{
+	PyTypeObject *class;
+	int pos = 0;
+	PyObject *key, *value, *gbindings;
+	GntBindableClass *bindable;
+
+	if (!PyArg_ParseTuple(args, "O!:gnt.register_bindings",
+				&PyType_Type, &class)) {
+		/* Make sure it's a GntBindableClass subclass */
+		PyErr_SetString(PyExc_TypeError,
+				"argument must be a GntBindable subclass");
+		return NULL;
+	}
+
+	gbindings = PyDict_GetItemString(class->tp_dict, "__gntbindings__");
+	if (!gbindings)
+		goto end;
+
+	if (!PyDict_Check(gbindings)) {
+		PyErr_SetString(PyExc_TypeError,
+				"__gntbindings__ attribute not a dict!");
+		return NULL;
+	}
+
+	bindable = g_type_class_ref(pyg_type_from_object((PyObject *)class));
+	while (PyDict_Next(gbindings, &pos, &key, &value)) {
+		const char *trigger, *callback, *name;
+		GList *list = NULL;
+
+		if (!PyString_Check(key)) {
+			PyErr_SetString(PyExc_TypeError,
+					"__gntbindings__ keys must be strings");
+			g_type_class_unref(bindable);
+			return NULL;
+		}
+		name = PyString_AsString(key);
+
+		if (!PyTuple_Check(value) ||
+				!PyArg_ParseTuple(value, "ss", &callback, &trigger)) {
+			PyErr_SetString(PyExc_TypeError,
+					"__gntbindings__ values must be (callback, trigger) tupples");
+			g_type_class_unref(bindable);
+			return NULL;
+		}
+
+		gnt_bindable_class_register_action(bindable, name, pygnt_binding_callback,
+				trigger, g_strdup(callback), NULL);
+	}
+	if (gbindings)
+		PyDict_DelItemString(class->tp_dict, "__gntbindings__");
+	g_type_class_unref(bindable);
+
+end:
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/gntbox.override	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,38 @@
+/**
+ * pygnt- Python bindings for the GNT toolkit.
+ * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im>
+ *
+ *   gntbox.override: overrides for the box widget.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+%%
+override gnt_box_add_widget kwargs
+static PyObject *
+_wrap_gnt_box_add_widget(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+    static char *kwlist[] = { "widget", NULL };
+    PyGObject *widget;
+
+    if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O!:GntBox.add_widget", kwlist, &PyGntWidget_Type, &widget))
+        return NULL;
+    
+    gnt_box_add_widget(GNT_BOX(self->obj), GNT_WIDGET(widget->obj));
+	Py_INCREF(widget);
+    
+    Py_INCREF(Py_None);
+    return Py_None;
+}
--- a/finch/libgnt/pygnt/gntmodule.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/gntmodule.c	Tue Aug 14 02:45:41 2007 +0000
@@ -9,11 +9,12 @@
     PyObject *m, *d;
  
     init_pygobject ();
- 
+
     m = Py_InitModule ("gnt", gnt_functions);
     d = PyModule_GetDict (m);
  
     gnt_register_classes (d);
+    gnt_add_constants(m, "GNT_");
  
     if (PyErr_Occurred ()) {
         Py_FatalError ("can't initialise module sad");
--- a/finch/libgnt/pygnt/gnttree.override	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/gnttree.override	Tue Aug 14 02:45:41 2007 +0000
@@ -20,7 +20,7 @@
  * USA
  */
 %%
-headrs
+headers
 #include "common.h"
 %%
 ignore 
@@ -49,7 +49,7 @@
 		return NULL;
 	}
 	while (list) {
-		PyObject *obj = pyg_pointer_new(G_TYPE_POINTER, list->data);
+		PyObject *obj = list->data;
 		PyList_Append(py_list, obj);
 		Py_DECREF(obj);
 		list = list->next;
@@ -100,4 +100,79 @@
 	Py_INCREF(Py_None);
 	return Py_None;
 }
+%%
+override gnt_tree_get_selection_data noargs
+static PyObject *
+_wrap_gnt_tree_get_selection_data(PyGObject *self)
+{
+	PyObject *ret = gnt_tree_get_selection_data(GNT_TREE(self->obj));
+	if (!ret)
+		ret = Py_None;
+	Py_INCREF(ret);
+	return ret;
+}
+%%
+override gnt_tree_change_text
+static PyObject *
+_wrap_gnt_tree_change_text(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = { "key", "colno", "text", NULL };
+	char *text;
+	int colno;
+	gpointer key;
 
+	if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Ois:GntTree.change_text", kwlist, &key, &colno, &text))
+		return NULL;
+
+	gnt_tree_change_text(GNT_TREE(self->obj), key, colno, text);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+override gnt_tree_set_row_flags
+static PyObject *
+_wrap_gnt_tree_set_row_flags(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = { "key", "flag", NULL };
+	int flag;
+	gpointer key;
+
+	if (!PyArg_ParseTupleAndKeywords(args, kwargs,"Oi:GntTree.set_row_flags", kwlist, &key, &flag))
+		return NULL;
+
+	gnt_tree_set_row_flags(GNT_TREE(self->obj), key, flag);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+override gnt_tree_remove
+static PyObject *
+_wrap_gnt_tree_remove(PyGObject *self, PyObject *args, PyObject *kwargs)
+{
+	static char *kwlist[] = { "key", NULL };
+	gpointer key;
+
+	if (!PyArg_ParseTupleAndKeywords(args, kwargs,"O:GntTree.remove", kwlist, &key))
+		return NULL;
+
+	gnt_tree_remove(GNT_TREE(self->obj), key);
+
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+%%
+override gnt_tree_set_selected
+static PyObject *
+_wrap_gnt_tree_set_selected(PyGObject *self, PyObject *args)
+{
+	gpointer key;
+	if (!PyArg_ParseTuple(args, "O:GntTree.set_selected", &key)) {
+		return NULL;
+	}
+	gnt_tree_set_selected(GNT_TREE(self->obj), key);
+	Py_INCREF(Py_None);
+	return Py_None;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/finch/libgnt/pygnt/gntwidget.override	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,36 @@
+/**
+ * pygnt- Python bindings for the GNT toolkit.
+ * Copyright (C) 2007 Sadrul Habib Chowdhury <sadrul@pidgin.im>
+ *
+ *   gntwidget.override: overrides for generic widgets.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+%%
+override gnt_widget_get_size args
+static PyObject *
+_wrap_gnt_widget_get_size(PyGObject *self)
+{
+    PyObject *list = PyList_New(0);
+    int x = 0, y = 0;
+
+    gnt_widget_get_size(GNT_WIDGET(self->obj), &x, &y);
+    PyList_Append(list, PyInt_FromLong((long)x));
+    PyList_Append(list, PyInt_FromLong((long)y));
+
+    return list;
+}
+
--- a/finch/libgnt/pygnt/test.py	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/pygnt/test.py	Tue Aug 14 02:45:41 2007 +0000
@@ -1,18 +1,59 @@
 #!/usr/bin/python
+import gobject
 import gnt
 
+class MyObject(gobject.GObject):
+    __gproperties__ = {
+        'mytype': (gobject.TYPE_INT, 'mytype', 'the type of the object',
+                0, 10000, 0, gobject.PARAM_READWRITE),
+        'string': (gobject.TYPE_STRING, 'string property', 'the string',
+                None, gobject.PARAM_READWRITE),
+        'gobject': (gobject.TYPE_OBJECT, 'object property', 'the object',
+                gobject.PARAM_READWRITE),
+    }
+
+    def __init__(self, type = 'string', value = None):
+        self.__gobject_init__()
+        self.set_property(type, value)
+
+    def do_set_property(self, pspec, value):
+        if pspec.name == 'string':
+            self.string = value
+            self.type = gobject.TYPE_STRING
+        elif pspec.name == 'gobject':
+            self.gobject = value
+            self.type = gobject.TYPE_OBJECT
+        else:
+            raise AttributeError, 'unknown property %s' % pspec.name
+    def do_get_property(self, pspec):
+        if pspec.name == 'string':
+            return self.string
+        elif pspec.name == 'gobject':
+            return self.gobject
+        elif pspec.name == 'mytype':
+            return self.type
+        else:
+            raise AttributeError, 'unknown property %s' % pspec.name
+gobject.type_register(MyObject)
+
 def button_activate(button, tree):
-	list = tree.get_selection_text_list()
-	str = ""
-	for i in list:
-		str = str + i
-	entry.set_text("clicked!!!" + str)
+    list = tree.get_selection_text_list()
+    ent = tree.get_selection_data()
+    if ent.type == gobject.TYPE_STRING:
+        str = ""
+        for i in list:
+            str = str + i
+        entry.set_text("clicked!!!" + str)
+    elif ent.type == gobject.TYPE_OBJECT:
+        ent.gobject.set_text("mwhahaha!!!")
 
 gnt.gnt_init()
 
 win = gnt.Window()
 
 entry = gnt.Entry("")
+obj = MyObject()
+obj.set_property('gobject', entry)
 
 win.add_widget(entry)
 win.set_title("Entry")
@@ -27,12 +68,20 @@
 # so random non-string values can be used as the key for a row in a GntTree!
 last = None
 for i in range(1, 100):
-	tree.add_row_after(i, [str(i), ""], None, i-1)
-tree.add_row_after(entry, ["asd"], None, None)
-tree.add_row_after("b", ["123", ""], entry, None)
+    key = MyObject('string', str(i))
+    tree.add_row_after(key, [str(i)], None, last)
+    last = key
+
+tree.add_row_after(MyObject('gobject', entry), ["asd"], None, None)
+tree.add_row_after(MyObject('string', "b"), ["123"], MyObject('gobject', entry), None)
 
 button.connect("activate", button_activate, tree)
 
+tv = gnt.TextView()
+
+win.add_widget(tv)
+tv.append_text_with_flags("What up!!", gnt.TEXT_FLAG_BOLD)
+
 win.show()
 
 gnt.gnt_main()
--- a/finch/libgnt/wms/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/wms/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -1,10 +1,16 @@
+if PURPLE_AVAILABLE
+# These custom wms depend on libpurple
+purple_wms = s.la irssi.la
+else
+purple_wms =
+endif
+
 s_la_LDFLAGS             = -module -avoid-version
 irssi_la_LDFLAGS         = -module -avoid-version
 
 plugin_LTLIBRARIES = \
-	s.la \
-	irssi.la
-
+	$(purple_wms)
+ 
 plugindir = $(libdir)/gnt
 
 irssi_la_SOURCES = irssi.c
--- a/finch/libgnt/wms/s.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/finch/libgnt/wms/s.c	Tue Aug 14 02:45:41 2007 +0000
@@ -15,6 +15,10 @@
 
 #define TYPE_S				(s_get_gtype())
 
+#ifdef _S
+#undef _S
+#endif
+
 typedef struct _S
 {
 	GntWM inherit;
--- a/libpurple/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -148,7 +148,7 @@
 # purple dbus server
 
 dbus_sources  = dbus-server.c dbus-useful.c
-dbus_headers  = dbus-bindings.h dbus-purple.h dbus-server.h dbus-useful.h dbus-define-api.h
+dbus_headers  = dbus-bindings.h dbus-purple.h dbus-server.h dbus-useful.h dbus-define-api.h dbus-types.h
 
 dbus_exported = dbus-useful.h dbus-define-api.h account.h blist.h buddyicon.h \
                 connection.h conversation.h core.h ft.h log.h notify.h prefs.h roomlist.h \
--- a/libpurple/account.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/account.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1130,8 +1130,8 @@
 
 void *
 purple_account_request_authorization(PurpleAccount *account, const char *remote_user,
-			           const char *id, const char *alias, const char *message, gboolean on_list,
-				   GCallback auth_cb, GCallback deny_cb, void *user_data)
+				     const char *id, const char *alias, const char *message, gboolean on_list,
+				     PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data)
 {
 	PurpleAccountUiOps *ui_ops;
 	PurpleAccountRequestInfo *info;
@@ -1146,8 +1146,8 @@
 		info->type      = PURPLE_ACCOUNT_REQUEST_AUTHORIZATION;
 		info->account   = account;
 		info->ui_handle = ui_ops->request_authorize(account, remote_user, id, alias, message,
-									on_list, auth_cb, deny_cb, user_data);
-		
+							    on_list, auth_cb, deny_cb, user_data);
+
 		handles = g_list_append(handles, info);
 		return info->ui_handle;
 	}
--- a/libpurple/account.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/account.h	Tue Aug 14 02:45:41 2007 +0000
@@ -51,20 +51,50 @@
 	PURPLE_ACCOUNT_REQUEST_AUTHORIZATION = 0 /* Account authorization request */
 } PurpleAccountRequestType;
 
+
+/**  Account UI operations, used to notify the user of status changes and when
+ *   buddies add this account to their buddy lists.
+ */
 struct _PurpleAccountUiOps
 {
-	/* A buddy we already have added us to their buddy list. */
-	void (*notify_added)(PurpleAccount *account, const char *remote_user,
-	                    const char *id, const char *alias,
+	/** A buddy who is already on this account's buddy list added this account
+	 *  to their buddy list.
+	 */
+	void (*notify_added)(PurpleAccount *account,
+	                     const char *remote_user,
+	                     const char *id,
+	                     const char *alias,
 	                     const char *message);
-	void (*status_changed)(PurpleAccount *account, PurpleStatus *status);
-	/* Someone we don't have on our list added us. Will prompt to add them. */
-	void (*request_add)(PurpleAccount *account, const char *remote_user,
-	                    const char *id, const char *alias,
+
+	/** This account's status changed. */
+	void (*status_changed)(PurpleAccount *account,
+	                       PurpleStatus *status);
+
+	/** Someone we don't have on our list added us; prompt to add them. */
+	void (*request_add)(PurpleAccount *account,
+	                    const char *remote_user,
+	                    const char *id,
+	                    const char *alias,
 	                    const char *message);
-	void *(*request_authorize)(PurpleAccount *account, const char *remote_user, const char *id,
-				 const char *alias, const char *message, gboolean on_list, 
-				 GCallback authorize_cb, GCallback deny_cb, void *user_data);
+
+	/** Prompt for authorization when someone adds this account to their buddy
+	 * list.  To authorize them to see this account's presence, call \a
+	 * authorize_cb (\a user_data); otherwise call \a deny_cb (\a user_data);
+	 * @return a UI-specific handle, as passed to #close_account_request.
+	 */
+	void *(*request_authorize)(PurpleAccount *account,
+	                           const char *remote_user,
+	                           const char *id,
+	                           const char *alias,
+	                           const char *message,
+	                           gboolean on_list,
+	                           PurpleAccountRequestAuthorizationCb authorize_cb,
+	                           PurpleAccountRequestAuthorizationCb deny_cb,
+	                           void *user_data);
+
+	/** Close a pending request for authorization.  \a ui_handle is a handle
+	 *  as returned by #request_authorize.
+	 */
 	void (*close_account_request)(void *ui_handle);
 
 	void (*_purple_reserved1)(void);
@@ -193,7 +223,7 @@
 
 /**
  * Notifies the user that a remote user has wants to add the local user
- * to his or her buddy list and requires authorization to d oso.
+ * to his or her buddy list and requires authorization to do so.
  *
  * This will present a dialog informing the user of this and ask if the 
  * user authorizes or denies the remote user from adding him.
@@ -212,7 +242,7 @@
  */
 void *purple_account_request_authorization(PurpleAccount *account, const char *remote_user,
 					const char *id, const char *alias, const char *message, gboolean on_list,
-					GCallback auth_cb, GCallback deny_cb, void *user_data);
+					PurpleAccountRequestAuthorizationCb auth_cb, PurpleAccountRequestAuthorizationCb deny_cb, void *user_data);
 
 /**
  * Close account requests registered for the given PurpleAccount
--- a/libpurple/connection.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/connection.h	Tue Aug 14 02:45:41 2007 +0000
@@ -60,15 +60,52 @@
 #include "plugin.h"
 #include "status.h"
 
+/** Connection UI operations.  Used to notify the user of changes to
+ *  connections, such as being disconnected, and to respond to the
+ *  underlying network connection appearing and disappearing.  UIs should
+ *  call #purple_connections_set_ui_ops() with an instance of this struct.
+ *
+ *  @see @ref ui-ops
+ */
 typedef struct
 {
-	void (*connect_progress)(PurpleConnection *gc, const char *text,
-							 size_t step, size_t step_count);
+	/** When an account is connecting, this operation is called to notify
+	 *  the UI of what is happening, as well as which @a step out of @a
+	 *  step_count has been reached (which might be displayed as a progress
+	 *  bar).
+	 */
+	void (*connect_progress)(PurpleConnection *gc,
+	                         const char *text,
+	                         size_t step,
+	                         size_t step_count);
+	/** Called when a connection is established (just before the
+	 *  @ref signed-on signal).
+	 */
 	void (*connected)(PurpleConnection *gc);
+	/** Called when a connection is ended (between the @ref signing-off
+	 *  and @ref signed-off signals).
+	 */
 	void (*disconnected)(PurpleConnection *gc);
+	/** Used to display connection-specific notices.  (Pidgin's Gtk user
+	 *  interface implements this as a no-op; #purple_connection_notice(),
+	 *  which uses this operation, is not used by any of the protocols
+	 *  shipped with libpurple.)
+	 */
 	void (*notice)(PurpleConnection *gc, const char *text);
+	/** Called when an error causes a connection to be disconnected.
+	 *  Called before #disconnected.
+	 *  @param text  a localized error message.
+	 */
 	void (*report_disconnect)(PurpleConnection *gc, const char *text);
+	/** Called when libpurple discovers that the computer's network
+	 *  connection is active.  On Linux, this uses Network Manager if
+	 *  available; on Windows, it uses Win32's network change notification
+	 *  infrastructure.
+	 */
 	void (*network_connected)();
+	/** Called when libpurple discovers that the computer's network
+	 *  connection has gone away.
+	 */
 	void (*network_disconnected)();
 
 	void (*_purple_reserved1)(void);
--- a/libpurple/conversation.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/conversation.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1106,7 +1106,7 @@
 
 	c = purple_conv_im_get_conversation(im);
 
-	/* Raise the window, if specified in prefs. */
+	/* Pass this on to either the ops structure or the default write func. */
 	if (c->ui_ops != NULL && c->ui_ops->write_im != NULL)
 		c->ui_ops->write_im(c, who, message, flags, mtime);
 	else
--- a/libpurple/conversation.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/conversation.h	Tue Aug 14 02:45:41 2007 +0000
@@ -149,27 +149,74 @@
  */
 struct _PurpleConversationUiOps
 {
+	/** Called when @a conv is created (but before the @ref
+	 *  conversation-created signal is emitted).
+	 */
 	void (*create_conversation)(PurpleConversation *conv);
+
+	/** Called just before @a conv is freed. */
 	void (*destroy_conversation)(PurpleConversation *conv);
+	/** Write a message to a chat.  If this field is @c NULL, libpurple will
+	 *  fall back to using #write_conv.
+	 *  @see purple_conv_chat_write()
+	 */
 	void (*write_chat)(PurpleConversation *conv, const char *who,
 	                   const char *message, PurpleMessageFlags flags,
 	                   time_t mtime);
+	/** Write a message to an IM conversation.  If this field is @c NULL,
+	 *  libpurple will fall back to using #write_conv.
+	 *  @see purple_conv_im_write()
+	 */
 	void (*write_im)(PurpleConversation *conv, const char *who,
 	                 const char *message, PurpleMessageFlags flags,
 	                 time_t mtime);
-	void (*write_conv)(PurpleConversation *conv, const char *name, const char *alias,
-	                   const char *message, PurpleMessageFlags flags,
+	/** Write a message to a conversation.  This is used rather than
+	 *  the chat- or im-specific ops for generic messages, such as system
+	 *  messages like "x is now know as y".
+	 *  @see purple_conversation_write()
+	 */
+	void (*write_conv)(PurpleConversation *conv,
+	                   const char *name,
+	                   const char *alias,
+	                   const char *message,
+	                   PurpleMessageFlags flags,
 	                   time_t mtime);
 
-	void (*chat_add_users)(PurpleConversation *conv, GList *cbuddies, gboolean new_arrivals);	
-	
+	/** Add @a cbuddies to a chat.
+	 *  @param cbuddies      A @C GList of #PurpleConvChatBuddy structs.
+	 *  @param new_arrivals  Whether join notices should be shown.
+	 *                       (Join notices are actually written to the
+	 *                       conversation by #purple_conv_chat_add_users().)
+	 */
+	void (*chat_add_users)(PurpleConversation *conv,
+	                       GList *cbuddies,
+	                       gboolean new_arrivals);
+	/** Rename the user in this chat named @a old_name to @a new_name.  (The
+	 *  rename message is written to the conversation by libpurple.)
+	 *  @param new_alias  @a new_name's new alias, if they have one.
+	 *  @see purple_conv_chat_add_users()
+	 */
 	void (*chat_rename_user)(PurpleConversation *conv, const char *old_name,
 	                         const char *new_name, const char *new_alias);
+	/** Remove @a users from a chat.
+	 *  @param users    A @C GList of <tt>const char *</tt>s.
+	 *  @see purple_conv_chat_rename_user()
+	 */
 	void (*chat_remove_users)(PurpleConversation *conv, GList *users);
+	/** Called when a user's flags are changed.
+	 *  @see purple_conv_chat_user_set_flags()
+	 */
 	void (*chat_update_user)(PurpleConversation *conv, const char *user);
 
+	/** Present this conversation to the user; for example, by displaying
+	 *  the IM dialog.
+	 */
 	void (*present)(PurpleConversation *conv);
 
+	/** If this UI has a concept of focus (as in a windowing system) and
+	 *  this conversation has the focus, return @c TRUE; otherwise, return
+	 *  @c FALSE.
+	 */
 	gboolean (*has_focus)(PurpleConversation *conv);
 
 	/* Custom Smileys */
@@ -178,6 +225,11 @@
 	                            const guchar *data, gsize size);
 	void (*custom_smiley_close)(PurpleConversation *conv, const char *smile);
 
+	/** Prompt the user for confirmation to send @a message.  This function
+	 *  should arrange for the message to be sent if the user accepts.  If
+	 *  this field is @c NULL, libpurple will fall back to using
+	 *  #purple_request_action().
+	 */
 	void (*send_confirm)(PurpleConversation *conv, const char *message);
 
 	void (*_purple_reserved1)(void);
--- a/libpurple/dbus-analyze-functions.py	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/dbus-analyze-functions.py	Tue Aug 14 02:45:41 2007 +0000
@@ -39,6 +39,7 @@
     "purple_prefs_get_string_list",
     "purple_uri_list_extract_filenames",
     "purple_uri_list_extract_uris",
+    "purple_prefs_get_children_names",
 ]
 
 # This is a list of functions that return a GList* or GSList* that should
--- a/libpurple/log.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/log.c	Tue Aug 14 02:45:41 2007 +0000
@@ -718,9 +718,9 @@
 		if (tmp < start)
 			g_string_append_len(newmsg, tmp, start - tmp);
 
-		idstr = g_datalist_get_data(&attributes, "id");
+		if ((idstr = g_datalist_get_data(&attributes, "id")) != NULL)
+			imgid = atoi(idstr);
 
-		imgid = atoi(idstr);
 		if (imgid != 0)
 		{
 			FILE *image_file;
@@ -735,6 +735,7 @@
 			if (image == NULL)
 			{
 				/* This should never happen. */
+				/* This *does* happen for failed Direct-IMs -DAA */
 				g_string_free(newmsg, TRUE);
 				g_return_val_if_reached((char *)msg);
 			}
--- a/libpurple/plugins/perl/perl-handlers.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/plugins/perl/perl-handlers.c	Tue Aug 14 02:45:41 2007 +0000
@@ -22,6 +22,7 @@
 	gchar *hvname;
 	PurplePlugin *plugin;
 	PurplePerlScript *gps;
+	STRLEN na;
 	dSP;
 
 	plugin = action->plugin;
@@ -45,9 +46,16 @@
 	XPUSHs(purple_perl_bless_object(gps->plugin, "Purple::Plugin"));
 	PUTBACK;
 
-	call_sv(*callback, G_VOID | G_DISCARD);
+	call_sv(*callback, G_EVAL | G_VOID | G_DISCARD);
+
 	SPAGAIN;
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl plugin action function exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	PUTBACK;
 	FREETMPS;
 	LEAVE;
@@ -59,6 +67,7 @@
 	GList *l = NULL;
 	PurplePerlScript *gps;
 	int i = 0, count = 0;
+	STRLEN na;
 	dSP;
 
 	gps = (PurplePerlScript *)plugin->info->extra_info;
@@ -77,10 +86,16 @@
 		XPUSHs(&PL_sv_undef);
 	PUTBACK;
 
-	count = call_pv(gps->plugin_action_sub, G_ARRAY);
+	count = call_pv(gps->plugin_action_sub, G_EVAL | G_ARRAY);
 
 	SPAGAIN;
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl plugin actions lookup exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	if (count == 0)
 		croak("The plugin_actions sub didn't return anything.\n");
 
@@ -113,6 +128,7 @@
 	MAGIC *mg;
 	GtkWidget *ret;
 	PurplePerlScript *gps;
+	STRLEN na;
 	dSP;
 
 	gps = (PurplePerlScript *)plugin->info->extra_info;
@@ -120,13 +136,19 @@
 	ENTER;
 	SAVETMPS;
 
-	count = call_pv(gps->gtk_prefs_sub, G_SCALAR | G_NOARGS);
+	count = call_pv(gps->gtk_prefs_sub, G_EVAL | G_SCALAR | G_NOARGS);
 	if (count != 1)
 		croak("call_pv: Did not return the correct number of values.\n");
 
 	/* the frame was created in a perl sub and is returned */
 	SPAGAIN;
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl gtk plugin frame init exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	/* We have a Gtk2::Frame on top of the stack */
 	sv = POPs;
 
@@ -150,6 +172,7 @@
 	int count;
 	PurplePerlScript *gps;
 	PurplePluginPrefFrame *ret_frame;
+	STRLEN na;
 	dSP;
 
 	gps = (PurplePerlScript *)plugin->info->extra_info;
@@ -161,10 +184,16 @@
 	PUSHMARK(SP);
 	PUTBACK;
 
-	count = call_pv(gps->prefs_sub, G_SCALAR | G_NOARGS);
+	count = call_pv(gps->prefs_sub, G_EVAL | G_SCALAR | G_NOARGS);
 
 	SPAGAIN;
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl plugin prefs frame init exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	if (count != 1)
 		croak("call_pv: Did not return the correct number of values.\n");
 	/* the frame was created in a perl sub and is returned */
@@ -215,6 +244,7 @@
 {
 	PurplePerlTimeoutHandler *handler = (PurplePerlTimeoutHandler *)data;
 	gboolean ret = FALSE;
+	STRLEN na;
 
 	dSP;
 	ENTER;
@@ -225,6 +255,12 @@
 	call_sv(handler->callback, G_EVAL | G_SCALAR);
 	SPAGAIN;
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl timeout function exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	ret = POPi;
 
 	PUTBACK;
@@ -285,7 +321,7 @@
 		else
 			ret_val = purple_perl_data_from_sv(ret_value, POPs);
 	} else {
-		call_sv(handler->callback, G_SCALAR);
+		call_sv(handler->callback, G_EVAL | G_SCALAR);
 
 		SPAGAIN;
 	}
@@ -501,6 +537,7 @@
             gchar **args, gchar **error, void *data)
 {
 	int i = 0, count, ret_value = PURPLE_CMD_RET_OK;
+	STRLEN na;
 	SV *cmdSV, *tmpSV, *convSV;
 	PurplePerlCmdHandler *handler = (PurplePerlCmdHandler *)data;
 
@@ -532,11 +569,17 @@
 	}
 
 	PUTBACK;
-	count = call_sv(handler->callback, G_EVAL|G_SCALAR);
+	count = call_sv(handler->callback, G_EVAL | G_SCALAR);
 
 	if (count != 1)
 		croak("call_sv: Did not return the correct number of values.\n");
 
+	if (SvTRUE(ERRSV)) {
+		purple_debug_error("perl",
+		                 "Perl plugin command function exited abnormally: %s\n",
+		                 SvPV(ERRSV, na));
+	}
+
 	SPAGAIN;
 
 	ret_value = POPi;
--- a/libpurple/prefs.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/prefs.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1341,7 +1341,6 @@
 		list = g_list_append(list, g_strdup_printf("%s%s%s", name, sep, child->name));
 	}
 	return list;
-
 }
 
 void
--- a/libpurple/protocols/bonjour/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -22,9 +22,9 @@
 if MDNS_AVAHI
   BONJOURSOURCES += mdns_avahi.c
 else
-  if MDNS_HOWL
+if MDNS_HOWL
     BONJOURSOURCES += mdns_howl.c
-  endif
+endif
 endif
 
 AM_CFLAGS = $(st)
@@ -42,9 +42,9 @@
 if MDNS_AVAHI
   libbonjour_a_LIBADD  += $(AVAHI_LIBS)
 else
-  if MDNS_HOWL
+if MDNS_HOWL
     libbonjour_a_LIBADD  += $(HOWL_LIBS)
-  endif
+endif
 endif
 
 else
@@ -56,9 +56,9 @@
 if MDNS_AVAHI
   libbonjour_la_LIBADD  += $(AVAHI_LIBS)
 else
-  if MDNS_HOWL
+if MDNS_HOWL
     libbonjour_la_LIBADD  += $(HOWL_LIBS)
-  endif
+endif
 endif
 
 endif
@@ -74,8 +74,8 @@
 if MDNS_AVAHI
   AM_CPPFLAGS += $(AVAHI_CFLAGS)
 else
-  if MDNS_HOWL
+if MDNS_HOWL
     AM_CPPFLAGS += $(HOWL_CFLAGS)
-  endif
+endif
 endif
 
--- a/libpurple/protocols/bonjour/bonjour.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/bonjour.c	Tue Aug 14 02:45:41 2007 +0000
@@ -138,6 +138,8 @@
 		return;
 	}
 
+	bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data);
+
 	/* Create a group for bonjour buddies */
 	bonjour_group = purple_group_new(BONJOUR_GROUP_NAME);
 	purple_blist_add_group(bonjour_group, NULL);
@@ -283,17 +285,33 @@
 	bb->conversation = NULL;
 }
 
+static
+void bonjour_set_buddy_icon(PurpleConnection *conn, PurpleStoredImage *img)
+{
+	BonjourData *bd = conn->proto_data;
+	bonjour_dns_sd_update_buddy_icon(bd->dns_sd_data);
+}
+
+
 static char *
 bonjour_status_text(PurpleBuddy *buddy)
 {
-	PurplePresence *presence;
+	const PurplePresence *presence;
+	const PurpleStatus *status;
+	const char *message;
+	gchar *ret = NULL;
 
 	presence = purple_buddy_get_presence(buddy);
+	status = purple_presence_get_active_status(presence);
 
-	if (purple_presence_is_online(presence) && !purple_presence_is_available(presence))
-		return g_strdup(_("Away"));
+	message = purple_status_get_attr_string(status, "message");
 
-	return NULL;
+	if (message != NULL) {
+		ret = g_markup_escape_text(message, -1);
+		purple_util_chrreplace(ret, '\n', ' ');
+	}
+
+	return ret;
 }
 
 static void
@@ -301,6 +319,7 @@
 {
 	PurplePresence *presence;
 	PurpleStatus *status;
+	BonjourBuddy *bb = buddy->proto_data;
 	const char *status_description;
 	const char *message;
 
@@ -318,6 +337,23 @@
 	purple_notify_user_info_add_pair(user_info, _("Status"), status_description);
 	if (message != NULL)
 		purple_notify_user_info_add_pair(user_info, _("Message"), message);
+
+	/* Only show first/last name if there is a nickname set (to avoid duplication) */
+	if (bb->nick != NULL) {
+		if (bb->first != NULL)
+			purple_notify_user_info_add_pair(user_info, _("First name"), bb->first);
+		if (bb->first != NULL)
+			purple_notify_user_info_add_pair(user_info, _("Last name"), bb->last);
+	}
+
+	if (bb->email != NULL)
+		purple_notify_user_info_add_pair(user_info, _("E-Mail"), bb->email);
+
+	if (bb->AIM != NULL)
+		purple_notify_user_info_add_pair(user_info, _("AIM Account"), bb->AIM);
+
+	if (bb->jid!= NULL)
+		purple_notify_user_info_add_pair(user_info, _("XMPP Account"), bb->jid);
 }
 
 static gboolean
@@ -339,10 +375,9 @@
 	OPT_PROTO_NO_PASSWORD,
 	NULL,                                                    /* user_splits */
 	NULL,                                                    /* protocol_options */
-	/* {"png", 0, 0, 96, 96, 0, PURPLE_ICON_SCALE_DISPLAY}, */ /* icon_spec */
-	NO_BUDDY_ICONS, /* not yet */                            /* icon_spec */
+	{"png,gif,jpeg", 0, 0, 96, 96, 65535, PURPLE_ICON_SCALE_DISPLAY}, /* icon_spec */
 	bonjour_list_icon,                                       /* list_icon */
-	NULL,													 /* list_emblem */
+	NULL,                                                    /* list_emblem */
 	bonjour_status_text,                                     /* status_text */
 	bonjour_tooltip_text,                                    /* tooltip_text */
 	bonjour_status_types,                                    /* status_types */
@@ -384,7 +419,7 @@
 	NULL,                                                    /* buddy_free */
 	bonjour_convo_closed,                                    /* convo_closed */
 	NULL,                                                    /* normalize */
-	NULL,                                                    /* set_buddy_icon */
+	bonjour_set_buddy_icon,                                  /* set_buddy_icon */
 	NULL,                                                    /* remove_group */
 	NULL,                                                    /* get_cb_real_name */
 	NULL,                                                    /* set_chat_topic */
@@ -412,11 +447,11 @@
 	PURPLE_PLUGIN_MAGIC,
 	PURPLE_MAJOR_VERSION,
 	PURPLE_MINOR_VERSION,
-	PURPLE_PLUGIN_PROTOCOL,                             /**< type           */
+	PURPLE_PLUGIN_PROTOCOL,                           /**< type           */
 	NULL,                                             /**< ui_requirement */
 	0,                                                /**< flags          */
 	NULL,                                             /**< dependencies   */
-	PURPLE_PRIORITY_DEFAULT,                            /**< priority       */
+	PURPLE_PRIORITY_DEFAULT,                          /**< priority       */
 
 	"prpl-bonjour",                                   /**< id             */
 	"Bonjour",                                        /**< name           */
@@ -426,10 +461,10 @@
 	                                                  /**  description    */
 	N_("Bonjour Protocol Plugin"),
 	NULL,                                             /**< author         */
-	PURPLE_WEBSITE,                                     /**< homepage       */
+	PURPLE_WEBSITE,                                   /**< homepage       */
 
 	NULL,                                             /**< load           */
-	plugin_unload,                                             /**< unload         */
+	plugin_unload,                                    /**< unload         */
 	NULL,                                             /**< destroy        */
 
 	NULL,                                             /**< ui_info        */
@@ -533,7 +568,7 @@
 	{
 		default_firstname = g_strndup(fullname, splitpoint - fullname);
 		tmp = &splitpoint[1];
-		
+
 		/* The last name may be followed by a comma and additional data.
 		 * Only use the last name itself.
 		 */
--- a/libpurple/protocols/bonjour/buddy.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/buddy.c	Tue Aug 14 02:45:41 2007 +0000
@@ -18,6 +18,7 @@
 #include <stdlib.h>
 
 #include "internal.h"
+#include "cipher.h"
 #include "buddy.h"
 #include "account.h"
 #include "blist.h"
@@ -41,6 +42,26 @@
 	return buddy;
 }
 
+#define _B_CLR(x) g_free(x); x = NULL;
+
+void clear_bonjour_buddy_values(BonjourBuddy *buddy) {
+
+	_B_CLR(buddy->first)
+	_B_CLR(buddy->email);
+	_B_CLR(buddy->ext);
+	_B_CLR(buddy->jid);
+	_B_CLR(buddy->last);
+	_B_CLR(buddy->msg);
+	_B_CLR(buddy->nick);
+	_B_CLR(buddy->node);
+	_B_CLR(buddy->phsh);
+	_B_CLR(buddy->status);
+	_B_CLR(buddy->vc);
+	_B_CLR(buddy->ver);
+	_B_CLR(buddy->AIM);
+
+}
+
 void
 set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len){
 	gchar **fld = NULL;
@@ -106,11 +127,10 @@
 	PurpleBuddy *buddy;
 	PurpleGroup *group;
 	PurpleAccount *account = bonjour_buddy->account;
-	const char *status_id, *first, *last, *old_hash, *new_hash;
-	gchar *alias = NULL;
+	const char *status_id, *old_hash, *new_hash;
 
 	/* Translate between the Bonjour status and the Purple status */
-	if (g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0)
+	if (bonjour_buddy->status != NULL && g_ascii_strcasecmp("dnd", bonjour_buddy->status) == 0)
 		status_id = BONJOUR_STATUS_ID_AWAY;
 	else
 		status_id = BONJOUR_STATUS_ID_AVAILABLE;
@@ -138,15 +158,21 @@
 	}
 
 	/* Create the alias for the buddy using the first and the last name */
-	first = bonjour_buddy->first;
-	last = bonjour_buddy->last;
-	if ((first && *first) || (last && *last))
-		alias = g_strdup_printf("%s%s%s",
-					(first && *first ? first : ""),
-					(first && *first && last && *last ? " " : ""),
-					(last && *last ? last : ""));
-	serv_got_alias(purple_account_get_connection(account), buddy->name, alias);
-	g_free(alias);
+	if (bonjour_buddy->nick)
+		serv_got_alias(purple_account_get_connection(account), buddy->name, bonjour_buddy->nick);
+	else {
+		gchar *alias = NULL;
+		const char *first, *last;
+		first = bonjour_buddy->first;
+		last = bonjour_buddy->last;
+		if ((first && *first) || (last && *last))
+			alias = g_strdup_printf("%s%s%s",
+						(first && *first ? first : ""),
+						(first && *first && last && *last ? " " : ""),
+						(last && *last ? last : ""));
+		serv_got_alias(purple_account_get_connection(account), buddy->name, alias);
+		g_free(alias);
+	}
 
 	/* Set the user's status */
 	if (bonjour_buddy->msg != NULL)
@@ -166,12 +192,46 @@
 	new_hash = (bonjour_buddy->phsh && *(bonjour_buddy->phsh)) ? bonjour_buddy->phsh : NULL;
 	if (new_hash && (!old_hash || strcmp(old_hash, new_hash) != 0)) {
 		/* Look up the new icon data */
+		/* TODO: Make sure the hash assigned to the retrieved buddy icon is the same
+		 * as what we looked up. */
 		bonjour_dns_sd_retrieve_buddy_icon(bonjour_buddy);
-	} else
+	} else if (!new_hash)
 		purple_buddy_icons_set_for_user(account, buddy->name, NULL, 0, NULL);
 }
 
 /**
+ * We got the buddy icon data; deal with it
+ */
+void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len) {
+	/* Recalculate the hash instead of using the current phsh to make sure it is accurate for the icon. */
+	int i;
+	gchar *enc;
+	char *p, hash[41];
+	unsigned char hashval[20];
+
+	if (data == NULL || len == 0)
+		return;
+
+	enc = purple_base64_encode(data, len);
+
+	purple_cipher_digest_region("sha1", data,
+				    len, sizeof(hashval),
+				    hashval, NULL);
+
+	p = hash;
+	for(i=0; i<20; i++, p+=2)
+		snprintf(p, 3, "%02x", hashval[i]);
+
+	purple_debug_info("bonjour", "Got buddy icon for %s icon hash='%s' phsh='%s'.\n", buddy->name,
+			  hash, buddy->phsh ? buddy->phsh : "(null)");
+
+	purple_buddy_icons_set_for_user(buddy->account, buddy->name,
+		g_memdup(data, len), len, hash);
+
+	g_free(enc);
+}
+
+/**
  * Deletes a buddy from memory.
  */
 void
@@ -179,8 +239,6 @@
 {
 	g_free(buddy->name);
 	g_free(buddy->ip);
-	g_free(buddy->full_service_name);
-
 	g_free(buddy->first);
 	g_free(buddy->phsh);
 	g_free(buddy->status);
--- a/libpurple/protocols/bonjour/buddy.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/buddy.h	Tue Aug 14 02:45:41 2007 +0000
@@ -29,7 +29,6 @@
 	gchar *name;
 	/* TODO: Remove and just use the hostname */
 	gchar *ip;
-	gchar *full_service_name;
 	gint port_p2pj;
 
 	gchar *first;
@@ -76,9 +75,15 @@
 BonjourBuddy *bonjour_buddy_new(const gchar *name, PurpleAccount *account);
 
 /**
+ * Clear any existing values from the buddy.
+ * This is called before updating so that we can notice removals
+ */
+void clear_bonjour_buddy_values(BonjourBuddy *buddy);
+
+/**
  * Sets a value in the BonjourBuddy struct, destroying the old value
  */
-void set_bonjour_buddy_value(BonjourBuddy* buddy, const char *record_key, const char *value, uint32_t len);
+void set_bonjour_buddy_value(BonjourBuddy *buddy, const char *record_key, const char *value, uint32_t len);
 
 /**
  * Check if all the compulsory buddy data is present.
@@ -91,6 +96,11 @@
 void bonjour_buddy_add_to_purple(BonjourBuddy *buddy);
 
 /**
+ * We got the buddy icon data; deal with it
+ */
+void bonjour_buddy_got_buddy_icon(BonjourBuddy *buddy, gconstpointer data, gsize len);
+
+/**
  * Deletes a buddy from memory.
  */
 void bonjour_buddy_delete(BonjourBuddy *buddy);
--- a/libpurple/protocols/bonjour/issues.txt	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/issues.txt	Tue Aug 14 02:45:41 2007 +0000
@@ -3,6 +3,5 @@
 ==========================================
 
 * Status changes don't work
-* Avatars
 * File transfers
 * Typing notifications
--- a/libpurple/protocols/bonjour/mdns_avahi.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_avahi.c	Tue Aug 14 02:45:41 2007 +0000
@@ -19,6 +19,7 @@
 #include "mdns_interface.h"
 #include "debug.h"
 #include "buddy.h"
+#include "bonjour.h"
 
 #include <avahi-client/client.h>
 #include <avahi-client/lookup.h>
@@ -32,14 +33,25 @@
 #include <avahi-glib/glib-malloc.h>
 #include <avahi-glib/glib-watch.h>
 
+/* For some reason, this is missing from the Avahi type defines */
+#ifndef AVAHI_DNS_TYPE_NULL
+#define AVAHI_DNS_TYPE_NULL 0x0A
+#endif
+
 /* data used by avahi bonjour implementation */
 typedef struct _avahi_session_impl_data {
 	AvahiClient *client;
 	AvahiGLibPoll *glib_poll;
 	AvahiServiceBrowser *sb;
 	AvahiEntryGroup *group;
+	AvahiEntryGroup *buddy_icon_group;
 } AvahiSessionImplData;
 
+typedef struct _avahi_buddy_impl_data {
+	AvahiServiceResolver *resolver;
+	AvahiRecordBrowser *buddy_icon_rec_browser;
+} AvahiBuddyImplData;
+
 static void
 _resolver_callback(AvahiServiceResolver *r, AvahiIfIndex interface, AvahiProtocol protocol,
 		  AvahiResolverEvent event, const char *name, const char *type, const char *domain,
@@ -59,11 +71,14 @@
 		case AVAHI_RESOLVER_FAILURE:
 			purple_debug_error("bonjour", "_resolve_callback - Failure: %s\n",
 				avahi_strerror(avahi_client_errno(avahi_service_resolver_get_client(r))));
+			avahi_service_resolver_free(r);
 			break;
 		case AVAHI_RESOLVER_FOUND:
 			/* create a buddy record */
 			buddy = bonjour_buddy_new(name, account);
 
+			((AvahiBuddyImplData *)buddy->mdns_impl_data)->resolver = r;
+
 			/* Get the ip as a string */
 			buddy->ip = g_malloc(AVAHI_ADDRESS_STR_MAX);
 			avahi_address_snprint(buddy->ip, AVAHI_ADDRESS_STR_MAX, a);
@@ -71,6 +86,7 @@
 			buddy->port_p2pj = port;
 
 			/* Obtain the parameters from the text_record */
+			clear_bonjour_buddy_values(buddy);
 			l = txt;
 			while (l != NULL) {
 				ret = avahi_string_list_get_pair(l, &key, &value, &size);
@@ -95,7 +111,6 @@
 			purple_debug_info("bonjour", "Unrecognized Service Resolver event: %d.\n", event);
 	}
 
-	avahi_service_resolver_free(r);
 }
 
 static void
@@ -145,6 +160,30 @@
 }
 
 static void
+_buddy_icon_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
+	BonjourDnsSd *data = userdata;
+	AvahiSessionImplData *idata = data->mdns_impl_data;
+
+	g_return_if_fail(g == idata->buddy_icon_group || idata->buddy_icon_group == NULL);
+
+	switch(state) {
+		case AVAHI_ENTRY_GROUP_ESTABLISHED:
+			purple_debug_info("bonjour", "Successfully registered buddy icon data.\n");
+		case AVAHI_ENTRY_GROUP_COLLISION:
+			purple_debug_error("bonjour", "Collision registering buddy icon data.\n");
+			break;
+		case AVAHI_ENTRY_GROUP_FAILURE:
+			purple_debug_error("bonjour", "Error registering buddy icon data: %s\n.",
+				avahi_strerror(avahi_client_errno(avahi_entry_group_get_client(g))));
+			break;
+		case AVAHI_ENTRY_GROUP_UNCOMMITED:
+		case AVAHI_ENTRY_GROUP_REGISTERING:
+			break;
+	}
+
+}
+
+static void
 _entry_group_cb(AvahiEntryGroup *g, AvahiEntryGroupState state, void *userdata) {
 	AvahiSessionImplData *idata = userdata;
 
@@ -170,6 +209,31 @@
 
 }
 
+static void
+_buddy_icon_record_cb(AvahiRecordBrowser *b, AvahiIfIndex interface, AvahiProtocol protocol,
+		      AvahiBrowserEvent event, const char *name, uint16_t clazz, uint16_t type,
+		      const void *rdata, size_t size, AvahiLookupResultFlags flags, void *userdata) {
+	BonjourBuddy *buddy = userdata;
+	AvahiBuddyImplData *idata = buddy->mdns_impl_data;
+
+	switch (event) {
+		case AVAHI_BROWSER_NEW:
+			bonjour_buddy_got_buddy_icon(buddy, rdata, size);
+			break;
+		case AVAHI_BROWSER_REMOVE:
+		case AVAHI_BROWSER_CACHE_EXHAUSTED:
+		case AVAHI_BROWSER_ALL_FOR_NOW:
+		case AVAHI_BROWSER_FAILURE:
+			purple_debug_error("bonjour", "Error rerieving buddy icon record: %s\n",
+				avahi_strerror(avahi_client_errno(avahi_record_browser_get_client(b))));
+			break;
+	}
+
+	/* Stop listening */
+	avahi_record_browser_free(idata->buddy_icon_rec_browser);
+	idata->buddy_icon_rec_browser = NULL;
+}
+
 /****************************
  * mdns_interface functions *
  ****************************/
@@ -203,10 +267,8 @@
 	return TRUE;
 }
 
-gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) {
+gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) {
 	int publish_result = 0;
-	char portstring[6];
-	const char *jid, *aim, *email;
 	AvahiSessionImplData *idata = data->mdns_impl_data;
 	AvahiStringList *lst = NULL;
 
@@ -223,44 +285,11 @@
 		}
 	}
 
-	/* Convert the port to a string */
-	snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj);
-
-	jid = purple_account_get_string(data->account, "jid", NULL);
-	aim = purple_account_get_string(data->account, "AIM", NULL);
-	email = purple_account_get_string(data->account, "email", NULL);
-
-	/* We should try to follow XEP-0174, but some clients have "issues", so we humor them.
-	 * See http://telepathy.freedesktop.org/wiki/SalutInteroperability
-	 */
-
-	/* Needed by iChat */
-	lst = avahi_string_list_add_pair(lst,"txtvers", "1");
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	lst = avahi_string_list_add_pair(lst, "1st", data->first);
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	lst = avahi_string_list_add_pair(lst, "last", data->last);
-	/* Needed by Adium */
-	lst = avahi_string_list_add_pair(lst, "port.p2pj", portstring);
-	/* Needed by iChat, Gaim/Pidgin <= 2.0.1 */
-	lst = avahi_string_list_add_pair(lst, "status", data->status);
-	/* Currently always set to "!" since we don't support AV and wont ever be in a conference */
-	lst = avahi_string_list_add_pair(lst, "vc", data->vc);
-	lst = avahi_string_list_add_pair(lst, "ver", VERSION);
-	if (email != NULL && *email != '\0')
-		lst = avahi_string_list_add_pair(lst, "email", email);
-	if (jid != NULL && *jid != '\0')
-		lst = avahi_string_list_add_pair(lst, "jid", jid);
-	/* Nonstandard, but used by iChat */
-	if (aim != NULL && *aim != '\0')
-		lst = avahi_string_list_add_pair(lst, "AIM", aim);
-	if (data->msg != NULL && *data->msg != '\0')
-		lst = avahi_string_list_add_pair(lst, "msg", data->msg);
-	if (data->phsh != NULL && *data->phsh != '\0')
-		lst = avahi_string_list_add_pair(lst, "phsh", data->phsh);
-
-	/* TODO: ext, nick, node */
-
+	while (records) {
+		PurpleKeyValuePair *kvp = records->data;
+		lst = avahi_string_list_add_pair(lst, kvp->key, kvp->value);
+		records = records->next;
+	}
 
 	/* Publish the service */
 	switch (type) {
@@ -290,7 +319,8 @@
 		return FALSE;
 	}
 
-	if ((publish_result = avahi_entry_group_commit(idata->group)) < 0) {
+	if (type == PUBLISH_START
+			&& (publish_result = avahi_entry_group_commit(idata->group)) < 0) {
 		purple_debug_error("bonjour",
 			"Failed to commit " ICHAT_SERVICE " service. Error: %s\n",
 			avahi_strerror(publish_result));
@@ -317,9 +347,71 @@
 	return TRUE;
 }
 
-/* This is done differently than with Howl/Apple Bonjour */
-guint _mdns_register_to_mainloop(BonjourDnsSd *data) {
-	return 0;
+gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) {
+	AvahiSessionImplData *idata = data->mdns_impl_data;
+
+	if (idata == NULL || idata->client == NULL)
+		return FALSE;
+
+	if (avatar_data != NULL) {
+		gboolean new_group = FALSE;
+		gchar *svc_name;
+		int ret;
+		AvahiPublishFlags flags = 0;
+
+		if (idata->buddy_icon_group == NULL) {
+			purple_debug_info("bonjour", "Setting new buddy icon.\n");
+			new_group = TRUE;
+
+			idata->buddy_icon_group = avahi_entry_group_new(idata->client,
+				_buddy_icon_group_cb, data);
+		} else {
+			purple_debug_info("bonjour", "Updating existing buddy icon.\n");
+			flags |= AVAHI_PUBLISH_UPDATE;
+		}
+
+		if (idata->buddy_icon_group == NULL) {
+			purple_debug_error("bonjour",
+				"Unable to initialize the buddy icon group (%s).\n",
+				avahi_strerror(avahi_client_errno(idata->client)));
+			return FALSE;
+		}
+
+		svc_name = g_strdup_printf("%s." ICHAT_SERVICE "local",
+				purple_account_get_username(data->account));
+
+		ret = avahi_entry_group_add_record(idata->buddy_icon_group, AVAHI_IF_UNSPEC,
+			AVAHI_PROTO_UNSPEC, flags, svc_name,
+			AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 120, avatar_data, avatar_len);
+
+		g_free(svc_name);
+
+		if (ret < 0) {
+			purple_debug_error("bonjour",
+				"Failed to register buddy icon. Error: %s\n", avahi_strerror(ret));
+			if (new_group) {
+				avahi_entry_group_free(idata->buddy_icon_group);
+				idata->buddy_icon_group = NULL;
+			}
+			return FALSE;
+		}
+
+		if (new_group && (ret = avahi_entry_group_commit(idata->buddy_icon_group)) < 0) {
+			purple_debug_error("bonjour",
+				"Failed to commit buddy icon group. Error: %s\n", avahi_strerror(ret));
+			if (new_group) {
+				avahi_entry_group_free(idata->buddy_icon_group);
+				idata->buddy_icon_group = NULL;
+			}
+			return FALSE;
+		}
+	} else if (idata->buddy_icon_group != NULL) {
+		purple_debug_info("bonjour", "Removing existing buddy icon.\n");
+		avahi_entry_group_free(idata->buddy_icon_group);
+		idata->buddy_icon_group = NULL;
+	}
+
+	return TRUE;
 }
 
 void _mdns_stop(BonjourDnsSd *data) {
@@ -340,10 +432,50 @@
 }
 
 void _mdns_init_buddy(BonjourBuddy *buddy) {
+	buddy->mdns_impl_data = g_new0(AvahiBuddyImplData, 1);
 }
 
 void _mdns_delete_buddy(BonjourBuddy *buddy) {
+	AvahiBuddyImplData *idata = buddy->mdns_impl_data;
+
+	g_return_if_fail(idata != NULL);
+
+	if (idata->buddy_icon_rec_browser != NULL)
+		avahi_record_browser_free(idata->buddy_icon_rec_browser);
+
+	if (idata->resolver != NULL)
+		avahi_service_resolver_free(idata->resolver);
+
+	g_free(idata);
+
+	buddy->mdns_impl_data = NULL;
 }
 
-void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) {
+void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) {
+	PurpleConnection *conn = purple_account_get_connection(buddy->account);
+	BonjourData *bd = conn->proto_data;
+	AvahiSessionImplData *session_idata = bd->dns_sd_data->mdns_impl_data;
+	AvahiBuddyImplData *idata = buddy->mdns_impl_data;
+	gchar *name;
+
+	g_return_if_fail(idata != NULL);
+
+	if (idata->buddy_icon_rec_browser != NULL)
+		avahi_record_browser_free(idata->buddy_icon_rec_browser);
+
+	purple_debug_info("bonjour", "Retrieving buddy icon for '%s'.\n", buddy->name);
+
+	name = g_strdup_printf("%s." ICHAT_SERVICE "local", buddy->name);
+	idata->buddy_icon_rec_browser = avahi_record_browser_new(session_idata->client, AVAHI_IF_UNSPEC,
+		AVAHI_PROTO_UNSPEC, name, AVAHI_DNS_CLASS_IN, AVAHI_DNS_TYPE_NULL, 0,
+		_buddy_icon_record_cb, buddy);
+	g_free(name);
+
+	if (!idata->buddy_icon_rec_browser) {
+		purple_debug_error("bonjour",
+			"Unable to initialize buddy icon record browser.  Error: %s\n.",
+			avahi_strerror(avahi_client_errno(session_idata->client)));
+	}
+
 }
+
--- a/libpurple/protocols/bonjour/mdns_common.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_common.c	Tue Aug 14 02:45:41 2007 +0000
@@ -17,30 +17,27 @@
 #include <string.h>
 
 #include "internal.h"
+#include "cipher.h"
+#include "debug.h"
+
 #include "mdns_common.h"
 #include "mdns_interface.h"
 #include "bonjour.h"
 #include "buddy.h"
-#include "debug.h"
 
 
 /**
  * Allocate space for the dns-sd data.
  */
-BonjourDnsSd *
-bonjour_dns_sd_new()
-{
+BonjourDnsSd * bonjour_dns_sd_new() {
 	BonjourDnsSd *data = g_new0(BonjourDnsSd, 1);
-
 	return data;
 }
 
 /**
  * Deallocate the space of the dns-sd data.
  */
-void
-bonjour_dns_sd_free(BonjourDnsSd *data)
-{
+void bonjour_dns_sd_free(BonjourDnsSd *data) {
 	g_free(data->first);
 	g_free(data->last);
 	g_free(data->phsh);
@@ -50,12 +47,90 @@
 	g_free(data);
 }
 
+static GSList *generate_presence_txt_records(BonjourDnsSd *data) {
+	GSList *ret = NULL;
+	PurpleKeyValuePair *kvp;
+	char portstring[6];
+	const char *jid, *aim, *email;
+
+	/* Convert the port to a string */
+	snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj);
+
+	jid = purple_account_get_string(data->account, "jid", NULL);
+	aim = purple_account_get_string(data->account, "AIM", NULL);
+	email = purple_account_get_string(data->account, "email", NULL);
+
+#define _M_ADD_R(k, v) \
+	kvp = g_new0(PurpleKeyValuePair, 1); \
+	kvp->key = g_strdup(k); \
+	kvp->value = g_strdup(v); \
+	ret = g_slist_prepend(ret, kvp); \
+
+	/* We should try to follow XEP-0174, but some clients have "issues", so we humor them.
+	 * See http://telepathy.freedesktop.org/wiki/SalutInteroperability
+	 */
+
+	/* Needed by iChat */
+	_M_ADD_R("txtvers", "1")
+	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
+	_M_ADD_R("1st", data->first)
+	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
+	_M_ADD_R("last", data->last)
+	/* Needed by Adium */
+	_M_ADD_R("port.p2pj", portstring)
+	/* Needed by iChat, Gaim/Pidgin <= 2.0.1 */
+	_M_ADD_R("status", data->status)
+	_M_ADD_R("node", "libpurple")
+	_M_ADD_R("ver", VERSION)
+	/* Currently always set to "!" since we don't support AV and wont ever be in a conference */
+	_M_ADD_R("vc", data->vc)
+	if (email != NULL && *email != '\0') {
+		_M_ADD_R("email", email)
+	}
+	if (jid != NULL && *jid != '\0') {
+		_M_ADD_R("jid", jid)
+	}
+	/* Nonstandard, but used by iChat */
+	if (aim != NULL && *aim != '\0') {
+		_M_ADD_R("AIM", aim)
+	}
+	if (data->msg != NULL && *data->msg != '\0') {
+		_M_ADD_R("msg", data->msg)
+	}
+	if (data->phsh != NULL && *data->phsh != '\0') {
+		_M_ADD_R("phsh", data->phsh)
+	}
+
+	/* TODO: ext, nick */
+	return ret;
+}
+
+static void free_presence_txt_records(GSList *lst) {
+	PurpleKeyValuePair *kvp;
+	while(lst) {
+		kvp = lst->data;
+		g_free(kvp->key);
+		g_free(kvp->value);
+		g_free(kvp);
+		lst = g_slist_remove(lst, lst->data);
+	}
+}
+
+static gboolean publish_presence(BonjourDnsSd *data, PublishType type) {
+	GSList *txt_records;
+	gboolean ret;
+
+	txt_records = generate_presence_txt_records(data);
+	ret = _mdns_publish(data, type, txt_records);
+	free_presence_txt_records(txt_records);
+
+	return ret;
+}
+
 /**
  * Send a new dns-sd packet updating our status.
  */
-void
-bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message)
-{
+void bonjour_dns_sd_send_status(BonjourDnsSd *data, const char *status, const char *status_message) {
 	g_free(data->status);
 	g_free(data->msg);
 
@@ -63,26 +138,78 @@
 	data->msg = g_strdup(status_message);
 
 	/* Update our text record with the new status */
-	_mdns_publish(data, PUBLISH_UPDATE); /* <--We must control the errors */
+	publish_presence(data, PUBLISH_UPDATE);
+}
+
+/**
+ * Retrieve the buddy icon blob
+ */
+void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) {
+	_mdns_retrieve_buddy_icon(buddy);
+}
+
+void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data) {
+	PurpleStoredImage *img;
+
+	if ((img = purple_buddy_icons_find_account_icon(data->account))) {
+		gconstpointer avatar_data;
+		gsize avatar_len;
+
+		avatar_data = purple_imgstore_get_data(img);
+		avatar_len = purple_imgstore_get_size(img);
+
+		if (_mdns_set_buddy_icon_data(data, avatar_data, avatar_len)) {
+			int i;
+			gchar *enc;
+			char *p, hash[41];
+			unsigned char hashval[20];
+
+			enc = purple_base64_encode(avatar_data, avatar_len);
+
+			purple_cipher_digest_region("sha1", avatar_data,
+						    avatar_len, sizeof(hashval),
+						    hashval, NULL);
+
+			p = hash;
+			for(i=0; i<20; i++, p+=2)
+				snprintf(p, 3, "%02x", hashval[i]);
+
+			g_free(data->phsh);
+			data->phsh = g_strdup(hash);
+
+			g_free(enc);
+
+			/* Update our TXT record */
+			publish_presence(data, PUBLISH_UPDATE);
+		}
+
+		purple_imgstore_unref(img);
+	} else {
+		/* We need to do this regardless of whether data->phsh is set so that we
+		 * cancel any icons that are currently in the process of being set */
+		_mdns_set_buddy_icon_data(data, NULL, 0);
+		if (data->phsh != NULL) {
+			/* Clear the buddy icon */
+			g_free(data->phsh);
+			data->phsh = NULL;
+			/* Update our TXT record */
+			publish_presence(data, PUBLISH_UPDATE);
+		}
+	}
 }
 
 /**
  * Advertise our presence within the dns-sd daemon and start browsing
  * for other bonjour peers.
  */
-gboolean
-bonjour_dns_sd_start(BonjourDnsSd *data)
-{
-	PurpleConnection *gc;
-
-	gc = purple_account_get_connection(data->account);
+gboolean bonjour_dns_sd_start(BonjourDnsSd *data) {
 
 	/* Initialize the dns-sd data and session */
 	if (!_mdns_init_session(data))
 		return FALSE;
 
 	/* Publish our bonjour IM client at the mDNS daemon */
-	if (!_mdns_publish(data, PUBLISH_START))
+	if (!publish_presence(data, PUBLISH_START))
 		return FALSE;
 
 	/* Advise the daemon that we are waiting for connections */
@@ -91,11 +218,6 @@
 		return FALSE;
 	}
 
-
-	/* Get the socket that communicates with the mDNS daemon and bind it to a */
-	/* callback that will handle the dns_sd packets */
-	gc->inpa = _mdns_register_to_mainloop(data);
-
 	return TRUE;
 }
 
@@ -103,14 +225,6 @@
  * Unregister the "_presence._tcp" service at the mDNS daemon.
  */
 
-void
-bonjour_dns_sd_stop(BonjourDnsSd *data)
-{
-	PurpleConnection *gc;
-
+void bonjour_dns_sd_stop(BonjourDnsSd *data) {
 	_mdns_stop(data);
-
-	gc = purple_account_get_connection(data->account);
-	if (gc->inpa > 0)
-		purple_input_remove(gc->inpa);
 }
--- a/libpurple/protocols/bonjour/mdns_common.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_common.h	Tue Aug 14 02:45:41 2007 +0000
@@ -42,6 +42,11 @@
 void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy);
 
 /**
+ * Deal with a buddy icon update
+ */
+void bonjour_dns_sd_update_buddy_icon(BonjourDnsSd *data);
+
+/**
  * Advertise our presence within the dns-sd daemon and start
  * browsing for other bonjour peers.
  */
--- a/libpurple/protocols/bonjour/mdns_howl.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_howl.c	Tue Aug 14 02:45:41 2007 +0000
@@ -26,6 +26,7 @@
 typedef struct _howl_impl_data {
 	sw_discovery session;
 	sw_discovery_oid session_id;
+	guint session_handler;
 } HowlSessionImplData;
 
 static sw_result HOWL_API
@@ -84,6 +85,7 @@
 	/* Obtain the parameters from the text_record */
 	if ((text_record_len > 0) && (text_record) && (*text_record != '\0'))
 	{
+		clear_bonjour_buddy_values(buddy);
 		sw_text_record_iterator_init(&iterator, text_record, text_record_len);
 		while (sw_text_record_iterator_next(iterator, key, (sw_octet *)value, &value_length) == SW_OKAY)
 			set_bonjour_buddy_value(buddy, key, value, value_length);
@@ -192,11 +194,9 @@
 }
 
 
-gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) {
+gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) {
 	sw_text_record dns_data;
 	sw_result publish_result = SW_OKAY;
-	char portstring[6];
-	const char *jid, *aim, *email;
 	HowlSessionImplData *idata = data->mdns_impl_data;
 
 	g_return_val_if_fail(idata != NULL, FALSE);
@@ -207,43 +207,11 @@
 		return FALSE;
 	}
 
-	/* Convert the port to a string */
-	snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj);
-
-	jid = purple_account_get_string(data->account, "jid", NULL);
-	aim = purple_account_get_string(data->account, "AIM", NULL);
-	email = purple_account_get_string(data->account, "email", NULL);
-
-	/* We should try to follow XEP-0174, but some clients have "issues", so we humor them.
-	 * See http://telepathy.freedesktop.org/wiki/SalutInteroperability
-	 */
-
-	/* Needed by iChat */
-	sw_text_record_add_key_and_string_value(dns_data, "txtvers", "1");
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	sw_text_record_add_key_and_string_value(dns_data, "1st", data->first);
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	sw_text_record_add_key_and_string_value(dns_data, "last", data->last);
-	/* Needed by Adium */
-	sw_text_record_add_key_and_string_value(dns_data, "port.p2pj", portstring);
-	/* Needed by iChat, Gaim/Pidgin <= 2.0.1 */
-	sw_text_record_add_key_and_string_value(dns_data, "status", data->status);
-	/* Currently always set to "!" since we don't support AV and wont ever be in a conference */
-	sw_text_record_add_key_and_string_value(dns_data, "vc", data->vc);
-	sw_text_record_add_key_and_string_value(dns_data, "ver", VERSION);
-	if (email != NULL && *email != '\0')
-		sw_text_record_add_key_and_string_value(dns_data, "email", email);
-	if (jid != NULL && *jid != '\0')
-		sw_text_record_add_key_and_string_value(dns_data, "jid", jid);
-	/* Nonstandard, but used by iChat */
-	if (aim != NULL && *aim != '\0')
-		sw_text_record_add_key_and_string_value(dns_data, "AIM", aim);
-	if (data->msg != NULL && *data->msg != '\0')
-		sw_text_record_add_key_and_string_value(dns_data, "msg", data->msg);
-	if (data->phsh != NULL && *data->phsh != '\0')
-		sw_text_record_add_key_and_string_value(dns_data, "phsh", data->phsh);
-
-	/* TODO: ext, nick, node */
+	while (records) {
+		PurpleKeyValuePair *kvp = records->data;
+		sw_text_record_add_key_and_string_value(dns_data, kvp->key, kvp->value);
+		records = records->next;
+	}
 
 	/* Publish the service */
 	switch (type) {
@@ -276,17 +244,18 @@
 
 	g_return_val_if_fail(idata != NULL, FALSE);
 
-	return (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply,
-				    data->account, &session_id) == SW_OKAY);
+	if (sw_discovery_browse(idata->session, 0, ICHAT_SERVICE, NULL, _browser_reply,
+				    data->account, &session_id) == SW_OKAY) {
+		idata->session_handler = purple_input_add(sw_discovery_socket(idata->session),
+				PURPLE_INPUT_READ, _mdns_handle_event, idata->session);
+		return TRUE;
+	}
+
+	return FALSE;
 }
 
-guint _mdns_register_to_mainloop(BonjourDnsSd *data) {
-	HowlSessionImplData *idata = data->mdns_impl_data;
-
-	g_return_val_if_fail(idata != NULL, 0);
-
-	return purple_input_add(sw_discovery_socket(idata->session),
-				PURPLE_INPUT_READ, _mdns_handle_event, idata->session);
+gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) {
+	return FALSE;
 }
 
 void _mdns_stop(BonjourDnsSd *data) {
@@ -297,6 +266,8 @@
 
 	sw_discovery_cancel(idata->session, idata->session_id);
 
+	purple_input_remove(idata->session_handler);
+
 	/* TODO: should this really be g_free()'d ??? */
 	g_free(idata->session);
 
@@ -311,5 +282,7 @@
 void _mdns_delete_buddy(BonjourBuddy *buddy) {
 }
 
-void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) {
+void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) {
 }
+
+
--- a/libpurple/protocols/bonjour/mdns_interface.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_interface.h	Tue Aug 14 02:45:41 2007 +0000
@@ -22,19 +22,18 @@
 
 gboolean _mdns_init_session(BonjourDnsSd *data);
 
-gboolean _mdns_publish(BonjourDnsSd *data, PublishType type);
+gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records);
 
 gboolean _mdns_browse(BonjourDnsSd *data);
 
-guint _mdns_register_to_mainloop(BonjourDnsSd *data);
+void _mdns_stop(BonjourDnsSd *data);
 
-void _mdns_stop(BonjourDnsSd *data);
+gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len);
 
 void _mdns_init_buddy(BonjourBuddy *buddy);
 
 void _mdns_delete_buddy(BonjourBuddy *buddy);
 
-/* This doesn't quite belong here, but there really isn't any shared functionality */
-void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy);
+void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy);
 
 #endif
--- a/libpurple/protocols/bonjour/mdns_win32.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/bonjour/mdns_win32.c	Tue Aug 14 02:45:41 2007 +0000
@@ -21,12 +21,14 @@
 #include "mdns_interface.h"
 #include "dns_sd_proxy.h"
 #include "dnsquery.h"
+#include "mdns_common.h"
 
 
 /* data structure for the resolve callback */
 typedef struct _ResolveCallbackArgs {
 	DNSServiceRef resolver;
 	guint resolver_handler;
+	gchar *full_service_name;
 
 	PurpleDnsQueryData *query;
 
@@ -35,10 +37,12 @@
 
 /* data used by win32 bonjour implementation */
 typedef struct _win32_session_impl_data {
-	DNSServiceRef advertisement;
-	DNSServiceRef browser;
+	DNSServiceRef presence_svc;
+	DNSServiceRef browser_svc;
+	DNSRecordRef buddy_icon_rec;
 
-	guint advertisement_handler; /* hack... windows bonjour is broken, so we have to have this */
+	guint presence_handler;
+	guint browser_handler;
 } Win32SessionImplData;
 
 typedef struct _win32_buddy_impl_data {
@@ -60,6 +64,7 @@
 	uint8_t txt_len;
 	int i;
 
+	clear_bonjour_buddy_values(buddy);
 	for (i = 0; buddy_TXT_records[i] != NULL; i++) {
 		txt_entry = TXTRecordGetValuePtr(record_len, record, buddy_TXT_records[i], &txt_len);
 		if (txt_entry != NULL)
@@ -68,7 +73,7 @@
 }
 
 static void DNSSD_API
-_mdns_text_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags,
+_mdns_record_query_callback(DNSServiceRef DNSServiceRef, DNSServiceFlags flags,
 	uint32_t interfaceIndex, DNSServiceErrorType errorCode, const char *fullname,
 	uint16_t rrtype, uint16_t rrclass, uint16_t rdlen, const void *rdata,
 	uint32_t ttl, void *context)
@@ -90,8 +95,7 @@
 
 			g_return_if_fail(idata != NULL);
 
-			purple_buddy_icons_set_for_user(buddy->account, buddy->name,
-							g_memdup(rdata, rdlen), rdlen, buddy->phsh);
+			bonjour_buddy_got_buddy_icon(buddy, rdata, rdlen);
 
 			/* We've got what we need; stop listening */
 			purple_input_remove(idata->null_query_handler);
@@ -120,14 +124,16 @@
 
 		/* finally, set up the continuous txt record watcher, and add the buddy to purple */
 
-		if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, 0, 0, buddy->full_service_name,
-				kDNSServiceType_TXT, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) {
-			int fd = DNSServiceRefSockFD(idata->txt_query);
-			idata->txt_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query);
+		if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->txt_query, kDNSServiceFlagsLongLivedQuery,
+				kDNSServiceInterfaceIndexAny, args->full_service_name, kDNSServiceType_TXT,
+				kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) {
+
+			purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj);
+
+			idata->txt_query_handler = purple_input_add(DNSServiceRefSockFD(idata->txt_query),
+				PURPLE_INPUT_READ, _mdns_handle_event, idata->txt_query);
 
 			bonjour_buddy_add_to_purple(buddy);
-
-			purple_debug_info("bonjour", "Found buddy %s at %s:%d\n", buddy->name, buddy->ip, buddy->port_p2pj);
 		} else
 			bonjour_buddy_delete(buddy);
 
@@ -138,6 +144,7 @@
 
 	/* free the remaining args memory */
 	purple_dnsquery_destroy(args->query);
+	g_free(args->full_service_name);
 	g_free(args);
 }
 
@@ -165,13 +172,14 @@
 		_mdns_parse_text_record(args->buddy, txtRecord, txtLen);
 
 		/* set more arguments, and start the host resolver */
-		args->buddy->full_service_name = g_strdup(fullname);
+		args->full_service_name = g_strdup(fullname);
 
 		if (!(args->query =
 			purple_dnsquery_a(hosttarget, port, _mdns_resolve_host_callback, args)))
 		{
 			purple_debug_error("bonjour", "service resolver - host resolution failed.\n");
 			bonjour_buddy_delete(args->buddy);
+			g_free(args->full_service_name);
 			g_free(args);
 		}
 	}
@@ -180,11 +188,11 @@
 
 static void DNSSD_API
 _mdns_service_register_callback(DNSServiceRef sdRef, DNSServiceFlags flags, DNSServiceErrorType errorCode,
-    const char *name, const char *regtype, const char *domain, void *context)
-{
-	/* we don't actually care about anything said in this callback - this is only here because Bonjour for windows is broken */
+				const char *name, const char *regtype, const char *domain, void *context) {
+
+	/* TODO: deal with collision */
 	if (kDNSServiceErr_NoError != errorCode)
-		purple_debug_error("bonjour", "service advertisement - callback error.\n");
+		purple_debug_error("bonjour", "service advertisement - callback error (%d).\n", errorCode);
 	else
 		purple_debug_info("bonjour", "service advertisement - callback.\n");
 }
@@ -235,61 +243,23 @@
 	return TRUE;
 }
 
-gboolean _mdns_publish(BonjourDnsSd *data, PublishType type) {
+gboolean _mdns_publish(BonjourDnsSd *data, PublishType type, GSList *records) {
 	TXTRecordRef dns_data;
-	char portstring[6];
 	gboolean ret = TRUE;
-	const char *jid, *aim, *email;
-	DNSServiceErrorType set_ret;
+	DNSServiceErrorType set_ret = kDNSServiceErr_NoError;
 	Win32SessionImplData *idata = data->mdns_impl_data;
 
 	g_return_val_if_fail(idata != NULL, FALSE);
 
 	TXTRecordCreate(&dns_data, 256, NULL);
 
-	/* Convert the port to a string */
-	snprintf(portstring, sizeof(portstring), "%d", data->port_p2pj);
-
-	jid = purple_account_get_string(data->account, "jid", NULL);
-	aim = purple_account_get_string(data->account, "AIM", NULL);
-	email = purple_account_get_string(data->account, "email", NULL);
-
-	/* We should try to follow XEP-0174, but some clients have "issues", so we humor them.
-	 * See http://telepathy.freedesktop.org/wiki/SalutInteroperability
-	 */
-
-	/* Needed by iChat */
-	set_ret = TXTRecordSetValue(&dns_data, "txtvers", 1, "1");
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "1st", strlen(data->first), data->first);
-	/* Needed by Gaim/Pidgin <= 2.0.1 (remove at some point) */
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "last", strlen(data->last), data->last);
-	/* Needed by Adium */
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "port.p2pj", strlen(portstring), portstring);
-	/* Needed by iChat, Gaim/Pidgin <= 2.0.1 */
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "status", strlen(data->status), data->status);
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "ver", strlen(VERSION), VERSION);
-	/* Currently always set to "!" since we don't support AV and wont ever be in a conference */
-	if (set_ret == kDNSServiceErr_NoError)
-		set_ret = TXTRecordSetValue(&dns_data, "vc", strlen(data->vc), data->vc);
-	if (set_ret == kDNSServiceErr_NoError && email != NULL && *email != '\0')
-		set_ret = TXTRecordSetValue(&dns_data, "email", strlen(email), email);
-	if (set_ret == kDNSServiceErr_NoError && jid != NULL && *jid != '\0')
-		set_ret = TXTRecordSetValue(&dns_data, "jid", strlen(jid), jid);
-	/* Nonstandard, but used by iChat */
-	if (set_ret == kDNSServiceErr_NoError && aim != NULL && *aim != '\0')
-		set_ret = TXTRecordSetValue(&dns_data, "AIM", strlen(aim), aim);
-	if (set_ret == kDNSServiceErr_NoError && data->msg != NULL && *data->msg != '\0')
-		set_ret = TXTRecordSetValue(&dns_data, "msg", strlen(data->msg), data->msg);
-	if (set_ret == kDNSServiceErr_NoError && data->phsh != NULL && *data->phsh != '\0')
-		set_ret = TXTRecordSetValue(&dns_data, "phsh", strlen(data->phsh), data->phsh);
-
-	/* TODO: ext, nick, node */
+	while (records) {
+		PurpleKeyValuePair *kvp = records->data;
+		set_ret = TXTRecordSetValue(&dns_data, kvp->key, strlen(kvp->value), kvp->value);
+		if (set_ret != kDNSServiceErr_NoError)
+			break;
+		records = records->next;
+	}
 
 	if (set_ret != kDNSServiceErr_NoError) {
 		purple_debug_error("bonjour", "Unable to allocate memory for text record.\n");
@@ -301,14 +271,15 @@
 
 		switch (type) {
 			case PUBLISH_START:
-				purple_debug_info("bonjour", "Registering service on port %d\n", data->port_p2pj);
-				err = DNSServiceRegister(&idata->advertisement, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE,
+				purple_debug_info("bonjour", "Registering presence on port %d\n", data->port_p2pj);
+				err = DNSServiceRegister(&idata->presence_svc, 0, 0, purple_account_get_username(data->account), ICHAT_SERVICE,
 					NULL, NULL, htons(data->port_p2pj), TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data),
 					_mdns_service_register_callback, NULL);
 				break;
 
 			case PUBLISH_UPDATE:
-				err = DNSServiceUpdateRecord(idata->advertisement, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0);
+				purple_debug_info("bonjour", "Updating presence.\n");
+				err = DNSServiceUpdateRecord(idata->presence_svc, NULL, 0, TXTRecordGetLength(&dns_data), TXTRecordGetBytesPtr(&dns_data), 0);
 				break;
 		}
 
@@ -316,9 +287,12 @@
 			purple_debug_error("bonjour", "Failed to publish presence service.\n");
 			ret = FALSE;
 		} else if (type == PUBLISH_START) {
-			/* hack: Bonjour on windows is broken. We don't care about the callback but we have to listen anyway */
-			gint fd = DNSServiceRefSockFD(idata->advertisement);
-			idata->advertisement_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->advertisement);
+			/* We need to do this because according to the Apple docs:
+			 * "the client is responsible for ensuring that DNSServiceProcessResult() is called
+			 * whenever there is a reply from the daemon - the daemon may terminate its connection
+			 * with a client that does not process the daemon's responses */
+			idata->presence_handler = purple_input_add(DNSServiceRefSockFD(idata->presence_svc),
+				PURPLE_INPUT_READ, _mdns_handle_event, idata->presence_svc);
 		}
 	}
 
@@ -332,37 +306,64 @@
 
 	g_return_val_if_fail(idata != NULL, FALSE);
 
-	return (DNSServiceBrowse(&idata->browser, 0, 0, ICHAT_SERVICE, NULL,
+	if (DNSServiceBrowse(&idata->browser_svc, 0, 0, ICHAT_SERVICE, NULL,
 				 _mdns_service_browse_callback, data->account)
-			== kDNSServiceErr_NoError);
-}
+			== kDNSServiceErr_NoError) {
+		idata->browser_handler = purple_input_add(DNSServiceRefSockFD(idata->browser_svc),
+			PURPLE_INPUT_READ, _mdns_handle_event, idata->browser_svc);
+		return TRUE;
+	}
 
-guint _mdns_register_to_mainloop(BonjourDnsSd *data) {
-	Win32SessionImplData *idata = data->mdns_impl_data;
-
-	g_return_val_if_fail(idata != NULL, 0);
-
-	return purple_input_add(DNSServiceRefSockFD(idata->browser),
-				PURPLE_INPUT_READ, _mdns_handle_event, idata->browser);
+	return FALSE;
 }
 
 void _mdns_stop(BonjourDnsSd *data) {
 	Win32SessionImplData *idata = data->mdns_impl_data;
 
-	if (idata == NULL || idata->advertisement == NULL || idata->browser == NULL)
+	if (idata == NULL)
 		return;
 
-	/* hack: for win32, we need to stop listening to the advertisement pipe too */
-	purple_input_remove(idata->advertisement_handler);
+	if (idata->presence_svc != NULL) {
+		purple_input_remove(idata->presence_handler);
+		DNSServiceRefDeallocate(idata->presence_svc);
+	}
 
-	DNSServiceRefDeallocate(idata->advertisement);
-	DNSServiceRefDeallocate(idata->browser);
+	if (idata->browser_svc != NULL) {
+		purple_input_remove(idata->browser_handler);
+		DNSServiceRefDeallocate(idata->browser_svc);
+	}
 
 	g_free(idata);
 
 	data->mdns_impl_data = NULL;
 }
 
+gboolean _mdns_set_buddy_icon_data(BonjourDnsSd *data, gconstpointer avatar_data, gsize avatar_len) {
+	Win32SessionImplData *idata = data->mdns_impl_data;
+	DNSServiceErrorType err = kDNSServiceErr_NoError;
+
+	g_return_val_if_fail(idata != NULL, FALSE);
+
+	if (avatar_data != NULL && idata->buddy_icon_rec == NULL) {
+		purple_debug_info("bonjour", "Setting new buddy icon.\n");
+		err = DNSServiceAddRecord(idata->presence_svc, &idata->buddy_icon_rec,
+			0, kDNSServiceType_NULL, avatar_len, avatar_data, 0);
+	} else if (avatar_data != NULL) {
+		purple_debug_info("bonjour", "Updating existing buddy icon.\n");
+		err = DNSServiceUpdateRecord(idata->presence_svc, idata->buddy_icon_rec,
+			0, avatar_len, avatar_data, 0);
+	} else if (idata->buddy_icon_rec != NULL) {
+		purple_debug_info("bonjour", "Removing existing buddy icon.\n");
+		DNSServiceRemoveRecord(idata->presence_svc, idata->buddy_icon_rec, 0);
+		idata->buddy_icon_rec = NULL;
+	}
+
+	if (err != kDNSServiceErr_NoError)
+		purple_debug_error("bonjour", "Error (%d) setting buddy icon record.\n", err);
+
+	return (err == kDNSServiceErr_NoError);
+}
+
 void _mdns_init_buddy(BonjourBuddy *buddy) {
 	buddy->mdns_impl_data = g_new0(Win32BuddyImplData, 1);
 }
@@ -387,8 +388,9 @@
 	buddy->mdns_impl_data = NULL;
 }
 
-void bonjour_dns_sd_retrieve_buddy_icon(BonjourBuddy* buddy) {
+void _mdns_retrieve_buddy_icon(BonjourBuddy* buddy) {
 	Win32BuddyImplData *idata = buddy->mdns_impl_data;
+	char svc_name[kDNSServiceMaxDomainName];
 
 	g_return_if_fail(idata != NULL);
 
@@ -400,10 +402,11 @@
 		idata->null_query = NULL;
 	}
 
-	if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, 0, buddy->full_service_name,
-			kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_text_record_query_callback, buddy)) {
-		int fd = DNSServiceRefSockFD(idata->null_query);
-		idata->null_query_handler = purple_input_add(fd, PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query);
+	DNSServiceConstructFullName(svc_name, buddy->name, ICHAT_SERVICE, "local");
+	if (kDNSServiceErr_NoError == DNSServiceQueryRecord(&idata->null_query, 0, kDNSServiceInterfaceIndexAny, svc_name,
+			kDNSServiceType_NULL, kDNSServiceClass_IN, _mdns_record_query_callback, buddy)) {
+		idata->null_query_handler = purple_input_add(DNSServiceRefSockFD(idata->null_query),
+			PURPLE_INPUT_READ, _mdns_handle_event, idata->null_query);
 	}
 
 }
--- a/libpurple/protocols/jabber/auth.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/jabber/auth.c	Tue Aug 14 02:45:41 2007 +0000
@@ -296,7 +296,7 @@
 					purple_request_yes_no(js->gc, _("Plaintext Authentication"),
 							_("Plaintext Authentication"),
 							msg,
-							2, js->gc->account, NULL, NULL, NULL,
+							2, js->gc->account, NULL, NULL, js->gc->account,
 							allow_cyrus_plaintext_auth,
 							disallow_plaintext_auth);
 					g_free(msg);
@@ -682,11 +682,11 @@
 
 	gchar *a1, *convnode=NULL, *convpasswd = NULL, *ha1, *ha2, *kd, *x, *z;
 
-	if((convnode = g_convert(jid->node, strlen(jid->node), "iso-8859-1", "utf-8",
+	if((convnode = g_convert(jid->node, -1, "iso-8859-1", "utf-8",
 					NULL, NULL, NULL)) == NULL) {
 		convnode = g_strdup(jid->node);
 	}
-	if(passwd && ((convpasswd = g_convert(passwd, strlen(passwd), "iso-8859-1",
+	if(passwd && ((convpasswd = g_convert(passwd, -1, "iso-8859-1",
 						"utf-8", NULL, NULL, NULL)) == NULL)) {
 		convpasswd = g_strdup(passwd);
 	}
--- a/libpurple/protocols/jabber/jabber.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1718,7 +1718,7 @@
 static PurpleCmdRet jabber_cmd_chat_role(PurpleConversation *conv,
 		const char *cmd, char **args, char **error, void *data)
 {
-	JabberChat *chat;
+	JabberChat *chat = jabber_chat_find_by_conv(conv);
 
 	if (!chat || !args || !args[0] || !args[1])
 		return PURPLE_CMD_RET_FAILED;
@@ -1731,8 +1731,6 @@
 		return PURPLE_CMD_RET_FAILED;
 	}
 
-	chat = jabber_chat_find_by_conv(conv);
-
 	if (!jabber_chat_role_user(chat, args[0], args[1])) {
 		*error = g_strdup_printf(_("Unable to set role \"%s\" for user: %s"),
 		                         args[1], args[0]);
--- a/libpurple/protocols/jabber/presence.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/jabber/presence.c	Tue Aug 14 02:45:41 2007 +0000
@@ -193,16 +193,18 @@
 	char *who;
 };
 
-static void authorize_add_cb(struct _jabber_add_permit *jap)
+static void authorize_add_cb(gpointer data)
 {
+	struct _jabber_add_permit *jap = data;
 	jabber_presence_subscription_set(jap->gc->proto_data, jap->who,
 			"subscribed");
 	g_free(jap->who);
 	g_free(jap);
 }
 
-static void deny_add_cb(struct _jabber_add_permit *jap)
+static void deny_add_cb(gpointer data)
 {
+	struct _jabber_add_permit *jap = data;
 	jabber_presence_subscription_set(jap->gc->proto_data, jap->who,
 			"unsubscribed");
 
@@ -305,7 +307,7 @@
 		jap->js = js;
 
 		purple_account_request_authorization(purple_connection_get_account(js->gc), from, NULL, NULL, NULL, onlist,
-				G_CALLBACK(authorize_add_cb), G_CALLBACK(deny_add_cb), jap);
+				authorize_add_cb, deny_add_cb, jap);
 		jabber_id_free(jid);
 		return;
 	} else if(type && !strcmp(type, "subscribed")) {
--- a/libpurple/protocols/msn/userlist.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/msn/userlist.c	Tue Aug 14 02:45:41 2007 +0000
@@ -38,8 +38,9 @@
  * Callbacks
  **************************************************************************/
 static void
-msn_accept_add_cb(MsnPermitAdd *pa)
+msn_accept_add_cb(gpointer data)
 {
+	MsnPermitAdd *pa = data;
 	MsnSession *session = pa->gc->proto_data;
 	MsnUserList *userlist = session->userlist;
 
@@ -51,8 +52,9 @@
 }
 
 static void
-msn_cancel_add_cb(MsnPermitAdd *pa)
+msn_cancel_add_cb(gpointer data)
 {
+	MsnPermitAdd *pa = data;
 	MsnSession *session = pa->gc->proto_data;
 	MsnUserList *userlist = session->userlist;
 
@@ -75,7 +77,7 @@
 	
 	purple_account_request_authorization(purple_connection_get_account(gc), passport, NULL, friendly, NULL,
 					   purple_find_buddy(purple_connection_get_account(gc), passport) != NULL,
-					   G_CALLBACK(msn_accept_add_cb), G_CALLBACK(msn_cancel_add_cb), pa);
+					   msn_accept_add_cb, msn_cancel_add_cb, pa);
 }
 
 /**************************************************************************
--- a/libpurple/protocols/myspace/myspace.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/myspace/myspace.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1,4107 +1,4107 @@
-/* MySpaceIM Protocol Plugin
- *
- * \author Jeff Connelly
- *
- * Copyright (C) 2007, Jeff Connelly <jeff2@soc.pidgin.im>
- *
- * Based on Purple's "C Plugin HOWTO" hello world example.
- *
- * Code also drawn from mockprpl:
- *  http://snarfed.org/space/purple+mock+protocol+plugin
- *  Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org>
- *
- * and some constructs also based on existing Purple plugins, which are:
- *   Copyright (C) 2003, Robbert Haarman <purple@inglorion.net>
- *   Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu>
- *   Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com>
- *   Copyright (C) 1998-1999, Mark Spencer <markster@marko.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 PURPLE_PLUGIN
-
-#include "message.h"
-#include "persist.h"
-#include "myspace.h"
-
-
-/* Loosely based on Miranda plugin by Scott Ellis, formatting.cpp, 
- * https://server.scottellis.com.au/websvn/filedetails.php?repname=Miranda+Plugins&path=%2FMySpace%2Fformatting.cpp&rev=0&sc=0 */
-
-/* The names in in emoticon_names (for <i n=whatever>) map to corresponding 
- * entries in emoticon_symbols (for the ASCII representation of the emoticon).
- *
- * Multiple emoticon symbols in Pidgin can map to one name. List the
- * canonical form, as inserted by the "Smile!" dialog, first. For example,
- * :) comes before :-), because although both are recognized as 'happy',
- * the first is inserted by the smiley button. 
- *
- * Note that symbols are case-sensitive in Pidgin -- :-X is not :-x. */
-static struct MSIM_EMOTICON
-{
-	gchar *name;
-	gchar *symbol;
-} msim_emoticons[] = {
-	/* Unfortunately, this list duplicates much of the file
-	 * pidgin/pidgin/pixmaps/emotes/default/22/default.theme.in, because
-	 * that file is part of Pidgin, but we're part of libpurple.
-	 */
-	{ "bigsmile", ":D" },
-	{ "bigsmile", ":-D" },
-	{ "devil", "}:)" },
-	{ "frazzled", ":Z" },
-	{ "geek", "B)" },
-	{ "googles", "%)" },
-	{ "growl", ":E" },
-	{ "laugh", ":))" },		/* Must be before ':)' */
-	{ "happy", ":)" },
-	{ "happy", ":-)" },
-	{ "happi", ":)" },
-	{ "heart", ":X" },
-	{ "mohawk", "-:" },
-	{ "mad", "X(" },
-	{ "messed", "X)" },
-	{ "nerd", "Q)" },
-	{ "oops", ":G" },
-	{ "pirate", "P)" },
-	{ "scared", ":O" },
-	{ "sidefrown", ":{" },
-	{ "sinister", ":B" },
-	{ "smirk", ":," },
-	{ "straight", ":|" },
-	{ "tongue", ":P" },
-	{ "tongue", ":p" },
-	{ "tongy", ":P" },
-	{ "upset", "B|" },
-	{ "wink", ";-)" },
-	{ "wink", ";)" },
-	{ "winc", ";)" },
-	{ "worried", ":[" },
-	{ "kiss", ":x" },
-	{ NULL, NULL }
-};
-
-/* Internal functions */
-static void msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr);
-
-#ifdef MSIM_DEBUG_MSG
-static void print_hash_item(gpointer key, gpointer value, gpointer user_data);
-#endif
-
-static int msim_send_really_raw(PurpleConnection *gc, const char *buf,
-		int total_bytes);
-static gboolean msim_login_challenge(MsimSession *session, MsimMessage *msg);
-static const gchar *msim_compute_login_response(
-		const gchar nonce[2 * NONCE_SIZE], const gchar *email, 
-		const gchar *password, guint *response_len);
-static gboolean msim_send_bm(MsimSession *session, const gchar *who, 
-		const gchar *text, int type);
-
-static guint msim_point_to_purple_size(MsimSession *session, guint point);
-static guint msim_purple_size_to_point(MsimSession *session, guint size);
-static guint msim_height_to_point(MsimSession *session, guint height);
-static guint msim_point_to_height(MsimSession *session, guint point);
-
-static void msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note);
-
-static void msim_markup_tag_to_html(MsimSession *, xmlnode *root, 
-		gchar **begin, gchar **end);
-static void html_tag_to_msim_markup(MsimSession *, xmlnode *root, 
-		gchar **begin, gchar **end);
-static gchar *msim_convert_xml(MsimSession *, const gchar *raw, 
-		MSIM_XMLNODE_CONVERT f);
-static gchar *msim_convert_smileys_to_markup(gchar *before);
-
-/* High-level msim markup <=> html conversion functions. */
-static gchar *msim_markup_to_html(MsimSession *, const gchar *raw);
-static gchar *html_to_msim_markup(MsimSession *, const gchar *raw);
-
-static MsimUser *msim_get_user_from_buddy(PurpleBuddy *buddy);
-static MsimUser *msim_find_user(MsimSession *session, const gchar *username);
-
-static gboolean msim_incoming_bm_record_cv(MsimSession *session, 
-		MsimMessage *msg);
-static gboolean msim_incoming_bm(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_status(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_im(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_zap(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_action(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_media(MsimSession *session, MsimMessage *msg);
-static gboolean msim_incoming_unofficial_client(MsimSession *session, 
-		MsimMessage *msg);
-
-#ifdef MSIM_SEND_CLIENT_VERSION
-static gboolean msim_send_unofficial_client(MsimSession *session, 
-		gchar *username);
-#endif
-
-static void msim_get_info_cb(MsimSession *session, MsimMessage *userinfo, gpointer data);
-static gchar *msim_format_now_playing(gchar *band, gchar *song);
-
-static void msim_set_status_code(MsimSession *session, guint code, 
-		gchar *statstring);
-
-static void msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full);
-
-static void msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text,
-		gsize len, const gchar *error_message);
-
-static void msim_store_user_info_each(gpointer key, gpointer value, 
-		gpointer user_data);
-static gboolean msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user);
-static gboolean msim_process_server_info(MsimSession *session, 
-		MsimMessage *msg);
-static gboolean msim_web_challenge(MsimSession *session, MsimMessage *msg); 
-static gboolean msim_process_reply(MsimSession *session, MsimMessage *msg);
-
-static gboolean msim_preprocess_incoming(MsimSession *session,MsimMessage *msg);
-
-#ifdef MSIM_USE_KEEPALIVE
-static gboolean msim_check_alive(gpointer data);
-#endif
-
-static gboolean msim_we_are_logged_on(MsimSession *session, MsimMessage *msg);
-
-static gboolean msim_process(MsimSession *session, MsimMessage *msg);
-
-static MsimMessage *msim_do_postprocessing(MsimMessage *msg, 
-		const gchar *uid_field_name, const gchar *uid_before, guint uid);
-static void msim_postprocess_outgoing_cb(MsimSession *session, 
-		MsimMessage *userinfo, gpointer data);
-static gboolean msim_postprocess_outgoing(MsimSession *session, 
-		MsimMessage *msg, const gchar *username, const gchar *uid_field_name, 
-		const gchar *uid_before); 
-
-static gboolean msim_error(MsimSession *session, MsimMessage *msg);
-
-static void msim_check_inbox_cb(MsimSession *session, MsimMessage *userinfo, 
-		gpointer data);
-static gboolean msim_check_inbox(gpointer data);
-
-static void msim_input_cb(gpointer gc_uncasted, gint source, 
-		PurpleInputCondition cond);
-
-static guint msim_new_reply_callback(MsimSession *session, 
-		MSIM_USER_LOOKUP_CB cb, gpointer data);
-
-static void msim_connect_cb(gpointer data, gint source, 
-		const gchar *error_message);
-
-static gboolean msim_is_userid(const gchar *user);
-static gboolean msim_is_email(const gchar *user);
-
-static void msim_lookup_user(MsimSession *session, const gchar *user, 
-		MSIM_USER_LOOKUP_CB cb, gpointer data);
-
-double msim_round(double round);
-
-/* round is part of C99, but sometimes is unavailable before then.
- * Based on http://forums.belution.com/en/cpp/000/050/13.shtml
- */
-double msim_round(double value)
-{
-	if (value < 0) {
-		return -(floor(-value + 0.5));
-	} else {
-		return   floor( value + 0.5);
-	}
-}
-
-/** 
- * Load the plugin.
- */
-gboolean 
-msim_load(PurplePlugin *plugin)
-{
-	/* If compiled to use RC4 from libpurple, check if it is really there. */
-	if (!purple_ciphers_find_cipher("rc4")) {
-		purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n");
-		purple_notify_error(plugin, _("Missing Cipher"), 
-				_("The RC4 cipher could not be found"),
-				_("Upgrade "
-					"to a libpurple with RC4 support (>= 2.0.1). MySpaceIM "
-					"plugin will not be loaded."));
-		return FALSE;
-	}
-	return TRUE;
-}
-
-/**
- * Get possible user status types. Based on mockprpl.
- *
- * @return GList of status types.
- */
-GList *
-msim_status_types(PurpleAccount *acct)
-{
-	GList *types;
-	PurpleStatusType *status;
-
-	purple_debug_info("myspace", "returning status types\n");
-
-	types = NULL;
-
-    /* Statuses are almost all the same. Define a macro to reduce code repetition. */
-#define _MSIM_ADD_NEW_STATUS(prim) status =                         \
-	purple_status_type_new_with_attrs(                          \
-	prim,   /* PurpleStatusPrimitive */                         \
-	NULL,   /* id - use default */                              \
-	NULL,   /* name - use default */                            \
-	TRUE,   /* savable */                                       \
-	TRUE,   /* user_settable */                                 \
-	FALSE,  /* not independent */                               \
-	                                                            \
-	/* Attributes - each status can have a message. */          \
-	"message",                                                  \
-	_("Message"),                                               \
-	purple_value_new(PURPLE_TYPE_STRING),                       \
-	NULL);                                                      \
-	                                                            \
-	                                                            \
-	types = g_list_append(types, status)
-
-
-	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AVAILABLE);
-	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AWAY);
-	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_OFFLINE);
-	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_INVISIBLE);
-
-
-	return types;
-}
-
-/** Zap someone. Callback from msim_blist_node_menu zap menu. */
-static void
-msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr)
-{
-	PurpleBuddy *buddy;
-	PurpleConnection *gc;
-	MsimSession *session;
-	gchar *username, *zap_string, *zap_text;
-	guint zap;
-	const gchar *zap_gerund[10];
-
-	if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) {
-		/* Only know about buddies for now. */
-		return;
-	}
-
-	zap_gerund[0] = _("Zapping");
-	zap_gerund[1] = _("Whacking");
-	zap_gerund[2] = _("Torching");
-	zap_gerund[3] = _("Smooching");
-	zap_gerund[4] = _("Hugging");
-	zap_gerund[5] = _("Bslapping");
-	zap_gerund[6] = _("Goosing");
-	zap_gerund[7] = _("Hi-fiving");
-	zap_gerund[8] = _("Punking");
-	zap_gerund[9] = _("Raspberry'ing");
- 
-	g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node));
-
-	buddy = (PurpleBuddy *)node;
-	gc = purple_account_get_connection(buddy->account);
-	g_return_if_fail(gc != NULL);
-
-	session = (MsimSession *)gc->proto_data;
-	g_return_if_fail(session != NULL);
-
-	username = buddy->name;
-	g_return_if_fail(username != NULL);
-
-	zap = GPOINTER_TO_INT(zap_num_ptr);
-	zap_string = g_strdup_printf("!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", zap);
-	zap_text = g_strdup_printf("*** %s! ***", zap_gerund[zap]);
-
-	serv_got_im(session->gc, username, zap_text, 
-			PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_SYSTEM, time(NULL));
-
-	if (!msim_send_bm(session, username, zap_string, MSIM_BM_ACTION)) {
-		purple_debug_info("msim_send_zap", "msim_send_bm failed: zapping %s with %s",
-				username, zap_string);
-	}
-
-	g_free(zap_string);
-	g_free(zap_text);
-	return;
-}
-
-
-/** Return menu, if any, for a buddy list node. */
-GList *
-msim_blist_node_menu(PurpleBlistNode *node)
-{
-	GList *menu, *zap_menu;
-	PurpleMenuAction *act;
-	const gchar *zap_names[10];
-	guint i;
-
-	if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) {
-		/* Only know about buddies for now. */
-		return NULL;
-	}
-
-	/* Names from official client. */
-	zap_names[0] = _("zap");
-	zap_names[1] = _("whack");
-	zap_names[2] = _("torch");
-	zap_names[3] = _("smooch");
-	zap_names[4] = _("hug");
-	zap_names[5] = _("bslap");
-	zap_names[6] = _("goose");
-	zap_names[7] = _("hi-five");
-	zap_names[8] = _("punk'd");
-	zap_names[9] = _("raspberry");
- 
-	menu = zap_menu = NULL;
-
-	/* TODO: move to / command, or better yet new API  */
-	for (i = 0; i < sizeof(zap_names) / sizeof(zap_names[0]); ++i) {
-		act = purple_menu_action_new(zap_names[i], PURPLE_CALLBACK(msim_send_zap),
-				GUINT_TO_POINTER(i), NULL);
-		zap_menu = g_list_append(zap_menu, act);
-	}
-
-	act = purple_menu_action_new(_("Zap"), NULL, NULL, zap_menu);
-	menu = g_list_append(menu, act);
-
-	return menu;
-}
-
-/**
- * Return the icon name for a buddy and account.
- *
- * @param acct The account to find the icon for, or NULL for protocol icon.
- * @param buddy The buddy to find the icon for, or NULL for the account icon.
- *
- * @return The base icon name string.
- */
-const gchar *
-msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy)
-{
-	/* Use a MySpace icon submitted by hbons at
-	 * http://developer.pidgin.im/wiki/MySpaceIM. */
-	return "myspace";
-}
-
-/**
- * Replace 'old' with 'new' in 'str'.
- *
- * @param str The original string.
- * @param old The substring of 'str' to replace.
- * @param new The replacement for 'old' within 'str'.
- *
- * @return A _new_ string, based on 'str', with 'old' replaced
- *         by 'new'. Must be g_free()'d by caller.
- *
- * This string replace method is based on
- * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html
- *
- */
-gchar *
-str_replace(const gchar *str, const gchar *old, const gchar *new)
-{
-	gchar **items;
-	gchar *ret;
-
-	items = g_strsplit(str, old, -1);
-	ret = g_strjoinv(new, items);
-	g_free(items);
-	return ret;
-}
-
-#ifdef MSIM_DEBUG_MSG
-static void 
-print_hash_item(gpointer key, gpointer value, gpointer user_data)
-{
-	purple_debug_info("msim", "%s=%s\n",
-			key ? (gchar *)key : "(NULL)", 
-			value ? (gchar *)value : "(NULL)");
-}
-#endif
-
-/** 
- * Send raw data (given as a NUL-terminated string) to the server.
- *
- * @param session 
- * @param msg The raw data to send, in a NUL-terminated string.
- *
- * @return TRUE if succeeded, FALSE if not.
- *
- */
-gboolean 
-msim_send_raw(MsimSession *session, const gchar *msg)
-{
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-	
-	purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg);
-
-	return msim_send_really_raw(session->gc, msg, strlen(msg)) ==
-		strlen(msg);
-}
-
-/** Send raw data to the server, possibly with embedded NULs. 
- *
- * Used in prpl_info struct, so that plugins can have the most possible
- * control of what is sent over the connection. Inside this prpl, 
- * msim_send_raw() is used, since it sends NUL-terminated strings (easier).
- *
- * @param gc PurpleConnection
- * @param buf Buffer to send
- * @param total_bytes Size of buffer to send
- *
- * @return Bytes successfully sent, or -1 on error.
- */
-static int 
-msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes)
-{
-	int total_bytes_sent;
-	MsimSession *session;
-
-	g_return_val_if_fail(gc != NULL, -1);
-	g_return_val_if_fail(buf != NULL, -1);
-	g_return_val_if_fail(total_bytes >= 0, -1);
-
-	session = (MsimSession *)gc->proto_data;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
-	
-	/* Loop until all data is sent, or a failure occurs. */
-	total_bytes_sent = 0;
-	do {
-		int bytes_sent;
-
-		bytes_sent = send(session->fd, buf + total_bytes_sent, 
-				total_bytes - total_bytes_sent, 0);
-
-		if (bytes_sent < 0) {
-			purple_debug_info("msim", "msim_send_raw(%s): send() failed: %s\n",
-					buf, g_strerror(errno));
-			return total_bytes_sent;
-		}
-		total_bytes_sent += bytes_sent;
-
-	} while(total_bytes_sent < total_bytes);
-
-	return total_bytes_sent;
-}
-
-
-/** 
- * Start logging in to the MSIM servers.
- * 
- * @param acct Account information to use to login.
- */
-void 
-msim_login(PurpleAccount *acct)
-{
-	PurpleConnection *gc;
-	const gchar *host;
-	int port;
-
-	g_return_if_fail(acct != NULL);
-	g_return_if_fail(acct->username != NULL);
-
-	purple_debug_info("msim", "logging in %s\n", acct->username);
-
-	gc = purple_account_get_connection(acct);
-	gc->proto_data = msim_session_new(acct);
-	gc->flags |= PURPLE_CONNECTION_HTML | PURPLE_CONNECTION_NO_URLDESC;
-
-	/* Passwords are limited in length. */
-	if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) {
-		gchar *str;
-
-		str = g_strdup_printf(
-				_("Sorry, passwords over %d characters in length (yours is "
-				"%d) are not supported by MySpace."), 
-				MSIM_MAX_PASSWORD_LENGTH,
-				(int)strlen(acct->password));
-
-		/* Notify an error message also, because this is important! */
-		purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL);
-
-		purple_connection_error(gc, str);
-		
-		g_free(str);
-	}
-
-	/* 1. connect to server */
-	purple_connection_update_progress(gc, _("Connecting"),
-								  0,   /* which connection step this is */
-								  4);  /* total number of steps */
-
-	host = purple_account_get_string(acct, "server", MSIM_SERVER);
-	port = purple_account_get_int(acct, "port", MSIM_PORT);
-
-	/* From purple.sf.net/api:
-	 * """Note that this function name can be misleading--although it is called 
-	 * "proxy connect," it is used for establishing any outgoing TCP connection, 
-	 * whether through a proxy or not.""" */
-
-	/* Calls msim_connect_cb when connected. */
-	if (!purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc)) {
-		/* TODO: try other ports if in auto mode, then save
-		 * working port and try that first next time. */
-		purple_connection_error(gc, _("Couldn't create socket"));
-		return;
-	}
-}
-
-/**
- * Process a login challenge, sending a response. 
- *
- * @param session 
- * @param msg Login challenge message.
- *
- * @return TRUE if successful, FALSE if not
- */
-static gboolean 
-msim_login_challenge(MsimSession *session, MsimMessage *msg) 
-{
-	PurpleAccount *account;
-	const gchar *response;
-	guint response_len;
-	gchar *nc;
-	gsize nc_len;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE);
-
-	account = session->account;
-
-	g_return_val_if_fail(account != NULL, FALSE);
-
-	purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4);
-
-	purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len);
-
-	if (nc_len != MSIM_AUTH_CHALLENGE_LENGTH) {
-		purple_debug_info("msim", "bad nc length: %x != 0x%x\n", nc_len, MSIM_AUTH_CHALLENGE_LENGTH);
-		purple_connection_error(session->gc, _("Unexpected challenge length from server"));
-		return FALSE;
-	}
-
-	purple_connection_update_progress(session->gc, _("Logging in"), 2, 4);
-
-	response_len = 0;
-	response = msim_compute_login_response(nc, account->username, account->password, &response_len);
-
-	g_free(nc);
-
-	return msim_send(session, 
-			"login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM,
-			/* This is actually user's email address. */
-			"username", MSIM_TYPE_STRING, g_strdup(account->username),
-			/* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */
-			"response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len),
-			"clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION,
-			"langid", MSIM_TYPE_INTEGER, MSIM_LANGUAGE_ID_ENGLISH,
-			"imlang", MSIM_TYPE_STRING, g_strdup(MSIM_LANGUAGE_NAME_ENGLISH),
-			"reconn", MSIM_TYPE_INTEGER, 0,
-			"status", MSIM_TYPE_INTEGER, 100,
-			"id", MSIM_TYPE_INTEGER, 1,
-			NULL);
-}
-
-/**
- * Compute the base64'd login challenge response based on username, password, nonce, and IPs.
- *
- * @param nonce The base64 encoded nonce ('nc') field from the server.
- * @param email User's email address (used as login name).
- * @param password User's cleartext password.
- * @param response_len Will be written with response length.
- *
- * @return Binary login challenge response, ready to send to the server. 
- * Must be g_free()'d when finished. NULL if error.
- */
-static const gchar *
-msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], 
-		const gchar *email, const gchar *password, guint *response_len)
-{
-	PurpleCipherContext *key_context;
-	PurpleCipher *sha1;
-	PurpleCipherContext *rc4;
-
-	guchar hash_pw[HASH_SIZE];
-	guchar key[HASH_SIZE];
-	gchar *password_utf16le, *password_utf8_lc;
-	guchar *data;
-	guchar *data_out;
-	size_t data_len, data_out_len;
-	gsize conv_bytes_read, conv_bytes_written;
-	GError *conv_error;
-#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
-	int i;
-#endif
-
-	g_return_val_if_fail(nonce != NULL, NULL);
-	g_return_val_if_fail(email != NULL, NULL);
-	g_return_val_if_fail(password != NULL, NULL);
-	g_return_val_if_fail(response_len != NULL, NULL);
-
-	/* Convert password to lowercase (required for passwords containing
-	 * uppercase characters). MySpace passwords are lowercase,
-	 * see ticket #2066. */
-	password_utf8_lc = g_utf8_strdown(password, -1);
-
-	/* Convert ASCII password to UTF16 little endian */
-	purple_debug_info("msim", "converting password to UTF-16LE\n");
-	conv_error = NULL;
-	password_utf16le = g_convert(password_utf8_lc, -1, "UTF-16LE", "UTF-8", 
-			&conv_bytes_read, &conv_bytes_written, &conv_error);
-	g_free(password_utf8_lc);
-
-	g_return_val_if_fail(conv_bytes_read == strlen(password), NULL);
-
-	if (conv_error != NULL) {
-		purple_debug_error("msim", 
-				"g_convert password UTF8->UTF16LE failed: %s",
-				conv_error->message);
-		g_error_free(conv_error);
-		return NULL;
-	}
-
-	/* Compute password hash */ 
-	purple_cipher_digest_region("sha1", (guchar *)password_utf16le, 
-			conv_bytes_written, sizeof(hash_pw), hash_pw, NULL);
-	g_free(password_utf16le);
-
-#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
-	purple_debug_info("msim", "pwhash = ");
-	for (i = 0; i < sizeof(hash_pw); i++)
-		purple_debug_info("msim", "%.2x ", hash_pw[i]);
-	purple_debug_info("msim", "\n");
-#endif
-
-	/* key = sha1(sha1(pw) + nonce2) */
-	sha1 = purple_ciphers_find_cipher("sha1");
-	key_context = purple_cipher_context_new(sha1, NULL);
-	purple_cipher_context_append(key_context, hash_pw, HASH_SIZE);
-	purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE);
-	purple_cipher_context_digest(key_context, sizeof(key), key, NULL);
-
-#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
-	purple_debug_info("msim", "key = ");
-	for (i = 0; i < sizeof(key); i++) {
-		purple_debug_info("msim", "%.2x ", key[i]);
-	}
-	purple_debug_info("msim", "\n");
-#endif
-
-	rc4 = purple_cipher_context_new_by_name("rc4", NULL);
-
-	/* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), 
-	 * but only first 0x10 used for the RC4 key. */
-	purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10);
-	purple_cipher_context_set_key(rc4, key);
-
-	/* TODO: obtain IPs of network interfaces */
-
-	/* rc4 encrypt:
-	 * nonce1+email+IP list */
-
-	data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN;
-	data = g_new0(guchar, data_len);
-	memcpy(data, nonce, NONCE_SIZE);
-	memcpy(data + NONCE_SIZE, email, strlen(email));
-	memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN);
-
-	data_out = g_new0(guchar, data_len);
-
-	purple_cipher_context_encrypt(rc4, (const guchar *)data, 
-			data_len, data_out, &data_out_len);
-	purple_cipher_context_destroy(rc4);
-
-	g_assert(data_out_len == data_len);
-
-#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
-	purple_debug_info("msim", "response=<%s>\n", data_out);
-#endif
-
-	*response_len = data_out_len;
-
-	return (const gchar *)data_out;
-}
-
-/**
- * Schedule an IM to be sent once the user ID is looked up. 
- *
- * @param gc Connection.
- * @param who A user id, email, or username to send the message to.
- * @param message Instant message text to send.
- * @param flags Flags.
- *
- * @return 1 if successful or postponed, -1 if failed
- *
- * Allows sending to a user by username, email address, or userid. If
- * a username or email address is given, the userid must be looked up.
- * This function does that by calling msim_postprocess_outgoing().
- */
-int 
-msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, 
-		PurpleMessageFlags flags)
-{
-	MsimSession *session;
-	gchar *message_msim;
-	int rc;
-	
-	g_return_val_if_fail(gc != NULL, -1);
-	g_return_val_if_fail(who != NULL, -1);
-	g_return_val_if_fail(message != NULL, -1);
-
-	/* 'flags' has many options, not used here. */
-
-	session = (MsimSession *)gc->proto_data;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
-
-	message_msim = html_to_msim_markup(session, message);
-
-	if (msim_send_bm(session, who, message_msim, MSIM_BM_INSTANT)) {
-		/* Return 1 to have Purple show this IM as being sent, 0 to not. I always
-		 * return 1 even if the message could not be sent, since I don't know if
-		 * it has failed yet--because the IM is only sent after the userid is
-		 * retrieved from the server (which happens after this function returns).
-		 */
-		/* TODO: maybe if message is delayed, don't echo to conv window,
-		 * but do echo it to conv window manually once it is actually
-		 * sent? Would be complicated. */
-		rc = 1;
-	} else {
-		rc = -1;
-	}
-
-	g_free(message_msim);
-
-	/*
-	 * In MySpace, you login with your email address, but don't talk to other
-	 * users using their email address. So there is currently an asymmetry in the 
-	 * IM windows when using this plugin:
-	 *
-	 * you@example.com: hello
-	 * some_other_user: what's going on?
-	 * you@example.com: just coding a prpl
-	 *
-	 * TODO: Make the sent IM's appear as from the user's username, instead of
-	 * their email address. Purple uses the login (in MSIM, the email)--change this.
-	 */
-
-	return rc;
-}
-
-/** Send a buddy message of a given type.
- *
- * @param session
- * @param who Username to send message to.
- * @param text Message text to send. Not freed; will be copied.
- * @param type A MSIM_BM_* constant.
- *
- * @return TRUE if success, FALSE if fail.
- *
- * Buddy messages ('bm') include instant messages, action messages, status messages, etc.
- *
- */
-static gboolean 
-msim_send_bm(MsimSession *session, const gchar *who, const gchar *text, 
-		int type)
-{
-	gboolean rc;
-	MsimMessage *msg;
-	const gchar *from_username;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(who != NULL, FALSE);
-	g_return_val_if_fail(text != NULL, FALSE);
-   
-	from_username = session->account->username;
-
-	g_return_val_if_fail(from_username != NULL, FALSE);
-
-	purple_debug_info("msim", "sending %d message from %s to %s: %s\n",
-				  type, from_username, who, text);
-
-	msg = msim_msg_new(
-            "bm", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(type),
-			"sesskey", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(session->sesskey),
-			/* 't' will be inserted here */
-			"cv", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(MSIM_CLIENT_VERSION),
-			"msg", MSIM_TYPE_STRING, g_strdup(text),
-			NULL);
-
-	rc = msim_postprocess_outgoing(session, msg, who, "t", "cv");
-
-	msim_msg_free(msg);
-
-	return rc;
-}
-
-/* Indexes of this array + 1 map HTML font size to scale of normal font size. *
- * Based on _point_sizes from libpurple/gtkimhtml.c 
- *                                 1    2  3    4     5      6       7 */
-static gdouble _font_scale[] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736 };
-
-#define MAX_FONT_SIZE                   7       /* Purple maximum font size */
-#define POINTS_PER_INCH                 72      /* How many pt's in an inch */
-
-/** Convert typographical font point size to HTML font size. 
- * Based on libpurple/gtkimhtml.c */
-static guint
-msim_point_to_purple_size(MsimSession *session, guint point)
-{
-	guint size, this_point, base;
-	gdouble scale;
-	
-	base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
-   
-	for (size = 0; 
-			size < sizeof(_font_scale) / sizeof(_font_scale[0]);
-			++size) {
-		scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1];
-		this_point = (guint)msim_round(scale * base);
-
-		if (this_point >= point) {
-			purple_debug_info("msim", "msim_point_to_purple_size: %d pt -> size=%d\n",
-					point, size);
-			return size;
-		}
-	}
-
-	/* No HTML font size was this big; return largest possible. */
-	return this_point;
-}
-
-/** Convert HTML font size to point size. */
-static guint
-msim_purple_size_to_point(MsimSession *session, guint size)
-{
-	gdouble scale;
-	guint point;
-	guint base;
-
-	scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1];
-
-	base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
-
-	point = (guint)msim_round(scale * base);
-
-	purple_debug_info("msim", "msim_purple_size_to_point: size=%d -> %d pt\n",
-					size, point);
-
-	return point;
-}
-
-/** Convert a msim markup font pixel height to the more usual point size, for incoming messages. */
-static guint 
-msim_height_to_point(MsimSession *session, guint height)
-{
-	guint dpi;
-
-	dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI);
-
-	return (guint)msim_round((POINTS_PER_INCH * 1. / dpi) * height);
-
-	/* See also: libpurple/protocols/bonjour/jabber.c
-	 * _font_size_ichat_to_purple */
-}
-
-/** Convert point size to msim pixel height font size specification, for outgoing messages. */
-static guint
-msim_point_to_height(MsimSession *session, guint point)
-{
-	guint dpi;
-
-	dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI);
-
-	return (guint)msim_round((dpi * 1. / POINTS_PER_INCH) * point);
-}
-
-/** Convert the msim markup <f> (font) tag into HTML. */
-static void 
-msim_markup_f_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	const gchar *face, *height_str, *decor_str;
-	GString *gs_end, *gs_begin;
-	guint decor, height;
-
-	face = xmlnode_get_attrib(root, "f");
-	height_str = xmlnode_get_attrib(root, "h");
-	decor_str = xmlnode_get_attrib(root, "s");
-
-	if (height_str) {
-		height = atol(height_str);
-	} else {
-		height = 12;
-	}
-
-	if (decor_str) {
-		decor = atol(decor_str);
-	} else {
-		decor = 0;
-	}
-
-	gs_begin = g_string_new("");
-	/* TODO: get font size working */
-	if (height && !face) {
-		g_string_printf(gs_begin, "<font size='%d'>", 
-				msim_point_to_purple_size(session, msim_height_to_point(session, height)));
-	} else if (height && face) {
-		g_string_printf(gs_begin, "<font face='%s' size='%d'>", face,  
-				msim_point_to_purple_size(session, msim_height_to_point(session, height)));
-	} else {
-		g_string_printf(gs_begin, "<font>");
-	}
-
-	/* No support for font-size CSS? */
-	/* g_string_printf(gs_begin, "<span style='font-family: %s; font-size: %dpt'>", face, 
-			msim_height_to_point(height)); */
-
-	gs_end = g_string_new("</font>");
-
-	if (decor & MSIM_TEXT_BOLD) {
-		g_string_append(gs_begin, "<b>");
-		g_string_prepend(gs_end, "</b>");
-	}
-
-	if (decor & MSIM_TEXT_ITALIC) {
-		g_string_append(gs_begin, "<i>");
-		g_string_append(gs_end, "</i>");
-	}
-
-	if (decor & MSIM_TEXT_UNDERLINE) {
-		g_string_append(gs_begin, "<u>");
-		g_string_append(gs_end, "</u>");
-	}
-
-
-	*begin = gs_begin->str;
-	*end = gs_end->str;
-}
-
-/** Convert a msim markup color to a color suitable for libpurple.
-  *
-  * @param msim Either a color name, or an rgb(x,y,z) code.
-  *
-  * @return A new string, either a color name or #rrggbb code. Must g_free(). 
-  */
-static char *
-msim_color_to_purple(const char *msim)
-{
-	guint red, green, blue;
-
-	if (!msim) {
-		return g_strdup("black");
-	}
-
-	if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) {
-		/* Color name. */
-		return g_strdup(msim);
-	}
-	/* TODO: rgba (alpha). */
-
-	return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue);
-}
-
-/** Convert the msim markup <a> (anchor) tag into HTML. */
-static void 
-msim_markup_a_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	const gchar *href;
-
-	href = xmlnode_get_attrib(root, "h");
-	if (!href) {
-		href = "";
-	}
-
-	*begin = g_strdup_printf("<a href=\"%s\">%s", href, href);
-	*end = g_strdup("</a>");
-}
-
-/** Convert the msim markup <p> (paragraph) tag into HTML. */
-static void 
-msim_markup_p_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	/* Just pass through unchanged. 
-	 *
-	 * Note: attributes currently aren't passed, if there are any. */
-	*begin = g_strdup("<p>");
-	*end = g_strdup("</p>");
-}
-
-/** Convert the msim markup <c> tag (text color) into HTML. TODO: Test */
-static void 
-msim_markup_c_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	const gchar *color;
-	gchar *purple_color;
-
-	color = xmlnode_get_attrib(root, "v");
-	if (!color) {
-		purple_debug_info("msim", "msim_markup_c_to_html: <c> tag w/o v attr");
-		*begin = g_strdup("");
-		*end = g_strdup("");
-		/* TODO: log as unrecognized */
-		return;
-	}
-
-	purple_color = msim_color_to_purple(color);
-
-	*begin = g_strdup_printf("<font color='%s'>", purple_color); 
-
-	g_free(purple_color);
-
-	/* *begin = g_strdup_printf("<span style='color: %s'>", color); */
-	*end = g_strdup("</font>");
-}
-
-/** Convert the msim markup <b> tag (background color) into HTML. TODO: Test */
-static void 
-msim_markup_b_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	const gchar *color;
-	gchar *purple_color;
-
-	color = xmlnode_get_attrib(root, "v");
-	if (!color) {
-		*begin = g_strdup("");
-		*end = g_strdup("");
-		purple_debug_info("msim", "msim_markup_b_to_html: <b> w/o v attr");
-		/* TODO: log as unrecognized. */
-		return;
-	}
-
-	purple_color = msim_color_to_purple(color);
-
-	/* TODO: find out how to set background color. */
-	*begin = g_strdup_printf("<span style='background-color: %s'>", 
-			purple_color);
-	g_free(purple_color);
-
-	*end = g_strdup("</p>");
-}
-
-/** Convert the msim markup <i> tag (emoticon image) into HTML. */
-static void 
-msim_markup_i_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
-{
-	const gchar *name;
-	guint i;
-	struct MSIM_EMOTICON *emote;
-
-	name = xmlnode_get_attrib(root, "n");
-	if (!name) {
-		purple_debug_info("msim", "msim_markup_i_to_html: <i> w/o n");
-		*begin = g_strdup("");
-		*end = g_strdup("");
-		/* TODO: log as unrecognized */
-		return;
-	}
-
-	/* Find and use canonical form of smiley symbol. */
-	for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) {
-		if (!strcmp(name, emote->name)) {
-			*begin = g_strdup(emote->symbol);
-			*end = g_strdup("");
-			return;
-		}
-	}
-
-	/* Couldn't find it, sorry. Try to degrade gracefully. */
-	*begin = g_strdup_printf("**%s**", name);
-	*end = g_strdup("");
-}
-
-/** Convert an individual msim markup tag to HTML. */
-static void 
-msim_markup_tag_to_html(MsimSession *session, xmlnode *root, gchar **begin, 
-		gchar **end)
-{
-	if (!strcmp(root->name, "f")) {
-		msim_markup_f_to_html(session, root, begin, end);
-	} else if (!strcmp(root->name, "a")) {
-		msim_markup_a_to_html(session, root, begin, end);
-	} else if (!strcmp(root->name, "p")) {
-		msim_markup_p_to_html(session, root, begin, end);
-	} else if (!strcmp(root->name, "c")) {
-		msim_markup_c_to_html(session, root, begin, end);
-	} else if (!strcmp(root->name, "b")) {
-		msim_markup_b_to_html(session, root, begin, end);
-	} else if (!strcmp(root->name, "i")) {
-		msim_markup_i_to_html(session, root, begin, end);
-	} else {
-		purple_debug_info("msim", "msim_markup_tag_to_html: "
-				"unknown tag name=%s, ignoring", 
-				(root && root->name) ? root->name : "(NULL)");
-		*begin = g_strdup("");
-		*end = g_strdup("");
-	}
-}
-
-/** Convert an individual HTML tag to msim markup. */
-static void 
-html_tag_to_msim_markup(MsimSession *session, xmlnode *root, gchar **begin, 
-		gchar **end)
-{
-	/* TODO: Coalesce nested tags into one <f> tag!
-	 * Currently, the 's' value will be overwritten when b/i/u is nested
-	 * within another one, and only the inner-most formatting will be 
-	 * applied to the text. */
-	if (!strcmp(root->name, "root")) {
-		*begin = g_strdup("");
-		*end = g_strdup("");
-	} else if (!strcmp(root->name, "b") || !strcmp(root->name, "B")) {
-		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_BOLD);
-		*end = g_strdup("</f>");
-	} else if (!strcmp(root->name, "i") || !strcmp(root->name, "I")) {
-		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_ITALIC);
-		*end = g_strdup("</f>");
-	} else if (!strcmp(root->name, "u") || !strcmp(root->name, "U")) {
-		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_UNDERLINE);
-		*end = g_strdup("</f>");
-	} else if (!strcmp(root->name, "a") || !strcmp(root->name, "A")) {
-		const gchar *href, *link_text;
-
-		href = xmlnode_get_attrib(root, "href");
-
-		if (!href) {
-			href = xmlnode_get_attrib(root, "HREF");
-		}
-
-		link_text = xmlnode_get_data(root);
-
-		if (href) {
-			if (!strcmp(link_text, href)) {
-				/* Purple gives us: <a href="URL">URL</a>
-				 * Translate to <a h='URL' />
-				 * Displayed as text of URL with link to URL
-				 */
-				*begin = g_strdup_printf("<a h='%s' />", href);
-			} else {
-				/* But if we get: <a href="URL">text</a>
-				 * Translate to: text: <a h='URL' />
-				 *
-				 * Because official client only supports self-closed <a>
-				 * tags; you can't change the link text.
-				 */
-				*begin = g_strdup_printf("%s: <a h='%s' />", link_text, href);
-			}
-		} else {
-			*begin = g_strdup("<a />");
-		}
-
-		/* Sorry, kid. MySpace doesn't support you within <a> tags. */
-		xmlnode_free(root->child);
-		root->child = NULL;
-
-		*end = g_strdup("");
-	} else if (!strcmp(root->name, "font") || !strcmp(root->name, "FONT")) {
-		const gchar *size;
-		const gchar *face;
-
-		size = xmlnode_get_attrib(root, "size");
-		face = xmlnode_get_attrib(root, "face");
-
-		if (face && size) {
-			*begin = g_strdup_printf("<f f='%s' h='%d'>", face, 
-					msim_point_to_height(session,
-						msim_purple_size_to_point(session, atoi(size))));
-		} else if (face) {
-			*begin = g_strdup_printf("<f f='%s'>", face);
-		} else if (size) {
-			*begin = g_strdup_printf("<f h='%d'>", 
-					 msim_point_to_height(session,
-						 msim_purple_size_to_point(session, atoi(size))));
-		} else {
-			*begin = g_strdup("<f>");
-		}
-
-		*end = g_strdup("</f>");
-
-		/* TODO: color (bg uses <body>), emoticons */
-	} else {
-		*begin = g_strdup_printf("[%s]", root->name);
-		*end = g_strdup_printf("[/%s]", root->name);
-	}
-}
-
-/** Convert an xmlnode of msim markup or HTML to an HTML string or msim markup.
- *
- * @param f Function to convert tags.
- *
- * @return An HTML string. Caller frees.
- */
-static gchar *
-msim_convert_xmlnode(MsimSession *session, xmlnode *root, MSIM_XMLNODE_CONVERT f)
-{
-	xmlnode *node;
-	gchar *begin, *inner, *end;
-	GString *final;
-
-	if (!root || !root->name) {
-		return g_strdup("");
-	}
-
-	purple_debug_info("msim", "msim_convert_xmlnode: got root=%s\n",
-			root->name);
-
-	begin = inner = end = NULL;
-
-	final = g_string_new("");
-
-	f(session, root, &begin, &end);
-	
-	g_string_append(final, begin);
-
-	/* Loop over all child nodes. */
-	for (node = root->child; node != NULL; node = node->next) {
-		switch (node->type) {
-		case XMLNODE_TYPE_ATTRIB:
-			/* Attributes handled above. */
-			break;
-
-		case XMLNODE_TYPE_TAG:
-			/* A tag or tag with attributes. Recursively descend. */
-			inner = msim_convert_xmlnode(session, node, f);
-			g_return_val_if_fail(inner != NULL, NULL);
-
-			purple_debug_info("msim", " ** node name=%s\n", 
-					(node && node->name) ? node->name : "(NULL)");
-			break;
-	
-		case XMLNODE_TYPE_DATA:
-			/* Literal text. */
-			inner = g_new0(char, node->data_sz + 1);
-			strncpy(inner, node->data, node->data_sz);
-			inner[node->data_sz] = 0;
-
-			purple_debug_info("msim", " ** node data=%s\n", 
-					inner ? inner : "(NULL)");
-			break;
-			
-		default:
-			purple_debug_info("msim",
-					"msim_convert_xmlnode: strange node\n");
-			inner = g_strdup("");
-		}
-
-		if (inner) {
-			g_string_append(final, inner);
-		}
-	}
-
-	/* TODO: Note that msim counts each piece of text enclosed by <f> as
-	 * a paragraph and will display each on its own line. You actually have
-	 * to _nest_ <f> tags to intersperse different text in one paragraph!
-	 * Comment out this line below to see. */
-	g_string_append(final, end);
-
-	purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n",
-			(final && final->str) ? final->str : "(NULL)");
-
-	return final->str;
-}
-
-/** Convert XML to something based on MSIM_XMLNODE_CONVERT. */
-static gchar *
-msim_convert_xml(MsimSession *session, const gchar *raw, MSIM_XMLNODE_CONVERT f)
-{
-	xmlnode *root;
-	gchar *str;
-	gchar *enclosed_raw;
-
-	g_return_val_if_fail(raw != NULL, NULL);
-
-	/* Enclose text in one root tag, to try to make it valid XML for parsing. */
-	enclosed_raw = g_strconcat("<root>", raw, "</root>", NULL);
-
-	root = xmlnode_from_str(enclosed_raw, -1);
-
-	if (!root) {
-		purple_debug_info("msim", "msim_markup_to_html: couldn't parse "
-				"%s as XML, returning raw: %s\n", enclosed_raw, raw);
-		/* TODO: msim_unrecognized */
-		g_free(enclosed_raw);
-		return g_strdup(raw);
-	}
-
-	g_free(enclosed_raw);
-
-	str = msim_convert_xmlnode(session, root, f);
-	g_return_val_if_fail(str != NULL, NULL);
-	purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str);
-
-	xmlnode_free(root);
-
-	return str;
-}
-
-/** Convert plaintext smileys to <i> markup tags.
- *
- * @param before Original text with ASCII smileys. Will be freed.
- * @return A new string with <i> tags, if applicable. Must be g_free()'d.
- */
-static gchar *
-msim_convert_smileys_to_markup(gchar *before)
-{
-	gchar *old, *new, *replacement;
-	guint i;
-	struct MSIM_EMOTICON *emote;
-
-	old = before;
-	new = NULL;
-
-	for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) {
-		gchar *name, *symbol;
-
-		name = emote->name;
-		symbol = emote->symbol;
-
-		replacement = g_strdup_printf("<i n=\"%s\"/>", name);
-
-		purple_debug_info("msim", "msim_convert_smileys_to_markup: %s->%s\n",
-				symbol ? symbol : "(NULL)", 
-				replacement ? replacement : "(NULL)");
-		new = str_replace(old, symbol, replacement);
-		
-		g_free(replacement);
-		g_free(old);
-
-		old = new;
-	}
-
-	return new;
-}
-	
-
-/** High-level function to convert MySpaceIM markup to Purple (HTML) markup. 
- *
- * @return Purple markup string, must be g_free()'d. */
-static gchar *
-msim_markup_to_html(MsimSession *session, const gchar *raw)
-{
-	return msim_convert_xml(session, raw, 
-			(MSIM_XMLNODE_CONVERT)(msim_markup_tag_to_html));
-}
-
-/** High-level function to convert Purple (HTML) to MySpaceIM markup.
- *
- * @return HTML markup string, must be g_free()'d. */
-static gchar *
-html_to_msim_markup(MsimSession *session, const gchar *raw)
-{
-	gchar *markup;
-
-	markup = msim_convert_xml(session, raw,
-			(MSIM_XMLNODE_CONVERT)(html_tag_to_msim_markup));
-	
-	if (purple_account_get_bool(session->account, "emoticons", TRUE)) {
-		/* Frees markup and allocates a new one. */
-		markup = msim_convert_smileys_to_markup(markup);
-	}
-
-	return markup;
-}
-
-/** Get the MsimUser from a PurpleBuddy, creating it if needed. */
-static MsimUser *
-msim_get_user_from_buddy(PurpleBuddy *buddy)
-{
-	MsimUser *user;
-
-	if (!buddy) {
-		return NULL;
-	}
-
-	if (!buddy->proto_data) {
-		/* TODO: where is this freed? */
-		user = g_new0(MsimUser, 1);
-		user->buddy = buddy;
-		buddy->proto_data = (gpointer)user;
-		purple_debug_info("msim_get_user_from_buddy",
-				"creating new user for %s to %X\n",
-				buddy->name, buddy->proto_data);
-	} 
-
-	user = (MsimUser *)(buddy->proto_data);
-
-	return user;
-}
-
-/** Find and return an MsimUser * representing a user on the buddy list, or NULL. */
-static MsimUser *
-msim_find_user(MsimSession *session, const gchar *username)
-{
-	PurpleBuddy *buddy;
-	MsimUser *user;
-
-	buddy = purple_find_buddy(session->account, username);
-	if (!buddy) {
-		return NULL;
-	}
-
-	user = msim_get_user_from_buddy(buddy);
-
-	return user;
-}
-
-
-/** Record the client version in the buddy list, from an incoming message. */
-static gboolean
-msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg)
-{
-	gchar *username, *cv;
-	gboolean ret;
-	MsimUser *user;
-
-	username = msim_msg_get_string(msg, "_username");
-	cv = msim_msg_get_string(msg, "cv");
-
-	g_return_val_if_fail(username != NULL, FALSE);
-	if (!cv) {
-		/* No client version to record, don't worry about it. */
-		return FALSE;
-	}
-
-	user = msim_find_user(session, username);
-
-	if (user) {
-		user->client_cv = atol(cv);
-		ret = TRUE;
-	} else {
-		ret = FALSE;
-	}
-
-	g_free(username);
-	g_free(cv);
-
-	return ret;
-}
-
-/** Handle an incoming buddy message. */
-static gboolean
-msim_incoming_bm(MsimSession *session, MsimMessage *msg)
-{
-	guint bm;
-   
-	bm = msim_msg_get_integer(msg, "bm");
-
-	msim_incoming_bm_record_cv(session, msg);
-
-	switch (bm) {
-		case MSIM_BM_STATUS:
-			return msim_incoming_status(session, msg);
-		case MSIM_BM_INSTANT:
-			return msim_incoming_im(session, msg);
-		case MSIM_BM_ACTION:
-			return msim_incoming_action(session, msg);
-		case MSIM_BM_MEDIA:
-			return msim_incoming_media(session, msg);
-		case MSIM_BM_UNOFFICIAL_CLIENT:
-			return msim_incoming_unofficial_client(session, msg);
-		default:
-			/* Not really an IM, but show it for informational 
-			 * purposes during development. */
-			return msim_incoming_im(session, msg);
-	}
-}
-
-/**
- * Handle an incoming instant message.
- *
- * @param session The session
- * @param msg Message from the server, containing 'f' (userid from) and 'msg'. 
- *               Should also contain username in _username from preprocessing.
- *
- * @return TRUE if successful.
- */
-static gboolean 
-msim_incoming_im(MsimSession *session, MsimMessage *msg)
-{
-	gchar *username, *msg_msim_markup, *msg_purple_markup;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	username = msim_msg_get_string(msg, "_username");
-	g_return_val_if_fail(username != NULL, FALSE);
-
-	msg_msim_markup = msim_msg_get_string(msg, "msg");
-	g_return_val_if_fail(msg_msim_markup != NULL, FALSE);
-
-	msg_purple_markup = msim_markup_to_html(session, msg_msim_markup);
-	g_free(msg_msim_markup);
-
-	serv_got_im(session->gc, username, msg_purple_markup, 
-			PURPLE_MESSAGE_RECV, time(NULL));
-
-	g_free(username);
-	g_free(msg_purple_markup);
-
-	return TRUE;
-}
-
-/**
- * Process unrecognized information.
- *
- * @param session
- * @param msg An MsimMessage that was unrecognized, or NULL.
- * @param note Information on what was unrecognized, or NULL.
- */
-static void 
-msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note)
-{
-	/* TODO: Some more context, outwardly equivalent to a backtrace, 
-	 * for helping figure out what this msg is for. What was going on?
-	 * But not too much information so that a user
-	 * posting this dump reveals confidential information.
-	 */
-
-	/* TODO: dump unknown msgs to file, so user can send them to me
-	 * if they wish, to help add support for new messages (inspired
-	 * by Alexandr Shutko, who maintains OSCAR protocol documentation). */
-
-	purple_debug_info("msim", "Unrecognized data on account for %s\n", 
-			session->account->username ? session->account->username
-			: "(NULL)");
-	if (note) {
-		purple_debug_info("msim", "(Note: %s)\n", note);
-	}
-
-	if (msg) {
-		msim_msg_dump("Unrecognized message dump: %s\n", msg);
-	}
-}
-
-/** Process an incoming zap. */
-static gboolean
-msim_incoming_zap(MsimSession *session, MsimMessage *msg)
-{
-	gchar *msg_text, *username, *zap_text;
-	gint zap;
-	const gchar *zap_past_tense[10];
-
-	zap_past_tense[0] = _("zapped");
-	zap_past_tense[1] = _("whacked");
-	zap_past_tense[2] = _("torched");
-	zap_past_tense[3] = _("smooched");
-	zap_past_tense[4] = _("hugged");
-	zap_past_tense[5] = _("bslapped");
-	zap_past_tense[6] = _("goosed");
-	zap_past_tense[7] = _("hi-fived");
-	zap_past_tense[8] = _("punk'd");
-	zap_past_tense[9] = _("raspberried");
-
-	msg_text = msim_msg_get_string(msg, "msg");
-	username = msim_msg_get_string(msg, "_username");
-
-	g_return_val_if_fail(msg_text != NULL, FALSE);
-	g_return_val_if_fail(username != NULL, FALSE);
-
-	g_return_val_if_fail(sscanf(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", &zap) == 1, FALSE);
-
-	zap = CLAMP(zap, 0, sizeof(zap_past_tense) / sizeof(zap_past_tense[0]));
-
-	zap_text = g_strdup_printf(_("*** You have been %s! ***"), zap_past_tense[zap]);
-
-	serv_got_im(session->gc, username, zap_text, 
-			PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_SYSTEM, time(NULL));
-
-	g_free(zap_text);
-	g_free(msg_text);
-	g_free(username);
-
-	return TRUE;
-}
-
-/**
- * Handle an incoming action message.
- *
- * @param session
- * @param msg
- *
- * @return TRUE if successful.
- *
- */
-static gboolean 
-msim_incoming_action(MsimSession *session, MsimMessage *msg)
-{
-	gchar *msg_text, *username;
-	gboolean rc;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	msg_text = msim_msg_get_string(msg, "msg");
-	g_return_val_if_fail(msg_text != NULL, FALSE);
-
-	username = msim_msg_get_string(msg, "_username");
-	g_return_val_if_fail(username != NULL, FALSE);
-
-	purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", 
-			msg_text, username);
-
-	if (strcmp(msg_text, "%typing%") == 0) {
-		/* TODO: find out if msim repeatedly sends typing messages, so we can 
-		 * give it a timeout. Right now, there does seem to be an inordinately 
-		 * amount of time between typing stopped-typing notifications. */
-		serv_got_typing(session->gc, username, 0, PURPLE_TYPING);
-		rc = TRUE;
-	} else if (strcmp(msg_text, "%stoptyping%") == 0) {
-		serv_got_typing_stopped(session->gc, username);
-		rc = TRUE;
-	} else if (strstr(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_")) {
-		rc = msim_incoming_zap(session, msg);
-	} else {
-		msim_unrecognized(session, msg, 
-				"got to msim_incoming_action but unrecognized value for 'msg'");
-		rc = FALSE;
-	}
-
-	g_free(msg_text);
-	g_free(username);
-
-	return rc;
-}
-
-/* Process an incoming media (buddy icon) message. */
-static gboolean
-msim_incoming_media(MsimSession *session, MsimMessage *msg)
-{
-	gchar *username, *text;
-
-	username = msim_msg_get_string(msg, "_username");
-	text = msim_msg_get_string(msg, "msg");
-
-	g_return_val_if_fail(username != NULL, FALSE);
-	g_return_val_if_fail(text != NULL, FALSE);
-
-	purple_debug_info("msim", "msim_incoming_media: from %s, got msg=%s\n", username, text);
-
-	/* Media messages are sent when the user opens a window to someone.
-	 * Tell libpurple they started typing and stopped typing, to inform the Psychic
-	 * Mode plugin so it too can open a window to the user. */
-	serv_got_typing(session->gc, username, 0, PURPLE_TYPING);
-	serv_got_typing_stopped(session->gc, username);
-
-	g_free(username);
-
-	return TRUE;
-}
-
-/* Process an incoming "unofficial client" message. The plugin for
- * Miranda IM sends this message with the plugin information. */
-static gboolean
-msim_incoming_unofficial_client(MsimSession *session, MsimMessage *msg)
-{
-	MsimUser *user;
-	gchar *username, *client_info;
-
-	username = msim_msg_get_string(msg, "_username");
-	client_info = msim_msg_get_string(msg, "msg");
-
-	g_return_val_if_fail(username != NULL, FALSE);
-	g_return_val_if_fail(client_info != NULL, FALSE);
-
-	purple_debug_info("msim", "msim_incoming_unofficial_client: %s is using client %s\n",
-		username, client_info);
-
-	user = msim_find_user(session, username);
-	
-	g_return_val_if_fail(user != NULL, FALSE);
-
-	if (user->client_info) {
-		g_free(user->client_info);
-	}
-	user->client_info = client_info;
-
-	g_free(username);
-	/* Do not free client_info - the MsimUser now owns it. */
-
-	return TRUE;
-}
-
-
-#ifdef MSIM_SEND_CLIENT_VERSION
-/** Send our client version to another unofficial client that understands it. */
-static gboolean
-msim_send_unofficial_client(MsimSession *session, gchar *username)
-{
-	gchar *our_info;
-	gboolean ret;
-
-	our_info = g_strdup_printf("Libpurple %d.%d.%d - msimprpl %s", 
-			PURPLE_MAJOR_VERSION,
-			PURPLE_MINOR_VERSION,
-			PURPLE_MICRO_VERSION,
-			MSIM_PRPL_VERSION_STRING);
-
-	ret = msim_send_bm(session, username, our_info, MSIM_BM_UNOFFICIAL_CLIENT);
-
-	return ret;
-}
-#endif
-
-/** 
- * Handle when our user starts or stops typing to another user.
- *
- * @param gc
- * @param name The buddy name to which our user is typing to
- * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING
- *
- * @return 0
- */
-unsigned int 
-msim_send_typing(PurpleConnection *gc, const gchar *name, 
-		PurpleTypingState state)
-{
-	const gchar *typing_str;
-	MsimSession *session;
-
-	g_return_val_if_fail(gc != NULL, 0);
-	g_return_val_if_fail(name != NULL, 0);
-
-	session = (MsimSession *)gc->proto_data;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
-
-	switch (state) {
-		case PURPLE_TYPING: 
-			typing_str = "%typing%"; 
-			break;
-
-		case PURPLE_TYPED:
-		case PURPLE_NOT_TYPING:
-		default:
-			typing_str = "%stoptyping%";
-			break;
-	}
-
-	purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str);
-	msim_send_bm(session, name, typing_str, MSIM_BM_ACTION);
-	return 0;
-}
-
-/** Format the "now playing" indicator, showing the artist and song.
- * @return Return a new string (must be g_free()'d), or NULL.
- */
-static gchar *
-msim_format_now_playing(gchar *band, gchar *song)
-{
-	if ((band && strlen(band)) || (song && strlen(song))) {
-		return g_strdup_printf("%s - %s",
-			(band && strlen(band)) ? band : "Unknown Artist",
-			(song && strlen(song)) ? song : "Unknown Song");
-	} else {
-		return NULL;
-	}
-}
-
-/** Append user information to a PurpleNotifyUserInfo, given an MsimUser. 
- * Used by msim_tooltip_text() and msim_get_info_cb() to show a user's profile.
- */
-static void
-msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full)
-{
-	gchar *str;
-	guint uid;
-	guint cv;
-
-	/* Useful to identify the account the tooltip refers to. 
-	 *  Other prpls show this. */
-	if (user->username) {
-		purple_notify_user_info_add_pair(user_info, _("User"), user->username);
-	}
-
-	uid = purple_blist_node_get_int(&user->buddy->node, "UserID");
-
-	if (full) {
-		purple_notify_user_info_add_pair(user_info, _("User ID"), g_strdup_printf("%d", uid));
-	}
-
-
-	/* a/s/l...the vitals */
-	if (user->age) {
-		purple_notify_user_info_add_pair(user_info, _("Age"),
-				g_strdup_printf("%d", user->age));
-	}
-
-	if (user->gender && strlen(user->gender)) {
-		purple_notify_user_info_add_pair(user_info, _("Gender"), user->gender);
-	}
-
-	if (user->location && strlen(user->location)) {
-		purple_notify_user_info_add_pair(user_info, _("Location"), user->location);
-	}
-
-	/* Other information */
-	if (user->headline && strlen(user->headline)) {
-		purple_notify_user_info_add_pair(user_info, _("Headline"), user->headline);
-	}
-
-	str = msim_format_now_playing(user->band_name, user->song_name);
-	if (str && strlen(str)) {
-		purple_notify_user_info_add_pair(user_info, _("Song"), str);
-	}
-
-	/* Note: total friends only available if looked up by uid, not username. */
-	if (user->total_friends) {
-		purple_notify_user_info_add_pair(user_info, _("Total Friends"),
-			g_strdup_printf("%d", user->total_friends));
-	}
-
-	if (full) {
-		/* Client information */
-
-		str = user->client_info;
-		cv = user->client_cv;
-
-		if (str && cv != 0) {
-			purple_notify_user_info_add_pair(user_info, _("Client Version"),
-					g_strdup_printf("%s (build %d)", str, cv));
-		} else if (str) {
-			purple_notify_user_info_add_pair(user_info, _("Client Version"),
-					g_strdup(str));
-		} else if (cv) {
-			purple_notify_user_info_add_pair(user_info, _("Client Version"),
-					g_strdup_printf("Build %d", cv));
-		}
-	}
-}
-
-/** Callback for msim_get_info(), for when user info is received. */
-static void 
-msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, 
-		gpointer data)
-{
-	MsimMessage *msg;
-	gchar *username;
-	PurpleNotifyUserInfo *user_info;
-	MsimUser *user;
-	gboolean temporary_user;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-
-	/* Get user{name,id} from msim_get_info, passed as an MsimMessage for 
-	   orthogonality. */
-	msg = (MsimMessage *)data;
-	g_return_if_fail(msg != NULL);
-
-	username = msim_msg_get_string(msg, "user");
-	if (!username) {
-		purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg");
-		return;
-	}
-
-	msim_msg_free(msg);
-	purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", username);
-
-	user = msim_find_user(session, username);
-
-	if (!user) {
-		/* User isn't on blist, create a temporary user to store info. */
-		temporary_user = TRUE;
-		user = g_new0(MsimUser, 1);
-	} else {
-		temporary_user = FALSE;
-	}
-
-	/* Update user structure with new information */
-	msim_store_user_info(session, user_info_msg, user);
-
-	user_info = purple_notify_user_info_new();
-
-	/* Append data from MsimUser to PurpleNotifyUserInfo for display, full */
-	msim_append_user_info(session, user_info, user, TRUE);
-
-	purple_notify_userinfo(session->gc, username, user_info, NULL, NULL);
-	purple_debug_info("msim", "msim_get_info_cb: username=%s\n", username);
-
-	purple_notify_user_info_destroy(user_info);
-	/* TODO: do not free username, since it will be used by user_info? */
-
-	if (temporary_user) {
-		g_free(user->client_info);
-		g_free(user->gender);
-		g_free(user->location);
-		g_free(user->headline);
-		g_free(user->display_name);
-		g_free(user->username);
-		g_free(user->band_name);
-		g_free(user->song_name);
-		g_free(user->image_url);
-		g_free(user);
-	}
-
-}
-
-/** Retrieve a user's profile. 
- * @param username Username, user ID, or email address to lookup.
- */
-void 
-msim_get_info(PurpleConnection *gc, const gchar *username)
-{
-	MsimSession *session;
-	MsimUser *user;
-	guint uid;
-	gchar *user_to_lookup;
-	MsimMessage *user_msg;
-
-	g_return_if_fail(gc != NULL);
-	g_return_if_fail(username != NULL);
-
-	session = (MsimSession *)gc->proto_data;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-
-	/* Obtain uid of buddy. */
-	user = msim_find_user(session, username);
-
-	/* If is on buddy list, lookup by uid since it is faster. */
-	if (user && (uid = purple_blist_node_get_int(&user->buddy->node, "UserID"))) {
-		user_to_lookup = g_strdup_printf("%d", uid);
-	} else {
-		/* Looking up buddy not on blist. Lookup by whatever user entered. */
-		user_to_lookup = g_strdup(username);
-	}
-
-	/* Pass the username to msim_get_info_cb(), because since we lookup
-	 * by userid, the userinfo message will only contain the uid (not 
-	 * the username) but it would be useful to display the username too.
-	 */
-	user_msg = msim_msg_new(
-			"user", MSIM_TYPE_STRING, g_strdup(username),
-			NULL);
-	purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", username);
-
-	msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg);
-
-	g_free(user_to_lookup); 
-}
-
-/** Set your status - callback for when user manually sets it.  */
-void
-msim_set_status(PurpleAccount *account, PurpleStatus *status)
-{
-	PurpleStatusType *type;
-	MsimSession *session;
-	guint status_code;
-	const gchar *statstring;
-
-	session = (MsimSession *)account->gc->proto_data;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-
-	type = purple_status_get_type(status);
-
-	switch (purple_status_type_get_primitive(type)) {
-		case PURPLE_STATUS_AVAILABLE:
-			purple_debug_info("msim", "msim_set_status: available (%d->%d)\n", PURPLE_STATUS_AVAILABLE,
-					MSIM_STATUS_CODE_ONLINE);
-			status_code = MSIM_STATUS_CODE_ONLINE;
-			break;
-
-		case PURPLE_STATUS_INVISIBLE:
-			purple_debug_info("msim", "msim_set_status: invisible (%d->%d)\n", PURPLE_STATUS_INVISIBLE,
-					MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN);
-			status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN;
-			break;
-
-		case PURPLE_STATUS_AWAY:
-			purple_debug_info("msim", "msim_set_status: away (%d->%d)\n", PURPLE_STATUS_AWAY,
-					MSIM_STATUS_CODE_AWAY);
-			status_code = MSIM_STATUS_CODE_AWAY;
-			break;
-
-		default:
-			purple_debug_info("msim", "msim_set_status: unknown "
-					"status interpreting as online");
-			status_code = MSIM_STATUS_CODE_ONLINE;
-			break;
-	}
-
-	statstring = purple_status_get_attr_string(status, "message");
-
-	if (!statstring) {
-		statstring = g_strdup("");
-	}
-
-	msim_set_status_code(session, status_code, g_strdup(statstring));
-}
-
-/** Go idle. */
-void
-msim_set_idle(PurpleConnection *gc, int time)
-{
-	MsimSession *session;
-
-	g_return_if_fail(gc != NULL);
-
-	session = (MsimSession *)gc->proto_data;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-
-	if (time == 0) {
-		/* Going back from idle. In msim, idle is mutually exclusive 
-		 * from the other states (you can only be away or idle, but not
-		 * both, for example), so by going non-idle I go online.
-		 */
-		/* TODO: find out how to keep old status string? */
-		msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE, g_strdup(""));
-	} else {
-		/* msim doesn't support idle time, so just go idle */
-		msim_set_status_code(session, MSIM_STATUS_CODE_IDLE, g_strdup(""));
-	}
-}
-
-/** Set status using an MSIM_STATUS_CODE_* value.
- * @param status_code An MSIM_STATUS_CODE_* value.
- * @param statstring Status string, must be a dynamic string (will be freed by msim_send).
- */
-static void 
-msim_set_status_code(MsimSession *session, guint status_code, gchar *statstring)
-{
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-	g_return_if_fail(statstring != NULL);
-
-	purple_debug_info("msim", "msim_set_status_code: going to set status to code=%d,str=%s\n",
-			status_code, statstring);
-
-	if (!msim_send(session,
-			"status", MSIM_TYPE_INTEGER, status_code,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"statstring", MSIM_TYPE_STRING, statstring, 
-			"locstring", MSIM_TYPE_STRING, g_strdup(""),
-			NULL))
-	{
-		purple_debug_info("msim", "msim_set_status: failed to set status");
-	}
-
-}
-
-/** After a uid is resolved to username, tag it with the username and submit for processing. 
- * 
- * @param session
- * @param userinfo Response messsage to resolving request.
- * @param data MsimMessage *, the message to attach information to. 
- */
-static void 
-msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, 
-		gpointer data)
-{
-	gchar *body_str;
-	GHashTable *body;
-	gchar *username;
-	MsimMessage *msg;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-	g_return_if_fail(userinfo != NULL);
-
-	body_str = msim_msg_get_string(userinfo, "body");
-	g_return_if_fail(body_str != NULL);
-	body = msim_parse_body(body_str);
-	g_return_if_fail(body != NULL);
-	g_free(body_str);
-
-	username = g_hash_table_lookup(body, "UserName");
-	g_return_if_fail(username != NULL);
-
-
-	msg = (MsimMessage *)data;
-	g_return_if_fail(msg != NULL);
-
-	/* TODO: more elegant solution than below. attach whole message? */
-	/* Special elements name beginning with '_', we'll use internally within the
-	 * program (did not come directly from the wire). */
-	msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
-  
-	/* TODO: attach more useful information, like ImageURL */
-
-	msim_process(session, msg);
-
-	/* TODO: Free copy cloned from  msim_preprocess_incoming(). */
-	//XXX msim_msg_free(msg);
-	g_hash_table_destroy(body);
-}
-
-/* Lookup a username by userid, from buddy list. 
- *
- * @param wanted_uid
- *
- * @return Username of wanted_uid, if on blist, or NULL. Static string. 
- *
- */
-static const gchar *
-msim_uid2username_from_blist(MsimSession *session, guint wanted_uid)
-{
-	GSList *buddies, *cur;
-    gchar *ret;
-
-	buddies = purple_find_buddies(session->account, NULL); 
-
-	if (!buddies)
-	{
-		purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?");
-		return NULL;
-	}
-
-    ret = NULL;
-
-	for (cur = buddies; cur != NULL; cur = g_slist_next(cur))
-	{
-		PurpleBuddy *buddy;
-		guint uid;
-		const gchar *name;
-
-		/* See finch/gnthistory.c */
-		buddy = cur->data;
-
-		uid = purple_blist_node_get_int(&buddy->node, "UserID");
-		name = purple_buddy_get_name(buddy);
-
-		if (uid == wanted_uid)
-		{
-			ret = g_strdup(name);
-            break;
-		}
-	}
-
-	g_slist_free(buddies);
-	return ret;
-}
-
-/** Preprocess incoming messages, resolving as needed, calling msim_process() when ready to process.
- *
- * @param session
- * @param msg MsimMessage *, freed by caller.
- */
-static gboolean 
-msim_preprocess_incoming(MsimSession *session, MsimMessage *msg)
-{
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	if (msim_msg_get(msg, "bm") && msim_msg_get(msg, "f")) {
-		guint uid;
-		const gchar *username;
-
-		/* 'f' = userid message is from, in buddy messages */
-		uid = msim_msg_get_integer(msg, "f");
-
-		username = msim_uid2username_from_blist(session, uid); 
-
-		if (username) {
-			/* Know username already, use it. */
-			purple_debug_info("msim", "msim_preprocess_incoming: tagging with _username=%s\n",
-					username);
-			msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
-			return msim_process(session, msg);
-
-		} else {
-			gchar *from;
-
-			/* Send lookup request. */
-			/* XXX: where is msim_msg_get_string() freed? make _strdup and _nonstrdup. */
-			purple_debug_info("msim", "msim_incoming: sending lookup, setting up callback\n");
-			from = msim_msg_get_string(msg, "f");
-			msim_lookup_user(session, from, msim_incoming_resolved, msim_msg_clone(msg)); 
-			g_free(from);
-
-			/* indeterminate */
-			return TRUE;
-		}
-	} else {
-		/* Nothing to resolve - send directly to processing. */
-		return msim_process(session, msg);
-	}
-}
-
-#ifdef MSIM_USE_KEEPALIVE
-/** Check if the connection is still alive, based on last communication. */
-static gboolean
-msim_check_alive(gpointer data)
-{
-	MsimSession *session;
-	time_t delta;
-	gchar *errmsg;
-
-	session = (MsimSession *)data;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-
-	delta = time(NULL) - session->last_comm;
-	//purple_debug_info("msim", "msim_check_alive: delta=%d\n", delta);
-	if (delta >= MSIM_KEEPALIVE_INTERVAL) {
-		errmsg = g_strdup_printf(_("Connection to server lost (no data received within %d seconds)"), (int)delta);
-
-		purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n",
-				errmsg, MSIM_KEEPALIVE_INTERVAL);
-		purple_connection_error(session->gc, errmsg);
-
-		purple_notify_error(session->gc, NULL, errmsg, NULL);
-
-		g_free(errmsg);
-
-		return FALSE;
-	}
-
-	return TRUE;
-}
-#endif
-
-/** Handle mail reply checks. */
-static void
-msim_check_inbox_cb(MsimSession *session, MsimMessage *reply, gpointer data)
-{
-	GHashTable *body;
-	gchar *body_str;
-	GString *notification;
-	guint old_inbox_status;
-	guint i, n;
-	const gchar *froms[5], *tos[5], *urls[5], *subjects[5];
-
-	/* Three parallel arrays for each new inbox message type. */
-	static const gchar *inbox_keys[] = 
-	{ 
-		"Mail", 
-		"BlogComment", 
-		"ProfileComment", 
-		"FriendRequest", 
-		"PictureComment" 
-	};
-
-	static const guint inbox_bits[] = 
-	{ 
-		MSIM_INBOX_MAIL, 
-		MSIM_INBOX_BLOG_COMMENT,
-		MSIM_INBOX_PROFILE_COMMENT,
-		MSIM_INBOX_FRIEND_REQUEST,
-		MSIM_INBOX_PICTURE_COMMENT
-	};
-
-	static const gchar *inbox_urls[] =
-	{
-		"http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox",
-		"http://blog.myspace.com/index.cfm?fuseaction=blog",
-		"http://home.myspace.com/index.cfm?fuseaction=user",
-		"http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests",
-		"http://home.myspace.com/index.cfm?fuseaction=user"
-	};
-
-	static const gchar *inbox_text[5];
-
-	/* Can't write _()'d strings in array initializers. Workaround. */
-	inbox_text[0] = _("New mail messages");
-	inbox_text[1] = _("New blog comments");
-	inbox_text[2] = _("New profile comments");
-	inbox_text[3] = _("New friend requests!");
-	inbox_text[4] = _("New picture comments");
-
-	g_return_if_fail(reply != NULL);
-
-	msim_msg_dump("msim_check_inbox_cb: reply=%s\n", reply);
-
-	body_str = msim_msg_get_string(reply, "body");
-	g_return_if_fail(body_str != NULL);
-
-	body = msim_parse_body(body_str);
-	g_free(body_str);
-
-	notification = g_string_new("");
-
-	old_inbox_status = session->inbox_status;
-
-	n = 0;
-
-	for (i = 0; i < sizeof(inbox_keys) / sizeof(inbox_keys[0]); ++i) {
-		const gchar *key;
-		guint bit;
-		
-		key = inbox_keys[i];
-		bit = inbox_bits[i];
-
-		if (g_hash_table_lookup(body, key)) {
-			/* Notify only on when _changes_ from no mail -> has mail
-			 * (edge triggered) */
-			if (!(session->inbox_status & bit)) {
-				purple_debug_info("msim", "msim_check_inbox_cb: got %s, at %d\n",
-						key ? key : "(NULL)", n);
-
-				subjects[n] = inbox_text[i];
-				froms[n] = _("MySpace");
-				tos[n] = session->username;
-				/* TODO: append token, web challenge, so automatically logs in.
-				 * Would also need to free strings because they won't be static
-				 */
-				urls[n] = inbox_urls[i];
-
-				++n;
-			} else {
-				purple_debug_info("msim",
-						"msim_check_inbox_cb: already notified of %s\n",
-						key ? key : "(NULL)");
-			}
-
-			session->inbox_status |= bit;
-		}
-	}
-
-	if (n) {
-		purple_debug_info("msim",
-				"msim_check_inbox_cb: notifying of %d\n", n);
-
-		/* TODO: free strings with callback _if_ change to dynamic (w/ token) */
-		purple_notify_emails(session->gc,         /* handle */
-				n,                        /* count */
-				TRUE,                     /* detailed */
-				subjects, froms, tos, urls, 
-				NULL,                     /* PurpleNotifyCloseCallback cb */
-				NULL);                    /* gpointer user_data */
-
-	}
-
-	g_hash_table_destroy(body);
-}
-
-/* Send request to check if there is new mail. */
-static gboolean
-msim_check_inbox(gpointer data)
-{
-	MsimSession *session;
-
-	session = (MsimSession *)data;
-
-	purple_debug_info("msim", "msim_check_inbox: checking mail\n");
-	g_return_val_if_fail(msim_send(session, 
-			"persist", MSIM_TYPE_INTEGER, 1,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET,
-			"dsn", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_DSN,
-			"lid", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_LID,
-			"uid", MSIM_TYPE_INTEGER, session->userid,
-			"rid", MSIM_TYPE_INTEGER, 
-				msim_new_reply_callback(session, msim_check_inbox_cb, NULL),
-			"body", MSIM_TYPE_STRING, g_strdup(""),
-			NULL), TRUE);
-
-	/* Always return true, so that we keep checking for mail. */
-	return TRUE;
-}
-
-/** Called when the session key arrives. */
-static gboolean
-msim_we_are_logged_on(MsimSession *session, MsimMessage *msg)
-{
-	MsimMessage *body;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	session->sesskey = msim_msg_get_integer(msg, "sesskey");
-	purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey);
-
-	/* What is proof? Used to be uid, but now is 52 base64'd bytes... */
-
-	/* Comes with: proof,profileid,userid,uniquenick -- all same values
-	 * some of the time, but can vary. This is our own user ID. */
-	session->userid = msim_msg_get_integer(msg, "userid");
-
-	/* Not sure what profileid is used for. */
-	if (msim_msg_get_integer(msg, "profileid") != session->userid) {
-		msim_unrecognized(session, msg, 
-				"Profile ID didn't match user ID, don't know why");
-	}
-
-	/* We now know are our own username, only after we're logged in..
-	 * which is weird, but happens because you login with your email
-	 * address and not username. Will be freed in msim_session_destroy(). */
-	session->username = msim_msg_get_string(msg, "uniquenick");
-
-	/* The session is now set up, ready to be connected. This emits the
-	 * signedOn signal, so clients can now do anything with msimprpl, and
-	 * we're ready for it (session key, userid, username all setup). */
-	purple_connection_update_progress(session->gc, _("Connected"), 3, 4);
-	purple_connection_set_state(session->gc, PURPLE_CONNECTED);
-
-
-	/* Additional post-connect operations */
-
-
-	if (msim_msg_get_integer(msg, "uniquenick") == session->userid) {
-		purple_debug_info("msim_we_are_logged_on", "TODO: pick username");
-	}
-
-	body = msim_msg_new(
-			"UserID", MSIM_TYPE_INTEGER, session->userid,
-			NULL);
-
-	/* Request IM info about ourself. */
-	msim_send(session,
-			"persist", MSIM_TYPE_STRING, g_strdup("persist"),
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"dsn", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_DSN,
-			"uid", MSIM_TYPE_INTEGER, session->userid,
-			"lid", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_LID,
-			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
-			"body", MSIM_TYPE_DICTIONARY, body,
-			NULL);
-
-	/* Request MySpace info about ourself. */
-	msim_send(session,
-			"persist", MSIM_TYPE_STRING, g_strdup("persist"),
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"dsn", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_DSN,
-			"uid", MSIM_TYPE_INTEGER, session->userid,
-			"lid", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_LID,
-			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
-			"body", MSIM_TYPE_STRING, g_strdup(""),
-			NULL);
-
-	/* TODO: set options (persist cmd=514,dsn=1,lid=10) */
-	/* TODO: set blocklist */
-
-	/* Notify servers of our current status. */
-	purple_debug_info("msim", "msim_we_are_logged_on: notifying servers of status\n");
-	msim_set_status(session->account,
-			purple_account_get_active_status(session->account));
-
-	/* TODO: setinfo */
-	/*
-	body = msim_msg_new(
-		"TotalFriends", MSIM_TYPE_INTEGER, 666,
-		NULL);
-	msim_send(session,
-			"setinfo", MSIM_TYPE_BOOLEAN, TRUE,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"info", MSIM_TYPE_DICTIONARY, body,
-			NULL);
-			*/
-
-	/* Disable due to problems with timeouts. TODO: fix. */
-#ifdef MSIM_USE_KEEPALIVE
-	purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, 
-			(GSourceFunc)msim_check_alive, session);
-#endif
-
-	purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, 
-			(GSourceFunc)msim_check_inbox, session);
-
-	msim_check_inbox(session);
-
-	return TRUE;
-}
-
-/**
- * Process a message. 
- *
- * @param session
- * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees.
- *
- * @return TRUE if successful. FALSE if processing failed.
- */
-static gboolean 
-msim_process(MsimSession *session, MsimMessage *msg)
-{
-	g_return_val_if_fail(session != NULL, FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-#ifdef MSIM_DEBUG_MSG
-	msim_msg_dump("ready to process: %s\n", msg);
-#endif
-
-	if (msim_msg_get_integer(msg, "lc") == 1) {
-		return msim_login_challenge(session, msg);
-	} else if (msim_msg_get_integer(msg, "lc") == 2) {
-		return msim_we_are_logged_on(session, msg);
-	} else if (msim_msg_get(msg, "bm"))  {
-		return msim_incoming_bm(session, msg);
-	} else if (msim_msg_get(msg, "rid")) {
-		return msim_process_reply(session, msg);
-	} else if (msim_msg_get(msg, "error")) {
-		return msim_error(session, msg);
-	} else if (msim_msg_get(msg, "ka")) {
-		return TRUE;
-	} else {
-		msim_unrecognized(session, msg, "in msim_process");
-		return FALSE;
-	}
-}
-
-/** Callback for when a buddy icon finished being downloaded. */
-static void
-msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data,
-		gpointer user_data,
-		const gchar *url_text,
-		gsize len,
-		const gchar *error_message)
-{
-	MsimUser *user;
-
-	user = (MsimUser *)user_data;
-
-	purple_debug_info("msim_downloaded_buddy_icon",
-			"Downloaded %d bytes\n", len);
-
-	purple_buddy_icons_set_for_user(user->buddy->account,
-			user->buddy->name,
-			(gchar *)url_text, len, 
-			/*  Use URL itself as buddy icon "checksum" */
-			user->image_url);
-}
-
-/** Store a field of information about a buddy. */
-static void 
-msim_store_user_info_each(gpointer key, gpointer value, gpointer user_data)
-{
-	MsimUser *user;
-	gchar *key_str, *value_str;
-
-	user = (MsimUser *)user_data;
-	key_str = (gchar *)key;
-	value_str = (gchar *)value;
-
-	if (!strcmp(key_str, "UserID")) {
-		/* Save to buddy list, if it exists, for quick cached uid lookup with msim_uid2username_from_blist(). */
-		if (user->buddy)
-		{
-			purple_debug_info("msim", "associating uid %s with username %s\n", key_str, user->buddy->name);
-			purple_blist_node_set_int(&user->buddy->node, "UserID", atol(value_str));
-		}
-		/* Need to store in MsimUser, too? What if not on blist? */
-	} else if (!strcmp(key_str, "Age")) {
-		user->age = atol(value_str);
-	} else if (!strcmp(key_str, "Gender")) {
-		user->gender = g_strdup(value_str);
-	} else if (!strcmp(key_str, "Location")) {
-		user->location = g_strdup(value_str);
-	} else if (!strcmp(key_str, "TotalFriends")) {
-		user->total_friends = atol(value_str);
-	} else if (!strcmp(key_str, "DisplayName")) {
-		user->display_name = g_strdup(value_str);
-	} else if (!strcmp(key_str, "BandName")) {
-		user->band_name = g_strdup(value_str);
-	} else if (!strcmp(key_str, "SongName")) {
-		user->song_name = g_strdup(value_str);
-	} else if (!strcmp(key_str, "UserName")) {
-		/* Ignore because PurpleBuddy knows this already */
-		;
-	} else if (!strcmp(key_str, "ImageURL")) {
-		const gchar *previous_url;
-
-		user->image_url = g_strdup(value_str);
-		
-		previous_url = purple_buddy_icons_get_checksum_for_user(user->buddy);
-
-		/* Only download if URL changed */
-		if (!previous_url || strcmp(previous_url, user->image_url)) {
-			purple_util_fetch_url(user->image_url, TRUE, NULL, TRUE, msim_downloaded_buddy_icon, (gpointer)user);
-		}
-	} else {
-		/* TODO: other fields in MsimUser */
-		gchar *msg;
-
-		msg = g_strdup_printf("msim_store_user_info_each: unknown field %s=%s",
-				key_str, value_str);
-
-		msim_unrecognized(NULL, NULL, msg);
-
-		g_free(msg);
-	}
-}
-
-/** Save buddy information to the buddy list from a user info reply message.
- *
- * @param session
- * @param msg The user information reply, with any amount of information.
- * @param user The structure to save to, or NULL to save in PurpleBuddy->proto_data.
- *
- * Variable information is saved to the passed MsimUser structure. Permanent
- * information (UserID) is stored in the blist node of the buddy list (and
- * ends up in blist.xml, persisted to disk) if it exists.
- *
- * If the function has no buddy information, this function
- * is a no-op (and returns FALSE).
- *
- */
-static gboolean 
-msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user)
-{
-	GHashTable *body;
-	gchar *username, *body_str;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
- 
-	body_str = msim_msg_get_string(msg, "body");
-	if (!body_str) {
-		return FALSE;
-	}
-
-	g_return_val_if_fail(body_str != NULL, FALSE);
-	body = msim_parse_body(body_str);
-	g_free(body_str);
-
-
-	/* TODO: implement a better hash-like interface, and use it. */
-	username = g_hash_table_lookup(body, "UserName");
-
-	if (!username) {
-		purple_debug_info("msim", 
-			"msim_process_reply: not caching body, no UserName\n");
-		g_hash_table_destroy(body);
-		return FALSE;
-	}
-
-	/* Null user = find and store in PurpleBuddy's proto_data */
-	if (!user) {
-		user = msim_find_user(session, username);
-		g_return_val_if_fail(user != NULL, FALSE);
-	}
-
-	g_hash_table_foreach(body, msim_store_user_info_each, user);
-
-	if (msim_msg_get_integer(msg, "dsn") == MG_OWN_IM_INFO_DSN &&
-		msim_msg_get_integer(msg, "lid") == MG_OWN_IM_INFO_LID) {
-		/* TODO: do something with our own IM info, if we need it for some
-		 * specific purpose. Otherwise it is available on the buddy list,
-		 * if the user has themselves as their own buddy. 
-		 *
-		 * However, much of the info is already available in MsimSession,
-		 * stored in msim_we_are_logged_on(). */
-	} else if (msim_msg_get_integer(msg, "dsn") == MG_OWN_MYSPACE_INFO_DSN &&
-			msim_msg_get_integer(msg, "lid") == MG_OWN_MYSPACE_INFO_LID) {
-		/* TODO: same as above, but for MySpace info. */
-	}
-
-	g_hash_table_destroy(body);
-
-	return TRUE;
-}
-
-/** Process the initial server information from the server. */
-static gboolean
-msim_process_server_info(MsimSession *session, MsimMessage *msg)
-{
-	gchar *body_str;
-	GHashTable *body;
-
-	body_str = msim_msg_get_string(msg, "body");
-	g_return_val_if_fail(body_str != NULL, FALSE);
-	body = msim_parse_body(body_str);
-	g_free(body_str);
-	g_return_val_if_fail(body != NULL, FALSE);
- 
-	/* Example body:
-AdUnitRefreshInterval=10.
-AlertPollInterval=360.
-AllowChatRoomEmoticonSharing=False.
-ChatRoomUserIDs=78744676;163733130;1300326231;123521495;142663391.
-CurClientVersion=673.
-EnableIMBrowse=True.
-EnableIMStuffAvatars=False.
-EnableIMStuffZaps=False.
-MaxAddAllFriends=100.
-MaxContacts=1000.
-MinClientVersion=594.
-MySpaceIM_ENGLISH=78744676.
-MySpaceNowTimer=720.
-PersistenceDataTimeout=900.
-UseWebChallenge=1.
-WebTicketGoHome=False
-
-	Anything useful? TODO: use what is useful, and use it.
-*/
-	purple_debug_info("msim_process_server_info",
-			"maximum contacts: %s\n", 
-			g_hash_table_lookup(body, "MaxContacts") ? 
-			g_hash_table_lookup(body, "MaxContacts") : "(NULL)");
-
-	session->server_info = body;
-	/* session->server_info freed in msim_session_destroy */
-
-	return TRUE;
-}
-
-/** Process a web challenge, used to login to the web site. */
-static gboolean
-msim_web_challenge(MsimSession *session, MsimMessage *msg)
-{
-	/* TODO: web challenge, store token */
-	return FALSE;
-}
-
-/**
- * Process a persistance message reply from the server.
- *
- * @param session 
- * @param msg Message reply from server.
- *
- * @return TRUE if successful.
- *
- * msim_lookup_user sets callback for here 
- */
-static gboolean 
-msim_process_reply(MsimSession *session, MsimMessage *msg)
-{
-	MSIM_USER_LOOKUP_CB cb;
-	gpointer data;
-	guint rid, cmd, dsn, lid;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	msim_store_user_info(session, msg, NULL);
-
-	rid = msim_msg_get_integer(msg, "rid");
-	cmd = msim_msg_get_integer(msg, "cmd");
-	dsn = msim_msg_get_integer(msg, "dsn");
-	lid = msim_msg_get_integer(msg, "lid");
-
-	/* Unsolicited messages */
-	if (cmd == (MSIM_CMD_BIT_REPLY | MSIM_CMD_GET)) {
-		if (dsn == MG_SERVER_INFO_DSN && lid == MG_SERVER_INFO_LID) {
-			return msim_process_server_info(session, msg);
-		} else if (dsn == MG_WEB_CHALLENGE_DSN && lid == MG_WEB_CHALLENGE_LID) {
-			return msim_web_challenge(session, msg);
-		}
-	}
-
-	/* If a callback is registered for this userid lookup, call it. */
-	cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid));
-	data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
-
-	if (cb) {
-		purple_debug_info("msim", 
-				"msim_process_body: calling callback now\n");
-		/* Clone message, so that the callback 'cb' can use it (needs to free it also). */
-		cb(session, msim_msg_clone(msg), data);
-		g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid));
-		g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
-	} else {
-		purple_debug_info("msim", 
-				"msim_process_body: no callback for rid %d\n", rid);
-	}
-
-	return TRUE;
-}
-
-/**
- * Handle an error from the server.
- *
- * @param session 
- * @param msg The message.
- *
- * @return TRUE if successfully reported error.
- */
-static gboolean 
-msim_error(MsimSession *session, MsimMessage *msg)
-{
-	gchar *errmsg, *full_errmsg;
-	guint err;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	err = msim_msg_get_integer(msg, "err");
-	errmsg = msim_msg_get_string(msg, "errmsg");
-
-	full_errmsg = g_strdup_printf(_("Protocol error, code %d: %s"), err, 
-			errmsg ? errmsg : "no 'errmsg' given");
-
-	g_free(errmsg);
-
-	purple_debug_info("msim", "msim_error (sesskey=%d): %s\n", 
-			session->sesskey, full_errmsg);
-
-	purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), 
-			full_errmsg, NULL);
-
-	/* Destroy session if fatal. */
-	if (msim_msg_get(msg, "fatal")) {
-		purple_debug_info("msim", "fatal error, closing\n");
-		purple_connection_error(session->gc, full_errmsg);
-	}
-
-	return TRUE;
-}
-
-/**
- * Process incoming status messages.
- *
- * @param session
- * @param msg Status update message. Caller frees.
- *
- * @return TRUE if successful.
- */
-static gboolean 
-msim_incoming_status(MsimSession *session, MsimMessage *msg)
-{
-	PurpleBuddyList *blist;
-	MsimUser *user;
-	GList *list;
-	gchar *status_headline;
-	gint status_code, purple_status_code;
-	gchar *username;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	msim_msg_dump("msim_status msg=%s\n", msg);
-
-	/* Helpfully looked up by msim_incoming_resolve() for us. */
-	username = msim_msg_get_string(msg, "_username");
-	g_return_val_if_fail(username != NULL, FALSE);
-
-	{
-		gchar *ss;
-
-		ss = msim_msg_get_string(msg, "msg");
-		purple_debug_info("msim", 
-				"msim_status: updating status for <%s> to <%s>\n",
-				username, ss ? ss : "(NULL)");
-		g_free(ss);
-	}
-
-	/* Example fields: 
-	 *  |s|0|ss|Offline 
-	 *  |s|1|ss|:-)|ls||ip|0|p|0 
-	 */
-	list = msim_msg_get_list(msg, "msg");
-
-	status_code = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE));
-	purple_debug_info("msim", "msim_status: %s's status code = %d\n", username, status_code);
-	status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE);
-
-	blist = purple_get_blist();
-
-	/* Add buddy if not found */
-	user = msim_find_user(session, username);
-	if (!user) {
-		PurpleBuddy *buddy;
-
-		purple_debug_info("msim", 
-				"msim_status: making new buddy for %s\n", username);
-		buddy = purple_buddy_new(session->account, username, NULL);
-		purple_blist_add_buddy(buddy, NULL, NULL, NULL);
-
-		user = msim_get_user_from_buddy(buddy);
-
-		/* All buddies on list should have 'uid' integer associated with them. */
-		purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f"));
-		
-		msim_store_user_info(session, msg, NULL);
-	} else {
-		purple_debug_info("msim", "msim_status: found buddy %s\n", username);
-	}
-
-	user->headline = g_strdup(status_headline);
-  
-	/* Set user status */
-	switch (status_code) {
-		case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: 
-			purple_status_code = PURPLE_STATUS_OFFLINE;
-			break;
-
-		case MSIM_STATUS_CODE_ONLINE: 
-			purple_status_code = PURPLE_STATUS_AVAILABLE;
-			break;
-
-		case MSIM_STATUS_CODE_AWAY:
-			purple_status_code = PURPLE_STATUS_AWAY;
-			break;
-
-		case MSIM_STATUS_CODE_IDLE:
-			/* will be handled below */
-			purple_status_code = -1;
-			break;
-
-		default:
-				purple_debug_info("msim", "msim_status for %s, unknown status code %d, treating as available\n",
-						username, status_code);
-				purple_status_code = PURPLE_STATUS_AVAILABLE;
-	}
-
-	purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL);
-
-	if (status_code == MSIM_STATUS_CODE_IDLE) {
-		purple_debug_info("msim", "msim_status: got idle: %s\n", username);
-		purple_prpl_got_user_idle(session->account, username, TRUE, time(NULL));
-	} else {
-		/* All other statuses indicate going back to non-idle. */
-		purple_prpl_got_user_idle(session->account, username, FALSE, time(NULL));
-	}
-
-#ifdef MSIM_SEND_CLIENT_VERSION
-	if (status_code == MSIM_STATUS_CODE_ONLINE) {
-		/* Secretly whisper to unofficial clients our own version as they come online */
-		msim_send_unofficial_client(session, username);
-	}
-#endif
-
-	g_free(username);
-	msim_msg_list_free(list);
-
-	return TRUE;
-}
-
-/** Add a buddy to user's buddy list. */
-void 
-msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group)
-{
-	MsimSession *session;
-	MsimMessage *msg;
-	/* MsimMessage *msg_persist; */
-	MsimMessage *body;
-
-	session = (MsimSession *)gc->proto_data;
-	purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", 
-			buddy->name, (group && group->name) ? group->name : "(no group)");
-
-	msg = msim_msg_new(
-			"addbuddy", MSIM_TYPE_BOOLEAN, TRUE,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			/* "newprofileid" will be inserted here with uid. */
-			"reason", MSIM_TYPE_STRING, g_strdup(""),
-			NULL);
-
-	if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) {
-		purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed."));
-		msim_msg_free(msg);
-		return;
-	}
-	msim_msg_free(msg);
-	
-	/* TODO: if addbuddy fails ('error' message is returned), delete added buddy from
-	 * buddy list since Purple adds it locally. */
-
-	body = msim_msg_new(
-			"ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"),
-			"GroupName", MSIM_TYPE_STRING, g_strdup(group->name),
-			"Position", MSIM_TYPE_INTEGER, 1000,
-			"Visibility", MSIM_TYPE_INTEGER, 1,
-			"NickName", MSIM_TYPE_STRING, g_strdup(""),
-			"NameSelect", MSIM_TYPE_INTEGER, 0,
-			NULL);
-
-	/* TODO: Update blocklist. */
-
-#if 0
-	msg_persist = msim_msg_new(
-		"persist", MSIM_TYPE_INTEGER, 1,
-		"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-		"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT,
-		"dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN,
-		"lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID,
-		/* TODO: Use msim_new_reply_callback to get rid. */
-		"rid", MSIM_TYPE_INTEGER, session->next_rid++,
-		"body", MSIM_TYPE_DICTIONARY, body,
-		NULL);
-
-	if (!msim_postprocess_outgoing(session, msg_persist, buddy->name, "body", NULL))
-	{
-		purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed"));
-		msim_msg_free(msg_persist);
-		return;
-	}
-	msim_msg_free(msg_persist);
-#endif
-
-}
-
-/** Perform actual postprocessing on a message, adding userid as specified.
- *
- * @param msg The message to postprocess.
- * @param uid_before Name of field where to insert new field before, or NULL for end.
- * @param uid_field_name Name of field to add uid to.
- * @param uid The userid to insert.
- *
- * If the field named by uid_field_name already exists, then its string contents will
- * be used for the field, except "<uid>" will be replaced by the userid.
- *
- * If the field named by uid_field_name does not exist, it will be added before the
- * field named by uid_before, as an integer, with the userid.
- *
- * Does not handle sending, or scheduling userid lookup. For that, see msim_postprocess_outgoing().
- */ 
-static MsimMessage *
-msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, 
-		const gchar *uid_field_name, guint uid)
-{
-	msim_msg_dump("msim_do_postprocessing msg: %s\n", msg);
-
-	/* First, check - if the field already exists, replace <uid> within it */
-	if (msim_msg_get(msg, uid_field_name)) {
-		MsimMessageElement *elem;
-		gchar *fmt_string;
-		gchar *uid_str, *new_str;
-
-		/* Warning: this is a delicate, but safe, operation */
-
-		elem = msim_msg_get(msg, uid_field_name);
-
-		/* Get the packed element, flattening it. This allows <uid> to be
-		 * replaced within nested data structures, since the replacement is done
-		 * on the linear, packed data, not on a complicated data structure.
-		 *
-		 * For example, if the field was originally a dictionary or a list, you 
-		 * would have to iterate over all the items in it to see what needs to
-		 * be replaced. But by packing it first, the <uid> marker is easily replaced
-		 * just by a string replacement.
-		 */
-		fmt_string = msim_msg_pack_element_data(elem);
-
-		uid_str = g_strdup_printf("%d", uid);
-		new_str = str_replace(fmt_string, "<uid>", uid_str);
-		g_free(uid_str);
-		g_free(fmt_string);
-
-		/* Free the old element data */
-		msim_msg_free_element_data(elem->data);
-
-		/* Replace it with our new data */
-		elem->data = new_str;
-		elem->type = MSIM_TYPE_RAW;
-
-	} else {
-		/* Otherwise, insert new field into outgoing message. */
-		msg = msim_msg_insert_before(msg, uid_before, uid_field_name, MSIM_TYPE_INTEGER, GUINT_TO_POINTER(uid));
-	}
-
-	msim_msg_dump("msim_postprocess_outgoing_cb: postprocessed msg=%s\n", msg);
-
-	return msg;
-}
-
-/** Callback for msim_postprocess_outgoing() to add a userid to a message, and send it (once receiving userid).
- *
- * @param session
- * @param userinfo The user information reply message, containing the user ID
- * @param data The message to postprocess and send.
- *
- * The data message should contain these fields:
- *
- *  _uid_field_name: string, name of field to add with userid from userinfo message
- *  _uid_before: string, name of field before field to insert, or NULL for end
- *
- *
-*/
-static void 
-msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, 
-		gpointer data)
-{
-	gchar *body_str;
-	GHashTable *body;
-	gchar *uid, *uid_field_name, *uid_before;
-	MsimMessage *msg;
-
-	msg = (MsimMessage *)data;
-
-	msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg);
-
-	/* Obtain userid from userinfo message. */
-	body_str = msim_msg_get_string(userinfo, "body");
-	g_return_if_fail(body_str != NULL);
-	body = msim_parse_body(body_str);
-	g_free(body_str);
-
-	uid = g_strdup(g_hash_table_lookup(body, "UserID"));
-	g_hash_table_destroy(body);
-
-	uid_field_name = msim_msg_get_string(msg, "_uid_field_name");
-	uid_before = msim_msg_get_string(msg, "_uid_before");
-
-	msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid));
-
-	/* Send */
-	if (!msim_msg_send(session, msg)) {
-		msim_msg_dump("msim_postprocess_outgoing_cb: sending failed for message: %s\n", msg);
-	}
-
-
-	/* Free field names AFTER sending message, because MsimMessage does NOT copy
-	 * field names - instead, treats them as static strings (which they usually are).
-	 */
-	g_free(uid_field_name);
-	g_free(uid_before);
-
-	g_hash_table_destroy(body);
-
-	//msim_msg_free(msg);
-}
-
-/** Postprocess and send a message.
- *
- * @param session
- * @param msg Message to postprocess. Will NOT be freed.
- * @param username Username to resolve. Assumed to be a static string (will not be freed or copied).
- * @param uid_field_name Name of new field to add, containing uid of username. Static string.
- * @param uid_before Name of existing field to insert username field before. Static string.
- *
- * @return TRUE if successful.
- */
-gboolean 
-msim_postprocess_outgoing(MsimSession *session, MsimMessage *msg, 
-		const gchar *username, const gchar *uid_field_name, 
-		const gchar *uid_before)
-{
-	PurpleBuddy *buddy;
-	guint uid;
-	gboolean rc;
-
-	g_return_val_if_fail(msg != NULL, FALSE);
-
-	/* Store information for msim_postprocess_outgoing_cb(). */
-	msim_msg_dump("msim_postprocess_outgoing: msg before=%s\n", msg);
-	msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
-	msg = msim_msg_append(msg, "_uid_field_name", MSIM_TYPE_STRING, g_strdup(uid_field_name));
-	msg = msim_msg_append(msg, "_uid_before", MSIM_TYPE_STRING, g_strdup(uid_before));
-
-	/* First, try the most obvious. If numeric userid is given, use that directly. */
-	if (msim_is_userid(username)) {
-		uid = atol(username);
-	} else {
-		/* Next, see if on buddy list and know uid. */
-		buddy = purple_find_buddy(session->account, username);
-		if (buddy) {
-			uid = purple_blist_node_get_int(&buddy->node, "UserID");
-		} else {
-			uid = 0;
-		}
-
-		if (!buddy || !uid)
-		{
-			/* Don't have uid offhand - need to ask for it, and wait until hear back before sending. */
-			purple_debug_info("msim", ">>> msim_postprocess_outgoing: couldn't find username %s in blist\n",
-					username ? username : "(NULL)");
-			msim_msg_dump("msim_postprocess_outgoing - scheduling lookup, msg=%s\n", msg);
-			/* TODO: where is cloned message freed? Should be in _cb. */
-			msim_lookup_user(session, username, msim_postprocess_outgoing_cb, msim_msg_clone(msg));
-			return TRUE;       /* not sure of status yet - haven't sent! */
-		}
-	}
-	
-	/* Already have uid, postprocess and send msg immediately. */
-	purple_debug_info("msim", "msim_postprocess_outgoing: found username %s has uid %d\n",
-			username ? username : "(NULL)", uid);
-
-	msg = msim_do_postprocessing(msg, uid_before, uid_field_name, uid);
-
-	msim_msg_dump("msim_postprocess_outgoing: msg after (uid immediate)=%s\n", msg);
-	
-	rc = msim_msg_send(session, msg);
-
-	//msim_msg_free(msg);
-
-	return rc;
-}
-
-/** Remove a buddy from the user's buddy list. */
-void 
-msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group)
-{
-	MsimSession *session;
-	MsimMessage *delbuddy_msg;
-	MsimMessage *persist_msg;
-	MsimMessage *blocklist_msg;
-	GList *blocklist_updates;
-
-	session = (MsimSession *)gc->proto_data;
-
-	delbuddy_msg = msim_msg_new(
-				"delbuddy", MSIM_TYPE_BOOLEAN, TRUE,
-				"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-				/* 'delprofileid' with uid will be inserted here. */
-				NULL);
-
-	if (!msim_postprocess_outgoing(session, delbuddy_msg, buddy->name, "delprofileid", NULL)) {
-		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed"));
-		msim_msg_free(delbuddy_msg);
-		return;
-	}
-	msim_msg_free(delbuddy_msg);
-
-	persist_msg = msim_msg_new(
-			"persist", MSIM_TYPE_INTEGER, 1,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_DELETE,
-			"dsn", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_DSN,
-			"lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID,
-			"uid", MSIM_TYPE_INTEGER, session->userid,
-			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
-			/* <uid> will be replaced by postprocessing */
-			"body", MSIM_TYPE_STRING, g_strdup("ContactID=<uid>"),
-			NULL);
-
-	if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) {
-		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed"));
-		msim_msg_free(persist_msg);
-		return;
-	}
-	msim_msg_free(persist_msg);
-
-	blocklist_updates = NULL;
-	blocklist_updates = g_list_prepend(blocklist_updates, "a-");
-	blocklist_updates = g_list_prepend(blocklist_updates, "<uid>");
-	blocklist_updates = g_list_prepend(blocklist_updates, "b-");
-	blocklist_updates = g_list_prepend(blocklist_updates, "<uid>");
-	blocklist_updates = g_list_reverse(blocklist_updates);
-
-	blocklist_msg = msim_msg_new(
-			"blocklist", MSIM_TYPE_BOOLEAN, TRUE,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			/* TODO: MsimMessage lists. Currently <uid> isn't replaced in lists. */
-			//"idlist", MSIM_TYPE_STRING, g_strdup("a-|<uid>|b-|<uid>"),
-			"idlist", MSIM_TYPE_LIST, blocklist_updates,
-			NULL);
-
-	if (!msim_postprocess_outgoing(session, blocklist_msg, buddy->name, "idlist", NULL)) {
-		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("blocklist command failed"));
-		msim_msg_free(blocklist_msg);
-		return;
-	}
-	msim_msg_free(blocklist_msg);
-}
-
-/** Return whether the buddy can be messaged while offline.
- *
- * The protocol supports offline messages in just the same way as online
- * messages.
- */
-gboolean 
-msim_offline_message(const PurpleBuddy *buddy)
-{
-	return TRUE;
-}
-
-/**
- * Callback when input available.
- *
- * @param gc_uncasted A PurpleConnection pointer.
- * @param source File descriptor.
- * @param cond PURPLE_INPUT_READ
- *
- * Reads the input, and calls msim_preprocess_incoming() to handle it.
- */
-static void 
-msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond)
-{
-	PurpleConnection *gc;
-	PurpleAccount *account;
-	MsimSession *session;
-	gchar *end;
-	int n;
-
-	g_return_if_fail(gc_uncasted != NULL);
-	g_return_if_fail(source >= 0);  /* Note: 0 is a valid fd */
-
-	gc = (PurpleConnection *)(gc_uncasted);
-	account = purple_connection_get_account(gc);
-	session = gc->proto_data;
-
-	/* libpurple/eventloop.h only defines these two */
-	if (cond != PURPLE_INPUT_READ && cond != PURPLE_INPUT_WRITE) {
-		purple_debug_info("msim_input_cb", "unknown condition=%d\n", cond);
-		purple_connection_error(gc, _("Invalid input condition"));
-		return;
-	}
-
-	g_return_if_fail(cond == PURPLE_INPUT_READ);
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-
-	/* Mark down that we got data, so don't timeout. */
-	session->last_comm = time(NULL);
-
-	/* Only can handle so much data at once... 
-	 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE.
-	 * Should be large enough to hold the largest protocol message.
-	 */
-	if (session->rxoff >= MSIM_READ_BUF_SIZE) {
-		purple_debug_error("msim", 
-				"msim_input_cb: %d-byte read buffer full! rxoff=%d\n",
-				MSIM_READ_BUF_SIZE, session->rxoff);
-		purple_connection_error(gc, _("Read buffer full"));
-		return;
-	}
-
-	purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n",
-			session->rxoff, MSIM_READ_BUF_SIZE, 
-			MSIM_READ_BUF_SIZE - session->rxoff);
-
-	/* Read into buffer. On Win32, need recv() not read(). session->fd also holds
-	 * the file descriptor, but it sometimes differs from the 'source' parameter.
-	 */
-	n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0);
-
-	if (n < 0 && errno == EAGAIN) {
-		return;
-	} else if (n < 0) {
-		purple_debug_error("msim", "msim_input_cb: read error, ret=%d, "
-			"error=%s, source=%d, fd=%d (%X))\n", 
-			n, strerror(errno), source, session->fd, session->fd);
-		purple_connection_error(gc, _("Read error"));
-		return;
-	} else if (n == 0) {
-		purple_debug_info("msim", "msim_input_cb: server disconnected\n");
-		purple_connection_error(gc, _("Server has disconnected"));
-		return;
-	}
-
-	if (n + session->rxoff >= MSIM_READ_BUF_SIZE) {
-		purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n",
-				n, n + session->rxoff, MSIM_READ_BUF_SIZE);
-		/* TODO: g_realloc like msn, yahoo, irc, jabber? */
-		purple_connection_error(gc, _("Read buffer full"));
-	}
-
-	/* Null terminate */
-	purple_debug_info("msim", "msim_input_cb: going to null terminate "
-			"at n=%d\n", n);
-	session->rxbuf[session->rxoff + n] = 0;
-
-#ifdef MSIM_CHECK_EMBEDDED_NULLS
-	/* Check for embedded NULs. I don't handle them, and they shouldn't occur. */
-	if (strlen(session->rxbuf + session->rxoff) != n) {
-		/* Occurs after login, but it is not a null byte. */
-		purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes"
-				"--null byte encountered?\n", 
-				strlen(session->rxbuf + session->rxoff), n);
-		//purple_connection_error(gc, "Invalid message - null byte on input");
-		return;
-	}
-#endif
-
-	session->rxoff += n;
-	purple_debug_info("msim", "msim_input_cb: read=%d\n", n);
-
-#ifdef MSIM_DEBUG_RXBUF
-	purple_debug_info("msim", "buf=<%s>\n", session->rxbuf);
-#endif
-
-	/* Look for \\final\\ end markers. If found, process message. */
-	while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) {
-		MsimMessage *msg;
-
-#ifdef MSIM_DEBUG_RXBUF
-		purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf);
-#endif
-		*end = 0;
-		msg = msim_parse(g_strdup(session->rxbuf));
-		if (!msg) {
-			purple_debug_info("msim", "msim_input_cb: couldn't parse rxbuf\n");
-			purple_connection_error(gc, _("Unparseable message"));
-		} else {
-			/* Process message and then free it (processing function should
-			 * clone message if it wants to keep it afterwards.) */
-			if (!msim_preprocess_incoming(session, msg)) {
-				msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg);
-			}
-			msim_msg_free(msg);
-		}
-
-		/* Move remaining part of buffer to beginning. */
-		session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING);
-		memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), 
-				MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf));
-
-		/* Clear end of buffer */
-		//memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf));
-	}
-}
-
-/* Setup a callback, to be called when a reply is received with the returned rid.
- *
- * @param cb The callback, an MSIM_USER_LOOKUP_CB.
- * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *).
- *
- * @return The request/reply ID, used to link replies with requests, or -1.
- *          Put the rid in your request, 'rid' field.
- *
- * TODO: Make more generic and more specific:
- * 1) MSIM_USER_LOOKUP_CB - make it for PERSIST_REPLY, not just user lookup
- * 2) data - make it an MsimMessage?
- */
-static guint 
-msim_new_reply_callback(MsimSession *session, MSIM_USER_LOOKUP_CB cb, 
-		gpointer data)
-{
-	guint rid;
-
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
-
-	rid = session->next_rid++;
-
-	g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb);
-	g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data);
-
-	return rid;
-}
-
-/**
- * Callback when connected. Sets up input handlers.
- *
- * @param data A PurpleConnection pointer.
- * @param source File descriptor.
- * @param error_message
- */
-static void 
-msim_connect_cb(gpointer data, gint source, const gchar *error_message)
-{
-	PurpleConnection *gc;
-	MsimSession *session;
-
-	g_return_if_fail(data != NULL);
-
-	gc = (PurpleConnection *)data;
-	session = (MsimSession *)gc->proto_data;
-
-	if (source < 0) {
-		purple_connection_error(gc, _("Couldn't connect to host"));
-		purple_connection_error(gc, g_strdup_printf(
-					_("Couldn't connect to host: %s (%d)"), 
-					error_message ? error_message : "no message given", 
-					source));
-		return;
-	}
-
-	session->fd = source; 
-
-	gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc);
-}
-
-/* Session methods */
-
-/**
- * Create a new MSIM session.
- *
- * @param acct The account to create the session from.
- *
- * @return Pointer to a new session. Free with msim_session_destroy.
- */
-MsimSession *
-msim_session_new(PurpleAccount *acct)
-{
-	MsimSession *session;
-
-	g_return_val_if_fail(acct != NULL, NULL);
-
-	session = g_new0(MsimSession, 1);
-
-	session->magic = MSIM_SESSION_STRUCT_MAGIC;
-	session->account = acct;
-	session->gc = purple_account_get_connection(acct);
-	session->sesskey = 0;
-	session->userid = 0;
-	session->username = NULL;
-	session->fd = -1;
-
-	/* TODO: Remove. */
-	session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, 
-			g_direct_equal, NULL, NULL);  /* do NOT free function pointers! (values) */
-	session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, 
-			g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are,
-											 they could be integers inside gpointers
-											 or strings, so I don't freed them.
-											 Figure this out, once free cache. */
-
-	/* Created in msim_process_server_info() */
-	session->server_info = NULL;
-
-	session->rxoff = 0;
-	session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE);
-	session->next_rid = 1;
-	session->last_comm = time(NULL);
-	session->inbox_status = 0;
-	
-	return session;
-}
-
-/**
- * Free a session.
- *
- * @param session The session to destroy.
- */
-void 
-msim_session_destroy(MsimSession *session)
-{
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-	
-	session->magic = -1;
-
-	g_free(session->rxbuf);
-	g_free(session->username);
-
-	/* TODO: Remove. */
-	g_hash_table_destroy(session->user_lookup_cb);
-	g_hash_table_destroy(session->user_lookup_cb_data);
-
-	if (session->server_info) {
-		g_hash_table_destroy(session->server_info);
-	}
-	
-	g_free(session);
-}
-				 
-/** 
- * Close the connection.
- * 
- * @param gc The connection.
- */
-void 
-msim_close(PurpleConnection *gc)
-{
-	MsimSession *session;
-
-	if (gc == NULL) {
-		return;
-	}
-
-	session = (MsimSession *)gc->proto_data;
-	if (session == NULL)
-		return;
-
-	gc->proto_data = NULL;
-
-	if (!MSIM_SESSION_VALID(session)) {
-		return;
-	}
-
-	if (session->gc->inpa) {
-		purple_input_remove(session->gc->inpa);
-	}
-
-	msim_session_destroy(session);
-}
-
-
-/**
- * Check if a string is a userid (all numeric).
- *
- * @param user The user id, email, or name.
- *
- * @return TRUE if is userid, FALSE if not.
- */
-static gboolean 
-msim_is_userid(const gchar *user)
-{
-	g_return_val_if_fail(user != NULL, FALSE);
-
-	return strspn(user, "0123456789") == strlen(user);
-}
-
-/**
- * Check if a string is an email address (contains an @).
- *
- * @param user The user id, email, or name.
- *
- * @return TRUE if is an email, FALSE if not.
- *
- * This function is not intended to be used as a generic
- * means of validating email addresses, but to distinguish
- * between a user represented by an email address from
- * other forms of identification.
- */ 
-static gboolean 
-msim_is_email(const gchar *user)
-{
-	g_return_val_if_fail(user != NULL, FALSE);
-
-	return strchr(user, '@') != NULL;
-}
-
-
-/**
- * Asynchronously lookup user information, calling callback when receive result.
- *
- * @param session
- * @param user The user id, email address, or username. Not freed.
- * @param cb Callback, called with user information when available.
- * @param data An arbitray data pointer passed to the callback.
- */
-/* TODO: change to not use callbacks */
-static void 
-msim_lookup_user(MsimSession *session, const gchar *user, 
-		MSIM_USER_LOOKUP_CB cb, gpointer data)
-{
-	MsimMessage *body;
-	gchar *field_name;
-	guint rid, cmd, dsn, lid;
-
-	g_return_if_fail(MSIM_SESSION_VALID(session));
-	g_return_if_fail(user != NULL);
-	g_return_if_fail(cb != NULL);
-
-	purple_debug_info("msim", "msim_lookup_userid: "
-			"asynchronously looking up <%s>\n", user);
-
-	msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data);
-
-	/* Setup callback. Response will be associated with request using 'rid'. */
-	rid = msim_new_reply_callback(session, cb, data);
-
-	/* Send request */
-
-	cmd = MSIM_CMD_GET;
-
-	if (msim_is_userid(user)) {
-		field_name = "UserID";
-		dsn = MG_MYSPACE_INFO_BY_ID_DSN; 
-		lid = MG_MYSPACE_INFO_BY_ID_LID; 
-	} else if (msim_is_email(user)) {
-		field_name = "Email";
-		dsn = MG_MYSPACE_INFO_BY_STRING_DSN;
-		lid = MG_MYSPACE_INFO_BY_STRING_LID;
-	} else {
-		field_name = "UserName";
-		dsn = MG_MYSPACE_INFO_BY_STRING_DSN;
-		lid = MG_MYSPACE_INFO_BY_STRING_LID;
-	}
-
-	body = msim_msg_new(
-			field_name, MSIM_TYPE_STRING, g_strdup(user),
-			NULL);
-
-	g_return_if_fail(msim_send(session,
-			"persist", MSIM_TYPE_INTEGER, 1,
-			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
-			"cmd", MSIM_TYPE_INTEGER, 1,
-			"dsn", MSIM_TYPE_INTEGER, dsn,
-			"uid", MSIM_TYPE_INTEGER, session->userid,
-			"lid", MSIM_TYPE_INTEGER, lid,
-			"rid", MSIM_TYPE_INTEGER, rid,
-			"body", MSIM_TYPE_DICTIONARY, body,
-			NULL));
-} 
-
-
-/**
- * Obtain the status text for a buddy.
- *
- * @param buddy The buddy to obtain status text for.
- *
- * @return Status text, or NULL if error. Caller g_free()'s.
- *
- */
-char *
-msim_status_text(PurpleBuddy *buddy)
-{
-	MsimSession *session;
-	MsimUser *user;
-	const gchar *display_name, *headline;
-
-	g_return_val_if_fail(buddy != NULL, NULL);
-
-	user = msim_get_user_from_buddy(buddy);
-
-	session = (MsimSession *)buddy->account->gc->proto_data;
-	g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL);
-
-	display_name = headline = NULL;
-
-	/* Retrieve display name and/or headline, depending on user preference. */
-	if (purple_account_get_bool(session->account, "show_display_name", TRUE)) {
-		display_name = user->display_name;
-	} 
-
-	if (purple_account_get_bool(session->account, "show_headline", FALSE)) {
-		headline = user->headline;
-	}
-
-	/* Return appropriate combination of display name and/or headline, or neither. */
-
-	if (display_name && headline) {
-		return g_strconcat(display_name, " ", headline, NULL);
-	} else if (display_name) {
-		return g_strdup(display_name);
-	} else if (headline) {
-		return g_strdup(headline);
-	}
-
-	return NULL;
-}
-
-/**
- * Obtain the tooltip text for a buddy.
- *
- * @param buddy Buddy to obtain tooltip text on.
- * @param user_info Variable modified to have the tooltip text.
- * @param full TRUE if should obtain full tooltip text.
- *
- */
-void 
-msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, 
-		gboolean full)
-{
-	MsimUser *user;
-
-	g_return_if_fail(buddy != NULL);
-	g_return_if_fail(user_info != NULL);
-
-	user = msim_get_user_from_buddy(buddy);
-
-	if (PURPLE_BUDDY_IS_ONLINE(buddy)) {
-		MsimSession *session;
-
-		session = (MsimSession *)buddy->account->gc->proto_data;
-
-		g_return_if_fail(MSIM_SESSION_VALID(session));
-
-		/* TODO: if (full), do something different? */
-
-		/* TODO: request information? have to figure out how to do
-		 * the asynchronous lookup like oscar does (tooltip shows
-		 * 'retrieving...' if not yet available, then changes when it is).
-		 *
-		 * Right now, only show what we have on hand.
-		 */
-
-		/* Show abbreviated user info. */
-		msim_append_user_info(session, user_info, user, FALSE);
-	}
-}
-
-/** Actions menu for account. */
-GList *
-msim_actions(PurplePlugin *plugin, gpointer context)
-{
-	PurpleConnection *gc;
-	GList *menu;
-	//PurplePluginAction *act;
-
-	gc = (PurpleConnection *)context;
-
-	menu = NULL;
-
-#if 0
-	/* TODO: find out how */
-	act = purple_plugin_action_new(_("Find people..."), msim_);
-	menu = g_list_append(menu, act);
-
-	act = purple_plugin_action_new(_("Import friends..."), NULL);
-	menu = g_list_append(menu, act);
-
-	act = purple_plugin_action_new(_("Change IM name..."), NULL);
-	menu = g_list_append(menu, act);
-#endif
-
-	return menu;
-}
-
-/** Callbacks called by Purple, to access this plugin. */
-PurplePluginProtocolInfo prpl_info = {
-	/* options */
-	  OPT_PROTO_USE_POINTSIZE        /* specify font size in sane point size */
-	| OPT_PROTO_MAIL_CHECK,
-
-	/* | OPT_PROTO_IM_IMAGE - TODO: direct images. */    
-	NULL,              /* user_splits */
-	NULL,              /* protocol_options */
-	NO_BUDDY_ICONS,    /* icon_spec - TODO: eventually should add this */
-	msim_list_icon,    /* list_icon */
-	NULL,              /* list_emblems */
-	msim_status_text,  /* status_text */
-	msim_tooltip_text, /* tooltip_text */
-	msim_status_types, /* status_types */
-	msim_blist_node_menu,  /* blist_node_menu */
-	NULL,              /* chat_info */
-	NULL,              /* chat_info_defaults */
-	msim_login,        /* login */
-	msim_close,        /* close */
-	msim_send_im,      /* send_im */
-	NULL,              /* set_info */
-	msim_send_typing,  /* send_typing */
-	msim_get_info,     /* get_info */
-	msim_set_status,   /* set_status */
-	msim_set_idle,     /* set_idle */
-	NULL,              /* change_passwd */
-	msim_add_buddy,    /* add_buddy */
-	NULL,              /* add_buddies */
-	msim_remove_buddy, /* remove_buddy */
-	NULL,              /* remove_buddies */
-	NULL,              /* add_permit */
-	NULL,              /* add_deny */
-	NULL,              /* rem_permit */
-	NULL,              /* rem_deny */
-	NULL,              /* set_permit_deny */
-	NULL,              /* join_chat */
-	NULL,              /* reject chat invite */
-	NULL,              /* get_chat_name */
-	NULL,              /* chat_invite */
-	NULL,              /* chat_leave */
-	NULL,              /* chat_whisper */
-	NULL,              /* chat_send */
-	NULL,              /* keepalive */
-	NULL,              /* register_user */
-	NULL,              /* get_cb_info */
-	NULL,              /* get_cb_away */
-	NULL,              /* alias_buddy */
-	NULL,              /* group_buddy */
-	NULL,              /* rename_group */
-	NULL,              /* buddy_free */
-	NULL,              /* convo_closed */
-	NULL,              /* normalize */
-	NULL,              /* set_buddy_icon */
-	NULL,              /* remove_group */
-	NULL,              /* get_cb_real_name */
-	NULL,              /* set_chat_topic */
-	NULL,              /* find_blist_chat */
-	NULL,              /* roomlist_get_list */
-	NULL,              /* roomlist_cancel */
-	NULL,              /* roomlist_expand_category */
-	NULL,              /* can_receive_file */
-	NULL,              /* send_file */
-	NULL,              /* new_xfer */
-	msim_offline_message, /* offline_message */
-	NULL,              /* whiteboard_prpl_ops */
-	msim_send_really_raw,     /* send_raw */
-	NULL,               /* roomlist_room_serialize */
-	NULL,               /* _purple_reserved1 */
-	NULL,               /* _purple_reserved2 */
-	NULL,               /* _purple_reserved3 */
-	NULL                /* _purple_reserved4 */
-};
-
-
-
-/** Based on MSN's plugin info comments. */
-PurplePluginInfo info = {
-	PURPLE_PLUGIN_MAGIC,                                
-	PURPLE_MAJOR_VERSION,
-	PURPLE_MINOR_VERSION,
-	PURPLE_PLUGIN_PROTOCOL,                           /**< type           */
-	NULL,                                             /**< ui_requirement */
-	0,                                                /**< flags          */
-	NULL,                                             /**< dependencies   */
-	PURPLE_PRIORITY_DEFAULT,                          /**< priority       */
-
-	"prpl-myspace",                                   /**< id             */
-	"MySpaceIM",                                      /**< name           */
-	MSIM_PRPL_VERSION_STRING,                         /**< version        */
-	                                                  /**  summary        */
-	"MySpaceIM Protocol Plugin",
-	                                                  /**  description    */
-	"MySpaceIM Protocol Plugin",
-	"Jeff Connelly <jeff2@soc.pidgin.im>",            /**< author         */
-	"http://developer.pidgin.im/wiki/MySpaceIM/",     /**< homepage       */
-
-	msim_load,                                        /**< load           */
-	NULL,                                             /**< unload         */
-	NULL,                                             /**< destroy        */
-	NULL,                                             /**< ui_info        */
-	&prpl_info,                                       /**< extra_info     */
-	NULL,                                             /**< prefs_info     */
-	msim_actions,                                     /**< msim_actions   */
-	NULL,                                             /**< reserved1      */
-	NULL,                                             /**< reserved2      */
-	NULL,                                             /**< reserved3      */
-	NULL                                              /**< reserved4      */
-};
-
-
-#ifdef MSIM_SELF_TEST
-/** Test functions.
- * Used to test or try out the internal workings of msimprpl. If you're reading
- * this code for the first time, these functions can be instructive in learning
- * how msimprpl is architected.
- */
-void 
-msim_test_all(void) {
-	guint failures;
-
-
-	failures = 0;
-	failures += msim_test_msg();
-	failures += msim_test_escaping();
-
-	if (failures) {
-		purple_debug_info("msim", "msim_test_all HAD FAILURES: %d\n", failures);
-	} else {
-		purple_debug_info("msim", "msim_test_all - all tests passed!\n");
-	}
-	exit(0);
-}
-
-/** Test MsimMessage for basic functionality. */
-int 
-msim_test_msg(void)
-{
-	MsimMessage *msg, *msg_cloned, *msg2;
-	GList *list;
-	gchar *packed, *packed_expected, *packed_cloned;
-	guint failures;
-
-	failures = 0;
-
-	purple_debug_info("msim", "\n\nTesting MsimMessage\n");
-	msg = msim_msg_new(NULL);      /* Create a new, empty message. */
-
-	/* Append some new elements. */
-	msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3));
-	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1"));
-	msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42));
-	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43"));
-	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy"));
-	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7"));
-	msim_msg_dump("msg debug str=%s\n", msg);
-	packed = msim_msg_pack(msg);
-
-	purple_debug_info("msim", "msg packed=%s\n", packed);
-
-	packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1"
-		"\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\";
-
-	if (0 != strcmp(packed, packed_expected)) {
-		purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n",
-				++failures, packed, packed_expected);
-	}
-
-
-	msg_cloned = msim_msg_clone(msg);
-	packed_cloned = msim_msg_pack(msg_cloned);
-
-	purple_debug_info("msim", "msg cloned=%s\n", packed_cloned);
-	if (0 != strcmp(packed, packed_cloned)) {
-		purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n",
-				++failures, packed_cloned, packed);
-	}
-
-	g_free(packed);
-	g_free(packed_cloned);
-	msim_msg_free(msg_cloned);
-	msim_msg_free(msg);
-
-	/* Try some of the more advanced functionality */
-	list = NULL;
-
-	list = g_list_prepend(list, "item3");
-	list = g_list_prepend(list, "item2");
-	list = g_list_prepend(list, "item1");
-	list = g_list_prepend(list, "item0");
-
-	msg = msim_msg_new(NULL);
-	msg = msim_msg_append(msg, "string", MSIM_TYPE_STRING, g_strdup("string value"));
-	msg = msim_msg_append(msg, "raw", MSIM_TYPE_RAW, g_strdup("raw value"));
-	msg = msim_msg_append(msg, "integer", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(3140));
-	msg = msim_msg_append(msg, "boolean", MSIM_TYPE_BOOLEAN, GUINT_TO_POINTER(FALSE));
-	msg = msim_msg_append(msg, "list", MSIM_TYPE_LIST, list);
-
-	msim_msg_dump("msg with list=%s\n", msg);
-	purple_debug_info("msim", "msg with list packed=%s\n", msim_msg_pack(msg));
-
-	msg2 = msim_msg_new(NULL);
-	msg2 = msim_msg_append(msg2, "outer", MSIM_TYPE_STRING, g_strdup("outer value"));
-	msg2 = msim_msg_append(msg2, "body", MSIM_TYPE_DICTIONARY, msg);
-	msim_msg_dump("msg with dict=%s\n", msg2);      /* msg2 now 'owns' msg */
-	purple_debug_info("msim", "msg with dict packed=%s\n", msim_msg_pack(msg2));
-
-	msim_msg_free(msg2);
-
-	return failures;
-}
-
-/** Test protocol-level escaping/unescaping. */
-int 
-msim_test_escaping(void)
-{
-	guint failures;
-	gchar *raw, *escaped, *unescaped, *expected;
-
-	failures = 0;
-
-	purple_debug_info("msim", "\n\nTesting escaping\n");
-
-	raw = "hello/world\\hello/world";
-
-	escaped = msim_escape(raw);
-	purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped);
-	expected = "hello/1world/2hello/1world";
-	if (0 != strcmp(escaped, expected)) {
-		purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n",
-				++failures, escaped, expected);
-	}
-
-
-	unescaped = msim_unescape(escaped);
-	g_free(escaped);
-	purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped);
-	if (0 != strcmp(raw, unescaped)) {
-		purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n",
-				++failures, raw, unescaped);
-	}
-
-	return failures;
-}
-#endif
-
-/** Initialize plugin. */
-void 
-init_plugin(PurplePlugin *plugin) 
-{
-	PurpleAccountOption *option;
-#ifdef MSIM_SELF_TEST
-	msim_test_all();
-	exit(0);
-#endif /* MSIM_SELF_TEST */
-
-
-	/* TODO: default to automatically try different ports. Make the user be
-	 * able to set the first port to try (like LastConnectedPort in Windows client).  */
-	option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-
-	option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-
-#ifdef MSIM_USER_WANTS_TO_CONFIGURE_STATUS_TEXT
-	option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-
-	option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-#endif
-
-#ifdef MSIM_USER_WANTS_TO_DISABLE_EMOTICONS
-	option = purple_account_option_bool_new(_("Send emoticons"), "emoticons", FALSE);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-#endif
-
-#ifdef MSIM_USER_REALLY_CARES_ABOUT_PRECISE_FONT_SIZES
-	option = purple_account_option_int_new(_("Screen resolution (dots per inch)"), "dpi", MSIM_DEFAULT_DPI);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-
-	option = purple_account_option_int_new(_("Base font size (points)"), "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
-	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
-#endif
-
-	/* TODO: /zap command. Problem with this is that there are different kinds of zaps,
-	 * and the selection is best made available in a drop-down menu, instead of forcing
-	 * the user to type the kind of zap and memorizing available zaps (or putting it in the
-	 * help menu). A new "attention" API, for zap/buzz/nudge (different protocols) will
-	 * solve this. */
-#if 0
-	purple_cmd_register("zap",                                        /* cmd */
-			"w",                                              /* args - accept a single word */
-			PURPLE_CMD_P_PRPL,                                /* priority */
-			PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_PRPL_ONLY,   /* flags */
-			"prpl-myspace",                                   /* prpl_id */
-			msim_cmd_zap,                                     /* func */
-			_("zap: zap a user to get their attention"),      /* helpstr */
-			NULL);                                            /* data */
-#endif
-}
-
-PURPLE_INIT_PLUGIN(myspace, init_plugin, info);
+/* MySpaceIM Protocol Plugin
+ *
+ * \author Jeff Connelly
+ *
+ * Copyright (C) 2007, Jeff Connelly <jeff2@soc.pidgin.im>
+ *
+ * Based on Purple's "C Plugin HOWTO" hello world example.
+ *
+ * Code also drawn from mockprpl:
+ *  http://snarfed.org/space/purple+mock+protocol+plugin
+ *  Copyright (C) 2004-2007, Ryan Barrett <mockprpl@ryanb.org>
+ *
+ * and some constructs also based on existing Purple plugins, which are:
+ *   Copyright (C) 2003, Robbert Haarman <purple@inglorion.net>
+ *   Copyright (C) 2003, Ethan Blanton <eblanton@cs.purdue.edu>
+ *   Copyright (C) 2000-2003, Rob Flynn <rob@tgflinux.com>
+ *   Copyright (C) 1998-1999, Mark Spencer <markster@marko.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 PURPLE_PLUGIN
+
+#include "message.h"
+#include "persist.h"
+#include "myspace.h"
+
+
+/* Loosely based on Miranda plugin by Scott Ellis, formatting.cpp, 
+ * https://server.scottellis.com.au/websvn/filedetails.php?repname=Miranda+Plugins&path=%2FMySpace%2Fformatting.cpp&rev=0&sc=0 */
+
+/* The names in in emoticon_names (for <i n=whatever>) map to corresponding 
+ * entries in emoticon_symbols (for the ASCII representation of the emoticon).
+ *
+ * Multiple emoticon symbols in Pidgin can map to one name. List the
+ * canonical form, as inserted by the "Smile!" dialog, first. For example,
+ * :) comes before :-), because although both are recognized as 'happy',
+ * the first is inserted by the smiley button. 
+ *
+ * Note that symbols are case-sensitive in Pidgin -- :-X is not :-x. */
+static struct MSIM_EMOTICON
+{
+	gchar *name;
+	gchar *symbol;
+} msim_emoticons[] = {
+	/* Unfortunately, this list duplicates much of the file
+	 * pidgin/pidgin/pixmaps/emotes/default/22/default.theme.in, because
+	 * that file is part of Pidgin, but we're part of libpurple.
+	 */
+	{ "bigsmile", ":D" },
+	{ "bigsmile", ":-D" },
+	{ "devil", "}:)" },
+	{ "frazzled", ":Z" },
+	{ "geek", "B)" },
+	{ "googles", "%)" },
+	{ "growl", ":E" },
+	{ "laugh", ":))" },		/* Must be before ':)' */
+	{ "happy", ":)" },
+	{ "happy", ":-)" },
+	{ "happi", ":)" },
+	{ "heart", ":X" },
+	{ "mohawk", "-:" },
+	{ "mad", "X(" },
+	{ "messed", "X)" },
+	{ "nerd", "Q)" },
+	{ "oops", ":G" },
+	{ "pirate", "P)" },
+	{ "scared", ":O" },
+	{ "sidefrown", ":{" },
+	{ "sinister", ":B" },
+	{ "smirk", ":," },
+	{ "straight", ":|" },
+	{ "tongue", ":P" },
+	{ "tongue", ":p" },
+	{ "tongy", ":P" },
+	{ "upset", "B|" },
+	{ "wink", ";-)" },
+	{ "wink", ";)" },
+	{ "winc", ";)" },
+	{ "worried", ":[" },
+	{ "kiss", ":x" },
+	{ NULL, NULL }
+};
+
+/* Internal functions */
+static void msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr);
+
+#ifdef MSIM_DEBUG_MSG
+static void print_hash_item(gpointer key, gpointer value, gpointer user_data);
+#endif
+
+static int msim_send_really_raw(PurpleConnection *gc, const char *buf,
+		int total_bytes);
+static gboolean msim_login_challenge(MsimSession *session, MsimMessage *msg);
+static const gchar *msim_compute_login_response(
+		const gchar nonce[2 * NONCE_SIZE], const gchar *email, 
+		const gchar *password, guint *response_len);
+static gboolean msim_send_bm(MsimSession *session, const gchar *who, 
+		const gchar *text, int type);
+
+static guint msim_point_to_purple_size(MsimSession *session, guint point);
+static guint msim_purple_size_to_point(MsimSession *session, guint size);
+static guint msim_height_to_point(MsimSession *session, guint height);
+static guint msim_point_to_height(MsimSession *session, guint point);
+
+static void msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note);
+
+static void msim_markup_tag_to_html(MsimSession *, xmlnode *root, 
+		gchar **begin, gchar **end);
+static void html_tag_to_msim_markup(MsimSession *, xmlnode *root, 
+		gchar **begin, gchar **end);
+static gchar *msim_convert_xml(MsimSession *, const gchar *raw, 
+		MSIM_XMLNODE_CONVERT f);
+static gchar *msim_convert_smileys_to_markup(gchar *before);
+
+/* High-level msim markup <=> html conversion functions. */
+static gchar *msim_markup_to_html(MsimSession *, const gchar *raw);
+static gchar *html_to_msim_markup(MsimSession *, const gchar *raw);
+
+static MsimUser *msim_get_user_from_buddy(PurpleBuddy *buddy);
+static MsimUser *msim_find_user(MsimSession *session, const gchar *username);
+
+static gboolean msim_incoming_bm_record_cv(MsimSession *session, 
+		MsimMessage *msg);
+static gboolean msim_incoming_bm(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_status(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_im(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_zap(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_action(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_media(MsimSession *session, MsimMessage *msg);
+static gboolean msim_incoming_unofficial_client(MsimSession *session, 
+		MsimMessage *msg);
+
+#ifdef MSIM_SEND_CLIENT_VERSION
+static gboolean msim_send_unofficial_client(MsimSession *session, 
+		gchar *username);
+#endif
+
+static void msim_get_info_cb(MsimSession *session, MsimMessage *userinfo, gpointer data);
+static gchar *msim_format_now_playing(gchar *band, gchar *song);
+
+static void msim_set_status_code(MsimSession *session, guint code, 
+		gchar *statstring);
+
+static void msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full);
+
+static void msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data, gpointer user_data, const gchar *url_text,
+		gsize len, const gchar *error_message);
+
+static void msim_store_user_info_each(gpointer key, gpointer value, 
+		gpointer user_data);
+static gboolean msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user);
+static gboolean msim_process_server_info(MsimSession *session, 
+		MsimMessage *msg);
+static gboolean msim_web_challenge(MsimSession *session, MsimMessage *msg); 
+static gboolean msim_process_reply(MsimSession *session, MsimMessage *msg);
+
+static gboolean msim_preprocess_incoming(MsimSession *session,MsimMessage *msg);
+
+#ifdef MSIM_USE_KEEPALIVE
+static gboolean msim_check_alive(gpointer data);
+#endif
+
+static gboolean msim_we_are_logged_on(MsimSession *session, MsimMessage *msg);
+
+static gboolean msim_process(MsimSession *session, MsimMessage *msg);
+
+static MsimMessage *msim_do_postprocessing(MsimMessage *msg, 
+		const gchar *uid_field_name, const gchar *uid_before, guint uid);
+static void msim_postprocess_outgoing_cb(MsimSession *session, 
+		MsimMessage *userinfo, gpointer data);
+static gboolean msim_postprocess_outgoing(MsimSession *session, 
+		MsimMessage *msg, const gchar *username, const gchar *uid_field_name, 
+		const gchar *uid_before); 
+
+static gboolean msim_error(MsimSession *session, MsimMessage *msg);
+
+static void msim_check_inbox_cb(MsimSession *session, MsimMessage *userinfo, 
+		gpointer data);
+static gboolean msim_check_inbox(gpointer data);
+
+static void msim_input_cb(gpointer gc_uncasted, gint source, 
+		PurpleInputCondition cond);
+
+static guint msim_new_reply_callback(MsimSession *session, 
+		MSIM_USER_LOOKUP_CB cb, gpointer data);
+
+static void msim_connect_cb(gpointer data, gint source, 
+		const gchar *error_message);
+
+static gboolean msim_is_userid(const gchar *user);
+static gboolean msim_is_email(const gchar *user);
+
+static void msim_lookup_user(MsimSession *session, const gchar *user, 
+		MSIM_USER_LOOKUP_CB cb, gpointer data);
+
+double msim_round(double round);
+
+/* round is part of C99, but sometimes is unavailable before then.
+ * Based on http://forums.belution.com/en/cpp/000/050/13.shtml
+ */
+double msim_round(double value)
+{
+	if (value < 0) {
+		return -(floor(-value + 0.5));
+	} else {
+		return   floor( value + 0.5);
+	}
+}
+
+/** 
+ * Load the plugin.
+ */
+gboolean 
+msim_load(PurplePlugin *plugin)
+{
+	/* If compiled to use RC4 from libpurple, check if it is really there. */
+	if (!purple_ciphers_find_cipher("rc4")) {
+		purple_debug_error("msim", "rc4 not in libpurple, but it is required - not loading MySpaceIM plugin!\n");
+		purple_notify_error(plugin, _("Missing Cipher"), 
+				_("The RC4 cipher could not be found"),
+				_("Upgrade "
+					"to a libpurple with RC4 support (>= 2.0.1). MySpaceIM "
+					"plugin will not be loaded."));
+		return FALSE;
+	}
+	return TRUE;
+}
+
+/**
+ * Get possible user status types. Based on mockprpl.
+ *
+ * @return GList of status types.
+ */
+GList *
+msim_status_types(PurpleAccount *acct)
+{
+	GList *types;
+	PurpleStatusType *status;
+
+	purple_debug_info("myspace", "returning status types\n");
+
+	types = NULL;
+
+    /* Statuses are almost all the same. Define a macro to reduce code repetition. */
+#define _MSIM_ADD_NEW_STATUS(prim) status =                         \
+	purple_status_type_new_with_attrs(                          \
+	prim,   /* PurpleStatusPrimitive */                         \
+	NULL,   /* id - use default */                              \
+	NULL,   /* name - use default */                            \
+	TRUE,   /* savable */                                       \
+	TRUE,   /* user_settable */                                 \
+	FALSE,  /* not independent */                               \
+	                                                            \
+	/* Attributes - each status can have a message. */          \
+	"message",                                                  \
+	_("Message"),                                               \
+	purple_value_new(PURPLE_TYPE_STRING),                       \
+	NULL);                                                      \
+	                                                            \
+	                                                            \
+	types = g_list_append(types, status)
+
+
+	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AVAILABLE);
+	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_AWAY);
+	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_OFFLINE);
+	_MSIM_ADD_NEW_STATUS(PURPLE_STATUS_INVISIBLE);
+
+
+	return types;
+}
+
+/** Zap someone. Callback from msim_blist_node_menu zap menu. */
+static void
+msim_send_zap(PurpleBlistNode *node, gpointer zap_num_ptr)
+{
+	PurpleBuddy *buddy;
+	PurpleConnection *gc;
+	MsimSession *session;
+	gchar *username, *zap_string, *zap_text;
+	guint zap;
+	const gchar *zap_gerund[10];
+
+	if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) {
+		/* Only know about buddies for now. */
+		return;
+	}
+
+	zap_gerund[0] = _("Zapping");
+	zap_gerund[1] = _("Whacking");
+	zap_gerund[2] = _("Torching");
+	zap_gerund[3] = _("Smooching");
+	zap_gerund[4] = _("Hugging");
+	zap_gerund[5] = _("Bslapping");
+	zap_gerund[6] = _("Goosing");
+	zap_gerund[7] = _("Hi-fiving");
+	zap_gerund[8] = _("Punking");
+	zap_gerund[9] = _("Raspberry'ing");
+ 
+	g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node));
+
+	buddy = (PurpleBuddy *)node;
+	gc = purple_account_get_connection(buddy->account);
+	g_return_if_fail(gc != NULL);
+
+	session = (MsimSession *)gc->proto_data;
+	g_return_if_fail(session != NULL);
+
+	username = buddy->name;
+	g_return_if_fail(username != NULL);
+
+	zap = GPOINTER_TO_INT(zap_num_ptr);
+	zap_string = g_strdup_printf("!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", zap);
+	zap_text = g_strdup_printf("*** %s! ***", zap_gerund[zap]);
+
+	serv_got_im(session->gc, username, zap_text, 
+			PURPLE_MESSAGE_SEND | PURPLE_MESSAGE_SYSTEM, time(NULL));
+
+	if (!msim_send_bm(session, username, zap_string, MSIM_BM_ACTION)) {
+		purple_debug_info("msim_send_zap", "msim_send_bm failed: zapping %s with %s",
+				username, zap_string);
+	}
+
+	g_free(zap_string);
+	g_free(zap_text);
+	return;
+}
+
+
+/** Return menu, if any, for a buddy list node. */
+GList *
+msim_blist_node_menu(PurpleBlistNode *node)
+{
+	GList *menu, *zap_menu;
+	PurpleMenuAction *act;
+	const gchar *zap_names[10];
+	guint i;
+
+	if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) {
+		/* Only know about buddies for now. */
+		return NULL;
+	}
+
+	/* Names from official client. */
+	zap_names[0] = _("zap");
+	zap_names[1] = _("whack");
+	zap_names[2] = _("torch");
+	zap_names[3] = _("smooch");
+	zap_names[4] = _("hug");
+	zap_names[5] = _("bslap");
+	zap_names[6] = _("goose");
+	zap_names[7] = _("hi-five");
+	zap_names[8] = _("punk'd");
+	zap_names[9] = _("raspberry");
+ 
+	menu = zap_menu = NULL;
+
+	/* TODO: move to / command, or better yet new API  */
+	for (i = 0; i < sizeof(zap_names) / sizeof(zap_names[0]); ++i) {
+		act = purple_menu_action_new(zap_names[i], PURPLE_CALLBACK(msim_send_zap),
+				GUINT_TO_POINTER(i), NULL);
+		zap_menu = g_list_append(zap_menu, act);
+	}
+
+	act = purple_menu_action_new(_("Zap"), NULL, NULL, zap_menu);
+	menu = g_list_append(menu, act);
+
+	return menu;
+}
+
+/**
+ * Return the icon name for a buddy and account.
+ *
+ * @param acct The account to find the icon for, or NULL for protocol icon.
+ * @param buddy The buddy to find the icon for, or NULL for the account icon.
+ *
+ * @return The base icon name string.
+ */
+const gchar *
+msim_list_icon(PurpleAccount *acct, PurpleBuddy *buddy)
+{
+	/* Use a MySpace icon submitted by hbons at
+	 * http://developer.pidgin.im/wiki/MySpaceIM. */
+	return "myspace";
+}
+
+/**
+ * Replace 'old' with 'new' in 'str'.
+ *
+ * @param str The original string.
+ * @param old The substring of 'str' to replace.
+ * @param new The replacement for 'old' within 'str'.
+ *
+ * @return A _new_ string, based on 'str', with 'old' replaced
+ *         by 'new'. Must be g_free()'d by caller.
+ *
+ * This string replace method is based on
+ * http://mail.gnome.org/archives/gtk-app-devel-list/2000-July/msg00201.html
+ *
+ */
+gchar *
+str_replace(const gchar *str, const gchar *old, const gchar *new)
+{
+	gchar **items;
+	gchar *ret;
+
+	items = g_strsplit(str, old, -1);
+	ret = g_strjoinv(new, items);
+	g_free(items);
+	return ret;
+}
+
+#ifdef MSIM_DEBUG_MSG
+static void 
+print_hash_item(gpointer key, gpointer value, gpointer user_data)
+{
+	purple_debug_info("msim", "%s=%s\n",
+			key ? (gchar *)key : "(NULL)", 
+			value ? (gchar *)value : "(NULL)");
+}
+#endif
+
+/** 
+ * Send raw data (given as a NUL-terminated string) to the server.
+ *
+ * @param session 
+ * @param msg The raw data to send, in a NUL-terminated string.
+ *
+ * @return TRUE if succeeded, FALSE if not.
+ *
+ */
+gboolean 
+msim_send_raw(MsimSession *session, const gchar *msg)
+{
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+	
+	purple_debug_info("msim", "msim_send_raw: writing <%s>\n", msg);
+
+	return msim_send_really_raw(session->gc, msg, strlen(msg)) ==
+		strlen(msg);
+}
+
+/** Send raw data to the server, possibly with embedded NULs. 
+ *
+ * Used in prpl_info struct, so that plugins can have the most possible
+ * control of what is sent over the connection. Inside this prpl, 
+ * msim_send_raw() is used, since it sends NUL-terminated strings (easier).
+ *
+ * @param gc PurpleConnection
+ * @param buf Buffer to send
+ * @param total_bytes Size of buffer to send
+ *
+ * @return Bytes successfully sent, or -1 on error.
+ */
+static int 
+msim_send_really_raw(PurpleConnection *gc, const char *buf, int total_bytes)
+{
+	int total_bytes_sent;
+	MsimSession *session;
+
+	g_return_val_if_fail(gc != NULL, -1);
+	g_return_val_if_fail(buf != NULL, -1);
+	g_return_val_if_fail(total_bytes >= 0, -1);
+
+	session = (MsimSession *)gc->proto_data;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
+	
+	/* Loop until all data is sent, or a failure occurs. */
+	total_bytes_sent = 0;
+	do {
+		int bytes_sent;
+
+		bytes_sent = send(session->fd, buf + total_bytes_sent, 
+				total_bytes - total_bytes_sent, 0);
+
+		if (bytes_sent < 0) {
+			purple_debug_info("msim", "msim_send_raw(%s): send() failed: %s\n",
+					buf, g_strerror(errno));
+			return total_bytes_sent;
+		}
+		total_bytes_sent += bytes_sent;
+
+	} while(total_bytes_sent < total_bytes);
+
+	return total_bytes_sent;
+}
+
+
+/** 
+ * Start logging in to the MSIM servers.
+ * 
+ * @param acct Account information to use to login.
+ */
+void 
+msim_login(PurpleAccount *acct)
+{
+	PurpleConnection *gc;
+	const gchar *host;
+	int port;
+
+	g_return_if_fail(acct != NULL);
+	g_return_if_fail(acct->username != NULL);
+
+	purple_debug_info("msim", "logging in %s\n", acct->username);
+
+	gc = purple_account_get_connection(acct);
+	gc->proto_data = msim_session_new(acct);
+	gc->flags |= PURPLE_CONNECTION_HTML | PURPLE_CONNECTION_NO_URLDESC;
+
+	/* Passwords are limited in length. */
+	if (strlen(acct->password) > MSIM_MAX_PASSWORD_LENGTH) {
+		gchar *str;
+
+		str = g_strdup_printf(
+				_("Sorry, passwords over %d characters in length (yours is "
+				"%d) are not supported by MySpace."), 
+				MSIM_MAX_PASSWORD_LENGTH,
+				(int)strlen(acct->password));
+
+		/* Notify an error message also, because this is important! */
+		purple_notify_error(acct, g_strdup(_("MySpaceIM Error")), str, NULL);
+
+		purple_connection_error(gc, str);
+		
+		g_free(str);
+	}
+
+	/* 1. connect to server */
+	purple_connection_update_progress(gc, _("Connecting"),
+								  0,   /* which connection step this is */
+								  4);  /* total number of steps */
+
+	host = purple_account_get_string(acct, "server", MSIM_SERVER);
+	port = purple_account_get_int(acct, "port", MSIM_PORT);
+
+	/* From purple.sf.net/api:
+	 * """Note that this function name can be misleading--although it is called 
+	 * "proxy connect," it is used for establishing any outgoing TCP connection, 
+	 * whether through a proxy or not.""" */
+
+	/* Calls msim_connect_cb when connected. */
+	if (!purple_proxy_connect(gc, acct, host, port, msim_connect_cb, gc)) {
+		/* TODO: try other ports if in auto mode, then save
+		 * working port and try that first next time. */
+		purple_connection_error(gc, _("Couldn't create socket"));
+		return;
+	}
+}
+
+/**
+ * Process a login challenge, sending a response. 
+ *
+ * @param session 
+ * @param msg Login challenge message.
+ *
+ * @return TRUE if successful, FALSE if not
+ */
+static gboolean 
+msim_login_challenge(MsimSession *session, MsimMessage *msg) 
+{
+	PurpleAccount *account;
+	const gchar *response;
+	guint response_len;
+	gchar *nc;
+	gsize nc_len;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	g_return_val_if_fail(msim_msg_get_binary(msg, "nc", &nc, &nc_len), FALSE);
+
+	account = session->account;
+
+	g_return_val_if_fail(account != NULL, FALSE);
+
+	purple_connection_update_progress(session->gc, _("Reading challenge"), 1, 4);
+
+	purple_debug_info("msim", "nc is %d bytes, decoded\n", nc_len);
+
+	if (nc_len != MSIM_AUTH_CHALLENGE_LENGTH) {
+		purple_debug_info("msim", "bad nc length: %x != 0x%x\n", nc_len, MSIM_AUTH_CHALLENGE_LENGTH);
+		purple_connection_error(session->gc, _("Unexpected challenge length from server"));
+		return FALSE;
+	}
+
+	purple_connection_update_progress(session->gc, _("Logging in"), 2, 4);
+
+	response_len = 0;
+	response = msim_compute_login_response(nc, account->username, account->password, &response_len);
+
+	g_free(nc);
+
+	return msim_send(session, 
+			"login2", MSIM_TYPE_INTEGER, MSIM_AUTH_ALGORITHM,
+			/* This is actually user's email address. */
+			"username", MSIM_TYPE_STRING, g_strdup(account->username),
+			/* GString and gchar * response will be freed in msim_msg_free() in msim_send(). */
+			"response", MSIM_TYPE_BINARY, g_string_new_len(response, response_len),
+			"clientver", MSIM_TYPE_INTEGER, MSIM_CLIENT_VERSION,
+			"langid", MSIM_TYPE_INTEGER, MSIM_LANGUAGE_ID_ENGLISH,
+			"imlang", MSIM_TYPE_STRING, g_strdup(MSIM_LANGUAGE_NAME_ENGLISH),
+			"reconn", MSIM_TYPE_INTEGER, 0,
+			"status", MSIM_TYPE_INTEGER, 100,
+			"id", MSIM_TYPE_INTEGER, 1,
+			NULL);
+}
+
+/**
+ * Compute the base64'd login challenge response based on username, password, nonce, and IPs.
+ *
+ * @param nonce The base64 encoded nonce ('nc') field from the server.
+ * @param email User's email address (used as login name).
+ * @param password User's cleartext password.
+ * @param response_len Will be written with response length.
+ *
+ * @return Binary login challenge response, ready to send to the server. 
+ * Must be g_free()'d when finished. NULL if error.
+ */
+static const gchar *
+msim_compute_login_response(const gchar nonce[2 * NONCE_SIZE], 
+		const gchar *email, const gchar *password, guint *response_len)
+{
+	PurpleCipherContext *key_context;
+	PurpleCipher *sha1;
+	PurpleCipherContext *rc4;
+
+	guchar hash_pw[HASH_SIZE];
+	guchar key[HASH_SIZE];
+	gchar *password_utf16le, *password_utf8_lc;
+	guchar *data;
+	guchar *data_out;
+	size_t data_len, data_out_len;
+	gsize conv_bytes_read, conv_bytes_written;
+	GError *conv_error;
+#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
+	int i;
+#endif
+
+	g_return_val_if_fail(nonce != NULL, NULL);
+	g_return_val_if_fail(email != NULL, NULL);
+	g_return_val_if_fail(password != NULL, NULL);
+	g_return_val_if_fail(response_len != NULL, NULL);
+
+	/* Convert password to lowercase (required for passwords containing
+	 * uppercase characters). MySpace passwords are lowercase,
+	 * see ticket #2066. */
+	password_utf8_lc = g_utf8_strdown(password, -1);
+
+	/* Convert ASCII password to UTF16 little endian */
+	purple_debug_info("msim", "converting password to UTF-16LE\n");
+	conv_error = NULL;
+	password_utf16le = g_convert(password_utf8_lc, -1, "UTF-16LE", "UTF-8", 
+			&conv_bytes_read, &conv_bytes_written, &conv_error);
+	g_free(password_utf8_lc);
+
+	g_return_val_if_fail(conv_bytes_read == strlen(password), NULL);
+
+	if (conv_error != NULL) {
+		purple_debug_error("msim", 
+				"g_convert password UTF8->UTF16LE failed: %s",
+				conv_error->message);
+		g_error_free(conv_error);
+		return NULL;
+	}
+
+	/* Compute password hash */ 
+	purple_cipher_digest_region("sha1", (guchar *)password_utf16le, 
+			conv_bytes_written, sizeof(hash_pw), hash_pw, NULL);
+	g_free(password_utf16le);
+
+#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
+	purple_debug_info("msim", "pwhash = ");
+	for (i = 0; i < sizeof(hash_pw); i++)
+		purple_debug_info("msim", "%.2x ", hash_pw[i]);
+	purple_debug_info("msim", "\n");
+#endif
+
+	/* key = sha1(sha1(pw) + nonce2) */
+	sha1 = purple_ciphers_find_cipher("sha1");
+	key_context = purple_cipher_context_new(sha1, NULL);
+	purple_cipher_context_append(key_context, hash_pw, HASH_SIZE);
+	purple_cipher_context_append(key_context, (guchar *)(nonce + NONCE_SIZE), NONCE_SIZE);
+	purple_cipher_context_digest(key_context, sizeof(key), key, NULL);
+
+#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
+	purple_debug_info("msim", "key = ");
+	for (i = 0; i < sizeof(key); i++) {
+		purple_debug_info("msim", "%.2x ", key[i]);
+	}
+	purple_debug_info("msim", "\n");
+#endif
+
+	rc4 = purple_cipher_context_new_by_name("rc4", NULL);
+
+	/* Note: 'key' variable is 0x14 bytes (from SHA-1 hash), 
+	 * but only first 0x10 used for the RC4 key. */
+	purple_cipher_context_set_option(rc4, "key_len", (gpointer)0x10);
+	purple_cipher_context_set_key(rc4, key);
+
+	/* TODO: obtain IPs of network interfaces */
+
+	/* rc4 encrypt:
+	 * nonce1+email+IP list */
+
+	data_len = NONCE_SIZE + strlen(email) + MSIM_LOGIN_IP_LIST_LEN;
+	data = g_new0(guchar, data_len);
+	memcpy(data, nonce, NONCE_SIZE);
+	memcpy(data + NONCE_SIZE, email, strlen(email));
+	memcpy(data + NONCE_SIZE + strlen(email), MSIM_LOGIN_IP_LIST, MSIM_LOGIN_IP_LIST_LEN);
+
+	data_out = g_new0(guchar, data_len);
+
+	purple_cipher_context_encrypt(rc4, (const guchar *)data, 
+			data_len, data_out, &data_out_len);
+	purple_cipher_context_destroy(rc4);
+
+	g_assert(data_out_len == data_len);
+
+#ifdef MSIM_DEBUG_LOGIN_CHALLENGE
+	purple_debug_info("msim", "response=<%s>\n", data_out);
+#endif
+
+	*response_len = data_out_len;
+
+	return (const gchar *)data_out;
+}
+
+/**
+ * Schedule an IM to be sent once the user ID is looked up. 
+ *
+ * @param gc Connection.
+ * @param who A user id, email, or username to send the message to.
+ * @param message Instant message text to send.
+ * @param flags Flags.
+ *
+ * @return 1 if successful or postponed, -1 if failed
+ *
+ * Allows sending to a user by username, email address, or userid. If
+ * a username or email address is given, the userid must be looked up.
+ * This function does that by calling msim_postprocess_outgoing().
+ */
+int 
+msim_send_im(PurpleConnection *gc, const gchar *who, const gchar *message, 
+		PurpleMessageFlags flags)
+{
+	MsimSession *session;
+	gchar *message_msim;
+	int rc;
+	
+	g_return_val_if_fail(gc != NULL, -1);
+	g_return_val_if_fail(who != NULL, -1);
+	g_return_val_if_fail(message != NULL, -1);
+
+	/* 'flags' has many options, not used here. */
+
+	session = (MsimSession *)gc->proto_data;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
+
+	message_msim = html_to_msim_markup(session, message);
+
+	if (msim_send_bm(session, who, message_msim, MSIM_BM_INSTANT)) {
+		/* Return 1 to have Purple show this IM as being sent, 0 to not. I always
+		 * return 1 even if the message could not be sent, since I don't know if
+		 * it has failed yet--because the IM is only sent after the userid is
+		 * retrieved from the server (which happens after this function returns).
+		 */
+		/* TODO: maybe if message is delayed, don't echo to conv window,
+		 * but do echo it to conv window manually once it is actually
+		 * sent? Would be complicated. */
+		rc = 1;
+	} else {
+		rc = -1;
+	}
+
+	g_free(message_msim);
+
+	/*
+	 * In MySpace, you login with your email address, but don't talk to other
+	 * users using their email address. So there is currently an asymmetry in the 
+	 * IM windows when using this plugin:
+	 *
+	 * you@example.com: hello
+	 * some_other_user: what's going on?
+	 * you@example.com: just coding a prpl
+	 *
+	 * TODO: Make the sent IM's appear as from the user's username, instead of
+	 * their email address. Purple uses the login (in MSIM, the email)--change this.
+	 */
+
+	return rc;
+}
+
+/** Send a buddy message of a given type.
+ *
+ * @param session
+ * @param who Username to send message to.
+ * @param text Message text to send. Not freed; will be copied.
+ * @param type A MSIM_BM_* constant.
+ *
+ * @return TRUE if success, FALSE if fail.
+ *
+ * Buddy messages ('bm') include instant messages, action messages, status messages, etc.
+ *
+ */
+static gboolean 
+msim_send_bm(MsimSession *session, const gchar *who, const gchar *text, 
+		int type)
+{
+	gboolean rc;
+	MsimMessage *msg;
+	const gchar *from_username;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(who != NULL, FALSE);
+	g_return_val_if_fail(text != NULL, FALSE);
+   
+	from_username = session->account->username;
+
+	g_return_val_if_fail(from_username != NULL, FALSE);
+
+	purple_debug_info("msim", "sending %d message from %s to %s: %s\n",
+				  type, from_username, who, text);
+
+	msg = msim_msg_new(
+            "bm", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(type),
+			"sesskey", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(session->sesskey),
+			/* 't' will be inserted here */
+			"cv", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(MSIM_CLIENT_VERSION),
+			"msg", MSIM_TYPE_STRING, g_strdup(text),
+			NULL);
+
+	rc = msim_postprocess_outgoing(session, msg, who, "t", "cv");
+
+	msim_msg_free(msg);
+
+	return rc;
+}
+
+/* Indexes of this array + 1 map HTML font size to scale of normal font size. *
+ * Based on _point_sizes from libpurple/gtkimhtml.c 
+ *                                 1    2  3    4     5      6       7 */
+static gdouble _font_scale[] = { .85, .95, 1, 1.2, 1.44, 1.728, 2.0736 };
+
+#define MAX_FONT_SIZE                   7       /* Purple maximum font size */
+#define POINTS_PER_INCH                 72      /* How many pt's in an inch */
+
+/** Convert typographical font point size to HTML font size. 
+ * Based on libpurple/gtkimhtml.c */
+static guint
+msim_point_to_purple_size(MsimSession *session, guint point)
+{
+	guint size, this_point, base;
+	gdouble scale;
+	
+	base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
+   
+	for (size = 0; 
+			size < sizeof(_font_scale) / sizeof(_font_scale[0]);
+			++size) {
+		scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1];
+		this_point = (guint)msim_round(scale * base);
+
+		if (this_point >= point) {
+			purple_debug_info("msim", "msim_point_to_purple_size: %d pt -> size=%d\n",
+					point, size);
+			return size;
+		}
+	}
+
+	/* No HTML font size was this big; return largest possible. */
+	return this_point;
+}
+
+/** Convert HTML font size to point size. */
+static guint
+msim_purple_size_to_point(MsimSession *session, guint size)
+{
+	gdouble scale;
+	guint point;
+	guint base;
+
+	scale = _font_scale[CLAMP(size, 1, MAX_FONT_SIZE) - 1];
+
+	base = purple_account_get_int(session->account, "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
+
+	point = (guint)msim_round(scale * base);
+
+	purple_debug_info("msim", "msim_purple_size_to_point: size=%d -> %d pt\n",
+					size, point);
+
+	return point;
+}
+
+/** Convert a msim markup font pixel height to the more usual point size, for incoming messages. */
+static guint 
+msim_height_to_point(MsimSession *session, guint height)
+{
+	guint dpi;
+
+	dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI);
+
+	return (guint)msim_round((POINTS_PER_INCH * 1. / dpi) * height);
+
+	/* See also: libpurple/protocols/bonjour/jabber.c
+	 * _font_size_ichat_to_purple */
+}
+
+/** Convert point size to msim pixel height font size specification, for outgoing messages. */
+static guint
+msim_point_to_height(MsimSession *session, guint point)
+{
+	guint dpi;
+
+	dpi = purple_account_get_int(session->account, "port", MSIM_DEFAULT_DPI);
+
+	return (guint)msim_round((dpi * 1. / POINTS_PER_INCH) * point);
+}
+
+/** Convert the msim markup <f> (font) tag into HTML. */
+static void 
+msim_markup_f_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	const gchar *face, *height_str, *decor_str;
+	GString *gs_end, *gs_begin;
+	guint decor, height;
+
+	face = xmlnode_get_attrib(root, "f");
+	height_str = xmlnode_get_attrib(root, "h");
+	decor_str = xmlnode_get_attrib(root, "s");
+
+	if (height_str) {
+		height = atol(height_str);
+	} else {
+		height = 12;
+	}
+
+	if (decor_str) {
+		decor = atol(decor_str);
+	} else {
+		decor = 0;
+	}
+
+	gs_begin = g_string_new("");
+	/* TODO: get font size working */
+	if (height && !face) {
+		g_string_printf(gs_begin, "<font size='%d'>", 
+				msim_point_to_purple_size(session, msim_height_to_point(session, height)));
+	} else if (height && face) {
+		g_string_printf(gs_begin, "<font face='%s' size='%d'>", face,  
+				msim_point_to_purple_size(session, msim_height_to_point(session, height)));
+	} else {
+		g_string_printf(gs_begin, "<font>");
+	}
+
+	/* No support for font-size CSS? */
+	/* g_string_printf(gs_begin, "<span style='font-family: %s; font-size: %dpt'>", face, 
+			msim_height_to_point(height)); */
+
+	gs_end = g_string_new("</font>");
+
+	if (decor & MSIM_TEXT_BOLD) {
+		g_string_append(gs_begin, "<b>");
+		g_string_prepend(gs_end, "</b>");
+	}
+
+	if (decor & MSIM_TEXT_ITALIC) {
+		g_string_append(gs_begin, "<i>");
+		g_string_append(gs_end, "</i>");
+	}
+
+	if (decor & MSIM_TEXT_UNDERLINE) {
+		g_string_append(gs_begin, "<u>");
+		g_string_append(gs_end, "</u>");
+	}
+
+
+	*begin = gs_begin->str;
+	*end = gs_end->str;
+}
+
+/** Convert a msim markup color to a color suitable for libpurple.
+  *
+  * @param msim Either a color name, or an rgb(x,y,z) code.
+  *
+  * @return A new string, either a color name or #rrggbb code. Must g_free(). 
+  */
+static char *
+msim_color_to_purple(const char *msim)
+{
+	guint red, green, blue;
+
+	if (!msim) {
+		return g_strdup("black");
+	}
+
+	if (sscanf(msim, "rgb(%d,%d,%d)", &red, &green, &blue) != 3) {
+		/* Color name. */
+		return g_strdup(msim);
+	}
+	/* TODO: rgba (alpha). */
+
+	return g_strdup_printf("#%.2x%.2x%.2x", red, green, blue);
+}
+
+/** Convert the msim markup <a> (anchor) tag into HTML. */
+static void 
+msim_markup_a_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	const gchar *href;
+
+	href = xmlnode_get_attrib(root, "h");
+	if (!href) {
+		href = "";
+	}
+
+	*begin = g_strdup_printf("<a href=\"%s\">%s", href, href);
+	*end = g_strdup("</a>");
+}
+
+/** Convert the msim markup <p> (paragraph) tag into HTML. */
+static void 
+msim_markup_p_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	/* Just pass through unchanged. 
+	 *
+	 * Note: attributes currently aren't passed, if there are any. */
+	*begin = g_strdup("<p>");
+	*end = g_strdup("</p>");
+}
+
+/** Convert the msim markup <c> tag (text color) into HTML. TODO: Test */
+static void 
+msim_markup_c_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	const gchar *color;
+	gchar *purple_color;
+
+	color = xmlnode_get_attrib(root, "v");
+	if (!color) {
+		purple_debug_info("msim", "msim_markup_c_to_html: <c> tag w/o v attr");
+		*begin = g_strdup("");
+		*end = g_strdup("");
+		/* TODO: log as unrecognized */
+		return;
+	}
+
+	purple_color = msim_color_to_purple(color);
+
+	*begin = g_strdup_printf("<font color='%s'>", purple_color); 
+
+	g_free(purple_color);
+
+	/* *begin = g_strdup_printf("<span style='color: %s'>", color); */
+	*end = g_strdup("</font>");
+}
+
+/** Convert the msim markup <b> tag (background color) into HTML. TODO: Test */
+static void 
+msim_markup_b_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	const gchar *color;
+	gchar *purple_color;
+
+	color = xmlnode_get_attrib(root, "v");
+	if (!color) {
+		*begin = g_strdup("");
+		*end = g_strdup("");
+		purple_debug_info("msim", "msim_markup_b_to_html: <b> w/o v attr");
+		/* TODO: log as unrecognized. */
+		return;
+	}
+
+	purple_color = msim_color_to_purple(color);
+
+	/* TODO: find out how to set background color. */
+	*begin = g_strdup_printf("<span style='background-color: %s'>", 
+			purple_color);
+	g_free(purple_color);
+
+	*end = g_strdup("</p>");
+}
+
+/** Convert the msim markup <i> tag (emoticon image) into HTML. */
+static void 
+msim_markup_i_to_html(MsimSession *session, xmlnode *root, gchar **begin, gchar **end)
+{
+	const gchar *name;
+	guint i;
+	struct MSIM_EMOTICON *emote;
+
+	name = xmlnode_get_attrib(root, "n");
+	if (!name) {
+		purple_debug_info("msim", "msim_markup_i_to_html: <i> w/o n");
+		*begin = g_strdup("");
+		*end = g_strdup("");
+		/* TODO: log as unrecognized */
+		return;
+	}
+
+	/* Find and use canonical form of smiley symbol. */
+	for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) {
+		if (!strcmp(name, emote->name)) {
+			*begin = g_strdup(emote->symbol);
+			*end = g_strdup("");
+			return;
+		}
+	}
+
+	/* Couldn't find it, sorry. Try to degrade gracefully. */
+	*begin = g_strdup_printf("**%s**", name);
+	*end = g_strdup("");
+}
+
+/** Convert an individual msim markup tag to HTML. */
+static void 
+msim_markup_tag_to_html(MsimSession *session, xmlnode *root, gchar **begin, 
+		gchar **end)
+{
+	if (!strcmp(root->name, "f")) {
+		msim_markup_f_to_html(session, root, begin, end);
+	} else if (!strcmp(root->name, "a")) {
+		msim_markup_a_to_html(session, root, begin, end);
+	} else if (!strcmp(root->name, "p")) {
+		msim_markup_p_to_html(session, root, begin, end);
+	} else if (!strcmp(root->name, "c")) {
+		msim_markup_c_to_html(session, root, begin, end);
+	} else if (!strcmp(root->name, "b")) {
+		msim_markup_b_to_html(session, root, begin, end);
+	} else if (!strcmp(root->name, "i")) {
+		msim_markup_i_to_html(session, root, begin, end);
+	} else {
+		purple_debug_info("msim", "msim_markup_tag_to_html: "
+				"unknown tag name=%s, ignoring", 
+				(root && root->name) ? root->name : "(NULL)");
+		*begin = g_strdup("");
+		*end = g_strdup("");
+	}
+}
+
+/** Convert an individual HTML tag to msim markup. */
+static void 
+html_tag_to_msim_markup(MsimSession *session, xmlnode *root, gchar **begin, 
+		gchar **end)
+{
+	/* TODO: Coalesce nested tags into one <f> tag!
+	 * Currently, the 's' value will be overwritten when b/i/u is nested
+	 * within another one, and only the inner-most formatting will be 
+	 * applied to the text. */
+	if (!purple_utf8_strcasecmp(root->name, "root")) {
+		*begin = g_strdup("");
+		*end = g_strdup("");
+	} else if (!purple_utf8_strcasecmp(root->name, "b")) {
+		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_BOLD);
+		*end = g_strdup("</f>");
+	} else if (!purple_utf8_strcasecmp(root->name, "i")) {
+		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_ITALIC);
+		*end = g_strdup("</f>");
+	} else if (!purple_utf8_strcasecmp(root->name, "u")) {
+		*begin = g_strdup_printf("<f s='%d'>", MSIM_TEXT_UNDERLINE);
+		*end = g_strdup("</f>");
+	} else if (!purple_utf8_strcasecmp(root->name, "a")) {
+		const gchar *href, *link_text;
+
+		href = xmlnode_get_attrib(root, "href");
+
+		if (!href) {
+			href = xmlnode_get_attrib(root, "HREF");
+		}
+
+		link_text = xmlnode_get_data(root);
+
+		if (href) {
+			if (!strcmp(link_text, href)) {
+				/* Purple gives us: <a href="URL">URL</a>
+				 * Translate to <a h='URL' />
+				 * Displayed as text of URL with link to URL
+				 */
+				*begin = g_strdup_printf("<a h='%s' />", href);
+			} else {
+				/* But if we get: <a href="URL">text</a>
+				 * Translate to: text: <a h='URL' />
+				 *
+				 * Because official client only supports self-closed <a>
+				 * tags; you can't change the link text.
+				 */
+				*begin = g_strdup_printf("%s: <a h='%s' />", link_text, href);
+			}
+		} else {
+			*begin = g_strdup("<a />");
+		}
+
+		/* Sorry, kid. MySpace doesn't support you within <a> tags. */
+		xmlnode_free(root->child);
+		root->child = NULL;
+
+		*end = g_strdup("");
+	} else if (!purple_utf8_strcasecmp(root->name, "font")) {
+		const gchar *size;
+		const gchar *face;
+
+		size = xmlnode_get_attrib(root, "size");
+		face = xmlnode_get_attrib(root, "face");
+
+		if (face && size) {
+			*begin = g_strdup_printf("<f f='%s' h='%d'>", face, 
+					msim_point_to_height(session,
+						msim_purple_size_to_point(session, atoi(size))));
+		} else if (face) {
+			*begin = g_strdup_printf("<f f='%s'>", face);
+		} else if (size) {
+			*begin = g_strdup_printf("<f h='%d'>", 
+					 msim_point_to_height(session,
+						 msim_purple_size_to_point(session, atoi(size))));
+		} else {
+			*begin = g_strdup("<f>");
+		}
+
+		*end = g_strdup("</f>");
+
+		/* TODO: color (bg uses <body>), emoticons */
+	} else {
+		*begin = g_strdup_printf("[%s]", root->name);
+		*end = g_strdup_printf("[/%s]", root->name);
+	}
+}
+
+/** Convert an xmlnode of msim markup or HTML to an HTML string or msim markup.
+ *
+ * @param f Function to convert tags.
+ *
+ * @return An HTML string. Caller frees.
+ */
+static gchar *
+msim_convert_xmlnode(MsimSession *session, xmlnode *root, MSIM_XMLNODE_CONVERT f)
+{
+	xmlnode *node;
+	gchar *begin, *inner, *end;
+	GString *final;
+
+	if (!root || !root->name) {
+		return g_strdup("");
+	}
+
+	purple_debug_info("msim", "msim_convert_xmlnode: got root=%s\n",
+			root->name);
+
+	begin = inner = end = NULL;
+
+	final = g_string_new("");
+
+	f(session, root, &begin, &end);
+	
+	g_string_append(final, begin);
+
+	/* Loop over all child nodes. */
+	for (node = root->child; node != NULL; node = node->next) {
+		switch (node->type) {
+		case XMLNODE_TYPE_ATTRIB:
+			/* Attributes handled above. */
+			break;
+
+		case XMLNODE_TYPE_TAG:
+			/* A tag or tag with attributes. Recursively descend. */
+			inner = msim_convert_xmlnode(session, node, f);
+			g_return_val_if_fail(inner != NULL, NULL);
+
+			purple_debug_info("msim", " ** node name=%s\n", 
+					(node && node->name) ? node->name : "(NULL)");
+			break;
+	
+		case XMLNODE_TYPE_DATA:
+			/* Literal text. */
+			inner = g_new0(char, node->data_sz + 1);
+			strncpy(inner, node->data, node->data_sz);
+			inner[node->data_sz] = 0;
+
+			purple_debug_info("msim", " ** node data=%s\n", 
+					inner ? inner : "(NULL)");
+			break;
+			
+		default:
+			purple_debug_info("msim",
+					"msim_convert_xmlnode: strange node\n");
+			inner = g_strdup("");
+		}
+
+		if (inner) {
+			g_string_append(final, inner);
+		}
+	}
+
+	/* TODO: Note that msim counts each piece of text enclosed by <f> as
+	 * a paragraph and will display each on its own line. You actually have
+	 * to _nest_ <f> tags to intersperse different text in one paragraph!
+	 * Comment out this line below to see. */
+	g_string_append(final, end);
+
+	purple_debug_info("msim", "msim_markup_xmlnode_to_gtkhtml: RETURNING %s\n",
+			(final && final->str) ? final->str : "(NULL)");
+
+	return final->str;
+}
+
+/** Convert XML to something based on MSIM_XMLNODE_CONVERT. */
+static gchar *
+msim_convert_xml(MsimSession *session, const gchar *raw, MSIM_XMLNODE_CONVERT f)
+{
+	xmlnode *root;
+	gchar *str;
+	gchar *enclosed_raw;
+
+	g_return_val_if_fail(raw != NULL, NULL);
+
+	/* Enclose text in one root tag, to try to make it valid XML for parsing. */
+	enclosed_raw = g_strconcat("<root>", raw, "</root>", NULL);
+
+	root = xmlnode_from_str(enclosed_raw, -1);
+
+	if (!root) {
+		purple_debug_info("msim", "msim_markup_to_html: couldn't parse "
+				"%s as XML, returning raw: %s\n", enclosed_raw, raw);
+		/* TODO: msim_unrecognized */
+		g_free(enclosed_raw);
+		return g_strdup(raw);
+	}
+
+	g_free(enclosed_raw);
+
+	str = msim_convert_xmlnode(session, root, f);
+	g_return_val_if_fail(str != NULL, NULL);
+	purple_debug_info("msim", "msim_markup_to_html: returning %s\n", str);
+
+	xmlnode_free(root);
+
+	return str;
+}
+
+/** Convert plaintext smileys to <i> markup tags.
+ *
+ * @param before Original text with ASCII smileys. Will be freed.
+ * @return A new string with <i> tags, if applicable. Must be g_free()'d.
+ */
+static gchar *
+msim_convert_smileys_to_markup(gchar *before)
+{
+	gchar *old, *new, *replacement;
+	guint i;
+	struct MSIM_EMOTICON *emote;
+
+	old = before;
+	new = NULL;
+
+	for (i = 0; (emote = &msim_emoticons[i]) && emote->name != NULL; ++i) {
+		gchar *name, *symbol;
+
+		name = emote->name;
+		symbol = emote->symbol;
+
+		replacement = g_strdup_printf("<i n=\"%s\"/>", name);
+
+		purple_debug_info("msim", "msim_convert_smileys_to_markup: %s->%s\n",
+				symbol ? symbol : "(NULL)", 
+				replacement ? replacement : "(NULL)");
+		new = str_replace(old, symbol, replacement);
+		
+		g_free(replacement);
+		g_free(old);
+
+		old = new;
+	}
+
+	return new;
+}
+	
+
+/** High-level function to convert MySpaceIM markup to Purple (HTML) markup. 
+ *
+ * @return Purple markup string, must be g_free()'d. */
+static gchar *
+msim_markup_to_html(MsimSession *session, const gchar *raw)
+{
+	return msim_convert_xml(session, raw, 
+			(MSIM_XMLNODE_CONVERT)(msim_markup_tag_to_html));
+}
+
+/** High-level function to convert Purple (HTML) to MySpaceIM markup.
+ *
+ * @return HTML markup string, must be g_free()'d. */
+static gchar *
+html_to_msim_markup(MsimSession *session, const gchar *raw)
+{
+	gchar *markup;
+
+	markup = msim_convert_xml(session, raw,
+			(MSIM_XMLNODE_CONVERT)(html_tag_to_msim_markup));
+	
+	if (purple_account_get_bool(session->account, "emoticons", TRUE)) {
+		/* Frees markup and allocates a new one. */
+		markup = msim_convert_smileys_to_markup(markup);
+	}
+
+	return markup;
+}
+
+/** Get the MsimUser from a PurpleBuddy, creating it if needed. */
+static MsimUser *
+msim_get_user_from_buddy(PurpleBuddy *buddy)
+{
+	MsimUser *user;
+
+	if (!buddy) {
+		return NULL;
+	}
+
+	if (!buddy->proto_data) {
+		/* TODO: where is this freed? */
+		user = g_new0(MsimUser, 1);
+		user->buddy = buddy;
+		buddy->proto_data = (gpointer)user;
+		purple_debug_info("msim_get_user_from_buddy",
+				"creating new user for %s to %X\n",
+				buddy->name, buddy->proto_data);
+	} 
+
+	user = (MsimUser *)(buddy->proto_data);
+
+	return user;
+}
+
+/** Find and return an MsimUser * representing a user on the buddy list, or NULL. */
+static MsimUser *
+msim_find_user(MsimSession *session, const gchar *username)
+{
+	PurpleBuddy *buddy;
+	MsimUser *user;
+
+	buddy = purple_find_buddy(session->account, username);
+	if (!buddy) {
+		return NULL;
+	}
+
+	user = msim_get_user_from_buddy(buddy);
+
+	return user;
+}
+
+
+/** Record the client version in the buddy list, from an incoming message. */
+static gboolean
+msim_incoming_bm_record_cv(MsimSession *session, MsimMessage *msg)
+{
+	gchar *username, *cv;
+	gboolean ret;
+	MsimUser *user;
+
+	username = msim_msg_get_string(msg, "_username");
+	cv = msim_msg_get_string(msg, "cv");
+
+	g_return_val_if_fail(username != NULL, FALSE);
+	if (!cv) {
+		/* No client version to record, don't worry about it. */
+		return FALSE;
+	}
+
+	user = msim_find_user(session, username);
+
+	if (user) {
+		user->client_cv = atol(cv);
+		ret = TRUE;
+	} else {
+		ret = FALSE;
+	}
+
+	g_free(username);
+	g_free(cv);
+
+	return ret;
+}
+
+/** Handle an incoming buddy message. */
+static gboolean
+msim_incoming_bm(MsimSession *session, MsimMessage *msg)
+{
+	guint bm;
+   
+	bm = msim_msg_get_integer(msg, "bm");
+
+	msim_incoming_bm_record_cv(session, msg);
+
+	switch (bm) {
+		case MSIM_BM_STATUS:
+			return msim_incoming_status(session, msg);
+		case MSIM_BM_INSTANT:
+			return msim_incoming_im(session, msg);
+		case MSIM_BM_ACTION:
+			return msim_incoming_action(session, msg);
+		case MSIM_BM_MEDIA:
+			return msim_incoming_media(session, msg);
+		case MSIM_BM_UNOFFICIAL_CLIENT:
+			return msim_incoming_unofficial_client(session, msg);
+		default:
+			/* Not really an IM, but show it for informational 
+			 * purposes during development. */
+			return msim_incoming_im(session, msg);
+	}
+}
+
+/**
+ * Handle an incoming instant message.
+ *
+ * @param session The session
+ * @param msg Message from the server, containing 'f' (userid from) and 'msg'. 
+ *               Should also contain username in _username from preprocessing.
+ *
+ * @return TRUE if successful.
+ */
+static gboolean 
+msim_incoming_im(MsimSession *session, MsimMessage *msg)
+{
+	gchar *username, *msg_msim_markup, *msg_purple_markup;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	username = msim_msg_get_string(msg, "_username");
+	g_return_val_if_fail(username != NULL, FALSE);
+
+	msg_msim_markup = msim_msg_get_string(msg, "msg");
+	g_return_val_if_fail(msg_msim_markup != NULL, FALSE);
+
+	msg_purple_markup = msim_markup_to_html(session, msg_msim_markup);
+	g_free(msg_msim_markup);
+
+	serv_got_im(session->gc, username, msg_purple_markup, 
+			PURPLE_MESSAGE_RECV, time(NULL));
+
+	g_free(username);
+	g_free(msg_purple_markup);
+
+	return TRUE;
+}
+
+/**
+ * Process unrecognized information.
+ *
+ * @param session
+ * @param msg An MsimMessage that was unrecognized, or NULL.
+ * @param note Information on what was unrecognized, or NULL.
+ */
+static void 
+msim_unrecognized(MsimSession *session, MsimMessage *msg, gchar *note)
+{
+	/* TODO: Some more context, outwardly equivalent to a backtrace, 
+	 * for helping figure out what this msg is for. What was going on?
+	 * But not too much information so that a user
+	 * posting this dump reveals confidential information.
+	 */
+
+	/* TODO: dump unknown msgs to file, so user can send them to me
+	 * if they wish, to help add support for new messages (inspired
+	 * by Alexandr Shutko, who maintains OSCAR protocol documentation). */
+
+	purple_debug_info("msim", "Unrecognized data on account for %s\n", 
+			session->account->username ? session->account->username
+			: "(NULL)");
+	if (note) {
+		purple_debug_info("msim", "(Note: %s)\n", note);
+	}
+
+	if (msg) {
+		msim_msg_dump("Unrecognized message dump: %s\n", msg);
+	}
+}
+
+/** Process an incoming zap. */
+static gboolean
+msim_incoming_zap(MsimSession *session, MsimMessage *msg)
+{
+	gchar *msg_text, *username, *zap_text;
+	gint zap;
+	const gchar *zap_past_tense[10];
+
+	zap_past_tense[0] = _("zapped");
+	zap_past_tense[1] = _("whacked");
+	zap_past_tense[2] = _("torched");
+	zap_past_tense[3] = _("smooched");
+	zap_past_tense[4] = _("hugged");
+	zap_past_tense[5] = _("bslapped");
+	zap_past_tense[6] = _("goosed");
+	zap_past_tense[7] = _("hi-fived");
+	zap_past_tense[8] = _("punk'd");
+	zap_past_tense[9] = _("raspberried");
+
+	msg_text = msim_msg_get_string(msg, "msg");
+	username = msim_msg_get_string(msg, "_username");
+
+	g_return_val_if_fail(msg_text != NULL, FALSE);
+	g_return_val_if_fail(username != NULL, FALSE);
+
+	g_return_val_if_fail(sscanf(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_%d", &zap) == 1, FALSE);
+
+	zap = CLAMP(zap, 0, sizeof(zap_past_tense) / sizeof(zap_past_tense[0]));
+
+	zap_text = g_strdup_printf(_("*** You have been %s! ***"), zap_past_tense[zap]);
+
+	serv_got_im(session->gc, username, zap_text, 
+			PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_SYSTEM, time(NULL));
+
+	g_free(zap_text);
+	g_free(msg_text);
+	g_free(username);
+
+	return TRUE;
+}
+
+/**
+ * Handle an incoming action message.
+ *
+ * @param session
+ * @param msg
+ *
+ * @return TRUE if successful.
+ *
+ */
+static gboolean 
+msim_incoming_action(MsimSession *session, MsimMessage *msg)
+{
+	gchar *msg_text, *username;
+	gboolean rc;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	msg_text = msim_msg_get_string(msg, "msg");
+	g_return_val_if_fail(msg_text != NULL, FALSE);
+
+	username = msim_msg_get_string(msg, "_username");
+	g_return_val_if_fail(username != NULL, FALSE);
+
+	purple_debug_info("msim", "msim_incoming_action: action <%s> from <%d>\n", 
+			msg_text, username);
+
+	if (strcmp(msg_text, "%typing%") == 0) {
+		/* TODO: find out if msim repeatedly sends typing messages, so we can 
+		 * give it a timeout. Right now, there does seem to be an inordinately 
+		 * amount of time between typing stopped-typing notifications. */
+		serv_got_typing(session->gc, username, 0, PURPLE_TYPING);
+		rc = TRUE;
+	} else if (strcmp(msg_text, "%stoptyping%") == 0) {
+		serv_got_typing_stopped(session->gc, username);
+		rc = TRUE;
+	} else if (strstr(msg_text, "!!!ZAP_SEND!!!=RTE_BTN_ZAPS_")) {
+		rc = msim_incoming_zap(session, msg);
+	} else {
+		msim_unrecognized(session, msg, 
+				"got to msim_incoming_action but unrecognized value for 'msg'");
+		rc = FALSE;
+	}
+
+	g_free(msg_text);
+	g_free(username);
+
+	return rc;
+}
+
+/* Process an incoming media (buddy icon) message. */
+static gboolean
+msim_incoming_media(MsimSession *session, MsimMessage *msg)
+{
+	gchar *username, *text;
+
+	username = msim_msg_get_string(msg, "_username");
+	text = msim_msg_get_string(msg, "msg");
+
+	g_return_val_if_fail(username != NULL, FALSE);
+	g_return_val_if_fail(text != NULL, FALSE);
+
+	purple_debug_info("msim", "msim_incoming_media: from %s, got msg=%s\n", username, text);
+
+	/* Media messages are sent when the user opens a window to someone.
+	 * Tell libpurple they started typing and stopped typing, to inform the Psychic
+	 * Mode plugin so it too can open a window to the user. */
+	serv_got_typing(session->gc, username, 0, PURPLE_TYPING);
+	serv_got_typing_stopped(session->gc, username);
+
+	g_free(username);
+
+	return TRUE;
+}
+
+/* Process an incoming "unofficial client" message. The plugin for
+ * Miranda IM sends this message with the plugin information. */
+static gboolean
+msim_incoming_unofficial_client(MsimSession *session, MsimMessage *msg)
+{
+	MsimUser *user;
+	gchar *username, *client_info;
+
+	username = msim_msg_get_string(msg, "_username");
+	client_info = msim_msg_get_string(msg, "msg");
+
+	g_return_val_if_fail(username != NULL, FALSE);
+	g_return_val_if_fail(client_info != NULL, FALSE);
+
+	purple_debug_info("msim", "msim_incoming_unofficial_client: %s is using client %s\n",
+		username, client_info);
+
+	user = msim_find_user(session, username);
+	
+	g_return_val_if_fail(user != NULL, FALSE);
+
+	if (user->client_info) {
+		g_free(user->client_info);
+	}
+	user->client_info = client_info;
+
+	g_free(username);
+	/* Do not free client_info - the MsimUser now owns it. */
+
+	return TRUE;
+}
+
+
+#ifdef MSIM_SEND_CLIENT_VERSION
+/** Send our client version to another unofficial client that understands it. */
+static gboolean
+msim_send_unofficial_client(MsimSession *session, gchar *username)
+{
+	gchar *our_info;
+	gboolean ret;
+
+	our_info = g_strdup_printf("Libpurple %d.%d.%d - msimprpl %s", 
+			PURPLE_MAJOR_VERSION,
+			PURPLE_MINOR_VERSION,
+			PURPLE_MICRO_VERSION,
+			MSIM_PRPL_VERSION_STRING);
+
+	ret = msim_send_bm(session, username, our_info, MSIM_BM_UNOFFICIAL_CLIENT);
+
+	return ret;
+}
+#endif
+
+/** 
+ * Handle when our user starts or stops typing to another user.
+ *
+ * @param gc
+ * @param name The buddy name to which our user is typing to
+ * @param state PURPLE_TYPING, PURPLE_TYPED, PURPLE_NOT_TYPING
+ *
+ * @return 0
+ */
+unsigned int 
+msim_send_typing(PurpleConnection *gc, const gchar *name, 
+		PurpleTypingState state)
+{
+	const gchar *typing_str;
+	MsimSession *session;
+
+	g_return_val_if_fail(gc != NULL, 0);
+	g_return_val_if_fail(name != NULL, 0);
+
+	session = (MsimSession *)gc->proto_data;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), 0);
+
+	switch (state) {
+		case PURPLE_TYPING: 
+			typing_str = "%typing%"; 
+			break;
+
+		case PURPLE_TYPED:
+		case PURPLE_NOT_TYPING:
+		default:
+			typing_str = "%stoptyping%";
+			break;
+	}
+
+	purple_debug_info("msim", "msim_send_typing(%s): %d (%s)\n", name, state, typing_str);
+	msim_send_bm(session, name, typing_str, MSIM_BM_ACTION);
+	return 0;
+}
+
+/** Format the "now playing" indicator, showing the artist and song.
+ * @return Return a new string (must be g_free()'d), or NULL.
+ */
+static gchar *
+msim_format_now_playing(gchar *band, gchar *song)
+{
+	if ((band && strlen(band)) || (song && strlen(song))) {
+		return g_strdup_printf("%s - %s",
+			(band && strlen(band)) ? band : "Unknown Artist",
+			(song && strlen(song)) ? song : "Unknown Song");
+	} else {
+		return NULL;
+	}
+}
+
+/** Append user information to a PurpleNotifyUserInfo, given an MsimUser. 
+ * Used by msim_tooltip_text() and msim_get_info_cb() to show a user's profile.
+ */
+static void
+msim_append_user_info(MsimSession *session, PurpleNotifyUserInfo *user_info, MsimUser *user, gboolean full)
+{
+	gchar *str;
+	guint uid;
+	guint cv;
+
+	/* Useful to identify the account the tooltip refers to. 
+	 *  Other prpls show this. */
+	if (user->username) {
+		purple_notify_user_info_add_pair(user_info, _("User"), user->username);
+	}
+
+	uid = purple_blist_node_get_int(&user->buddy->node, "UserID");
+
+	if (full) {
+		purple_notify_user_info_add_pair(user_info, _("User ID"), g_strdup_printf("%d", uid));
+	}
+
+
+	/* a/s/l...the vitals */
+	if (user->age) {
+		purple_notify_user_info_add_pair(user_info, _("Age"),
+				g_strdup_printf("%d", user->age));
+	}
+
+	if (user->gender && strlen(user->gender)) {
+		purple_notify_user_info_add_pair(user_info, _("Gender"), user->gender);
+	}
+
+	if (user->location && strlen(user->location)) {
+		purple_notify_user_info_add_pair(user_info, _("Location"), user->location);
+	}
+
+	/* Other information */
+	if (user->headline && strlen(user->headline)) {
+		purple_notify_user_info_add_pair(user_info, _("Headline"), user->headline);
+	}
+
+	str = msim_format_now_playing(user->band_name, user->song_name);
+	if (str && strlen(str)) {
+		purple_notify_user_info_add_pair(user_info, _("Song"), str);
+	}
+
+	/* Note: total friends only available if looked up by uid, not username. */
+	if (user->total_friends) {
+		purple_notify_user_info_add_pair(user_info, _("Total Friends"),
+			g_strdup_printf("%d", user->total_friends));
+	}
+
+	if (full) {
+		/* Client information */
+
+		str = user->client_info;
+		cv = user->client_cv;
+
+		if (str && cv != 0) {
+			purple_notify_user_info_add_pair(user_info, _("Client Version"),
+					g_strdup_printf("%s (build %d)", str, cv));
+		} else if (str) {
+			purple_notify_user_info_add_pair(user_info, _("Client Version"),
+					g_strdup(str));
+		} else if (cv) {
+			purple_notify_user_info_add_pair(user_info, _("Client Version"),
+					g_strdup_printf("Build %d", cv));
+		}
+	}
+}
+
+/** Callback for msim_get_info(), for when user info is received. */
+static void 
+msim_get_info_cb(MsimSession *session, MsimMessage *user_info_msg, 
+		gpointer data)
+{
+	MsimMessage *msg;
+	gchar *username;
+	PurpleNotifyUserInfo *user_info;
+	MsimUser *user;
+	gboolean temporary_user;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+
+	/* Get user{name,id} from msim_get_info, passed as an MsimMessage for 
+	   orthogonality. */
+	msg = (MsimMessage *)data;
+	g_return_if_fail(msg != NULL);
+
+	username = msim_msg_get_string(msg, "user");
+	if (!username) {
+		purple_debug_info("msim", "msim_get_info_cb: no 'user' in msg");
+		return;
+	}
+
+	msim_msg_free(msg);
+	purple_debug_info("msim", "msim_get_info_cb: got for user: %s\n", username);
+
+	user = msim_find_user(session, username);
+
+	if (!user) {
+		/* User isn't on blist, create a temporary user to store info. */
+		temporary_user = TRUE;
+		user = g_new0(MsimUser, 1);
+	} else {
+		temporary_user = FALSE;
+	}
+
+	/* Update user structure with new information */
+	msim_store_user_info(session, user_info_msg, user);
+
+	user_info = purple_notify_user_info_new();
+
+	/* Append data from MsimUser to PurpleNotifyUserInfo for display, full */
+	msim_append_user_info(session, user_info, user, TRUE);
+
+	purple_notify_userinfo(session->gc, username, user_info, NULL, NULL);
+	purple_debug_info("msim", "msim_get_info_cb: username=%s\n", username);
+
+	purple_notify_user_info_destroy(user_info);
+	/* TODO: do not free username, since it will be used by user_info? */
+
+	if (temporary_user) {
+		g_free(user->client_info);
+		g_free(user->gender);
+		g_free(user->location);
+		g_free(user->headline);
+		g_free(user->display_name);
+		g_free(user->username);
+		g_free(user->band_name);
+		g_free(user->song_name);
+		g_free(user->image_url);
+		g_free(user);
+	}
+
+}
+
+/** Retrieve a user's profile. 
+ * @param username Username, user ID, or email address to lookup.
+ */
+void 
+msim_get_info(PurpleConnection *gc, const gchar *username)
+{
+	MsimSession *session;
+	MsimUser *user;
+	guint uid;
+	gchar *user_to_lookup;
+	MsimMessage *user_msg;
+
+	g_return_if_fail(gc != NULL);
+	g_return_if_fail(username != NULL);
+
+	session = (MsimSession *)gc->proto_data;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+
+	/* Obtain uid of buddy. */
+	user = msim_find_user(session, username);
+
+	/* If is on buddy list, lookup by uid since it is faster. */
+	if (user && (uid = purple_blist_node_get_int(&user->buddy->node, "UserID"))) {
+		user_to_lookup = g_strdup_printf("%d", uid);
+	} else {
+		/* Looking up buddy not on blist. Lookup by whatever user entered. */
+		user_to_lookup = g_strdup(username);
+	}
+
+	/* Pass the username to msim_get_info_cb(), because since we lookup
+	 * by userid, the userinfo message will only contain the uid (not 
+	 * the username) but it would be useful to display the username too.
+	 */
+	user_msg = msim_msg_new(
+			"user", MSIM_TYPE_STRING, g_strdup(username),
+			NULL);
+	purple_debug_info("msim", "msim_get_info, setting up lookup, user=%s\n", username);
+
+	msim_lookup_user(session, user_to_lookup, msim_get_info_cb, user_msg);
+
+	g_free(user_to_lookup); 
+}
+
+/** Set your status - callback for when user manually sets it.  */
+void
+msim_set_status(PurpleAccount *account, PurpleStatus *status)
+{
+	PurpleStatusType *type;
+	MsimSession *session;
+	guint status_code;
+	const gchar *statstring;
+
+	session = (MsimSession *)account->gc->proto_data;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+
+	type = purple_status_get_type(status);
+
+	switch (purple_status_type_get_primitive(type)) {
+		case PURPLE_STATUS_AVAILABLE:
+			purple_debug_info("msim", "msim_set_status: available (%d->%d)\n", PURPLE_STATUS_AVAILABLE,
+					MSIM_STATUS_CODE_ONLINE);
+			status_code = MSIM_STATUS_CODE_ONLINE;
+			break;
+
+		case PURPLE_STATUS_INVISIBLE:
+			purple_debug_info("msim", "msim_set_status: invisible (%d->%d)\n", PURPLE_STATUS_INVISIBLE,
+					MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN);
+			status_code = MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN;
+			break;
+
+		case PURPLE_STATUS_AWAY:
+			purple_debug_info("msim", "msim_set_status: away (%d->%d)\n", PURPLE_STATUS_AWAY,
+					MSIM_STATUS_CODE_AWAY);
+			status_code = MSIM_STATUS_CODE_AWAY;
+			break;
+
+		default:
+			purple_debug_info("msim", "msim_set_status: unknown "
+					"status interpreting as online");
+			status_code = MSIM_STATUS_CODE_ONLINE;
+			break;
+	}
+
+	statstring = purple_status_get_attr_string(status, "message");
+
+	if (!statstring) {
+		statstring = g_strdup("");
+	}
+
+	msim_set_status_code(session, status_code, g_strdup(statstring));
+}
+
+/** Go idle. */
+void
+msim_set_idle(PurpleConnection *gc, int time)
+{
+	MsimSession *session;
+
+	g_return_if_fail(gc != NULL);
+
+	session = (MsimSession *)gc->proto_data;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+
+	if (time == 0) {
+		/* Going back from idle. In msim, idle is mutually exclusive 
+		 * from the other states (you can only be away or idle, but not
+		 * both, for example), so by going non-idle I go online.
+		 */
+		/* TODO: find out how to keep old status string? */
+		msim_set_status_code(session, MSIM_STATUS_CODE_ONLINE, g_strdup(""));
+	} else {
+		/* msim doesn't support idle time, so just go idle */
+		msim_set_status_code(session, MSIM_STATUS_CODE_IDLE, g_strdup(""));
+	}
+}
+
+/** Set status using an MSIM_STATUS_CODE_* value.
+ * @param status_code An MSIM_STATUS_CODE_* value.
+ * @param statstring Status string, must be a dynamic string (will be freed by msim_send).
+ */
+static void 
+msim_set_status_code(MsimSession *session, guint status_code, gchar *statstring)
+{
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+	g_return_if_fail(statstring != NULL);
+
+	purple_debug_info("msim", "msim_set_status_code: going to set status to code=%d,str=%s\n",
+			status_code, statstring);
+
+	if (!msim_send(session,
+			"status", MSIM_TYPE_INTEGER, status_code,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"statstring", MSIM_TYPE_STRING, statstring, 
+			"locstring", MSIM_TYPE_STRING, g_strdup(""),
+			NULL))
+	{
+		purple_debug_info("msim", "msim_set_status: failed to set status");
+	}
+
+}
+
+/** After a uid is resolved to username, tag it with the username and submit for processing. 
+ * 
+ * @param session
+ * @param userinfo Response messsage to resolving request.
+ * @param data MsimMessage *, the message to attach information to. 
+ */
+static void 
+msim_incoming_resolved(MsimSession *session, MsimMessage *userinfo, 
+		gpointer data)
+{
+	gchar *body_str;
+	GHashTable *body;
+	gchar *username;
+	MsimMessage *msg;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+	g_return_if_fail(userinfo != NULL);
+
+	body_str = msim_msg_get_string(userinfo, "body");
+	g_return_if_fail(body_str != NULL);
+	body = msim_parse_body(body_str);
+	g_return_if_fail(body != NULL);
+	g_free(body_str);
+
+	username = g_hash_table_lookup(body, "UserName");
+	g_return_if_fail(username != NULL);
+
+
+	msg = (MsimMessage *)data;
+	g_return_if_fail(msg != NULL);
+
+	/* TODO: more elegant solution than below. attach whole message? */
+	/* Special elements name beginning with '_', we'll use internally within the
+	 * program (did not come directly from the wire). */
+	msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
+  
+	/* TODO: attach more useful information, like ImageURL */
+
+	msim_process(session, msg);
+
+	/* TODO: Free copy cloned from  msim_preprocess_incoming(). */
+	//XXX msim_msg_free(msg);
+	g_hash_table_destroy(body);
+}
+
+/* Lookup a username by userid, from buddy list. 
+ *
+ * @param wanted_uid
+ *
+ * @return Username of wanted_uid, if on blist, or NULL. Static string. 
+ *
+ */
+static const gchar *
+msim_uid2username_from_blist(MsimSession *session, guint wanted_uid)
+{
+	GSList *buddies, *cur;
+    gchar *ret;
+
+	buddies = purple_find_buddies(session->account, NULL); 
+
+	if (!buddies)
+	{
+		purple_debug_info("msim", "msim_uid2username_from_blist: no buddies?");
+		return NULL;
+	}
+
+    ret = NULL;
+
+	for (cur = buddies; cur != NULL; cur = g_slist_next(cur))
+	{
+		PurpleBuddy *buddy;
+		guint uid;
+		const gchar *name;
+
+		/* See finch/gnthistory.c */
+		buddy = cur->data;
+
+		uid = purple_blist_node_get_int(&buddy->node, "UserID");
+		name = purple_buddy_get_name(buddy);
+
+		if (uid == wanted_uid)
+		{
+			ret = g_strdup(name);
+            break;
+		}
+	}
+
+	g_slist_free(buddies);
+	return ret;
+}
+
+/** Preprocess incoming messages, resolving as needed, calling msim_process() when ready to process.
+ *
+ * @param session
+ * @param msg MsimMessage *, freed by caller.
+ */
+static gboolean 
+msim_preprocess_incoming(MsimSession *session, MsimMessage *msg)
+{
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	if (msim_msg_get(msg, "bm") && msim_msg_get(msg, "f")) {
+		guint uid;
+		const gchar *username;
+
+		/* 'f' = userid message is from, in buddy messages */
+		uid = msim_msg_get_integer(msg, "f");
+
+		username = msim_uid2username_from_blist(session, uid); 
+
+		if (username) {
+			/* Know username already, use it. */
+			purple_debug_info("msim", "msim_preprocess_incoming: tagging with _username=%s\n",
+					username);
+			msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
+			return msim_process(session, msg);
+
+		} else {
+			gchar *from;
+
+			/* Send lookup request. */
+			/* XXX: where is msim_msg_get_string() freed? make _strdup and _nonstrdup. */
+			purple_debug_info("msim", "msim_incoming: sending lookup, setting up callback\n");
+			from = msim_msg_get_string(msg, "f");
+			msim_lookup_user(session, from, msim_incoming_resolved, msim_msg_clone(msg)); 
+			g_free(from);
+
+			/* indeterminate */
+			return TRUE;
+		}
+	} else {
+		/* Nothing to resolve - send directly to processing. */
+		return msim_process(session, msg);
+	}
+}
+
+#ifdef MSIM_USE_KEEPALIVE
+/** Check if the connection is still alive, based on last communication. */
+static gboolean
+msim_check_alive(gpointer data)
+{
+	MsimSession *session;
+	time_t delta;
+	gchar *errmsg;
+
+	session = (MsimSession *)data;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+
+	delta = time(NULL) - session->last_comm;
+	//purple_debug_info("msim", "msim_check_alive: delta=%d\n", delta);
+	if (delta >= MSIM_KEEPALIVE_INTERVAL) {
+		errmsg = g_strdup_printf(_("Connection to server lost (no data received within %d seconds)"), (int)delta);
+
+		purple_debug_info("msim", "msim_check_alive: %s > interval of %d, presumed dead\n",
+				errmsg, MSIM_KEEPALIVE_INTERVAL);
+		purple_connection_error(session->gc, errmsg);
+
+		purple_notify_error(session->gc, NULL, errmsg, NULL);
+
+		g_free(errmsg);
+
+		return FALSE;
+	}
+
+	return TRUE;
+}
+#endif
+
+/** Handle mail reply checks. */
+static void
+msim_check_inbox_cb(MsimSession *session, MsimMessage *reply, gpointer data)
+{
+	GHashTable *body;
+	gchar *body_str;
+	GString *notification;
+	guint old_inbox_status;
+	guint i, n;
+	const gchar *froms[5], *tos[5], *urls[5], *subjects[5];
+
+	/* Three parallel arrays for each new inbox message type. */
+	static const gchar *inbox_keys[] = 
+	{ 
+		"Mail", 
+		"BlogComment", 
+		"ProfileComment", 
+		"FriendRequest", 
+		"PictureComment" 
+	};
+
+	static const guint inbox_bits[] = 
+	{ 
+		MSIM_INBOX_MAIL, 
+		MSIM_INBOX_BLOG_COMMENT,
+		MSIM_INBOX_PROFILE_COMMENT,
+		MSIM_INBOX_FRIEND_REQUEST,
+		MSIM_INBOX_PICTURE_COMMENT
+	};
+
+	static const gchar *inbox_urls[] =
+	{
+		"http://messaging.myspace.com/index.cfm?fuseaction=mail.inbox",
+		"http://blog.myspace.com/index.cfm?fuseaction=blog",
+		"http://home.myspace.com/index.cfm?fuseaction=user",
+		"http://messaging.myspace.com/index.cfm?fuseaction=mail.friendRequests",
+		"http://home.myspace.com/index.cfm?fuseaction=user"
+	};
+
+	static const gchar *inbox_text[5];
+
+	/* Can't write _()'d strings in array initializers. Workaround. */
+	inbox_text[0] = _("New mail messages");
+	inbox_text[1] = _("New blog comments");
+	inbox_text[2] = _("New profile comments");
+	inbox_text[3] = _("New friend requests!");
+	inbox_text[4] = _("New picture comments");
+
+	g_return_if_fail(reply != NULL);
+
+	msim_msg_dump("msim_check_inbox_cb: reply=%s\n", reply);
+
+	body_str = msim_msg_get_string(reply, "body");
+	g_return_if_fail(body_str != NULL);
+
+	body = msim_parse_body(body_str);
+	g_free(body_str);
+
+	notification = g_string_new("");
+
+	old_inbox_status = session->inbox_status;
+
+	n = 0;
+
+	for (i = 0; i < sizeof(inbox_keys) / sizeof(inbox_keys[0]); ++i) {
+		const gchar *key;
+		guint bit;
+		
+		key = inbox_keys[i];
+		bit = inbox_bits[i];
+
+		if (g_hash_table_lookup(body, key)) {
+			/* Notify only on when _changes_ from no mail -> has mail
+			 * (edge triggered) */
+			if (!(session->inbox_status & bit)) {
+				purple_debug_info("msim", "msim_check_inbox_cb: got %s, at %d\n",
+						key ? key : "(NULL)", n);
+
+				subjects[n] = inbox_text[i];
+				froms[n] = _("MySpace");
+				tos[n] = session->username;
+				/* TODO: append token, web challenge, so automatically logs in.
+				 * Would also need to free strings because they won't be static
+				 */
+				urls[n] = inbox_urls[i];
+
+				++n;
+			} else {
+				purple_debug_info("msim",
+						"msim_check_inbox_cb: already notified of %s\n",
+						key ? key : "(NULL)");
+			}
+
+			session->inbox_status |= bit;
+		}
+	}
+
+	if (n) {
+		purple_debug_info("msim",
+				"msim_check_inbox_cb: notifying of %d\n", n);
+
+		/* TODO: free strings with callback _if_ change to dynamic (w/ token) */
+		purple_notify_emails(session->gc,         /* handle */
+				n,                        /* count */
+				TRUE,                     /* detailed */
+				subjects, froms, tos, urls, 
+				NULL,                     /* PurpleNotifyCloseCallback cb */
+				NULL);                    /* gpointer user_data */
+
+	}
+
+	g_hash_table_destroy(body);
+}
+
+/* Send request to check if there is new mail. */
+static gboolean
+msim_check_inbox(gpointer data)
+{
+	MsimSession *session;
+
+	session = (MsimSession *)data;
+
+	purple_debug_info("msim", "msim_check_inbox: checking mail\n");
+	g_return_val_if_fail(msim_send(session, 
+			"persist", MSIM_TYPE_INTEGER, 1,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_GET,
+			"dsn", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_DSN,
+			"lid", MSIM_TYPE_INTEGER, MG_CHECK_MAIL_LID,
+			"uid", MSIM_TYPE_INTEGER, session->userid,
+			"rid", MSIM_TYPE_INTEGER, 
+				msim_new_reply_callback(session, msim_check_inbox_cb, NULL),
+			"body", MSIM_TYPE_STRING, g_strdup(""),
+			NULL), TRUE);
+
+	/* Always return true, so that we keep checking for mail. */
+	return TRUE;
+}
+
+/** Called when the session key arrives. */
+static gboolean
+msim_we_are_logged_on(MsimSession *session, MsimMessage *msg)
+{
+	MsimMessage *body;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	session->sesskey = msim_msg_get_integer(msg, "sesskey");
+	purple_debug_info("msim", "SESSKEY=<%d>\n", session->sesskey);
+
+	/* What is proof? Used to be uid, but now is 52 base64'd bytes... */
+
+	/* Comes with: proof,profileid,userid,uniquenick -- all same values
+	 * some of the time, but can vary. This is our own user ID. */
+	session->userid = msim_msg_get_integer(msg, "userid");
+
+	/* Not sure what profileid is used for. */
+	if (msim_msg_get_integer(msg, "profileid") != session->userid) {
+		msim_unrecognized(session, msg, 
+				"Profile ID didn't match user ID, don't know why");
+	}
+
+	/* We now know are our own username, only after we're logged in..
+	 * which is weird, but happens because you login with your email
+	 * address and not username. Will be freed in msim_session_destroy(). */
+	session->username = msim_msg_get_string(msg, "uniquenick");
+
+	/* The session is now set up, ready to be connected. This emits the
+	 * signedOn signal, so clients can now do anything with msimprpl, and
+	 * we're ready for it (session key, userid, username all setup). */
+	purple_connection_update_progress(session->gc, _("Connected"), 3, 4);
+	purple_connection_set_state(session->gc, PURPLE_CONNECTED);
+
+
+	/* Additional post-connect operations */
+
+
+	if (msim_msg_get_integer(msg, "uniquenick") == session->userid) {
+		purple_debug_info("msim_we_are_logged_on", "TODO: pick username");
+	}
+
+	body = msim_msg_new(
+			"UserID", MSIM_TYPE_INTEGER, session->userid,
+			NULL);
+
+	/* Request IM info about ourself. */
+	msim_send(session,
+			"persist", MSIM_TYPE_STRING, g_strdup("persist"),
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"dsn", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_DSN,
+			"uid", MSIM_TYPE_INTEGER, session->userid,
+			"lid", MSIM_TYPE_INTEGER, MG_OWN_MYSPACE_INFO_LID,
+			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
+			"body", MSIM_TYPE_DICTIONARY, body,
+			NULL);
+
+	/* Request MySpace info about ourself. */
+	msim_send(session,
+			"persist", MSIM_TYPE_STRING, g_strdup("persist"),
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"dsn", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_DSN,
+			"uid", MSIM_TYPE_INTEGER, session->userid,
+			"lid", MSIM_TYPE_INTEGER, MG_OWN_IM_INFO_LID,
+			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
+			"body", MSIM_TYPE_STRING, g_strdup(""),
+			NULL);
+
+	/* TODO: set options (persist cmd=514,dsn=1,lid=10) */
+	/* TODO: set blocklist */
+
+	/* Notify servers of our current status. */
+	purple_debug_info("msim", "msim_we_are_logged_on: notifying servers of status\n");
+	msim_set_status(session->account,
+			purple_account_get_active_status(session->account));
+
+	/* TODO: setinfo */
+	/*
+	body = msim_msg_new(
+		"TotalFriends", MSIM_TYPE_INTEGER, 666,
+		NULL);
+	msim_send(session,
+			"setinfo", MSIM_TYPE_BOOLEAN, TRUE,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"info", MSIM_TYPE_DICTIONARY, body,
+			NULL);
+			*/
+
+	/* Disable due to problems with timeouts. TODO: fix. */
+#ifdef MSIM_USE_KEEPALIVE
+	purple_timeout_add(MSIM_KEEPALIVE_INTERVAL_CHECK, 
+			(GSourceFunc)msim_check_alive, session);
+#endif
+
+	purple_timeout_add(MSIM_MAIL_INTERVAL_CHECK, 
+			(GSourceFunc)msim_check_inbox, session);
+
+	msim_check_inbox(session);
+
+	return TRUE;
+}
+
+/**
+ * Process a message. 
+ *
+ * @param session
+ * @param msg A message from the server, ready for processing (possibly with resolved username information attached). Caller frees.
+ *
+ * @return TRUE if successful. FALSE if processing failed.
+ */
+static gboolean 
+msim_process(MsimSession *session, MsimMessage *msg)
+{
+	g_return_val_if_fail(session != NULL, FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+#ifdef MSIM_DEBUG_MSG
+	msim_msg_dump("ready to process: %s\n", msg);
+#endif
+
+	if (msim_msg_get_integer(msg, "lc") == 1) {
+		return msim_login_challenge(session, msg);
+	} else if (msim_msg_get_integer(msg, "lc") == 2) {
+		return msim_we_are_logged_on(session, msg);
+	} else if (msim_msg_get(msg, "bm"))  {
+		return msim_incoming_bm(session, msg);
+	} else if (msim_msg_get(msg, "rid")) {
+		return msim_process_reply(session, msg);
+	} else if (msim_msg_get(msg, "error")) {
+		return msim_error(session, msg);
+	} else if (msim_msg_get(msg, "ka")) {
+		return TRUE;
+	} else {
+		msim_unrecognized(session, msg, "in msim_process");
+		return FALSE;
+	}
+}
+
+/** Callback for when a buddy icon finished being downloaded. */
+static void
+msim_downloaded_buddy_icon(PurpleUtilFetchUrlData *url_data,
+		gpointer user_data,
+		const gchar *url_text,
+		gsize len,
+		const gchar *error_message)
+{
+	MsimUser *user;
+
+	user = (MsimUser *)user_data;
+
+	purple_debug_info("msim_downloaded_buddy_icon",
+			"Downloaded %d bytes\n", len);
+
+	purple_buddy_icons_set_for_user(user->buddy->account,
+			user->buddy->name,
+			(gchar *)url_text, len, 
+			/*  Use URL itself as buddy icon "checksum" */
+			user->image_url);
+}
+
+/** Store a field of information about a buddy. */
+static void 
+msim_store_user_info_each(gpointer key, gpointer value, gpointer user_data)
+{
+	MsimUser *user;
+	gchar *key_str, *value_str;
+
+	user = (MsimUser *)user_data;
+	key_str = (gchar *)key;
+	value_str = (gchar *)value;
+
+	if (!strcmp(key_str, "UserID")) {
+		/* Save to buddy list, if it exists, for quick cached uid lookup with msim_uid2username_from_blist(). */
+		if (user->buddy)
+		{
+			purple_debug_info("msim", "associating uid %s with username %s\n", key_str, user->buddy->name);
+			purple_blist_node_set_int(&user->buddy->node, "UserID", atol(value_str));
+		}
+		/* Need to store in MsimUser, too? What if not on blist? */
+	} else if (!strcmp(key_str, "Age")) {
+		user->age = atol(value_str);
+	} else if (!strcmp(key_str, "Gender")) {
+		user->gender = g_strdup(value_str);
+	} else if (!strcmp(key_str, "Location")) {
+		user->location = g_strdup(value_str);
+	} else if (!strcmp(key_str, "TotalFriends")) {
+		user->total_friends = atol(value_str);
+	} else if (!strcmp(key_str, "DisplayName")) {
+		user->display_name = g_strdup(value_str);
+	} else if (!strcmp(key_str, "BandName")) {
+		user->band_name = g_strdup(value_str);
+	} else if (!strcmp(key_str, "SongName")) {
+		user->song_name = g_strdup(value_str);
+	} else if (!strcmp(key_str, "UserName")) {
+		/* Ignore because PurpleBuddy knows this already */
+		;
+	} else if (!strcmp(key_str, "ImageURL")) {
+		const gchar *previous_url;
+
+		user->image_url = g_strdup(value_str);
+		
+		previous_url = purple_buddy_icons_get_checksum_for_user(user->buddy);
+
+		/* Only download if URL changed */
+		if (!previous_url || strcmp(previous_url, user->image_url)) {
+			purple_util_fetch_url(user->image_url, TRUE, NULL, TRUE, msim_downloaded_buddy_icon, (gpointer)user);
+		}
+	} else {
+		/* TODO: other fields in MsimUser */
+		gchar *msg;
+
+		msg = g_strdup_printf("msim_store_user_info_each: unknown field %s=%s",
+				key_str, value_str);
+
+		msim_unrecognized(NULL, NULL, msg);
+
+		g_free(msg);
+	}
+}
+
+/** Save buddy information to the buddy list from a user info reply message.
+ *
+ * @param session
+ * @param msg The user information reply, with any amount of information.
+ * @param user The structure to save to, or NULL to save in PurpleBuddy->proto_data.
+ *
+ * Variable information is saved to the passed MsimUser structure. Permanent
+ * information (UserID) is stored in the blist node of the buddy list (and
+ * ends up in blist.xml, persisted to disk) if it exists.
+ *
+ * If the function has no buddy information, this function
+ * is a no-op (and returns FALSE).
+ *
+ */
+static gboolean 
+msim_store_user_info(MsimSession *session, MsimMessage *msg, MsimUser *user)
+{
+	GHashTable *body;
+	gchar *username, *body_str;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+ 
+	body_str = msim_msg_get_string(msg, "body");
+	if (!body_str) {
+		return FALSE;
+	}
+
+	g_return_val_if_fail(body_str != NULL, FALSE);
+	body = msim_parse_body(body_str);
+	g_free(body_str);
+
+
+	/* TODO: implement a better hash-like interface, and use it. */
+	username = g_hash_table_lookup(body, "UserName");
+
+	if (!username) {
+		purple_debug_info("msim", 
+			"msim_process_reply: not caching body, no UserName\n");
+		g_hash_table_destroy(body);
+		return FALSE;
+	}
+
+	/* Null user = find and store in PurpleBuddy's proto_data */
+	if (!user) {
+		user = msim_find_user(session, username);
+		g_return_val_if_fail(user != NULL, FALSE);
+	}
+
+	g_hash_table_foreach(body, msim_store_user_info_each, user);
+
+	if (msim_msg_get_integer(msg, "dsn") == MG_OWN_IM_INFO_DSN &&
+		msim_msg_get_integer(msg, "lid") == MG_OWN_IM_INFO_LID) {
+		/* TODO: do something with our own IM info, if we need it for some
+		 * specific purpose. Otherwise it is available on the buddy list,
+		 * if the user has themselves as their own buddy. 
+		 *
+		 * However, much of the info is already available in MsimSession,
+		 * stored in msim_we_are_logged_on(). */
+	} else if (msim_msg_get_integer(msg, "dsn") == MG_OWN_MYSPACE_INFO_DSN &&
+			msim_msg_get_integer(msg, "lid") == MG_OWN_MYSPACE_INFO_LID) {
+		/* TODO: same as above, but for MySpace info. */
+	}
+
+	g_hash_table_destroy(body);
+
+	return TRUE;
+}
+
+/** Process the initial server information from the server. */
+static gboolean
+msim_process_server_info(MsimSession *session, MsimMessage *msg)
+{
+	gchar *body_str;
+	GHashTable *body;
+
+	body_str = msim_msg_get_string(msg, "body");
+	g_return_val_if_fail(body_str != NULL, FALSE);
+	body = msim_parse_body(body_str);
+	g_free(body_str);
+	g_return_val_if_fail(body != NULL, FALSE);
+ 
+	/* Example body:
+AdUnitRefreshInterval=10.
+AlertPollInterval=360.
+AllowChatRoomEmoticonSharing=False.
+ChatRoomUserIDs=78744676;163733130;1300326231;123521495;142663391.
+CurClientVersion=673.
+EnableIMBrowse=True.
+EnableIMStuffAvatars=False.
+EnableIMStuffZaps=False.
+MaxAddAllFriends=100.
+MaxContacts=1000.
+MinClientVersion=594.
+MySpaceIM_ENGLISH=78744676.
+MySpaceNowTimer=720.
+PersistenceDataTimeout=900.
+UseWebChallenge=1.
+WebTicketGoHome=False
+
+	Anything useful? TODO: use what is useful, and use it.
+*/
+	purple_debug_info("msim_process_server_info",
+			"maximum contacts: %s\n", 
+			g_hash_table_lookup(body, "MaxContacts") ? 
+			g_hash_table_lookup(body, "MaxContacts") : "(NULL)");
+
+	session->server_info = body;
+	/* session->server_info freed in msim_session_destroy */
+
+	return TRUE;
+}
+
+/** Process a web challenge, used to login to the web site. */
+static gboolean
+msim_web_challenge(MsimSession *session, MsimMessage *msg)
+{
+	/* TODO: web challenge, store token */
+	return FALSE;
+}
+
+/**
+ * Process a persistance message reply from the server.
+ *
+ * @param session 
+ * @param msg Message reply from server.
+ *
+ * @return TRUE if successful.
+ *
+ * msim_lookup_user sets callback for here 
+ */
+static gboolean 
+msim_process_reply(MsimSession *session, MsimMessage *msg)
+{
+	MSIM_USER_LOOKUP_CB cb;
+	gpointer data;
+	guint rid, cmd, dsn, lid;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	msim_store_user_info(session, msg, NULL);
+
+	rid = msim_msg_get_integer(msg, "rid");
+	cmd = msim_msg_get_integer(msg, "cmd");
+	dsn = msim_msg_get_integer(msg, "dsn");
+	lid = msim_msg_get_integer(msg, "lid");
+
+	/* Unsolicited messages */
+	if (cmd == (MSIM_CMD_BIT_REPLY | MSIM_CMD_GET)) {
+		if (dsn == MG_SERVER_INFO_DSN && lid == MG_SERVER_INFO_LID) {
+			return msim_process_server_info(session, msg);
+		} else if (dsn == MG_WEB_CHALLENGE_DSN && lid == MG_WEB_CHALLENGE_LID) {
+			return msim_web_challenge(session, msg);
+		}
+	}
+
+	/* If a callback is registered for this userid lookup, call it. */
+	cb = g_hash_table_lookup(session->user_lookup_cb, GUINT_TO_POINTER(rid));
+	data = g_hash_table_lookup(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
+
+	if (cb) {
+		purple_debug_info("msim", 
+				"msim_process_body: calling callback now\n");
+		/* Clone message, so that the callback 'cb' can use it (needs to free it also). */
+		cb(session, msim_msg_clone(msg), data);
+		g_hash_table_remove(session->user_lookup_cb, GUINT_TO_POINTER(rid));
+		g_hash_table_remove(session->user_lookup_cb_data, GUINT_TO_POINTER(rid));
+	} else {
+		purple_debug_info("msim", 
+				"msim_process_body: no callback for rid %d\n", rid);
+	}
+
+	return TRUE;
+}
+
+/**
+ * Handle an error from the server.
+ *
+ * @param session 
+ * @param msg The message.
+ *
+ * @return TRUE if successfully reported error.
+ */
+static gboolean 
+msim_error(MsimSession *session, MsimMessage *msg)
+{
+	gchar *errmsg, *full_errmsg;
+	guint err;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	err = msim_msg_get_integer(msg, "err");
+	errmsg = msim_msg_get_string(msg, "errmsg");
+
+	full_errmsg = g_strdup_printf(_("Protocol error, code %d: %s"), err, 
+			errmsg ? errmsg : "no 'errmsg' given");
+
+	g_free(errmsg);
+
+	purple_debug_info("msim", "msim_error (sesskey=%d): %s\n", 
+			session->sesskey, full_errmsg);
+
+	purple_notify_error(session->account, g_strdup(_("MySpaceIM Error")), 
+			full_errmsg, NULL);
+
+	/* Destroy session if fatal. */
+	if (msim_msg_get(msg, "fatal")) {
+		purple_debug_info("msim", "fatal error, closing\n");
+		purple_connection_error(session->gc, full_errmsg);
+	}
+
+	return TRUE;
+}
+
+/**
+ * Process incoming status messages.
+ *
+ * @param session
+ * @param msg Status update message. Caller frees.
+ *
+ * @return TRUE if successful.
+ */
+static gboolean 
+msim_incoming_status(MsimSession *session, MsimMessage *msg)
+{
+	PurpleBuddyList *blist;
+	MsimUser *user;
+	GList *list;
+	gchar *status_headline;
+	gint status_code, purple_status_code;
+	gchar *username;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), FALSE);
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	msim_msg_dump("msim_status msg=%s\n", msg);
+
+	/* Helpfully looked up by msim_incoming_resolve() for us. */
+	username = msim_msg_get_string(msg, "_username");
+	g_return_val_if_fail(username != NULL, FALSE);
+
+	{
+		gchar *ss;
+
+		ss = msim_msg_get_string(msg, "msg");
+		purple_debug_info("msim", 
+				"msim_status: updating status for <%s> to <%s>\n",
+				username, ss ? ss : "(NULL)");
+		g_free(ss);
+	}
+
+	/* Example fields: 
+	 *  |s|0|ss|Offline 
+	 *  |s|1|ss|:-)|ls||ip|0|p|0 
+	 */
+	list = msim_msg_get_list(msg, "msg");
+
+	status_code = atoi(g_list_nth_data(list, MSIM_STATUS_ORDINAL_ONLINE));
+	purple_debug_info("msim", "msim_status: %s's status code = %d\n", username, status_code);
+	status_headline = g_list_nth_data(list, MSIM_STATUS_ORDINAL_HEADLINE);
+
+	blist = purple_get_blist();
+
+	/* Add buddy if not found */
+	user = msim_find_user(session, username);
+	if (!user) {
+		PurpleBuddy *buddy;
+
+		purple_debug_info("msim", 
+				"msim_status: making new buddy for %s\n", username);
+		buddy = purple_buddy_new(session->account, username, NULL);
+		purple_blist_add_buddy(buddy, NULL, NULL, NULL);
+
+		user = msim_get_user_from_buddy(buddy);
+
+		/* All buddies on list should have 'uid' integer associated with them. */
+		purple_blist_node_set_int(&buddy->node, "UserID", msim_msg_get_integer(msg, "f"));
+		
+		msim_store_user_info(session, msg, NULL);
+	} else {
+		purple_debug_info("msim", "msim_status: found buddy %s\n", username);
+	}
+
+	user->headline = g_strdup(status_headline);
+  
+	/* Set user status */
+	switch (status_code) {
+		case MSIM_STATUS_CODE_OFFLINE_OR_HIDDEN: 
+			purple_status_code = PURPLE_STATUS_OFFLINE;
+			break;
+
+		case MSIM_STATUS_CODE_ONLINE: 
+			purple_status_code = PURPLE_STATUS_AVAILABLE;
+			break;
+
+		case MSIM_STATUS_CODE_AWAY:
+			purple_status_code = PURPLE_STATUS_AWAY;
+			break;
+
+		case MSIM_STATUS_CODE_IDLE:
+			/* will be handled below */
+			purple_status_code = -1;
+			break;
+
+		default:
+				purple_debug_info("msim", "msim_status for %s, unknown status code %d, treating as available\n",
+						username, status_code);
+				purple_status_code = PURPLE_STATUS_AVAILABLE;
+	}
+
+	purple_prpl_got_user_status(session->account, username, purple_primitive_get_id_from_type(purple_status_code), NULL);
+
+	if (status_code == MSIM_STATUS_CODE_IDLE) {
+		purple_debug_info("msim", "msim_status: got idle: %s\n", username);
+		purple_prpl_got_user_idle(session->account, username, TRUE, time(NULL));
+	} else {
+		/* All other statuses indicate going back to non-idle. */
+		purple_prpl_got_user_idle(session->account, username, FALSE, time(NULL));
+	}
+
+#ifdef MSIM_SEND_CLIENT_VERSION
+	if (status_code == MSIM_STATUS_CODE_ONLINE) {
+		/* Secretly whisper to unofficial clients our own version as they come online */
+		msim_send_unofficial_client(session, username);
+	}
+#endif
+
+	g_free(username);
+	msim_msg_list_free(list);
+
+	return TRUE;
+}
+
+/** Add a buddy to user's buddy list. */
+void 
+msim_add_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group)
+{
+	MsimSession *session;
+	MsimMessage *msg;
+	/* MsimMessage *msg_persist; */
+	MsimMessage *body;
+
+	session = (MsimSession *)gc->proto_data;
+	purple_debug_info("msim", "msim_add_buddy: want to add %s to %s\n", 
+			buddy->name, (group && group->name) ? group->name : "(no group)");
+
+	msg = msim_msg_new(
+			"addbuddy", MSIM_TYPE_BOOLEAN, TRUE,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			/* "newprofileid" will be inserted here with uid. */
+			"reason", MSIM_TYPE_STRING, g_strdup(""),
+			NULL);
+
+	if (!msim_postprocess_outgoing(session, msg, buddy->name, "newprofileid", "reason")) {
+		purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("'addbuddy' command failed."));
+		msim_msg_free(msg);
+		return;
+	}
+	msim_msg_free(msg);
+	
+	/* TODO: if addbuddy fails ('error' message is returned), delete added buddy from
+	 * buddy list since Purple adds it locally. */
+
+	body = msim_msg_new(
+			"ContactID", MSIM_TYPE_STRING, g_strdup("<uid>"),
+			"GroupName", MSIM_TYPE_STRING, g_strdup(group->name),
+			"Position", MSIM_TYPE_INTEGER, 1000,
+			"Visibility", MSIM_TYPE_INTEGER, 1,
+			"NickName", MSIM_TYPE_STRING, g_strdup(""),
+			"NameSelect", MSIM_TYPE_INTEGER, 0,
+			NULL);
+
+	/* TODO: Update blocklist. */
+
+#if 0
+	msg_persist = msim_msg_new(
+		"persist", MSIM_TYPE_INTEGER, 1,
+		"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+		"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_PUT,
+		"dsn", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_DSN,
+		"lid", MSIM_TYPE_INTEGER, MC_CONTACT_INFO_LID,
+		/* TODO: Use msim_new_reply_callback to get rid. */
+		"rid", MSIM_TYPE_INTEGER, session->next_rid++,
+		"body", MSIM_TYPE_DICTIONARY, body,
+		NULL);
+
+	if (!msim_postprocess_outgoing(session, msg_persist, buddy->name, "body", NULL))
+	{
+		purple_notify_error(NULL, NULL, _("Failed to add buddy"), _("persist command failed"));
+		msim_msg_free(msg_persist);
+		return;
+	}
+	msim_msg_free(msg_persist);
+#endif
+
+}
+
+/** Perform actual postprocessing on a message, adding userid as specified.
+ *
+ * @param msg The message to postprocess.
+ * @param uid_before Name of field where to insert new field before, or NULL for end.
+ * @param uid_field_name Name of field to add uid to.
+ * @param uid The userid to insert.
+ *
+ * If the field named by uid_field_name already exists, then its string contents will
+ * be used for the field, except "<uid>" will be replaced by the userid.
+ *
+ * If the field named by uid_field_name does not exist, it will be added before the
+ * field named by uid_before, as an integer, with the userid.
+ *
+ * Does not handle sending, or scheduling userid lookup. For that, see msim_postprocess_outgoing().
+ */ 
+static MsimMessage *
+msim_do_postprocessing(MsimMessage *msg, const gchar *uid_before, 
+		const gchar *uid_field_name, guint uid)
+{
+	msim_msg_dump("msim_do_postprocessing msg: %s\n", msg);
+
+	/* First, check - if the field already exists, replace <uid> within it */
+	if (msim_msg_get(msg, uid_field_name)) {
+		MsimMessageElement *elem;
+		gchar *fmt_string;
+		gchar *uid_str, *new_str;
+
+		/* Warning: this is a delicate, but safe, operation */
+
+		elem = msim_msg_get(msg, uid_field_name);
+
+		/* Get the packed element, flattening it. This allows <uid> to be
+		 * replaced within nested data structures, since the replacement is done
+		 * on the linear, packed data, not on a complicated data structure.
+		 *
+		 * For example, if the field was originally a dictionary or a list, you 
+		 * would have to iterate over all the items in it to see what needs to
+		 * be replaced. But by packing it first, the <uid> marker is easily replaced
+		 * just by a string replacement.
+		 */
+		fmt_string = msim_msg_pack_element_data(elem);
+
+		uid_str = g_strdup_printf("%d", uid);
+		new_str = str_replace(fmt_string, "<uid>", uid_str);
+		g_free(uid_str);
+		g_free(fmt_string);
+
+		/* Free the old element data */
+		msim_msg_free_element_data(elem->data);
+
+		/* Replace it with our new data */
+		elem->data = new_str;
+		elem->type = MSIM_TYPE_RAW;
+
+	} else {
+		/* Otherwise, insert new field into outgoing message. */
+		msg = msim_msg_insert_before(msg, uid_before, uid_field_name, MSIM_TYPE_INTEGER, GUINT_TO_POINTER(uid));
+	}
+
+	msim_msg_dump("msim_postprocess_outgoing_cb: postprocessed msg=%s\n", msg);
+
+	return msg;
+}
+
+/** Callback for msim_postprocess_outgoing() to add a userid to a message, and send it (once receiving userid).
+ *
+ * @param session
+ * @param userinfo The user information reply message, containing the user ID
+ * @param data The message to postprocess and send.
+ *
+ * The data message should contain these fields:
+ *
+ *  _uid_field_name: string, name of field to add with userid from userinfo message
+ *  _uid_before: string, name of field before field to insert, or NULL for end
+ *
+ *
+*/
+static void 
+msim_postprocess_outgoing_cb(MsimSession *session, MsimMessage *userinfo, 
+		gpointer data)
+{
+	gchar *body_str;
+	GHashTable *body;
+	gchar *uid, *uid_field_name, *uid_before;
+	MsimMessage *msg;
+
+	msg = (MsimMessage *)data;
+
+	msim_msg_dump("msim_postprocess_outgoing_cb() got msg=%s\n", msg);
+
+	/* Obtain userid from userinfo message. */
+	body_str = msim_msg_get_string(userinfo, "body");
+	g_return_if_fail(body_str != NULL);
+	body = msim_parse_body(body_str);
+	g_free(body_str);
+
+	uid = g_strdup(g_hash_table_lookup(body, "UserID"));
+	g_hash_table_destroy(body);
+
+	uid_field_name = msim_msg_get_string(msg, "_uid_field_name");
+	uid_before = msim_msg_get_string(msg, "_uid_before");
+
+	msg = msim_do_postprocessing(msg, uid_before, uid_field_name, atol(uid));
+
+	/* Send */
+	if (!msim_msg_send(session, msg)) {
+		msim_msg_dump("msim_postprocess_outgoing_cb: sending failed for message: %s\n", msg);
+	}
+
+
+	/* Free field names AFTER sending message, because MsimMessage does NOT copy
+	 * field names - instead, treats them as static strings (which they usually are).
+	 */
+	g_free(uid_field_name);
+	g_free(uid_before);
+
+	g_hash_table_destroy(body);
+
+	//msim_msg_free(msg);
+}
+
+/** Postprocess and send a message.
+ *
+ * @param session
+ * @param msg Message to postprocess. Will NOT be freed.
+ * @param username Username to resolve. Assumed to be a static string (will not be freed or copied).
+ * @param uid_field_name Name of new field to add, containing uid of username. Static string.
+ * @param uid_before Name of existing field to insert username field before. Static string.
+ *
+ * @return TRUE if successful.
+ */
+gboolean 
+msim_postprocess_outgoing(MsimSession *session, MsimMessage *msg, 
+		const gchar *username, const gchar *uid_field_name, 
+		const gchar *uid_before)
+{
+	PurpleBuddy *buddy;
+	guint uid;
+	gboolean rc;
+
+	g_return_val_if_fail(msg != NULL, FALSE);
+
+	/* Store information for msim_postprocess_outgoing_cb(). */
+	msim_msg_dump("msim_postprocess_outgoing: msg before=%s\n", msg);
+	msg = msim_msg_append(msg, "_username", MSIM_TYPE_STRING, g_strdup(username));
+	msg = msim_msg_append(msg, "_uid_field_name", MSIM_TYPE_STRING, g_strdup(uid_field_name));
+	msg = msim_msg_append(msg, "_uid_before", MSIM_TYPE_STRING, g_strdup(uid_before));
+
+	/* First, try the most obvious. If numeric userid is given, use that directly. */
+	if (msim_is_userid(username)) {
+		uid = atol(username);
+	} else {
+		/* Next, see if on buddy list and know uid. */
+		buddy = purple_find_buddy(session->account, username);
+		if (buddy) {
+			uid = purple_blist_node_get_int(&buddy->node, "UserID");
+		} else {
+			uid = 0;
+		}
+
+		if (!buddy || !uid)
+		{
+			/* Don't have uid offhand - need to ask for it, and wait until hear back before sending. */
+			purple_debug_info("msim", ">>> msim_postprocess_outgoing: couldn't find username %s in blist\n",
+					username ? username : "(NULL)");
+			msim_msg_dump("msim_postprocess_outgoing - scheduling lookup, msg=%s\n", msg);
+			/* TODO: where is cloned message freed? Should be in _cb. */
+			msim_lookup_user(session, username, msim_postprocess_outgoing_cb, msim_msg_clone(msg));
+			return TRUE;       /* not sure of status yet - haven't sent! */
+		}
+	}
+	
+	/* Already have uid, postprocess and send msg immediately. */
+	purple_debug_info("msim", "msim_postprocess_outgoing: found username %s has uid %d\n",
+			username ? username : "(NULL)", uid);
+
+	msg = msim_do_postprocessing(msg, uid_before, uid_field_name, uid);
+
+	msim_msg_dump("msim_postprocess_outgoing: msg after (uid immediate)=%s\n", msg);
+	
+	rc = msim_msg_send(session, msg);
+
+	//msim_msg_free(msg);
+
+	return rc;
+}
+
+/** Remove a buddy from the user's buddy list. */
+void 
+msim_remove_buddy(PurpleConnection *gc, PurpleBuddy *buddy, PurpleGroup *group)
+{
+	MsimSession *session;
+	MsimMessage *delbuddy_msg;
+	MsimMessage *persist_msg;
+	MsimMessage *blocklist_msg;
+	GList *blocklist_updates;
+
+	session = (MsimSession *)gc->proto_data;
+
+	delbuddy_msg = msim_msg_new(
+				"delbuddy", MSIM_TYPE_BOOLEAN, TRUE,
+				"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+				/* 'delprofileid' with uid will be inserted here. */
+				NULL);
+
+	if (!msim_postprocess_outgoing(session, delbuddy_msg, buddy->name, "delprofileid", NULL)) {
+		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("'delbuddy' command failed"));
+		msim_msg_free(delbuddy_msg);
+		return;
+	}
+	msim_msg_free(delbuddy_msg);
+
+	persist_msg = msim_msg_new(
+			"persist", MSIM_TYPE_INTEGER, 1,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"cmd", MSIM_TYPE_INTEGER, MSIM_CMD_BIT_ACTION | MSIM_CMD_DELETE,
+			"dsn", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_DSN,
+			"lid", MSIM_TYPE_INTEGER, MD_DELETE_BUDDY_LID,
+			"uid", MSIM_TYPE_INTEGER, session->userid,
+			"rid", MSIM_TYPE_INTEGER, session->next_rid++,
+			/* <uid> will be replaced by postprocessing */
+			"body", MSIM_TYPE_STRING, g_strdup("ContactID=<uid>"),
+			NULL);
+
+	if (!msim_postprocess_outgoing(session, persist_msg, buddy->name, "body", NULL)) {
+		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("persist command failed"));
+		msim_msg_free(persist_msg);
+		return;
+	}
+	msim_msg_free(persist_msg);
+
+	blocklist_updates = NULL;
+	blocklist_updates = g_list_prepend(blocklist_updates, "a-");
+	blocklist_updates = g_list_prepend(blocklist_updates, "<uid>");
+	blocklist_updates = g_list_prepend(blocklist_updates, "b-");
+	blocklist_updates = g_list_prepend(blocklist_updates, "<uid>");
+	blocklist_updates = g_list_reverse(blocklist_updates);
+
+	blocklist_msg = msim_msg_new(
+			"blocklist", MSIM_TYPE_BOOLEAN, TRUE,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			/* TODO: MsimMessage lists. Currently <uid> isn't replaced in lists. */
+			//"idlist", MSIM_TYPE_STRING, g_strdup("a-|<uid>|b-|<uid>"),
+			"idlist", MSIM_TYPE_LIST, blocklist_updates,
+			NULL);
+
+	if (!msim_postprocess_outgoing(session, blocklist_msg, buddy->name, "idlist", NULL)) {
+		purple_notify_error(NULL, NULL, _("Failed to remove buddy"), _("blocklist command failed"));
+		msim_msg_free(blocklist_msg);
+		return;
+	}
+	msim_msg_free(blocklist_msg);
+}
+
+/** Return whether the buddy can be messaged while offline.
+ *
+ * The protocol supports offline messages in just the same way as online
+ * messages.
+ */
+gboolean 
+msim_offline_message(const PurpleBuddy *buddy)
+{
+	return TRUE;
+}
+
+/**
+ * Callback when input available.
+ *
+ * @param gc_uncasted A PurpleConnection pointer.
+ * @param source File descriptor.
+ * @param cond PURPLE_INPUT_READ
+ *
+ * Reads the input, and calls msim_preprocess_incoming() to handle it.
+ */
+static void 
+msim_input_cb(gpointer gc_uncasted, gint source, PurpleInputCondition cond)
+{
+	PurpleConnection *gc;
+	PurpleAccount *account;
+	MsimSession *session;
+	gchar *end;
+	int n;
+
+	g_return_if_fail(gc_uncasted != NULL);
+	g_return_if_fail(source >= 0);  /* Note: 0 is a valid fd */
+
+	gc = (PurpleConnection *)(gc_uncasted);
+	account = purple_connection_get_account(gc);
+	session = gc->proto_data;
+
+	/* libpurple/eventloop.h only defines these two */
+	if (cond != PURPLE_INPUT_READ && cond != PURPLE_INPUT_WRITE) {
+		purple_debug_info("msim_input_cb", "unknown condition=%d\n", cond);
+		purple_connection_error(gc, _("Invalid input condition"));
+		return;
+	}
+
+	g_return_if_fail(cond == PURPLE_INPUT_READ);
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+
+	/* Mark down that we got data, so don't timeout. */
+	session->last_comm = time(NULL);
+
+	/* Only can handle so much data at once... 
+	 * If this happens, try recompiling with a higher MSIM_READ_BUF_SIZE.
+	 * Should be large enough to hold the largest protocol message.
+	 */
+	if (session->rxoff >= MSIM_READ_BUF_SIZE) {
+		purple_debug_error("msim", 
+				"msim_input_cb: %d-byte read buffer full! rxoff=%d\n",
+				MSIM_READ_BUF_SIZE, session->rxoff);
+		purple_connection_error(gc, _("Read buffer full"));
+		return;
+	}
+
+	purple_debug_info("msim", "buffer at %d (max %d), reading up to %d\n",
+			session->rxoff, MSIM_READ_BUF_SIZE, 
+			MSIM_READ_BUF_SIZE - session->rxoff);
+
+	/* Read into buffer. On Win32, need recv() not read(). session->fd also holds
+	 * the file descriptor, but it sometimes differs from the 'source' parameter.
+	 */
+	n = recv(session->fd, session->rxbuf + session->rxoff, MSIM_READ_BUF_SIZE - session->rxoff, 0);
+
+	if (n < 0 && errno == EAGAIN) {
+		return;
+	} else if (n < 0) {
+		purple_debug_error("msim", "msim_input_cb: read error, ret=%d, "
+			"error=%s, source=%d, fd=%d (%X))\n", 
+			n, strerror(errno), source, session->fd, session->fd);
+		purple_connection_error(gc, _("Read error"));
+		return;
+	} else if (n == 0) {
+		purple_debug_info("msim", "msim_input_cb: server disconnected\n");
+		purple_connection_error(gc, _("Server has disconnected"));
+		return;
+	}
+
+	if (n + session->rxoff >= MSIM_READ_BUF_SIZE) {
+		purple_debug_info("msim_input_cb", "received %d bytes, pushing rxoff to %d, over buffer size of %d\n",
+				n, n + session->rxoff, MSIM_READ_BUF_SIZE);
+		/* TODO: g_realloc like msn, yahoo, irc, jabber? */
+		purple_connection_error(gc, _("Read buffer full"));
+	}
+
+	/* Null terminate */
+	purple_debug_info("msim", "msim_input_cb: going to null terminate "
+			"at n=%d\n", n);
+	session->rxbuf[session->rxoff + n] = 0;
+
+#ifdef MSIM_CHECK_EMBEDDED_NULLS
+	/* Check for embedded NULs. I don't handle them, and they shouldn't occur. */
+	if (strlen(session->rxbuf + session->rxoff) != n) {
+		/* Occurs after login, but it is not a null byte. */
+		purple_debug_info("msim", "msim_input_cb: strlen=%d, but read %d bytes"
+				"--null byte encountered?\n", 
+				strlen(session->rxbuf + session->rxoff), n);
+		//purple_connection_error(gc, "Invalid message - null byte on input");
+		return;
+	}
+#endif
+
+	session->rxoff += n;
+	purple_debug_info("msim", "msim_input_cb: read=%d\n", n);
+
+#ifdef MSIM_DEBUG_RXBUF
+	purple_debug_info("msim", "buf=<%s>\n", session->rxbuf);
+#endif
+
+	/* Look for \\final\\ end markers. If found, process message. */
+	while((end = strstr(session->rxbuf, MSIM_FINAL_STRING))) {
+		MsimMessage *msg;
+
+#ifdef MSIM_DEBUG_RXBUF
+		purple_debug_info("msim", "in loop: buf=<%s>\n", session->rxbuf);
+#endif
+		*end = 0;
+		msg = msim_parse(g_strdup(session->rxbuf));
+		if (!msg) {
+			purple_debug_info("msim", "msim_input_cb: couldn't parse rxbuf\n");
+			purple_connection_error(gc, _("Unparseable message"));
+		} else {
+			/* Process message and then free it (processing function should
+			 * clone message if it wants to keep it afterwards.) */
+			if (!msim_preprocess_incoming(session, msg)) {
+				msim_msg_dump("msim_input_cb: preprocessing message failed on msg: %s\n", msg);
+			}
+			msim_msg_free(msg);
+		}
+
+		/* Move remaining part of buffer to beginning. */
+		session->rxoff -= strlen(session->rxbuf) + strlen(MSIM_FINAL_STRING);
+		memmove(session->rxbuf, end + strlen(MSIM_FINAL_STRING), 
+				MSIM_READ_BUF_SIZE - (end + strlen(MSIM_FINAL_STRING) - session->rxbuf));
+
+		/* Clear end of buffer */
+		//memset(end, 0, MSIM_READ_BUF_SIZE - (end - session->rxbuf));
+	}
+}
+
+/* Setup a callback, to be called when a reply is received with the returned rid.
+ *
+ * @param cb The callback, an MSIM_USER_LOOKUP_CB.
+ * @param data Arbitrary user data to be passed to callback (probably an MsimMessage *).
+ *
+ * @return The request/reply ID, used to link replies with requests, or -1.
+ *          Put the rid in your request, 'rid' field.
+ *
+ * TODO: Make more generic and more specific:
+ * 1) MSIM_USER_LOOKUP_CB - make it for PERSIST_REPLY, not just user lookup
+ * 2) data - make it an MsimMessage?
+ */
+static guint 
+msim_new_reply_callback(MsimSession *session, MSIM_USER_LOOKUP_CB cb, 
+		gpointer data)
+{
+	guint rid;
+
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), -1);
+
+	rid = session->next_rid++;
+
+	g_hash_table_insert(session->user_lookup_cb, GUINT_TO_POINTER(rid), cb);
+	g_hash_table_insert(session->user_lookup_cb_data, GUINT_TO_POINTER(rid), data);
+
+	return rid;
+}
+
+/**
+ * Callback when connected. Sets up input handlers.
+ *
+ * @param data A PurpleConnection pointer.
+ * @param source File descriptor.
+ * @param error_message
+ */
+static void 
+msim_connect_cb(gpointer data, gint source, const gchar *error_message)
+{
+	PurpleConnection *gc;
+	MsimSession *session;
+
+	g_return_if_fail(data != NULL);
+
+	gc = (PurpleConnection *)data;
+	session = (MsimSession *)gc->proto_data;
+
+	if (source < 0) {
+		purple_connection_error(gc, _("Couldn't connect to host"));
+		purple_connection_error(gc, g_strdup_printf(
+					_("Couldn't connect to host: %s (%d)"), 
+					error_message ? error_message : "no message given", 
+					source));
+		return;
+	}
+
+	session->fd = source; 
+
+	gc->inpa = purple_input_add(source, PURPLE_INPUT_READ, msim_input_cb, gc);
+}
+
+/* Session methods */
+
+/**
+ * Create a new MSIM session.
+ *
+ * @param acct The account to create the session from.
+ *
+ * @return Pointer to a new session. Free with msim_session_destroy.
+ */
+MsimSession *
+msim_session_new(PurpleAccount *acct)
+{
+	MsimSession *session;
+
+	g_return_val_if_fail(acct != NULL, NULL);
+
+	session = g_new0(MsimSession, 1);
+
+	session->magic = MSIM_SESSION_STRUCT_MAGIC;
+	session->account = acct;
+	session->gc = purple_account_get_connection(acct);
+	session->sesskey = 0;
+	session->userid = 0;
+	session->username = NULL;
+	session->fd = -1;
+
+	/* TODO: Remove. */
+	session->user_lookup_cb = g_hash_table_new_full(g_direct_hash, 
+			g_direct_equal, NULL, NULL);  /* do NOT free function pointers! (values) */
+	session->user_lookup_cb_data = g_hash_table_new_full(g_direct_hash, 
+			g_direct_equal, NULL, NULL);/* TODO: we don't know what the values are,
+											 they could be integers inside gpointers
+											 or strings, so I don't freed them.
+											 Figure this out, once free cache. */
+
+	/* Created in msim_process_server_info() */
+	session->server_info = NULL;
+
+	session->rxoff = 0;
+	session->rxbuf = g_new0(gchar, MSIM_READ_BUF_SIZE);
+	session->next_rid = 1;
+	session->last_comm = time(NULL);
+	session->inbox_status = 0;
+	
+	return session;
+}
+
+/**
+ * Free a session.
+ *
+ * @param session The session to destroy.
+ */
+void 
+msim_session_destroy(MsimSession *session)
+{
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+	
+	session->magic = -1;
+
+	g_free(session->rxbuf);
+	g_free(session->username);
+
+	/* TODO: Remove. */
+	g_hash_table_destroy(session->user_lookup_cb);
+	g_hash_table_destroy(session->user_lookup_cb_data);
+
+	if (session->server_info) {
+		g_hash_table_destroy(session->server_info);
+	}
+	
+	g_free(session);
+}
+				 
+/** 
+ * Close the connection.
+ * 
+ * @param gc The connection.
+ */
+void 
+msim_close(PurpleConnection *gc)
+{
+	MsimSession *session;
+
+	if (gc == NULL) {
+		return;
+	}
+
+	session = (MsimSession *)gc->proto_data;
+	if (session == NULL)
+		return;
+
+	gc->proto_data = NULL;
+
+	if (!MSIM_SESSION_VALID(session)) {
+		return;
+	}
+
+	if (session->gc->inpa) {
+		purple_input_remove(session->gc->inpa);
+	}
+
+	msim_session_destroy(session);
+}
+
+
+/**
+ * Check if a string is a userid (all numeric).
+ *
+ * @param user The user id, email, or name.
+ *
+ * @return TRUE if is userid, FALSE if not.
+ */
+static gboolean 
+msim_is_userid(const gchar *user)
+{
+	g_return_val_if_fail(user != NULL, FALSE);
+
+	return strspn(user, "0123456789") == strlen(user);
+}
+
+/**
+ * Check if a string is an email address (contains an @).
+ *
+ * @param user The user id, email, or name.
+ *
+ * @return TRUE if is an email, FALSE if not.
+ *
+ * This function is not intended to be used as a generic
+ * means of validating email addresses, but to distinguish
+ * between a user represented by an email address from
+ * other forms of identification.
+ */ 
+static gboolean 
+msim_is_email(const gchar *user)
+{
+	g_return_val_if_fail(user != NULL, FALSE);
+
+	return strchr(user, '@') != NULL;
+}
+
+
+/**
+ * Asynchronously lookup user information, calling callback when receive result.
+ *
+ * @param session
+ * @param user The user id, email address, or username. Not freed.
+ * @param cb Callback, called with user information when available.
+ * @param data An arbitray data pointer passed to the callback.
+ */
+/* TODO: change to not use callbacks */
+static void 
+msim_lookup_user(MsimSession *session, const gchar *user, 
+		MSIM_USER_LOOKUP_CB cb, gpointer data)
+{
+	MsimMessage *body;
+	gchar *field_name;
+	guint rid, cmd, dsn, lid;
+
+	g_return_if_fail(MSIM_SESSION_VALID(session));
+	g_return_if_fail(user != NULL);
+	g_return_if_fail(cb != NULL);
+
+	purple_debug_info("msim", "msim_lookup_userid: "
+			"asynchronously looking up <%s>\n", user);
+
+	msim_msg_dump("msim_lookup_user: data=%s\n", (MsimMessage *)data);
+
+	/* Setup callback. Response will be associated with request using 'rid'. */
+	rid = msim_new_reply_callback(session, cb, data);
+
+	/* Send request */
+
+	cmd = MSIM_CMD_GET;
+
+	if (msim_is_userid(user)) {
+		field_name = "UserID";
+		dsn = MG_MYSPACE_INFO_BY_ID_DSN; 
+		lid = MG_MYSPACE_INFO_BY_ID_LID; 
+	} else if (msim_is_email(user)) {
+		field_name = "Email";
+		dsn = MG_MYSPACE_INFO_BY_STRING_DSN;
+		lid = MG_MYSPACE_INFO_BY_STRING_LID;
+	} else {
+		field_name = "UserName";
+		dsn = MG_MYSPACE_INFO_BY_STRING_DSN;
+		lid = MG_MYSPACE_INFO_BY_STRING_LID;
+	}
+
+	body = msim_msg_new(
+			field_name, MSIM_TYPE_STRING, g_strdup(user),
+			NULL);
+
+	g_return_if_fail(msim_send(session,
+			"persist", MSIM_TYPE_INTEGER, 1,
+			"sesskey", MSIM_TYPE_INTEGER, session->sesskey,
+			"cmd", MSIM_TYPE_INTEGER, 1,
+			"dsn", MSIM_TYPE_INTEGER, dsn,
+			"uid", MSIM_TYPE_INTEGER, session->userid,
+			"lid", MSIM_TYPE_INTEGER, lid,
+			"rid", MSIM_TYPE_INTEGER, rid,
+			"body", MSIM_TYPE_DICTIONARY, body,
+			NULL));
+} 
+
+
+/**
+ * Obtain the status text for a buddy.
+ *
+ * @param buddy The buddy to obtain status text for.
+ *
+ * @return Status text, or NULL if error. Caller g_free()'s.
+ *
+ */
+char *
+msim_status_text(PurpleBuddy *buddy)
+{
+	MsimSession *session;
+	MsimUser *user;
+	const gchar *display_name, *headline;
+
+	g_return_val_if_fail(buddy != NULL, NULL);
+
+	user = msim_get_user_from_buddy(buddy);
+
+	session = (MsimSession *)buddy->account->gc->proto_data;
+	g_return_val_if_fail(MSIM_SESSION_VALID(session), NULL);
+
+	display_name = headline = NULL;
+
+	/* Retrieve display name and/or headline, depending on user preference. */
+	if (purple_account_get_bool(session->account, "show_display_name", TRUE)) {
+		display_name = user->display_name;
+	} 
+
+	if (purple_account_get_bool(session->account, "show_headline", FALSE)) {
+		headline = user->headline;
+	}
+
+	/* Return appropriate combination of display name and/or headline, or neither. */
+
+	if (display_name && headline) {
+		return g_strconcat(display_name, " ", headline, NULL);
+	} else if (display_name) {
+		return g_strdup(display_name);
+	} else if (headline) {
+		return g_strdup(headline);
+	}
+
+	return NULL;
+}
+
+/**
+ * Obtain the tooltip text for a buddy.
+ *
+ * @param buddy Buddy to obtain tooltip text on.
+ * @param user_info Variable modified to have the tooltip text.
+ * @param full TRUE if should obtain full tooltip text.
+ *
+ */
+void 
+msim_tooltip_text(PurpleBuddy *buddy, PurpleNotifyUserInfo *user_info, 
+		gboolean full)
+{
+	MsimUser *user;
+
+	g_return_if_fail(buddy != NULL);
+	g_return_if_fail(user_info != NULL);
+
+	user = msim_get_user_from_buddy(buddy);
+
+	if (PURPLE_BUDDY_IS_ONLINE(buddy)) {
+		MsimSession *session;
+
+		session = (MsimSession *)buddy->account->gc->proto_data;
+
+		g_return_if_fail(MSIM_SESSION_VALID(session));
+
+		/* TODO: if (full), do something different? */
+
+		/* TODO: request information? have to figure out how to do
+		 * the asynchronous lookup like oscar does (tooltip shows
+		 * 'retrieving...' if not yet available, then changes when it is).
+		 *
+		 * Right now, only show what we have on hand.
+		 */
+
+		/* Show abbreviated user info. */
+		msim_append_user_info(session, user_info, user, FALSE);
+	}
+}
+
+/** Actions menu for account. */
+GList *
+msim_actions(PurplePlugin *plugin, gpointer context)
+{
+	PurpleConnection *gc;
+	GList *menu;
+	//PurplePluginAction *act;
+
+	gc = (PurpleConnection *)context;
+
+	menu = NULL;
+
+#if 0
+	/* TODO: find out how */
+	act = purple_plugin_action_new(_("Find people..."), msim_);
+	menu = g_list_append(menu, act);
+
+	act = purple_plugin_action_new(_("Import friends..."), NULL);
+	menu = g_list_append(menu, act);
+
+	act = purple_plugin_action_new(_("Change IM name..."), NULL);
+	menu = g_list_append(menu, act);
+#endif
+
+	return menu;
+}
+
+/** Callbacks called by Purple, to access this plugin. */
+PurplePluginProtocolInfo prpl_info = {
+	/* options */
+	  OPT_PROTO_USE_POINTSIZE        /* specify font size in sane point size */
+	| OPT_PROTO_MAIL_CHECK,
+
+	/* | OPT_PROTO_IM_IMAGE - TODO: direct images. */    
+	NULL,              /* user_splits */
+	NULL,              /* protocol_options */
+	NO_BUDDY_ICONS,    /* icon_spec - TODO: eventually should add this */
+	msim_list_icon,    /* list_icon */
+	NULL,              /* list_emblems */
+	msim_status_text,  /* status_text */
+	msim_tooltip_text, /* tooltip_text */
+	msim_status_types, /* status_types */
+	msim_blist_node_menu,  /* blist_node_menu */
+	NULL,              /* chat_info */
+	NULL,              /* chat_info_defaults */
+	msim_login,        /* login */
+	msim_close,        /* close */
+	msim_send_im,      /* send_im */
+	NULL,              /* set_info */
+	msim_send_typing,  /* send_typing */
+	msim_get_info,     /* get_info */
+	msim_set_status,   /* set_status */
+	msim_set_idle,     /* set_idle */
+	NULL,              /* change_passwd */
+	msim_add_buddy,    /* add_buddy */
+	NULL,              /* add_buddies */
+	msim_remove_buddy, /* remove_buddy */
+	NULL,              /* remove_buddies */
+	NULL,              /* add_permit */
+	NULL,              /* add_deny */
+	NULL,              /* rem_permit */
+	NULL,              /* rem_deny */
+	NULL,              /* set_permit_deny */
+	NULL,              /* join_chat */
+	NULL,              /* reject chat invite */
+	NULL,              /* get_chat_name */
+	NULL,              /* chat_invite */
+	NULL,              /* chat_leave */
+	NULL,              /* chat_whisper */
+	NULL,              /* chat_send */
+	NULL,              /* keepalive */
+	NULL,              /* register_user */
+	NULL,              /* get_cb_info */
+	NULL,              /* get_cb_away */
+	NULL,              /* alias_buddy */
+	NULL,              /* group_buddy */
+	NULL,              /* rename_group */
+	NULL,              /* buddy_free */
+	NULL,              /* convo_closed */
+	NULL,              /* normalize */
+	NULL,              /* set_buddy_icon */
+	NULL,              /* remove_group */
+	NULL,              /* get_cb_real_name */
+	NULL,              /* set_chat_topic */
+	NULL,              /* find_blist_chat */
+	NULL,              /* roomlist_get_list */
+	NULL,              /* roomlist_cancel */
+	NULL,              /* roomlist_expand_category */
+	NULL,              /* can_receive_file */
+	NULL,              /* send_file */
+	NULL,              /* new_xfer */
+	msim_offline_message, /* offline_message */
+	NULL,              /* whiteboard_prpl_ops */
+	msim_send_really_raw,     /* send_raw */
+	NULL,               /* roomlist_room_serialize */
+	NULL,               /* _purple_reserved1 */
+	NULL,               /* _purple_reserved2 */
+	NULL,               /* _purple_reserved3 */
+	NULL                /* _purple_reserved4 */
+};
+
+
+
+/** Based on MSN's plugin info comments. */
+PurplePluginInfo info = {
+	PURPLE_PLUGIN_MAGIC,                                
+	PURPLE_MAJOR_VERSION,
+	PURPLE_MINOR_VERSION,
+	PURPLE_PLUGIN_PROTOCOL,                           /**< type           */
+	NULL,                                             /**< ui_requirement */
+	0,                                                /**< flags          */
+	NULL,                                             /**< dependencies   */
+	PURPLE_PRIORITY_DEFAULT,                          /**< priority       */
+
+	"prpl-myspace",                                   /**< id             */
+	"MySpaceIM",                                      /**< name           */
+	MSIM_PRPL_VERSION_STRING,                         /**< version        */
+	                                                  /**  summary        */
+	"MySpaceIM Protocol Plugin",
+	                                                  /**  description    */
+	"MySpaceIM Protocol Plugin",
+	"Jeff Connelly <jeff2@soc.pidgin.im>",            /**< author         */
+	"http://developer.pidgin.im/wiki/MySpaceIM/",     /**< homepage       */
+
+	msim_load,                                        /**< load           */
+	NULL,                                             /**< unload         */
+	NULL,                                             /**< destroy        */
+	NULL,                                             /**< ui_info        */
+	&prpl_info,                                       /**< extra_info     */
+	NULL,                                             /**< prefs_info     */
+	msim_actions,                                     /**< msim_actions   */
+	NULL,                                             /**< reserved1      */
+	NULL,                                             /**< reserved2      */
+	NULL,                                             /**< reserved3      */
+	NULL                                              /**< reserved4      */
+};
+
+
+#ifdef MSIM_SELF_TEST
+/** Test functions.
+ * Used to test or try out the internal workings of msimprpl. If you're reading
+ * this code for the first time, these functions can be instructive in learning
+ * how msimprpl is architected.
+ */
+void 
+msim_test_all(void) {
+	guint failures;
+
+
+	failures = 0;
+	failures += msim_test_msg();
+	failures += msim_test_escaping();
+
+	if (failures) {
+		purple_debug_info("msim", "msim_test_all HAD FAILURES: %d\n", failures);
+	} else {
+		purple_debug_info("msim", "msim_test_all - all tests passed!\n");
+	}
+	exit(0);
+}
+
+/** Test MsimMessage for basic functionality. */
+int 
+msim_test_msg(void)
+{
+	MsimMessage *msg, *msg_cloned, *msg2;
+	GList *list;
+	gchar *packed, *packed_expected, *packed_cloned;
+	guint failures;
+
+	failures = 0;
+
+	purple_debug_info("msim", "\n\nTesting MsimMessage\n");
+	msg = msim_msg_new(NULL);      /* Create a new, empty message. */
+
+	/* Append some new elements. */
+	msg = msim_msg_append(msg, "bx", MSIM_TYPE_BINARY, g_string_new_len(g_strdup("XXX"), 3));
+	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v1"));
+	msg = msim_msg_append(msg, "k1", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(42));
+	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v43"));
+	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v52/xxx\\yyy"));
+	msg = msim_msg_append(msg, "k1", MSIM_TYPE_STRING, g_strdup("v7"));
+	msim_msg_dump("msg debug str=%s\n", msg);
+	packed = msim_msg_pack(msg);
+
+	purple_debug_info("msim", "msg packed=%s\n", packed);
+
+	packed_expected = "\\bx\\WFhY\\k1\\v1\\k1\\42\\k1"
+		"\\v43\\k1\\v52/1xxx/2yyy\\k1\\v7\\final\\";
+
+	if (0 != strcmp(packed, packed_expected)) {
+		purple_debug_info("msim", "!!!(%d), msim_msg_pack not what expected: %s != %s\n",
+				++failures, packed, packed_expected);
+	}
+
+
+	msg_cloned = msim_msg_clone(msg);
+	packed_cloned = msim_msg_pack(msg_cloned);
+
+	purple_debug_info("msim", "msg cloned=%s\n", packed_cloned);
+	if (0 != strcmp(packed, packed_cloned)) {
+		purple_debug_info("msim", "!!!(%d), msim_msg_pack on cloned message not equal to original: %s != %s\n",
+				++failures, packed_cloned, packed);
+	}
+
+	g_free(packed);
+	g_free(packed_cloned);
+	msim_msg_free(msg_cloned);
+	msim_msg_free(msg);
+
+	/* Try some of the more advanced functionality */
+	list = NULL;
+
+	list = g_list_prepend(list, "item3");
+	list = g_list_prepend(list, "item2");
+	list = g_list_prepend(list, "item1");
+	list = g_list_prepend(list, "item0");
+
+	msg = msim_msg_new(NULL);
+	msg = msim_msg_append(msg, "string", MSIM_TYPE_STRING, g_strdup("string value"));
+	msg = msim_msg_append(msg, "raw", MSIM_TYPE_RAW, g_strdup("raw value"));
+	msg = msim_msg_append(msg, "integer", MSIM_TYPE_INTEGER, GUINT_TO_POINTER(3140));
+	msg = msim_msg_append(msg, "boolean", MSIM_TYPE_BOOLEAN, GUINT_TO_POINTER(FALSE));
+	msg = msim_msg_append(msg, "list", MSIM_TYPE_LIST, list);
+
+	msim_msg_dump("msg with list=%s\n", msg);
+	purple_debug_info("msim", "msg with list packed=%s\n", msim_msg_pack(msg));
+
+	msg2 = msim_msg_new(NULL);
+	msg2 = msim_msg_append(msg2, "outer", MSIM_TYPE_STRING, g_strdup("outer value"));
+	msg2 = msim_msg_append(msg2, "body", MSIM_TYPE_DICTIONARY, msg);
+	msim_msg_dump("msg with dict=%s\n", msg2);      /* msg2 now 'owns' msg */
+	purple_debug_info("msim", "msg with dict packed=%s\n", msim_msg_pack(msg2));
+
+	msim_msg_free(msg2);
+
+	return failures;
+}
+
+/** Test protocol-level escaping/unescaping. */
+int 
+msim_test_escaping(void)
+{
+	guint failures;
+	gchar *raw, *escaped, *unescaped, *expected;
+
+	failures = 0;
+
+	purple_debug_info("msim", "\n\nTesting escaping\n");
+
+	raw = "hello/world\\hello/world";
+
+	escaped = msim_escape(raw);
+	purple_debug_info("msim", "msim_test_escaping: raw=%s, escaped=%s\n", raw, escaped);
+	expected = "hello/1world/2hello/1world";
+	if (0 != strcmp(escaped, expected)) {
+		purple_debug_info("msim", "!!!(%d), msim_escape failed: %s != %s\n",
+				++failures, escaped, expected);
+	}
+
+
+	unescaped = msim_unescape(escaped);
+	g_free(escaped);
+	purple_debug_info("msim", "msim_test_escaping: unescaped=%s\n", unescaped);
+	if (0 != strcmp(raw, unescaped)) {
+		purple_debug_info("msim", "!!!(%d), msim_unescape failed: %s != %s\n",
+				++failures, raw, unescaped);
+	}
+
+	return failures;
+}
+#endif
+
+/** Initialize plugin. */
+void 
+init_plugin(PurplePlugin *plugin) 
+{
+	PurpleAccountOption *option;
+#ifdef MSIM_SELF_TEST
+	msim_test_all();
+	exit(0);
+#endif /* MSIM_SELF_TEST */
+
+
+	/* TODO: default to automatically try different ports. Make the user be
+	 * able to set the first port to try (like LastConnectedPort in Windows client).  */
+	option = purple_account_option_string_new(_("Connect server"), "server", MSIM_SERVER);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = purple_account_option_int_new(_("Connect port"), "port", MSIM_PORT);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+#ifdef MSIM_USER_WANTS_TO_CONFIGURE_STATUS_TEXT
+	option = purple_account_option_bool_new(_("Show display name in status text"), "show_display_name", TRUE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = purple_account_option_bool_new(_("Show headline in status text"), "show_headline", TRUE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+#endif
+
+#ifdef MSIM_USER_WANTS_TO_DISABLE_EMOTICONS
+	option = purple_account_option_bool_new(_("Send emoticons"), "emoticons", FALSE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+#endif
+
+#ifdef MSIM_USER_REALLY_CARES_ABOUT_PRECISE_FONT_SIZES
+	option = purple_account_option_int_new(_("Screen resolution (dots per inch)"), "dpi", MSIM_DEFAULT_DPI);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+
+	option = purple_account_option_int_new(_("Base font size (points)"), "base_font_size", MSIM_BASE_FONT_POINT_SIZE);
+	prpl_info.protocol_options = g_list_append(prpl_info.protocol_options, option);
+#endif
+
+	/* TODO: /zap command. Problem with this is that there are different kinds of zaps,
+	 * and the selection is best made available in a drop-down menu, instead of forcing
+	 * the user to type the kind of zap and memorizing available zaps (or putting it in the
+	 * help menu). A new "attention" API, for zap/buzz/nudge (different protocols) will
+	 * solve this. */
+#if 0
+	purple_cmd_register("zap",                                        /* cmd */
+			"w",                                              /* args - accept a single word */
+			PURPLE_CMD_P_PRPL,                                /* priority */
+			PURPLE_CMD_FLAG_IM | PURPLE_CMD_FLAG_PRPL_ONLY,   /* flags */
+			"prpl-myspace",                                   /* prpl_id */
+			msim_cmd_zap,                                     /* func */
+			_("zap: zap a user to get their attention"),      /* helpstr */
+			NULL);                                            /* data */
+#endif
+}
+
+PURPLE_INIT_PLUGIN(myspace, init_plugin, info);
--- a/libpurple/protocols/oscar/flap_connection.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/oscar/flap_connection.c	Tue Aug 14 02:45:41 2007 +0000
@@ -129,8 +129,8 @@
 
 			new_current = rateclass_get_new_current(conn, rateclass, &now);
 
+			/* (Add 100ms padding to account for inaccuracies in the calculation) */
 			if (new_current < rateclass->alert + 100)
-				/* (Add 100ms padding to account for inaccuracies in the calculation) */
 				/* Not ready to send this SNAC yet--keep waiting. */
 				return TRUE;
 
@@ -186,9 +186,9 @@
 		gettimeofday(&now, NULL);
 		new_current = rateclass_get_new_current(conn, rateclass, &now);
 
+		/* (Add 100ms padding to account for inaccuracies in the calculation) */
 		if (new_current < rateclass->alert + 100)
 		{
-			/* (Add 100ms padding to account for inaccuracies in the calculation) */
 			enqueue = TRUE;
 		}
 		else
--- a/libpurple/protocols/oscar/oscar.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/oscar/oscar.c	Tue Aug 14 02:45:41 2007 +0000
@@ -161,7 +161,6 @@
 static int purple_conv_chat_info_update (OscarData *, FlapConnection *, FlapFrame *, ...);
 static int purple_conv_chat_incoming_msg(OscarData *, FlapConnection *, FlapFrame *, ...);
 static int purple_email_parseupdate(OscarData *, FlapConnection *, FlapFrame *, ...);
-static int purple_icon_error       (OscarData *, FlapConnection *, FlapFrame *, ...);
 static int purple_icon_parseicon   (OscarData *, FlapConnection *, FlapFrame *, ...);
 static int oscar_icon_req        (OscarData *, FlapConnection *, FlapFrame *, ...);
 static int purple_parse_msgack     (OscarData *, FlapConnection *, FlapFrame *, ...);
@@ -195,7 +194,7 @@
 static int purple_ssi_authreply    (OscarData *, FlapConnection *, FlapFrame *, ...);
 static int purple_ssi_gotadded     (OscarData *, FlapConnection *, FlapFrame *, ...);
 
-static gboolean purple_icon_timerfunc(gpointer data);
+static void purple_icons_fetch(PurpleConnection *gc);
 
 static void recent_buddies_cb(const char *name, PurplePrefType type, gconstpointer value, gpointer data);
 void oscar_set_info(PurpleConnection *gc, const char *info);
@@ -483,7 +482,7 @@
 
 	/* Attempt to send as ASCII */
 	if (oscar_charset_check(from) == AIM_CHARSET_ASCII) {
-		*msg = g_convert(from, strlen(from), "ASCII", "UTF-8", NULL, &msglen, NULL);
+		*msg = g_convert(from, -1, "ASCII", "UTF-8", NULL, &msglen, NULL);
 		*charset = AIM_CHARSET_ASCII;
 		*charsubset = 0x0000;
 		*msglen_int = msglen;
@@ -505,7 +504,7 @@
 		b = purple_find_buddy(account, destsn);
 		if ((b != NULL) && (PURPLE_BUDDY_IS_ONLINE(b)))
 		{
-			*msg = g_convert(from, strlen(from), "UCS-2BE", "UTF-8", NULL, &msglen, NULL);
+			*msg = g_convert(from, -1, "UCS-2BE", "UTF-8", NULL, &msglen, NULL);
 			if (*msg != NULL)
 			{
 				*charset = AIM_CHARSET_UNICODE;
@@ -528,7 +527,7 @@
 	 * XXX - We need a way to only attempt to convert if we KNOW "from"
 	 * can be converted to "charsetstr"
 	 */
-	*msg = g_convert(from, strlen(from), charsetstr, "UTF-8", NULL, &msglen, NULL);
+	*msg = g_convert(from, -1, charsetstr, "UTF-8", NULL, &msglen, NULL);
 	if (*msg != NULL) {
 		*charset = AIM_CHARSET_CUSTOM;
 		*charsubset = 0x0000;
@@ -539,7 +538,7 @@
 	/*
 	 * Nothing else worked, so send as UCS-2BE.
 	 */
-	*msg = g_convert(from, strlen(from), "UCS-2BE", "UTF-8", NULL, &msglen, &err);
+	*msg = g_convert(from, -1, "UCS-2BE", "UTF-8", NULL, &msglen, &err);
 	if (*msg != NULL) {
 		*charset = AIM_CHARSET_UNICODE;
 		*charsubset = 0x0000;
@@ -1156,8 +1155,7 @@
 
 	od->iconconnecting = FALSE;
 
-	if (od->icontimer == 0)
-		od->icontimer = purple_timeout_add(100, purple_icon_timerfunc, gc);
+	purple_icons_fetch(gc);
 }
 
 static int
@@ -1203,7 +1201,6 @@
 	oscar_data_addhandler(od, SNAC_FAMILY_AUTH, 0x0003, purple_parse_auth_resp, 0);
 	oscar_data_addhandler(od, SNAC_FAMILY_AUTH, 0x0007, purple_parse_login, 0);
 	oscar_data_addhandler(od, SNAC_FAMILY_AUTH, SNAC_SUBTYPE_AUTH_SECURID_REQUEST, purple_parse_auth_securid_request, 0);
-	oscar_data_addhandler(od, SNAC_FAMILY_BART, SNAC_SUBTYPE_BART_ERROR, purple_icon_error, 0);
 	oscar_data_addhandler(od, SNAC_FAMILY_BART, SNAC_SUBTYPE_BART_RESPONSE, purple_icon_parseicon, 0);
 	oscar_data_addhandler(od, SNAC_FAMILY_BOS, 0x0001, purple_parse_genericerr, 0);
 	oscar_data_addhandler(od, SNAC_FAMILY_BOS, 0x0003, purple_bosrights, 0);
@@ -1873,13 +1870,12 @@
 			saved_b16 = purple_buddy_icons_get_checksum_for_user(b);
 
 		if (!b16 || !saved_b16 || strcmp(b16, saved_b16)) {
-			GSList *cur = od->requesticon;
-			while (cur && aim_sncmp((char *)cur->data, info->sn))
-				cur = cur->next;
-			if (!cur) {
-				od->requesticon = g_slist_append(od->requesticon, g_strdup(purple_normalize(account, info->sn)));
-				if (od->icontimer == 0)
-					od->icontimer = purple_timeout_add(500, purple_icon_timerfunc, gc);
+			if (g_slist_find_custom(od->requesticon, info->sn,
+					(GCompareFunc)aim_sncmp) == NULL)
+			{
+				od->requesticon = g_slist_prepend(od->requesticon,
+						g_strdup(purple_normalize(account, info->sn)));
+				purple_icons_fetch(gc);
 			}
 		}
 		g_free(b16);
@@ -2265,8 +2261,9 @@
 
 /* When other people ask you for authorization */
 static void
-purple_auth_grant(struct name_data *data)
+purple_auth_grant(gpointer cbdata)
 {
+	struct name_data *data = cbdata;
 	PurpleConnection *gc = data->gc;
 	OscarData *od = gc->proto_data;
 
@@ -2286,8 +2283,9 @@
 }
 
 static void
-purple_auth_dontgrant_msgprompt(struct name_data *data)
+purple_auth_dontgrant_msgprompt(gpointer cbdata)
 {
+	struct name_data *data = cbdata;
 	purple_request_input(data->gc, NULL, _("Authorization Denied Message:"),
 					   NULL, _("No reason given."), TRUE, FALSE, NULL,
 					   _("_OK"), G_CALLBACK(purple_auth_dontgrant),
@@ -2408,8 +2406,8 @@
 
 				purple_account_request_authorization(account, sn, NULL, NULL,
 						reason, purple_find_buddy(account, sn) != NULL,
-						G_CALLBACK(purple_auth_grant),
-						G_CALLBACK(purple_auth_dontgrant_msgprompt), data);
+						purple_auth_grant,
+						purple_auth_dontgrant_msgprompt, data);
 				g_free(reason);
 			}
 		} break;
@@ -3237,24 +3235,8 @@
 	return 1;
 }
 
-static int purple_icon_error(OscarData *od, FlapConnection *conn, FlapFrame *fr, ...) {
-	PurpleConnection *gc = od->gc;
-	char *sn;
-
-	sn = od->requesticon->data;
-	purple_debug_misc("oscar", "removing %s from hash table\n", sn);
-	od->requesticon = g_slist_remove(od->requesticon, sn);
-	g_free(sn);
-
-	if (od->icontimer == 0)
-		od->icontimer = purple_timeout_add(500, purple_icon_timerfunc, gc);
-
-	return 1;
-}
-
 static int purple_icon_parseicon(OscarData *od, FlapConnection *conn, FlapFrame *fr, ...) {
 	PurpleConnection *gc = od->gc;
-	GSList *cur;
 	va_list ap;
 	char *sn;
 	guint8 iconcsumtype, *iconcsum, *icon;
@@ -3280,38 +3262,23 @@
 		g_free(b16);
 	}
 
-	cur = od->requesticon;
-	while (cur) {
-		char *cursn = cur->data;
-		if (!aim_sncmp(cursn, sn)) {
-			od->requesticon = g_slist_remove(od->requesticon, cursn);
-			g_free(cursn);
-			cur = od->requesticon;
-		} else
-			cur = cur->next;
-	}
-
-	if (od->icontimer == 0)
-		od->icontimer = purple_timeout_add(250, purple_icon_timerfunc, gc);
-
 	return 1;
 }
 
-static gboolean purple_icon_timerfunc(gpointer data) {
-	PurpleConnection *gc = data;
+static void
+purple_icons_fetch(PurpleConnection *gc)
+{
 	OscarData *od = gc->proto_data;
 	aim_userinfo_t *userinfo;
 	FlapConnection *conn;
 
-	od->icontimer = 0;
-
 	conn = flap_connection_getbytype(od, SNAC_FAMILY_BART);
 	if (!conn) {
 		if (!od->iconconnecting) {
 			aim_srv_requestnew(od, SNAC_FAMILY_BART);
 			od->iconconnecting = TRUE;
 		}
-		return FALSE;
+		return;
 	}
 
 	if (od->set_icon) {
@@ -3322,32 +3289,24 @@
 		} else {
 			purple_debug_info("oscar",
 				   "Uploading icon to icon server\n");
-			aim_bart_upload(od, purple_imgstore_get_data(img), 
+			aim_bart_upload(od, purple_imgstore_get_data(img),
 			                purple_imgstore_get_size(img));
 			purple_imgstore_unref(img);
 		}
 		od->set_icon = FALSE;
 	}
 
-	if (!od->requesticon) {
-		purple_debug_misc("oscar",
-				   "no more icons to request\n");
-		return FALSE;
-	}
-
-	userinfo = aim_locate_finduserinfo(od, (char *)od->requesticon->data);
-	if ((userinfo != NULL) && (userinfo->iconcsumlen > 0)) {
-		aim_bart_request(od, od->requesticon->data, userinfo->iconcsumtype, userinfo->iconcsum, userinfo->iconcsumlen);
-		return FALSE;
-	} else {
-		gchar *sn = od->requesticon->data;
-		od->requesticon = g_slist_remove(od->requesticon, sn);
-		g_free(sn);
-	}
-
-	od->icontimer = purple_timeout_add(100, purple_icon_timerfunc, gc);
-
-	return FALSE;
+	while (od->requesticon != NULL)
+	{
+		userinfo = aim_locate_finduserinfo(od, (char *)od->requesticon->data);
+		if ((userinfo != NULL) && (userinfo->iconcsumlen > 0))
+			aim_bart_request(od, od->requesticon->data, userinfo->iconcsumtype, userinfo->iconcsum, userinfo->iconcsumlen);
+
+		g_free(od->requesticon->data);
+		od->requesticon = g_slist_delete_link(od->requesticon, od->requesticon);
+	}
+
+	purple_debug_misc("oscar", "no more icons to request\n");
 }
 
 /*
@@ -4389,10 +4348,10 @@
 
 	charset = oscar_charset_check(str);
 	if (charset == AIM_CHARSET_UNICODE) {
-		encoded = g_convert(str, strlen(str), "UCS-2BE", "UTF-8", NULL, ret_len, NULL);
+		encoded = g_convert(str, -1, "UCS-2BE", "UTF-8", NULL, ret_len, NULL);
 		*encoding = "unicode-2-0";
 	} else if (charset == AIM_CHARSET_CUSTOM) {
-		encoded = g_convert(str, strlen(str), "ISO-8859-1", "UTF-8", NULL, ret_len, NULL);
+		encoded = g_convert(str, -1, "ISO-8859-1", "UTF-8", NULL, ret_len, NULL);
 		*encoding = "iso-8859-1";
 	} else {
 		encoded = g_strdup(str);
@@ -5213,8 +5172,8 @@
 
 	purple_account_request_authorization(account, sn, NULL,
 			(buddy ? purple_buddy_get_alias_only(buddy) : NULL),
-			reason, buddy != NULL, G_CALLBACK(purple_auth_grant),
-			G_CALLBACK(purple_auth_dontgrant_msgprompt), data);
+			reason, buddy != NULL, purple_auth_grant,
+			purple_auth_dontgrant_msgprompt, data);
 	g_free(reason);
 
 	return 1;
--- a/libpurple/protocols/oscar/oscar.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/oscar/oscar.h	Tue Aug 14 02:45:41 2007 +0000
@@ -449,7 +449,6 @@
 	GSList *requesticon;
 
 	gboolean icq;
-	guint icontimer;
 	guint getblisttimer;
 	guint getinfotimer;
 
--- a/libpurple/protocols/oscar/oscar_data.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/oscar/oscar_data.c	Tue Aug 14 02:45:41 2007 +0000
@@ -95,8 +95,6 @@
 	g_free(od->email);
 	g_free(od->newp);
 	g_free(od->oldp);
-	if (od->icontimer > 0)
-		purple_timeout_remove(od->icontimer);
 	if (od->getblisttimer > 0)
 		purple_timeout_remove(od->getblisttimer);
 	if (od->getinfotimer > 0)
--- a/libpurple/protocols/yahoo/util.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/yahoo/util.c	Tue Aug 14 02:45:41 2007 +0000
@@ -61,7 +61,7 @@
 	else
 		to_codeset = purple_account_get_string(purple_connection_get_account(gc), "local_charset",  "ISO-8859-1");
 
-	ret = g_convert_with_fallback(str, strlen(str), to_codeset, "UTF-8", "?", NULL, NULL, NULL);
+	ret = g_convert_with_fallback(str, -1, to_codeset, "UTF-8", "?", NULL, NULL, NULL);
 	if (ret)
 		return ret;
 	else
@@ -92,7 +92,7 @@
 	else
 		from_codeset = purple_account_get_string(purple_connection_get_account(gc), "local_charset",  "ISO-8859-1");
 
-	ret = g_convert_with_fallback(str, strlen(str), "UTF-8", from_codeset, NULL, NULL, NULL, NULL);
+	ret = g_convert_with_fallback(str, -1, "UTF-8", from_codeset, NULL, NULL, NULL, NULL);
 
 	if (ret)
 		return ret;
--- a/libpurple/protocols/yahoo/yahoo.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/yahoo/yahoo.c	Tue Aug 14 02:45:41 2007 +0000
@@ -953,7 +953,8 @@
 };
 
 static void
-yahoo_buddy_add_authorize_cb(struct yahoo_add_request *add_req) {
+yahoo_buddy_add_authorize_cb(gpointer data) {
+	struct yahoo_add_request *add_req = data;
 	g_free(add_req->id);
 	g_free(add_req->who);
 	g_free(add_req->msg);
@@ -997,7 +998,8 @@
 }
 
 static void
-yahoo_buddy_add_deny_reason_cb(struct yahoo_add_request *add_req) {
+yahoo_buddy_add_deny_reason_cb(gpointer data) {
+	struct yahoo_add_request *add_req = data;
 	purple_request_input(add_req->gc, NULL, _("Authorization denied message:"),
 			NULL, _("No reason given."), TRUE, FALSE, NULL,
 			_("OK"), G_CALLBACK(yahoo_buddy_add_deny_cb),
@@ -1042,8 +1044,8 @@
 		 */
 		 purple_account_request_authorization(purple_connection_get_account(gc), add_req->who, add_req->id,
                                                     NULL, add_req->msg, purple_find_buddy(purple_connection_get_account(gc),add_req->who) != NULL,
-						    G_CALLBACK(yahoo_buddy_add_authorize_cb),
-						    G_CALLBACK(yahoo_buddy_add_deny_reason_cb),
+						    yahoo_buddy_add_authorize_cb,
+						    yahoo_buddy_add_deny_reason_cb,
                                                     add_req);
 	} else {
 		g_free(add_req->id);
@@ -3681,8 +3683,18 @@
 
 	group2 = yahoo_string_encode(gc, group, NULL);
 	pkt = yahoo_packet_new(YAHOO_SERVICE_ADDBUDDY, YAHOO_STATUS_AVAILABLE, 0);
-	yahoo_packet_hash(pkt, "ssss", 1, purple_connection_get_display_name(gc),
-	                  7, buddy->name, 65, group2, 14, "");
+	yahoo_packet_hash(pkt, "ssssssssss",
+	                  14, "",
+	                  65, group2,
+	                  97, "1",
+	                  1, purple_connection_get_display_name(gc),
+	                  302, "319",
+	                  300, "319",
+	                  7, buddy->name,
+	                  334, "0",
+	                  301, "319",
+	                  303, "319"
+	);
 	yahoo_packet_send_and_free(pkt, yd);
 	g_free(group2);
 }
@@ -3819,16 +3831,12 @@
 		return;
 	}
 
-	/* Step 1:  Add buddy to new group. */
-	pkt = yahoo_packet_new(YAHOO_SERVICE_ADDBUDDY, YAHOO_STATUS_AVAILABLE, 0);
-	yahoo_packet_hash(pkt, "ssss", 1, purple_connection_get_display_name(gc),
-	                  7, who, 65, gpn, 14, "");
+	pkt = yahoo_packet_new(YAHOO_SERVICE_CHGRP_15, YAHOO_STATUS_AVAILABLE, 0);
+	yahoo_packet_hash(pkt, "ssssssss", 1, purple_connection_get_display_name(gc),
+	                  302, "240", 300, "240", 7, who, 224, gpo, 264, gpn, 301,
+	                  "240", 303, "240");
 	yahoo_packet_send_and_free(pkt, yd);
 
-	/* Step 2:  Remove buddy from old group */
-	pkt = yahoo_packet_new(YAHOO_SERVICE_REMBUDDY, YAHOO_STATUS_AVAILABLE, 0);
-	yahoo_packet_hash(pkt, "sss", 1, purple_connection_get_display_name(gc), 7, who, 65, gpo);
-	yahoo_packet_send_and_free(pkt, yd);
 	g_free(gpn);
 	g_free(gpo);
 }
--- a/libpurple/protocols/yahoo/yahoo_packet.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/yahoo/yahoo_packet.c	Tue Aug 14 02:45:41 2007 +0000
@@ -223,6 +223,11 @@
 	GSList *l = pkt->hash;
 	int pos = 0;
 
+	/* This is only called from one place, and the list is
+	 * always backwards */
+
+	l = g_slist_reverse(l);
+
 	while (l) {
 		struct yahoo_pair *pair = l->data;
 		gchar buf[100];
--- a/libpurple/protocols/yahoo/yahoo_packet.h	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/protocols/yahoo/yahoo_packet.h	Tue Aug 14 02:45:41 2007 +0000
@@ -98,6 +98,7 @@
 	YAHOO_SERVICE_AVATAR_UPDATE = 0xc7,
 	YAHOO_SERVICE_VERIFY_ID_EXISTS = 0xc8,
 	YAHOO_SERVICE_AUDIBLE = 0xd0,
+	YAHOO_SERVICE_CHGRP_15 = 0xe7,
 	YAHOO_SERVICE_STATUS_15 = 0xf0,
 	YAHOO_SERVICE_LIST_15 = 0Xf1,
 	YAHOO_SERVICE_WEBLOGIN = 0x0226,
--- a/libpurple/win32/global.mak	Tue Aug 14 02:45:06 2007 +0000
+++ b/libpurple/win32/global.mak	Tue Aug 14 02:45:41 2007 +0000
@@ -40,7 +40,6 @@
 PIDGIN_IDLETRACK_TOP := $(PIDGIN_TOP)/win32/IdleTracker
 PIDGIN_PIXMAPS_TOP := $(PIDGIN_TOP)/pixmaps
 PIDGIN_PLUGINS_TOP := $(PIDGIN_TOP)/plugins
-PIDGIN_SOUNDS_TOP := $(PIDGIN_TOP)/sounds
 PURPLE_PO_TOP := $(PIDGIN_TREE_TOP)/po
 PURPLE_PROTOS_TOP := $(PURPLE_TOP)/protocols
 
--- a/pidgin/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -65,7 +65,7 @@
 pkgconfigdir = $(libdir)/pkgconfig
 pkgconfig_DATA = pidgin.pc
 
-SUBDIRS = pixmaps plugins sounds
+SUBDIRS = pixmaps plugins
 
 bin_PROGRAMS = pidgin
 
--- a/pidgin/Makefile.mingw	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/Makefile.mingw	Tue Aug 14 02:45:41 2007 +0000
@@ -146,7 +146,6 @@
 install: install_shallow all
 	$(MAKE) -C $(PIDGIN_PLUGINS_TOP) -f $(MINGW_MAKEFILE) install
 	$(MAKE) -C $(PIDGIN_PIXMAPS_TOP) -f $(MINGW_MAKEFILE) install
-	$(MAKE) -C $(PIDGIN_SOUNDS_TOP) -f $(MINGW_MAKEFILE) install
 	$(MAKE) -C $(PIDGIN_IDLETRACK_TOP) -f $(MINGW_MAKEFILE) install
 
 win32/pidgin_dll_rc.rc: win32/pidgin_dll_rc.rc.in $(PIDGIN_TREE_TOP)/VERSION
--- a/pidgin/gtkaccount.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkaccount.c	Tue Aug 14 02:45:41 2007 +0000
@@ -2546,9 +2546,15 @@
 }
 
 static void *
-pidgin_accounts_request_authorization(PurpleAccount *account, const char *remote_user,
-					const char *id, const char *alias, const char *message, gboolean on_list,
-					GCallback auth_cb, GCallback deny_cb, void *user_data)
+pidgin_accounts_request_authorization(PurpleAccount *account,
+                                      const char *remote_user,
+                                      const char *id,
+                                      const char *alias,
+                                      const char *message,
+                                      gboolean on_list,
+                                      PurpleAccountRequestAuthorizationCb auth_cb,
+                                      PurpleAccountRequestAuthorizationCb deny_cb,
+                                      void *user_data)
 {
 	char *buffer;
 	PurpleConnection *gc;
@@ -2574,8 +2580,8 @@
 
 	if (!on_list) {
 		struct auth_and_add *aa = g_new0(struct auth_and_add, 1);
-		aa->auth_cb = (PurpleAccountRequestAuthorizationCb)auth_cb;
-		aa->deny_cb = (PurpleAccountRequestAuthorizationCb)deny_cb;
+		aa->auth_cb = auth_cb;
+		aa->deny_cb = deny_cb;
 		aa->data = user_data;
 		aa->username = g_strdup(remote_user);
 		aa->alias = g_strdup(alias);
--- a/pidgin/gtkblist.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkblist.c	Tue Aug 14 02:45:41 2007 +0000
@@ -2608,6 +2608,7 @@
 	gboolean tooltip_top = FALSE;
 	struct _pidgin_blist_node *gtknode;
 	GdkRectangle mon_size;
+	int sig;
 	
 	if (node == NULL)
 		return;
@@ -2618,7 +2619,6 @@
 	 */
 	pidgin_blist_tooltip_destroy();
 
-
 	gtkblist->tipwindow = gtk_window_new(GTK_WINDOW_POPUP);
 
 	if(PURPLE_BLIST_NODE_IS_CHAT(node) || PURPLE_BLIST_NODE_IS_BUDDY(node)) {
@@ -2673,7 +2673,6 @@
 			G_CALLBACK(pidgin_blist_paint_tip), NULL);
 	gtk_widget_ensure_style (gtkblist->tipwindow);
 
-
 #if GTK_CHECK_VERSION(2,2,0)
 	gdk_display_get_pointer(gdk_display_get_default(), &screen, &x, &y, NULL);
 	mon_num = gdk_screen_get_monitor_at_point(screen, x, y);
@@ -2722,6 +2721,10 @@
 	gtk_window_move(GTK_WINDOW(gtkblist->tipwindow), x, y);
 	gtk_widget_show(gtkblist->tipwindow);
 
+	/* Hide the tooltip when the widget is destroyed */
+	sig = g_signal_connect(G_OBJECT(widget), "destroy", G_CALLBACK(pidgin_blist_tooltip_destroy), NULL);
+	g_signal_connect_swapped(G_OBJECT(gtkblist->tipwindow), "destroy", G_CALLBACK(g_source_remove), GINT_TO_POINTER(sig));
+
 	return;
 }
 
--- a/pidgin/gtkconv.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkconv.c	Tue Aug 14 02:45:41 2007 +0000
@@ -213,8 +213,8 @@
  * Callbacks
  **************************************************************************/
 
-static gint
-close_conv_cb(GtkWidget *w, PidginConversation *gtkconv)
+static gboolean
+close_conv_cb(GtkWidget *w, GdkEventButton *event, PidginConversation *gtkconv)
 {
 	GList *list = g_list_copy(gtkconv->convs);
 
@@ -1331,7 +1331,7 @@
 {
 	PidginWindow *win = data;
 
-	close_conv_cb(NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
+	close_conv_cb(NULL, NULL, PIDGIN_CONVERSATION(pidgin_conv_window_get_active_conversation(win)));
 }
 
 static void
@@ -6256,7 +6256,7 @@
 			(fields & PIDGIN_CONV_SET_TITLE) ||
     			(fields & PIDGIN_CONV_TOPIC))
 	{
-		char *title, *truncate = NULL, truncchar;
+		char *title, *truncate = NULL, truncchar = '\0';
 		PurpleConvIm *im = NULL;
 		PurpleAccount *account = purple_conversation_get_account(conv);
 	 	PurpleBuddy *buddy = NULL;
@@ -6294,11 +6294,13 @@
 		} else if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_CHAT) {
 			PurpleConvChat *chat = PURPLE_CONV_CHAT(conv);
 			const char *topic = purple_conv_chat_get_topic(chat);
+			char *esc = topic ? g_markup_escape_text(topic, -1) : NULL;
 			markup = g_strdup_printf("%s%s<span color='%s' size='smaller'>%s</span>",
 						purple_conversation_get_title(conv),
-						topic ? "\n" : "",
+						esc ? "\n" : "",
 						pidgin_get_dim_grey_string(gtkconv->infopane),
-						topic ? topic : "");
+						esc ? esc : "");
+			g_free(esc);
 		}
 		gtk_list_store_set(gtkconv->infopane_model, &(gtkconv->infopane_iter),
 				CONV_TEXT_COLUMN, markup, -1);
@@ -6320,13 +6322,13 @@
 			style = "color=\"#c4a000\"";
 		} else if (gtkconv->unseen_state == PIDGIN_UNSEEN_NICK)	{
 			atk_object_set_description(accessibility_obj, _("Nick Said"));
-			style = "color=\"#204a87\" style=\"italic\" weight=\"bold\"";
+			style = "color=\"#204a87\" weight=\"bold\"";
 		} else if (gtkconv->unseen_state == PIDGIN_UNSEEN_TEXT)	{
 			atk_object_set_description(accessibility_obj, _("Unread Messages"));
 			style = "color=\"#cc0000\" weight=\"bold\"";
 		} else if (gtkconv->unseen_state == PIDGIN_UNSEEN_EVENT) {
 			atk_object_set_description(accessibility_obj, _("New Event"));
-			style = "color=\"#888a85\" style=\"italic\"";
+			style = "color=\"#888a85\" weight=\"bold\"";
 		} else {
 			style = "";
 		}
@@ -6599,6 +6601,7 @@
 
 	event = gtk_event_box_new();
 	gtk_container_add(GTK_CONTAINER(gtkconv->u.im->icon_container), event);
+	gtk_event_box_set_visible_window(GTK_EVENT_BOX(event), FALSE);
 	gtk_widget_add_events(event,
                               GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK);
 	g_signal_connect(G_OBJECT(event), "button-press-event",
@@ -7495,7 +7498,7 @@
 							_("Confirm close"),
 							GTK_WINDOW(gtkwin->window), GTK_DIALOG_MODAL,
 							GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
-							PIDGIN_STOCK_CLOSE_TABS, GTK_RESPONSE_OK, NULL);
+							GTK_STOCK_CLOSE, GTK_RESPONSE_OK, NULL);
 
 	gtk_dialog_set_default_response(GTK_DIALOG(warn_close_dialog),
 	                                GTK_RESPONSE_OK);
@@ -7787,7 +7790,7 @@
 			return FALSE;
 
 		gtkconv = pidgin_conv_window_get_gtkconv_at_index(win, tab_clicked);
-		close_conv_cb(NULL, gtkconv);
+		close_conv_cb(NULL, NULL, gtkconv);
 		return TRUE;
 	}
 
@@ -8046,7 +8049,7 @@
 
 		if (gconv != gtkconv)
 		{
-			close_conv_cb(NULL, gconv);
+			close_conv_cb(NULL, NULL, gconv);
 		}
 	}
 }
@@ -8058,7 +8061,7 @@
 	gtkconv = g_object_get_data(menu, "clicked_tab");
 
 	if (gtkconv)
-		close_conv_cb(NULL, gtkconv);
+		close_conv_cb(NULL, NULL, gtkconv);
 }
 
 static gboolean
@@ -8336,11 +8339,6 @@
 	if (gdk_window_get_state(w->window) & GDK_WINDOW_STATE_MAXIMIZED)
 		return FALSE;
 	
-	/* don't save if nothing changed */
-	if (x == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x") &&
-			y == purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"))
-		return FALSE; /* carry on normally */
-		
 	/* don't save off-screen positioning */
 	if (x + event->width < 0 ||
 	    y + event->height < 0 ||
@@ -8388,10 +8386,10 @@
 static void
 pidgin_conv_restore_position(PidginWindow *win) {
 	pidgin_conv_set_position_size(win,
-			purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/x"),
-			purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/y"),
-			purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/width"),
-			purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/height"));
+		purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/x"),
+		purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/y"),
+		purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/width"),
+		purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/im/height"));
 }
 
 PidginWindow *
@@ -8544,6 +8542,20 @@
 		                              gtkconv->tab_cont));
 }
 
+static gboolean
+close_button_left_cb(GtkWidget *widget, GdkEventCrossing *event, GtkLabel *label)
+{
+	gtk_label_set_markup(label, "×");
+	return FALSE;
+}
+
+static gboolean
+close_button_entered_cb(GtkWidget *widget, GdkEventCrossing *event, GtkLabel *label)
+{
+	gtk_label_set_markup(label, "<b>×</b>");
+	return FALSE;
+}
+
 void
 pidgin_conv_window_add_gtkconv(PidginWindow *win, PidginConversation *gtkconv)
 {
@@ -8553,7 +8565,6 @@
 	GtkWidget *close_image;
 	PurpleConversationType conv_type;
 	const gchar *tmp_lab;
-	gint close_button_width, close_button_height, focus_width, focus_pad;
 
 	conv_type = purple_conversation_get_type(conv);
 
@@ -8565,29 +8576,19 @@
 
 
 	/* Close button. */
-	gtkconv->close = gtk_button_new();
-	gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &close_button_width, &close_button_height);
-	if (gtk_check_version(2, 4, 2) == NULL) {
-		/* Need to account for extra padding around the gtkbutton */
-		gtk_widget_style_get(GTK_WIDGET(gtkconv->close),
-		                     "focus-line-width", &focus_width,
-		                     "focus-padding", &focus_pad,
-		                     NULL);
-		close_button_width += (focus_width + focus_pad) * 2;
-		close_button_height += (focus_width + focus_pad) * 2;
-	}
-	gtk_widget_set_size_request(GTK_WIDGET(gtkconv->close),
-	                            close_button_width, close_button_height);
-
-	gtk_button_set_relief(GTK_BUTTON(gtkconv->close), GTK_RELIEF_NONE);
-	close_image = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
+	gtkconv->close = gtk_event_box_new();
+	gtk_event_box_set_visible_window(GTK_EVENT_BOX(gtkconv->close), FALSE);
+	gtk_widget_set_events(gtkconv->close, GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK);
+	close_image = gtk_label_new("×");
+	g_signal_connect(G_OBJECT(gtkconv->close), "enter-notify-event", G_CALLBACK(close_button_entered_cb), close_image);
+	g_signal_connect(G_OBJECT(gtkconv->close), "leave-notify-event", G_CALLBACK(close_button_left_cb), close_image);
 	gtk_widget_show(close_image);
 	gtk_container_add(GTK_CONTAINER(gtkconv->close), close_image);
 	gtk_tooltips_set_tip(gtkconv->tooltips, gtkconv->close,
 	                     _("Close conversation"), NULL);
 
-	g_signal_connect(G_OBJECT(gtkconv->close), "clicked",
-	                 G_CALLBACK(close_conv_cb), gtkconv);
+	g_signal_connect(G_OBJECT(gtkconv->close), "button-press-event",
+			 G_CALLBACK(close_conv_cb), gtkconv);
 
 #if !GTK_CHECK_VERSION(2,6,0)
 	/*
@@ -8608,7 +8609,7 @@
 	gtkconv->tab_label = gtk_label_new(tmp_lab = purple_conversation_get_title(conv));
 
 	gtkconv->menu_tabby = gtk_hbox_new(FALSE, PIDGIN_HIG_BOX_SPACE);
-	gtkconv->menu_label = gtk_label_new(purple_conversation_get_title(gtkconv->active_conv));
+	gtkconv->menu_label = gtk_label_new(tmp_lab);
 	gtk_box_pack_start(GTK_BOX(gtkconv->menu_tabby), gtkconv->menu_icon, FALSE, FALSE, 0);
 
 	gtk_widget_show_all(gtkconv->menu_icon);
@@ -8618,7 +8619,6 @@
 	gtk_misc_set_alignment(GTK_MISC(gtkconv->menu_label), 0, 0);
 
 	gtk_widget_show(gtkconv->menu_tabby);
-	gtk_widget_set_size_request(gtkconv->menu_tabby, 0, -1);
 
 	if (purple_conversation_get_type(conv) == PURPLE_CONV_TYPE_IM)
 		pidgin_conv_update_buddy_icon(conv);
@@ -8633,9 +8633,6 @@
 	if (pidgin_conv_window_get_gtkconv_count(win) == 1) {
 		/* Er, bug in notebooks? Switch to the page manually. */
 		gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), 0);
-
-		gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), 
-					   purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"));
 	} else {
 		gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), TRUE);
 	}
@@ -8734,6 +8731,12 @@
 					   !tabs_side && !angle && pidgin_conv_window_get_gtkconv_count(win) > 1, 
 					   TRUE, GTK_PACK_START);
 
+	if (pidgin_conv_window_get_gtkconv_count(win) == 1) 
+		gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook),
+                                           !purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/im/show_buddy_icons") ||  
+                                           purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_LEFT ||
+                                           purple_prefs_get_int(PIDGIN_PREFS_ROOT "/conversations/tab_side") == GTK_POS_RIGHT);
+
 	/* show the widgets */
 /*	gtk_widget_show(gtkconv->icon); */
 	gtk_widget_show(gtkconv->tab_label);
@@ -8757,12 +8760,6 @@
 
 	gtk_notebook_remove_page(GTK_NOTEBOOK(win->notebook), index);
 
-	/* go back to tabless */
-	if (pidgin_conv_window_get_gtkconv_count(win) <= 2) {
-		gtk_notebook_set_show_tabs(GTK_NOTEBOOK(win->notebook), 
-  					   purple_prefs_get_bool(PIDGIN_PREFS_ROOT "/conversations/tabs"));
-	}
-
 	win->gtkconvs = g_list_remove(win->gtkconvs, gtkconv);
 
 	if (win->gtkconvs && win->gtkconvs->next == NULL)
@@ -9041,6 +9038,7 @@
 	PidginWindow *win;
 
 	win = pidgin_conv_window_new();
+
 	g_signal_connect(G_OBJECT(win->window), "configure_event", 
 			G_CALLBACK(gtk_conv_configure_cb), NULL);
 
--- a/pidgin/gtkdocklet.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkdocklet.c	Tue Aug 14 02:45:41 2007 +0000
@@ -636,7 +636,7 @@
 
 	purple_prefs_add_none(PIDGIN_PREFS_ROOT "/docklet");
 	purple_prefs_add_bool(PIDGIN_PREFS_ROOT "/docklet/blink", FALSE);
-	purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "pending");
+	purple_prefs_add_string(PIDGIN_PREFS_ROOT "/docklet/show", "always");
 	purple_prefs_connect_callback(docklet_handle, PIDGIN_PREFS_ROOT "/docklet/show",
 				    docklet_show_pref_changed_cb, NULL);
 
--- a/pidgin/gtkimhtml.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkimhtml.c	Tue Aug 14 02:45:41 2007 +0000
@@ -1037,7 +1037,7 @@
 {
 	char *tmp;
 
-	if (text == NULL)
+	if (text == NULL || !(*text))
 		return;
 
 	tmp = g_markup_escape_text(text, -1);
@@ -1053,7 +1053,7 @@
 	if (!gtk_text_view_get_editable(GTK_TEXT_VIEW(imhtml)))
 		return;
 
-	if (imhtml->wbfo || selection_data->length < 0) {
+	if (imhtml->wbfo || selection_data->length <= 0) {
 		gtk_clipboard_request_text(clipboard, paste_plaintext_received_cb, imhtml);
 		return;
 	} else {
--- a/pidgin/gtkimhtmltoolbar.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkimhtmltoolbar.c	Tue Aug 14 02:45:41 2007 +0000
@@ -939,7 +939,7 @@
 		*y -= widget->allocation.height;
 }
 
-static void pidgin_menu_clicked(GtkWidget *button, GdkEventButton *event, GtkMenu *menu)
+static void pidgin_menu_clicked(GtkWidget *button, GtkMenu *menu)
 {
 	gtk_widget_show_all(GTK_WIDGET(menu));
 	gtk_menu_popup(menu, NULL, NULL, menu_position_func, button, 0, gtk_get_current_event_time());
@@ -1096,6 +1096,13 @@
 	g_signal_handlers_unblock_by_func(G_OBJECT(item), G_CALLBACK(gtk_button_clicked), button);
 }
 
+static void
+enable_markup(GtkWidget *widget, gpointer null)
+{
+	if (GTK_IS_LABEL(widget))
+		g_object_set(G_OBJECT(widget), "use-markup", TRUE, NULL);
+}
+
 static void gtk_imhtmltoolbar_init (GtkIMHtmlToolbar *toolbar)
 {
 	GtkWidget *hbox = GTK_WIDGET(toolbar);
@@ -1114,14 +1121,17 @@
 		GtkWidget **button;
 		gboolean check;
 	} buttons[] = {
-		{_("_Bold"), &toolbar->bold, TRUE},
-		{_("_Italic"), &toolbar->italic, TRUE},
-		{_("_Underline"), &toolbar->underline, TRUE},
-		{_("_Larger"), &toolbar->larger_size, TRUE},
+		{_("<b>_Bold</b>"), &toolbar->bold, TRUE},
+		{_("<i>_Italic</i>"), &toolbar->italic, TRUE},
+		{_("<u>_Underline</u>"), &toolbar->underline, TRUE},
+		{_("<span size='larger'>_Larger</span>"), &toolbar->larger_size, TRUE},
 #if 0
 		{_("_Normal"), &toolbar->normal_size, TRUE},
 #endif
-		{_("_Smaller"), &toolbar->smaller_size, TRUE},
+		{_("<span size='smaller'>_Smaller</span>"), &toolbar->smaller_size, TRUE},
+		/* If we want to show the formatting for the following items, we would
+		 * need to update them when formatting changes. The above items don't need
+		 * no updating nor nothin' */
 		{_("_Font face"), &toolbar->font, TRUE},
 		{_("Foreground _color"), &toolbar->fgcolor, TRUE},
 		{_("Bac_kground color"), &toolbar->bgcolor, TRUE},
@@ -1175,9 +1185,11 @@
 		gtk_menu_shell_append(GTK_MENU_SHELL(font_menu), menuitem);
 		g_signal_connect(G_OBJECT(old), "notify::sensitive",
 				G_CALLBACK(button_sensitiveness_changed), menuitem);
+		gtk_container_foreach(GTK_CONTAINER(menuitem), (GtkCallback)enable_markup, NULL);
 	}
 
-	g_signal_connect(G_OBJECT(font_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), font_menu);
+	g_signal_connect_swapped(G_OBJECT(font_button), "button-press-event", G_CALLBACK(gtk_widget_activate), font_button);
+	g_signal_connect(G_OBJECT(font_button), "activate", G_CALLBACK(pidgin_menu_clicked), font_menu);
 	g_signal_connect(G_OBJECT(font_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), font_button);
 
 	/* Sep */
@@ -1218,7 +1230,8 @@
 	g_signal_connect(G_OBJECT(toolbar->link), "notify::sensitive",
 			G_CALLBACK(button_sensitiveness_changed), menuitem);
 
-	g_signal_connect(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(pidgin_menu_clicked), insert_menu);
+	g_signal_connect_swapped(G_OBJECT(insert_button), "button-press-event", G_CALLBACK(gtk_widget_activate), insert_button);
+	g_signal_connect(G_OBJECT(insert_button), "activate", G_CALLBACK(pidgin_menu_clicked), insert_menu);
 	g_signal_connect(G_OBJECT(insert_menu), "deactivate", G_CALLBACK(pidgin_menu_deactivate), insert_button);
 	toolbar->sml = NULL;
 }
--- a/pidgin/gtkmain.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkmain.c	Tue Aug 14 02:45:41 2007 +0000
@@ -695,7 +695,9 @@
 		return 1;
 	}
 
+#if GLIB_CHECK_VERSION(2,2,0)
 	g_set_application_name(_("Pidgin"));
+#endif /* glib-2.0 >= 2.2.0 */
 
 #ifdef _WIN32
 	winpidgin_init(hint);
--- a/pidgin/gtknotify.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtknotify.c	Tue Aug 14 02:45:41 2007 +0000
@@ -274,6 +274,7 @@
 
 	gtk_label_set_markup(GTK_LABEL(label), label_text);
 	gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
+	gtk_label_set_selectable(GTK_LABEL(label), TRUE);
 	gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
 	gtk_box_pack_start(GTK_BOX(hbox), label, FALSE, FALSE, 0);
 
@@ -609,6 +610,7 @@
 
 	gtk_label_set_markup(GTK_LABEL(label), label_text);
 	gtk_label_set_line_wrap(GTK_LABEL(label), TRUE);
+	gtk_label_set_selectable(GTK_LABEL(label), TRUE);
 	gtk_misc_set_alignment(GTK_MISC(label), 0, 0);
 	gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
 	gtk_widget_show(label);
@@ -626,6 +628,7 @@
 	button = gtk_button_new_from_stock(GTK_STOCK_CLOSE);
 	gtk_box_pack_start(GTK_BOX(vbox), button, FALSE, FALSE, 0);
 	gtk_widget_show(button);
+	gtk_widget_grab_focus(button);
 
 	g_signal_connect_swapped(G_OBJECT(button), "clicked",
 							 G_CALLBACK(gtk_widget_destroy), window);
--- a/pidgin/gtksound.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtksound.c	Tue Aug 14 02:45:41 2007 +0000
@@ -543,7 +543,8 @@
 		char *filename = g_strdup(purple_prefs_get_path(file_pref));
 		if(!filename || !strlen(filename)) {
 			g_free(filename);
-			filename = g_build_filename(DATADIR, "sounds", "pidgin", sounds[event].def, NULL);
+			/* XXX Consider creating a constant for "sounds/purple" to be shared with Finch */
+			filename = g_build_filename(DATADIR, "sounds", "purple", sounds[event].def, NULL);
 		}
 
 		purple_sound_play_file(filename, NULL);
--- a/pidgin/gtkutils.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/gtkutils.c	Tue Aug 14 02:45:41 2007 +0000
@@ -2047,11 +2047,11 @@
 														entry.entry.buddy->account,
 														entry.entry.buddy->name
 													 );
+#else
+					item->data = g_strdup(entry.entry.buddy->name);
+					g_completion_add_items(data->completion, item);
+#endif /* NEW_STYLE_COMPLETION */
 				}
-#else
-				item->data = g_strdup(buddy->name);
-				g_completion_add_items(data->completion, item);
-#endif /* NEW_STYLE_COMPLETION */
 			}
 		}
 	}
--- a/pidgin/pixmaps/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/pixmaps/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -12,7 +12,7 @@
 		pidgin.ico
 
 pidginbuttonpixdir = $(datadir)/pixmaps/pidgin/buttons
-pidginbuttonpix_DATA = edit.png pause.png
+pidginbuttonpix_DATA = edit.png pause.png 
 
 pidgindistpixdir = $(datadir)/pixmaps/pidgin
 pidgindistpix_DATA = logo.png arrow-down.xpm arrow-left.xpm arrow-right.xpm arrow-up.xpm
Binary file pidgin/pixmaps/emotes/default/22/act-up.png has changed
Binary file pidgin/pixmaps/emotes/default/22/alien.png has changed
Binary file pidgin/pixmaps/emotes/default/22/angel.png has changed
Binary file pidgin/pixmaps/emotes/default/22/angry.png has changed
Binary file pidgin/pixmaps/emotes/default/22/arrogant.png has changed
Binary file pidgin/pixmaps/emotes/default/22/at-wits-end.png has changed
Binary file pidgin/pixmaps/emotes/default/22/bashful.png has changed
Binary file pidgin/pixmaps/emotes/default/22/beat-up.png has changed
Binary file pidgin/pixmaps/emotes/default/22/beauty.png has changed
Binary file pidgin/pixmaps/emotes/default/22/blowkiss.png has changed
Binary file pidgin/pixmaps/emotes/default/22/bye.png has changed
Binary file pidgin/pixmaps/emotes/default/22/call-me.png has changed
Binary file pidgin/pixmaps/emotes/default/22/clap.png has changed
Binary file pidgin/pixmaps/emotes/default/22/confused.png has changed
Binary file pidgin/pixmaps/emotes/default/22/crying.png has changed
Binary file pidgin/pixmaps/emotes/default/22/curl-lip.png has changed
Binary file pidgin/pixmaps/emotes/default/22/curse.png has changed
Binary file pidgin/pixmaps/emotes/default/22/cute.png has changed
Binary file pidgin/pixmaps/emotes/default/22/dance.png has changed
Binary file pidgin/pixmaps/emotes/default/22/dazed.png has changed
Binary file pidgin/pixmaps/emotes/default/22/desire.png has changed
Binary file pidgin/pixmaps/emotes/default/22/devil.png has changed
Binary file pidgin/pixmaps/emotes/default/22/disapointed.png has changed
Binary file pidgin/pixmaps/emotes/default/22/disdain.png has changed
Binary file pidgin/pixmaps/emotes/default/22/doh.png has changed
Binary file pidgin/pixmaps/emotes/default/22/dont-know.png has changed
Binary file pidgin/pixmaps/emotes/default/22/drool.png has changed
Binary file pidgin/pixmaps/emotes/default/22/eat.png has changed
Binary file pidgin/pixmaps/emotes/default/22/embarrassed.png has changed
Binary file pidgin/pixmaps/emotes/default/22/excruciating.png has changed
Binary file pidgin/pixmaps/emotes/default/22/eyeroll.png has changed
Binary file pidgin/pixmaps/emotes/default/22/fingers-crossed.png has changed
Binary file pidgin/pixmaps/emotes/default/22/foot-in-mouth.png has changed
Binary file pidgin/pixmaps/emotes/default/22/freaked-out.png has changed
Binary file pidgin/pixmaps/emotes/default/22/glasses-cool.png has changed
Binary file pidgin/pixmaps/emotes/default/22/glasses-nerdy.png has changed
Binary file pidgin/pixmaps/emotes/default/22/go-away.png has changed
Binary file pidgin/pixmaps/emotes/default/22/handshake.png has changed
Binary file pidgin/pixmaps/emotes/default/22/highfive.png has changed
Binary file pidgin/pixmaps/emotes/default/22/hug-left.png has changed
Binary file pidgin/pixmaps/emotes/default/22/hug-right.png has changed
Binary file pidgin/pixmaps/emotes/default/22/hypnotized.png has changed
Binary file pidgin/pixmaps/emotes/default/22/in-love.png has changed
Binary file pidgin/pixmaps/emotes/default/22/jump.png has changed
Binary file pidgin/pixmaps/emotes/default/22/kiss.png has changed
Binary file pidgin/pixmaps/emotes/default/22/lashes.png has changed
Binary file pidgin/pixmaps/emotes/default/22/laugh.png has changed
Binary file pidgin/pixmaps/emotes/default/22/lying.png has changed
Binary file pidgin/pixmaps/emotes/default/22/mean.png has changed
Binary file pidgin/pixmaps/emotes/default/22/meeting.png has changed
Binary file pidgin/pixmaps/emotes/default/22/moneymouth.png has changed
Binary file pidgin/pixmaps/emotes/default/22/nailbiting.png has changed
Binary file pidgin/pixmaps/emotes/default/22/neutral.png has changed
Binary file pidgin/pixmaps/emotes/default/22/on-the-phone.png has changed
Binary file pidgin/pixmaps/emotes/default/22/party.png has changed
Binary file pidgin/pixmaps/emotes/default/22/pissed-off.png has changed
Binary file pidgin/pixmaps/emotes/default/22/pray.png has changed
Binary file pidgin/pixmaps/emotes/default/22/question.png has changed
Binary file pidgin/pixmaps/emotes/default/22/quiet.png has changed
Binary file pidgin/pixmaps/emotes/default/22/rotfl.png has changed
Binary file pidgin/pixmaps/emotes/default/22/sad.png has changed
Binary file pidgin/pixmaps/emotes/default/22/sarcastic.png has changed
Binary file pidgin/pixmaps/emotes/default/22/secret.png has changed
Binary file pidgin/pixmaps/emotes/default/22/shame.png has changed
Binary file pidgin/pixmaps/emotes/default/22/shock.png has changed
Binary file pidgin/pixmaps/emotes/default/22/shout.png has changed
Binary file pidgin/pixmaps/emotes/default/22/shut-mouth.png has changed
Binary file pidgin/pixmaps/emotes/default/22/sick.png has changed
Binary file pidgin/pixmaps/emotes/default/22/silly.png has changed
Binary file pidgin/pixmaps/emotes/default/22/skywalker.png has changed
Binary file pidgin/pixmaps/emotes/default/22/sleepy.png has changed
Binary file pidgin/pixmaps/emotes/default/22/smile-big.png has changed
Binary file pidgin/pixmaps/emotes/default/22/smile.png has changed
Binary file pidgin/pixmaps/emotes/default/22/smirk.png has changed
Binary file pidgin/pixmaps/emotes/default/22/snicker.png has changed
Binary file pidgin/pixmaps/emotes/default/22/soldier.png has changed
Binary file pidgin/pixmaps/emotes/default/22/starving.png has changed
Binary file pidgin/pixmaps/emotes/default/22/struggle.png has changed
Binary file pidgin/pixmaps/emotes/default/22/sweat.png has changed
Binary file pidgin/pixmaps/emotes/default/22/teeth.png has changed
Binary file pidgin/pixmaps/emotes/default/22/terror.png has changed
Binary file pidgin/pixmaps/emotes/default/22/thinking.png has changed
Binary file pidgin/pixmaps/emotes/default/22/time-out.png has changed
Binary file pidgin/pixmaps/emotes/default/22/tongue.png has changed
Binary file pidgin/pixmaps/emotes/default/22/tremble.png has changed
Binary file pidgin/pixmaps/emotes/default/22/vampire.png has changed
Binary file pidgin/pixmaps/emotes/default/22/victory.png has changed
Binary file pidgin/pixmaps/emotes/default/22/waiting.png has changed
Binary file pidgin/pixmaps/emotes/default/22/weep.png has changed
Binary file pidgin/pixmaps/emotes/default/22/wilt.png has changed
Binary file pidgin/pixmaps/emotes/default/22/wink.png has changed
Binary file pidgin/pixmaps/emotes/default/22/yawn.png has changed
--- a/pidgin/plugins/win32/transparency/win2ktrans.c	Tue Aug 14 02:45:06 2007 +0000
+++ b/pidgin/plugins/win32/transparency/win2ktrans.c	Tue Aug 14 02:45:41 2007 +0000
@@ -274,9 +274,11 @@
 		gtk_window_get_size(GTK_WINDOW(win), &width, &height);
 		gtk_box_pack_start(GTK_BOX(vbox),
 			slider_box, FALSE, FALSE, 0);
+#if 0 /*Now that we save window sizes, don't resize it or else it causes windows to grow*/
 		/* Make window taller so we don't slowly collapse its message area */
 		gtk_window_resize(GTK_WINDOW(win), width,
 			(height + slidereq.height));
+#endif
 		/* Add window to list, to track that it has a slider */
 		slidwin = g_new0(slider_win, 1);
 		slidwin->win = win;
@@ -292,6 +294,7 @@
 			slider_win *slidwin = (slider_win*) tmp->data;
 			if (slidwin != NULL &&
 					GTK_IS_WINDOW(slidwin->win)) {
+#if 0
 				GtkRequisition slidereq;
 				gint width, height;
 				/* Figure out how tall the slider was */
@@ -300,12 +303,13 @@
 				gtk_window_get_size(
 					GTK_WINDOW(slidwin->win),
 					&width, &height);
-
+#endif
 				gtk_widget_destroy(slidwin->slider);
-
+#if 0
 				gtk_window_resize(
 					GTK_WINDOW(slidwin->win),
 					width, (height - slidereq.height));
+#endif
 			}
 			g_free(slidwin);
 			tmp = tmp->next;
--- a/pidgin/sounds/Makefile.am	Tue Aug 14 02:45:06 2007 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,12 +0,0 @@
-soundsdir =	$(datadir)/sounds/pidgin
-sounds_DATA =	\
-		alert.wav \
-		login.wav \
-		logout.wav \
-		receive.wav \
-		send.wav
-
-EXTRA_DIST = \
-		Makefile.mingw \
-                $(sounds_DATA)
-
--- a/pidgin/sounds/Makefile.mingw	Tue Aug 14 02:45:06 2007 +0000
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,20 +0,0 @@
-#
-# Makefile.mingw
-#
-# Description: Makefile for win32 (mingw) version of Pidgin sounds
-#
-
-PIDGIN_TREE_TOP := ../..
-include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak
-
-datadir := $(PIDGIN_INSTALL_DIR)
-include ./Makefile.am
-
-.PHONY: install
-
-install:
-	if test '$(sounds_DATA)'; then \
-	  mkdir -p $(soundsdir); \
-	  cp $(sounds_DATA) $(soundsdir); \
-	fi;
-
Binary file pidgin/sounds/alert.wav has changed
Binary file pidgin/sounds/login.wav has changed
Binary file pidgin/sounds/logout.wav has changed
Binary file pidgin/sounds/receive.wav has changed
Binary file pidgin/sounds/send.wav has changed
--- a/po/POTFILES.in	Tue Aug 14 02:45:06 2007 +0000
+++ b/po/POTFILES.in	Tue Aug 14 02:45:41 2007 +0000
@@ -11,6 +11,7 @@
 finch/gntpounce.c
 finch/gntprefs.c
 finch/gntrequest.c
+finch/gntsound.c
 finch/gntstatus.c
 finch/gntui.c
 finch/libgnt/gntbox.c
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,4 @@
+
+SUBDIRS = sounds
+
+EXTRA_DIST = Makefile.mingw
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/Makefile.mingw	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,19 @@
+#
+# Makefile.mingw
+#
+# Description: Makefile for win32 (mingw) version
+#
+
+PIDGIN_TREE_TOP := ..
+include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak
+
+include ./Makefile.am
+
+.PHONY: install clean
+
+install:
+	if test '$(SUBDIRS)'; then \
+	  list='$(SUBDIRS)'; for subdir in $$list; do \
+	    $(MAKE) -C $$subdir -f $(MINGW_MAKEFILE) install || exit 1 ;\
+	  done; \
+	fi;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/sounds/Makefile.am	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,12 @@
+soundsdir =	$(datadir)/sounds/purple
+sounds_DATA =	\
+		alert.wav \
+		login.wav \
+		logout.wav \
+		receive.wav \
+		send.wav
+
+EXTRA_DIST = \
+		Makefile.mingw \
+                $(sounds_DATA)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/share/sounds/Makefile.mingw	Tue Aug 14 02:45:41 2007 +0000
@@ -0,0 +1,20 @@
+#
+# Makefile.mingw
+#
+# Description: Makefile for win32 (mingw) version of Pidgin sounds
+#
+
+PIDGIN_TREE_TOP := ../..
+include $(PIDGIN_TREE_TOP)/libpurple/win32/global.mak
+
+datadir := $(PIDGIN_INSTALL_DIR)
+include ./Makefile.am
+
+.PHONY: install
+
+install:
+	if test '$(sounds_DATA)'; then \
+	  mkdir -p $(soundsdir); \
+	  cp $(sounds_DATA) $(soundsdir); \
+	fi;
+
Binary file share/sounds/alert.wav has changed
Binary file share/sounds/login.wav has changed
Binary file share/sounds/logout.wav has changed
Binary file share/sounds/receive.wav has changed
Binary file share/sounds/send.wav has changed