diff libpurple/protocols/sametime/sametime.c @ 15374:5fe8042783c1

Rename gtk/ and libgaim/ to pidgin/ and libpurple/
author Sean Egan <seanegan@gmail.com>
date Sat, 20 Jan 2007 02:32:10 +0000
parents
children 0b6f337a46d5
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/libpurple/protocols/sametime/sametime.c	Sat Jan 20 02:32:10 2007 +0000
@@ -0,0 +1,5771 @@
+
+/*
+  Meanwhile Protocol Plugin for Gaim
+  Adds Lotus Sametime support to Gaim using the Meanwhile library
+
+  Copyright (C) 2004 Christopher (siege) O'Brien <siege@preoccupied.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.
+*/
+
+
+/* system includes */
+#include <stdlib.h>
+#include <time.h>
+
+/* glib includes */
+#include <glib.h>
+#include <glib/ghash.h>
+#include <glib/glist.h>
+
+/* gaim includes */
+#include "internal.h"
+#include "config.h"
+
+#include "account.h"
+#include "accountopt.h"
+#include "circbuffer.h"
+#include "conversation.h"
+#include "debug.h"
+#include "ft.h"
+#include "imgstore.h"
+#include "mime.h"
+#include "notify.h"
+#include "plugin.h"
+#include "privacy.h"
+#include "prpl.h"
+#include "request.h"
+#include "util.h"
+#include "version.h"
+
+/* meanwhile includes */
+#include <mw_cipher.h>
+#include <mw_common.h>
+#include <mw_error.h>
+#include <mw_service.h>
+#include <mw_session.h>
+#include <mw_srvc_aware.h>
+#include <mw_srvc_conf.h>
+#include <mw_srvc_ft.h>
+#include <mw_srvc_im.h>
+#include <mw_srvc_place.h>
+#include <mw_srvc_resolve.h>
+#include <mw_srvc_store.h>
+#include <mw_st_list.h>
+
+/* plugin includes */
+#include "sametime.h"
+
+
+/* considering that there's no display of this information for prpls,
+   I don't know why I even bother providing these. Oh valiant reader,
+   I do it all for you. */
+/* scratch that, I just added it to the prpl options panel */
+#define PLUGIN_ID        "prpl-meanwhile"
+#define PLUGIN_NAME      "Sametime"
+#define PLUGIN_SUMMARY   "Sametime Protocol Plugin"
+#define PLUGIN_DESC      "Open implementation of a Lotus Sametime client"
+#define PLUGIN_AUTHOR    "Christopher (siege) O'Brien <siege@preoccupied.net>"
+#define PLUGIN_HOMEPAGE  "http://meanwhile.sourceforge.net/"
+
+
+/* plugin preference names */
+#define MW_PRPL_OPT_BASE          "/plugins/prpl/meanwhile"
+#define MW_PRPL_OPT_BLIST_ACTION  MW_PRPL_OPT_BASE "/blist_action"
+#define MW_PRPL_OPT_PSYCHIC       MW_PRPL_OPT_BASE "/psychic"
+#define MW_PRPL_OPT_FORCE_LOGIN   MW_PRPL_OPT_BASE "/force_login"
+#define MW_PRPL_OPT_SAVE_DYNAMIC  MW_PRPL_OPT_BASE "/save_dynamic"
+
+
+/* stages of connecting-ness */
+#define MW_CONNECT_STEPS  11
+
+
+/* stages of conciousness */
+#define MW_STATE_OFFLINE      "offline"
+#define MW_STATE_ACTIVE       "active"
+#define MW_STATE_AWAY         "away"
+#define MW_STATE_BUSY         "dnd"
+#define MW_STATE_MESSAGE      "message"
+#define MW_STATE_ENLIGHTENED  "buddha"
+
+
+/* keys to get/set chat information */
+#define CHAT_KEY_CREATOR   "chat.creator"
+#define CHAT_KEY_NAME      "chat.name"
+#define CHAT_KEY_TOPIC     "chat.topic"
+#define CHAT_KEY_INVITE    "chat.invite"
+#define CHAT_KEY_IS_PLACE  "chat.is_place"
+
+
+/* key for associating a mwLoginType with a buddy */
+#define BUDDY_KEY_CLIENT  "meanwhile.client"
+
+/* store the remote alias so that we can re-create it easily */
+#define BUDDY_KEY_NAME    "meanwhile.shortname"
+
+/* enum mwSametimeUserType */
+#define BUDDY_KEY_TYPE    "meanwhile.type"
+
+
+/* key for the real group name for a meanwhile group */
+#define GROUP_KEY_NAME    "meanwhile.group"
+
+/* enum mwSametimeGroupType */
+#define GROUP_KEY_TYPE    "meanwhile.type"
+
+/* NAB group owning account */
+#define GROUP_KEY_OWNER   "meanwhile.account"
+
+/* key gtk blist uses to indicate a collapsed group */
+#define GROUP_KEY_COLLAPSED  "collapsed"
+
+
+/* verification replacement */
+#define mwSession_NO_SECRET  "meanwhile.no_secret"
+
+
+/* keys to get/set gaim plugin information */
+#define MW_KEY_HOST        "server"
+#define MW_KEY_PORT        "port"
+#define MW_KEY_FORCE       "force_login"
+#define MW_KEY_FAKE_IT     "fake_client_id"
+#define MW_KEY_CLIENT      "client_id_val"
+#define MW_KEY_MAJOR       "client_major"
+#define MW_KEY_MINOR       "client_minor"
+
+
+/** number of seconds from the first blist change before a save to the
+    storage service occurs. */
+#define BLIST_SAVE_SECONDS  15
+
+
+/** the possible buddy list storage settings */
+enum blist_choice {
+  blist_choice_LOCAL = 1, /**< local only */
+  blist_choice_MERGE = 2, /**< merge from server */
+  blist_choice_STORE = 3, /**< merge from and save to server */
+  blist_choice_SYNCH = 4, /**< sync with server */
+};
+
+
+/** the default blist storage option */
+#define BLIST_CHOICE_DEFAULT  blist_choice_SYNCH
+
+
+/* testing for the above */
+#define BLIST_PREF_IS(n) (gaim_prefs_get_int(MW_PRPL_OPT_BLIST_ACTION)==(n))
+#define BLIST_PREF_IS_LOCAL()  BLIST_PREF_IS(blist_choice_LOCAL)
+#define BLIST_PREF_IS_MERGE()  BLIST_PREF_IS(blist_choice_MERGE)
+#define BLIST_PREF_IS_STORE()  BLIST_PREF_IS(blist_choice_STORE)
+#define BLIST_PREF_IS_SYNCH()  BLIST_PREF_IS(blist_choice_SYNCH)
+
+
+/* debugging output */
+#define DEBUG_ERROR(a...)  gaim_debug_error(G_LOG_DOMAIN, a)
+#define DEBUG_INFO(a...)   gaim_debug_info(G_LOG_DOMAIN, a)
+#define DEBUG_MISC(a...)   gaim_debug_misc(G_LOG_DOMAIN, a)
+#define DEBUG_WARN(a...)   gaim_debug_warning(G_LOG_DOMAIN, a)
+
+
+/** ensure non-null strings */
+#ifndef NSTR
+# define NSTR(str) ((str)? (str): "(null)")
+#endif
+
+
+/** calibrates distinct secure channel nomenclature */
+static const unsigned char no_secret[] = {
+  0x2d, 0x2d, 0x20, 0x73, 0x69, 0x65, 0x67, 0x65,
+  0x20, 0x6c, 0x6f, 0x76, 0x65, 0x73, 0x20, 0x6a,
+  0x65, 0x6e, 0x6e, 0x69, 0x20, 0x61, 0x6e, 0x64,
+  0x20, 0x7a, 0x6f, 0x65, 0x20, 0x2d, 0x2d, 0x00,
+};
+
+
+/** handler IDs from g_log_set_handler in mw_plugin_init */
+static guint log_handler[2] = { 0, 0 };
+
+
+/** the gaim plugin data.
+    available as gc->proto_data and mwSession_getClientData */
+struct mwGaimPluginData {
+  struct mwSession *session;
+
+  struct mwServiceAware *srvc_aware;
+  struct mwServiceConference *srvc_conf;
+  struct mwServiceFileTransfer *srvc_ft;
+  struct mwServiceIm *srvc_im;
+  struct mwServicePlace *srvc_place;
+  struct mwServiceResolve *srvc_resolve;
+  struct mwServiceStorage *srvc_store;
+
+  /** map of GaimGroup:mwAwareList and mwAwareList:GaimGroup */
+  GHashTable *group_list_map;
+
+  /** event id for the buddy list save callback */
+  guint save_event;
+
+  /** socket fd */
+  int socket;
+  gint outpa;  /* like inpa, but the other way */
+
+  /** circular buffer for outgoing data */
+  GaimCircBuffer *sock_buf;
+
+  GaimConnection *gc;
+};
+
+
+typedef struct {
+  GaimBuddy *buddy;
+  GaimGroup *group;
+} BuddyAddData;
+
+
+/* blist and aware functions */
+
+static void blist_export(GaimConnection *gc, struct mwSametimeList *stlist);
+
+static void blist_store(struct mwGaimPluginData *pd);
+
+static void blist_schedule(struct mwGaimPluginData *pd);
+
+static void blist_merge(GaimConnection *gc, struct mwSametimeList *stlist);
+
+static void blist_sync(GaimConnection *gc, struct mwSametimeList *stlist);
+
+static gboolean buddy_is_external(GaimBuddy *b);
+
+static void buddy_add(struct mwGaimPluginData *pd, GaimBuddy *buddy);
+
+static GaimBuddy *
+buddy_ensure(GaimConnection *gc, GaimGroup *group,
+	     struct mwSametimeUser *stuser);
+
+static void group_add(struct mwGaimPluginData *pd, GaimGroup *group);
+
+static GaimGroup *
+group_ensure(GaimConnection *gc, struct mwSametimeGroup *stgroup);
+
+static struct mwAwareList *
+list_ensure(struct mwGaimPluginData *pd, GaimGroup *group);
+
+
+/* session functions */
+
+static struct mwSession *
+gc_to_session(GaimConnection *gc);
+
+static GaimConnection *session_to_gc(struct mwSession *session);
+
+
+/* conference functions */
+
+static struct mwConference *
+conf_find_by_id(struct mwGaimPluginData *pd, int id);
+
+
+/* conversation functions */
+
+struct convo_msg {
+  enum mwImSendType type;
+  gpointer data;
+  GDestroyNotify clear;
+};
+
+
+struct convo_data {
+  struct mwConversation *conv;
+  GList *queue;   /**< outgoing message queue, list of convo_msg */
+};
+
+static void convo_data_new(struct mwConversation *conv);
+
+static void convo_data_free(struct convo_data *conv);
+
+static void convo_features(struct mwConversation *conv);
+
+static GaimConversation *convo_get_gconv(struct mwConversation *conv);
+
+
+/* name and id */
+
+struct named_id {
+  char *id;
+  char *name;
+};
+
+
+/* connection functions */
+
+static void connect_cb(gpointer data, gint source, const gchar *error_message);
+
+
+/* ----- session ------ */
+
+
+/** resolves a mwSession from a GaimConnection */
+static struct mwSession *gc_to_session(GaimConnection *gc) {
+  struct mwGaimPluginData *pd;
+  
+  g_return_val_if_fail(gc != NULL, NULL);
+  
+  pd = gc->proto_data;
+  g_return_val_if_fail(pd != NULL, NULL);
+  
+  return pd->session;
+}
+
+
+/** resolves a GaimConnection from a mwSession */
+static GaimConnection *session_to_gc(struct mwSession *session) {
+  struct mwGaimPluginData *pd;
+
+  g_return_val_if_fail(session != NULL, NULL);
+
+  pd = mwSession_getClientData(session);
+  g_return_val_if_fail(pd != NULL, NULL);
+
+  return pd->gc;
+}
+
+
+static void write_cb(gpointer data, gint source, GaimInputCondition cond) {
+  struct mwGaimPluginData *pd = data;
+  GaimCircBuffer *circ = pd->sock_buf;
+  gsize avail;
+  int ret;
+
+  DEBUG_INFO("write_cb\n");
+
+  g_return_if_fail(circ != NULL);
+
+  avail = gaim_circ_buffer_get_max_read(circ);
+  if(BUF_LONG < avail) avail = BUF_LONG;
+
+  while(avail) {
+    ret = write(pd->socket, circ->outptr, avail);
+    
+    if(ret <= 0)
+      break;
+
+    gaim_circ_buffer_mark_read(circ, ret);
+    avail = gaim_circ_buffer_get_max_read(circ);
+    if(BUF_LONG < avail) avail = BUF_LONG;
+  }
+
+  if(! avail) {
+    gaim_input_remove(pd->outpa);
+    pd->outpa = 0;
+  }
+}
+
+
+static int mw_session_io_write(struct mwSession *session,
+			       const guchar *buf, gsize len) {
+  struct mwGaimPluginData *pd;
+  int ret = 0;
+  int err = 0;
+
+  pd = mwSession_getClientData(session);
+
+  /* socket was already closed. */
+  if(pd->socket == 0)
+    return 1;
+
+  if(pd->outpa) {
+    DEBUG_INFO("already pending INPUT_WRITE, buffering\n");
+    gaim_circ_buffer_append(pd->sock_buf, buf, len);
+    return 0;
+  }
+
+  while(len) {
+    ret = write(pd->socket, buf, (len > BUF_LEN)? BUF_LEN: len);
+
+    if(ret <= 0)
+      break;
+
+    len -= ret;
+    buf += ret;
+  }
+
+  if(ret <= 0)
+    err = errno;
+
+  if(err == EAGAIN) {
+    /* append remainder to circular buffer */
+    DEBUG_INFO("EAGAIN\n");
+    gaim_circ_buffer_append(pd->sock_buf, buf, len);
+    pd->outpa = gaim_input_add(pd->socket, GAIM_INPUT_WRITE, write_cb, pd);
+
+  } else if(len > 0) {
+    DEBUG_ERROR("write returned %i, %i bytes left unwritten\n", ret, len);
+    gaim_connection_error(pd->gc, _("Connection closed (writing)"));
+
+#if 0
+    close(pd->socket);
+    pd->socket = 0;
+#endif
+
+    return -1;
+  }
+
+  return 0;
+}
+
+
+static void mw_session_io_close(struct mwSession *session) {
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+
+  pd = mwSession_getClientData(session);
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  
+  if(pd->outpa) {
+    gaim_input_remove(pd->outpa);
+    pd->outpa = 0;
+  }
+
+  if(pd->socket) {
+    close(pd->socket);
+    pd->socket = 0;
+  }
+  
+  if(gc->inpa) {
+    gaim_input_remove(gc->inpa);
+    gc->inpa = 0;
+  }
+}
+
+
+static void mw_session_clear(struct mwSession *session) {
+  ; /* nothing for now */
+}
+
+
+/* ----- aware list ----- */
+
+
+static void blist_resolve_alias_cb(struct mwServiceResolve *srvc,
+				   guint32 id, guint32 code, GList *results,
+				   gpointer data) {
+  struct mwResolveResult *result;
+  struct mwResolveMatch *match;
+
+  g_return_if_fail(results != NULL);
+
+  result = results->data;
+  g_return_if_fail(result != NULL);
+  g_return_if_fail(result->matches != NULL);
+
+  match = result->matches->data;
+  g_return_if_fail(match != NULL);
+
+  gaim_blist_server_alias_buddy(data, match->name);
+  gaim_blist_node_set_string(data, BUDDY_KEY_NAME, match->name);
+}
+
+
+static void mw_aware_list_on_aware(struct mwAwareList *list,
+				   struct mwAwareSnapshot *aware) {
+
+  GaimConnection *gc;
+  GaimAccount *acct;
+    
+  struct mwGaimPluginData *pd;
+  guint32 idle;
+  guint stat;
+  const char *id;
+  const char *status = MW_STATE_ACTIVE;
+
+  gc = mwAwareList_getClientData(list);
+  acct = gaim_connection_get_account(gc);
+
+  pd = gc->proto_data;
+  idle = aware->status.time;
+  stat = aware->status.status;
+  id = aware->id.user;
+
+  if(idle) {
+    guint32 idle_len;       /*< how long a client has been idle */
+    guint32 ugly_idle_len;  /*< how long a broken client has been idle */
+    
+    DEBUG_INFO("%s has idle value 0x%x\n", NSTR(id), idle);
+
+    idle_len = time(NULL) - idle;
+    ugly_idle_len = ((time(NULL) * 1000) - idle) / 1000;
+
+    /* 
+       what's the deal here? Well, good clients are smart enough to
+       publish their idle time by using an attribute to indicate that
+       they went idle at some time UTC, in seconds since epoch. Bad
+       clients use milliseconds since epoch. So we're going to compute
+       the idle time for either method, then figure out the lower of
+       the two and use that. Blame the ST 7.5 development team for
+       this.
+     */
+
+    DEBUG_INFO("idle time: %u, ugly idle time: %u\n", idle_len, ugly_idle_len);
+
+#if 1
+    if(idle_len <= ugly_idle_len) {
+      ; /* DEBUG_INFO("sane idle value, let's use it\n"); */
+    } else {
+      idle = time(NULL) - ugly_idle_len;
+    }
+
+#else
+    if(idle < 0 || idle > time(NULL)) {
+      DEBUG_INFO("hiding a messy idle value 0x%x\n", NSTR(id), idle);
+      idle = -1;
+    }
+#endif
+  }
+
+  switch(stat) {
+  case mwStatus_ACTIVE:
+    status = MW_STATE_ACTIVE;
+    idle = 0;
+    break;
+
+  case mwStatus_IDLE:
+    if(! idle) idle = -1;
+    break;
+    
+  case mwStatus_AWAY:
+    status = MW_STATE_AWAY;
+    break;
+    
+  case mwStatus_BUSY:
+    status = MW_STATE_BUSY;
+    break;
+  }
+  
+  /* NAB group members */
+  if(aware->group) {
+    GaimGroup *group;
+    GaimBuddy *buddy;
+    GaimBlistNode *bnode;
+
+    group = g_hash_table_lookup(pd->group_list_map, list);
+    buddy = gaim_find_buddy_in_group(acct, id, group);
+    bnode = (GaimBlistNode *) buddy;
+
+    if(! buddy) {
+      struct mwServiceResolve *srvc;
+      GList *query;
+
+      buddy = gaim_buddy_new(acct, id, NULL);
+      gaim_blist_add_buddy(buddy, NULL, group, NULL);
+
+      bnode = (GaimBlistNode *) buddy;
+
+      srvc = pd->srvc_resolve;
+      query = g_list_append(NULL, (char *) id);
+
+      mwServiceResolve_resolve(srvc, query, mwResolveFlag_USERS,
+			       blist_resolve_alias_cb, buddy, NULL);
+      g_list_free(query);
+    }
+
+    gaim_blist_node_set_int(bnode, BUDDY_KEY_TYPE, mwSametimeUser_NORMAL);
+  }
+  
+  if(aware->online) {
+    gaim_prpl_got_user_status(acct, id, status, NULL);
+    gaim_prpl_got_user_idle(acct, id, !!idle, (time_t) idle);
+
+  } else {
+    gaim_prpl_got_user_status(acct, id, MW_STATE_OFFLINE, NULL);
+  }
+}
+
+
+static void mw_aware_list_on_attrib(struct mwAwareList *list,
+				    struct mwAwareIdBlock *id,
+				    struct mwAwareAttribute *attrib) {
+
+  ; /* nothing. We'll get attribute data as we need it */
+}
+
+
+static void mw_aware_list_clear(struct mwAwareList *list) {
+  ; /* nothing for now */
+}
+
+
+static struct mwAwareListHandler mw_aware_list_handler = {
+  .on_aware = mw_aware_list_on_aware,
+  .on_attrib = mw_aware_list_on_attrib,
+  .clear = mw_aware_list_clear,
+};
+
+
+/** Ensures that an Aware List is associated with the given group, and
+    returns that list. */
+static struct mwAwareList *
+list_ensure(struct mwGaimPluginData *pd, GaimGroup *group) {
+  
+  struct mwAwareList *list;
+  
+  g_return_val_if_fail(pd != NULL, NULL);
+  g_return_val_if_fail(group != NULL, NULL);
+  
+  list = g_hash_table_lookup(pd->group_list_map, group);
+  if(! list) {
+    list = mwAwareList_new(pd->srvc_aware, &mw_aware_list_handler);
+    mwAwareList_setClientData(list, pd->gc, NULL);
+    
+    mwAwareList_watchAttributes(list,
+				mwAttribute_AV_PREFS_SET,
+				mwAttribute_MICROPHONE,
+				mwAttribute_SPEAKERS,
+				mwAttribute_VIDEO_CAMERA,
+				mwAttribute_FILE_TRANSFER,
+				NULL);
+
+    g_hash_table_replace(pd->group_list_map, group, list);
+    g_hash_table_insert(pd->group_list_map, list, group);
+  }
+  
+  return list;
+}
+
+
+static void blist_export(GaimConnection *gc, struct mwSametimeList *stlist) {
+  /* - find the account for this connection
+     - iterate through the buddy list
+     - add each buddy matching this account to the stlist
+  */
+
+  GaimAccount *acct;
+  GaimBuddyList *blist;
+  GaimBlistNode *gn, *cn, *bn;
+  GaimGroup *grp;
+  GaimBuddy *bdy;
+
+  struct mwSametimeGroup *stg = NULL;
+  struct mwIdBlock idb = { NULL, NULL };
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  blist = gaim_get_blist();
+  g_return_if_fail(blist != NULL);
+
+  for(gn = blist->root; gn; gn = gn->next) {
+    const char *owner;
+    const char *gname;
+    enum mwSametimeGroupType gtype;
+    gboolean gopen;
+
+    if(! GAIM_BLIST_NODE_IS_GROUP(gn)) continue;
+    grp = (GaimGroup *) gn;
+
+    /* the group's type (normal or dynamic) */
+    gtype = gaim_blist_node_get_int(gn, GROUP_KEY_TYPE);
+    if(! gtype) gtype = mwSametimeGroup_NORMAL;
+
+    /* if it's a normal group with none of our people in it, skip it */
+    if(gtype == mwSametimeGroup_NORMAL && !gaim_group_on_account(grp, acct))
+      continue;
+    
+    /* if the group has an owner and we're not it, skip it */
+    owner = gaim_blist_node_get_string(gn, GROUP_KEY_OWNER);
+    if(owner && strcmp(owner, gaim_account_get_username(acct)))
+      continue;
+
+    /* the group's actual name may be different from the gaim group's
+       name. Find whichever is there */
+    gname = gaim_blist_node_get_string(gn, GROUP_KEY_NAME);
+    if(! gname) gname = grp->name;
+
+    /* we save this, but never actually honor it */
+    gopen = ! gaim_blist_node_get_bool(gn, GROUP_KEY_COLLAPSED);
+
+    stg = mwSametimeGroup_new(stlist, gtype, gname);
+    mwSametimeGroup_setAlias(stg, grp->name);
+    mwSametimeGroup_setOpen(stg, gopen);
+
+    /* don't attempt to put buddies in a dynamic group, it breaks
+       other clients */
+    if(gtype == mwSametimeGroup_DYNAMIC)
+      continue;
+
+    for(cn = gn->child; cn; cn = cn->next) {
+      if(! GAIM_BLIST_NODE_IS_CONTACT(cn)) continue;
+
+      for(bn = cn->child; bn; bn = bn->next) {
+	if(! GAIM_BLIST_NODE_IS_BUDDY(bn)) continue;
+	if(! GAIM_BLIST_NODE_SHOULD_SAVE(bn)) continue;
+
+	bdy = (GaimBuddy *) bn;
+
+	if(bdy->account == acct) {
+	  struct mwSametimeUser *stu;
+	  enum mwSametimeUserType utype;
+
+	  idb.user = bdy->name;
+
+	  utype = gaim_blist_node_get_int(bn, BUDDY_KEY_TYPE);
+	  if(! utype) utype = mwSametimeUser_NORMAL;
+
+	  stu = mwSametimeUser_new(stg, utype, &idb);
+	  mwSametimeUser_setShortName(stu, bdy->server_alias);
+	  mwSametimeUser_setAlias(stu, bdy->alias);
+	}
+      }
+    }
+  }  
+}
+
+
+static void blist_store(struct mwGaimPluginData *pd) {
+
+  struct mwSametimeList *stlist;
+  struct mwServiceStorage *srvc;
+  struct mwStorageUnit *unit;
+
+  GaimConnection *gc;
+
+  struct mwPutBuffer *b;
+  struct mwOpaque *o;
+
+  g_return_if_fail(pd != NULL);
+
+  srvc = pd->srvc_store;
+  g_return_if_fail(srvc != NULL);
+
+  gc = pd->gc;
+
+  if(BLIST_PREF_IS_LOCAL() || BLIST_PREF_IS_MERGE()) {
+    DEBUG_INFO("preferences indicate not to save remote blist\n");
+    return;
+
+  } else if(MW_SERVICE_IS_DEAD(srvc)) {
+    DEBUG_INFO("aborting save of blist: storage service is not alive\n");
+    return;
+
+  } else if(BLIST_PREF_IS_STORE() || BLIST_PREF_IS_SYNCH()) {
+    DEBUG_INFO("saving remote blist\n");
+
+  } else {
+    g_return_if_reached();
+  }
+
+  /* create and export to a list object */
+  stlist = mwSametimeList_new();
+  blist_export(gc, stlist);
+
+  /* write it to a buffer */
+  b = mwPutBuffer_new();
+  mwSametimeList_put(b, stlist);
+  mwSametimeList_free(stlist);
+
+  /* put the buffer contents into a storage unit */
+  unit = mwStorageUnit_new(mwStore_AWARE_LIST);
+  o = mwStorageUnit_asOpaque(unit);
+  mwPutBuffer_finalize(o, b);
+
+  /* save the storage unit to the service */
+  mwServiceStorage_save(srvc, unit, NULL, NULL, NULL);
+}
+
+
+static gboolean blist_save_cb(gpointer data) {
+  struct mwGaimPluginData *pd = data;
+
+  blist_store(pd);
+  pd->save_event = 0;
+  return FALSE;
+}
+
+
+/** schedules the buddy list to be saved to the server */
+static void blist_schedule(struct mwGaimPluginData *pd) {
+  if(pd->save_event) return;
+
+  pd->save_event = gaim_timeout_add(BLIST_SAVE_SECONDS * 1000,
+				    blist_save_cb, pd);
+}
+
+
+static gboolean buddy_is_external(GaimBuddy *b) {
+  g_return_val_if_fail(b != NULL, FALSE);
+  return gaim_str_has_prefix(b->name, "@E ");
+}
+
+
+/** Actually add a buddy to the aware service, and schedule the buddy
+    list to be saved to the server */
+static void buddy_add(struct mwGaimPluginData *pd,
+		      GaimBuddy *buddy) {
+
+  struct mwAwareIdBlock idb = { mwAware_USER, (char *) buddy->name, NULL };
+  struct mwAwareList *list;
+
+  GaimGroup *group;
+  GList *add;
+
+  add = g_list_prepend(NULL, &idb);
+
+  group = gaim_buddy_get_group(buddy);
+  list = list_ensure(pd, group);
+
+  if(mwAwareList_addAware(list, add)) {
+    gaim_blist_remove_buddy(buddy);
+  }
+
+  blist_schedule(pd);
+
+  g_list_free(add);  
+}
+
+
+/** ensure that a GaimBuddy exists in the group with data
+    appropriately matching the st user entry from the st list */
+static GaimBuddy *buddy_ensure(GaimConnection *gc, GaimGroup *group,
+			       struct mwSametimeUser *stuser) {
+
+  struct mwGaimPluginData *pd = gc->proto_data;
+  GaimBuddy *buddy;
+  GaimAccount *acct = gaim_connection_get_account(gc);
+
+  const char *id = mwSametimeUser_getUser(stuser);
+  const char *name = mwSametimeUser_getShortName(stuser);
+  const char *alias = mwSametimeUser_getAlias(stuser);
+  enum mwSametimeUserType type = mwSametimeUser_getType(stuser);
+
+  g_return_val_if_fail(id != NULL, NULL);
+  g_return_val_if_fail(strlen(id) > 0, NULL);
+
+  buddy = gaim_find_buddy_in_group(acct, id, group);
+  if(! buddy) {
+    buddy = gaim_buddy_new(acct, id, alias);
+  
+    gaim_blist_add_buddy(buddy, NULL, group, NULL);
+    buddy_add(pd, buddy);
+  }
+  
+  gaim_blist_alias_buddy(buddy, alias);
+  gaim_blist_server_alias_buddy(buddy, name);
+  gaim_blist_node_set_string((GaimBlistNode *) buddy, BUDDY_KEY_NAME, name);
+  gaim_blist_node_set_int((GaimBlistNode *) buddy, BUDDY_KEY_TYPE, type);
+
+  return buddy;
+}
+
+
+/** add aware watch for a dynamic group */
+static void group_add(struct mwGaimPluginData *pd,
+		      GaimGroup *group) {
+
+  struct mwAwareIdBlock idb = { mwAware_GROUP, NULL, NULL };
+  struct mwAwareList *list;
+  const char *n;
+  GList *add;
+  
+  n = gaim_blist_node_get_string((GaimBlistNode *) group, GROUP_KEY_NAME);
+  if(! n) n = group->name;
+
+  idb.user = (char *) n;
+  add = g_list_prepend(NULL, &idb);
+
+  list = list_ensure(pd, group);
+  mwAwareList_addAware(list, add);
+  g_list_free(add);
+}
+
+
+/** ensure that a GaimGroup exists in the blist with data
+    appropriately matching the st group entry from the st list */
+static GaimGroup *group_ensure(GaimConnection *gc,
+			       struct mwSametimeGroup *stgroup) {
+  GaimAccount *acct;
+  GaimGroup *group = NULL;
+  GaimBuddyList *blist;
+  GaimBlistNode *gn;
+  const char *name, *alias, *owner;
+  enum mwSametimeGroupType type;
+
+  acct = gaim_connection_get_account(gc);
+  owner = gaim_account_get_username(acct);
+
+  blist = gaim_get_blist();
+  g_return_val_if_fail(blist != NULL, NULL);
+
+  name = mwSametimeGroup_getName(stgroup);
+  alias = mwSametimeGroup_getAlias(stgroup);
+  type = mwSametimeGroup_getType(stgroup);
+
+  DEBUG_INFO("attempting to ensure group %s, called %s\n",
+	     NSTR(name), NSTR(alias));
+
+  /* first attempt at finding the group, by the name key */
+  for(gn = blist->root; gn; gn = gn->next) {
+    const char *n, *o;
+    if(! GAIM_BLIST_NODE_IS_GROUP(gn)) continue;
+    n = gaim_blist_node_get_string(gn, GROUP_KEY_NAME);
+    o = gaim_blist_node_get_string(gn, GROUP_KEY_OWNER);
+
+    DEBUG_INFO("found group named %s, owned by %s\n", NSTR(n), NSTR(o));
+
+    if(n && !strcmp(n, name)) {
+      if(!o || !strcmp(o, owner)) {
+	DEBUG_INFO("that'll work\n");
+	group = (GaimGroup *) gn;
+	break;
+      }
+    }
+  }
+
+  /* try again, by alias */
+  if(! group) {
+    DEBUG_INFO("searching for group by alias %s\n", NSTR(alias));
+    group = gaim_find_group(alias);
+  }
+
+  /* oh well, no such group. Let's create it! */
+  if(! group) {
+    DEBUG_INFO("creating group\n");
+    group = gaim_group_new(alias);
+    gaim_blist_add_group(group, NULL);
+  }
+
+  gn = (GaimBlistNode *) group;
+  gaim_blist_node_set_string(gn, GROUP_KEY_NAME, name);
+  gaim_blist_node_set_int(gn, GROUP_KEY_TYPE, type);
+
+  if(type == mwSametimeGroup_DYNAMIC) {
+    gaim_blist_node_set_string(gn, GROUP_KEY_OWNER, owner);
+    group_add(gc->proto_data, group);
+  }
+  
+  return group;
+}
+
+
+/** merge the entries from a st list into the gaim blist */
+static void blist_merge(GaimConnection *gc, struct mwSametimeList *stlist) {
+  struct mwSametimeGroup *stgroup;
+  struct mwSametimeUser *stuser;
+
+  GaimGroup *group;
+  GaimBuddy *buddy;
+
+  GList *gl, *gtl, *ul, *utl;
+
+  gl = gtl = mwSametimeList_getGroups(stlist);
+  for(; gl; gl = gl->next) {
+
+    stgroup = (struct mwSametimeGroup *) gl->data;
+    group = group_ensure(gc, stgroup);
+
+    ul = utl = mwSametimeGroup_getUsers(stgroup);
+    for(; ul; ul = ul->next) {
+
+      stuser = (struct mwSametimeUser *) ul->data;
+      buddy = buddy_ensure(gc, group, stuser);
+    }
+    g_list_free(utl);
+  }
+  g_list_free(gtl);
+}
+
+
+/** remove all buddies on account from group. If del is TRUE and group
+    is left empty, remove group as well */
+static void group_clear(GaimGroup *group, GaimAccount *acct, gboolean del) {
+  GaimConnection *gc;
+  GList *prune = NULL;
+  GaimBlistNode *gn, *cn, *bn;
+
+  g_return_if_fail(group != NULL);
+
+  DEBUG_INFO("clearing members from pruned group %s\n", NSTR(group->name));
+
+  gc = gaim_account_get_connection(acct);
+  g_return_if_fail(gc != NULL);
+
+  gn = (GaimBlistNode *) group;
+
+  for(cn = gn->child; cn; cn = cn->next) {
+    if(! GAIM_BLIST_NODE_IS_CONTACT(cn)) continue;
+
+    for(bn = cn->child; bn; bn = bn->next) {
+      GaimBuddy *gb = (GaimBuddy *) bn;
+
+      if(! GAIM_BLIST_NODE_IS_BUDDY(bn)) continue;
+      
+      if(gb->account == acct) {
+	DEBUG_INFO("clearing %s from group\n", NSTR(gb->name));
+	prune = g_list_prepend(prune, gb);
+      }
+    }
+  }
+
+  /* quickly unsubscribe from presence for the entire group */
+  gaim_account_remove_group(acct, group);
+
+  /* remove blist entries that need to go */
+  while(prune) {
+    gaim_blist_remove_buddy(prune->data);
+    prune = g_list_delete_link(prune, prune);
+  }
+  DEBUG_INFO("cleared buddies\n");
+
+  /* optionally remove group from blist */
+  if(del && !gaim_blist_get_group_size(group, TRUE)) {
+    DEBUG_INFO("removing empty group\n");
+    gaim_blist_remove_group(group);
+  }
+}
+
+
+/** prune out group members that shouldn't be there */
+static void group_prune(GaimConnection *gc, GaimGroup *group,
+			struct mwSametimeGroup *stgroup) {
+
+  GaimAccount *acct;
+  GaimBlistNode *gn, *cn, *bn;
+  
+  GHashTable *stusers;
+  GList *prune = NULL;
+  GList *ul, *utl;
+
+  g_return_if_fail(group != NULL);
+
+  DEBUG_INFO("pruning membership of group %s\n", NSTR(group->name));
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  stusers = g_hash_table_new(g_str_hash, g_str_equal);
+  
+  /* build a hash table for quick lookup while pruning the group
+     contents */
+  utl = mwSametimeGroup_getUsers(stgroup);
+  for(ul = utl; ul; ul = ul->next) {
+    const char *id = mwSametimeUser_getUser(ul->data);
+    g_hash_table_insert(stusers, (char *) id, ul->data);
+    DEBUG_INFO("server copy has %s\n", NSTR(id));
+  }
+  g_list_free(utl);
+
+  gn = (GaimBlistNode *) group;
+
+  for(cn = gn->child; cn; cn = cn->next) {
+    if(! GAIM_BLIST_NODE_IS_CONTACT(cn)) continue;
+
+    for(bn = cn->child; bn; bn = bn->next) {
+      GaimBuddy *gb = (GaimBuddy *) bn;
+
+      if(! GAIM_BLIST_NODE_IS_BUDDY(bn)) continue;
+
+      /* if the account is correct and they're not in our table, mark
+	 them for pruning */
+      if(gb->account == acct && !g_hash_table_lookup(stusers, gb->name)) {
+	DEBUG_INFO("marking %s for pruning\n", NSTR(gb->name));
+	prune = g_list_prepend(prune, gb);
+      }
+    }
+  }
+  DEBUG_INFO("done marking\n");
+
+  g_hash_table_destroy(stusers);
+
+  if(prune) {
+    gaim_account_remove_buddies(acct, prune, NULL);
+    while(prune) {
+      gaim_blist_remove_buddy(prune->data);
+      prune = g_list_delete_link(prune, prune);
+    }
+  }
+}
+
+
+/** synch the entries from a st list into the gaim blist, removing any
+    existing buddies that aren't in the st list */
+static void blist_sync(GaimConnection *gc, struct mwSametimeList *stlist) {
+
+  GaimAccount *acct;
+  GaimBuddyList *blist;
+  GaimBlistNode *gn;
+
+  GHashTable *stgroups;
+  GList *g_prune = NULL;
+
+  GList *gl, *gtl;
+
+  const char *acct_n;
+
+  DEBUG_INFO("synchronizing local buddy list from server list\n");
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  acct_n = gaim_account_get_username(acct);
+
+  blist = gaim_get_blist();
+  g_return_if_fail(blist != NULL);
+
+  /* build a hash table for quick lookup while pruning the local
+     list, mapping group name to group structure */
+  stgroups = g_hash_table_new(g_str_hash, g_str_equal);
+
+  gtl = mwSametimeList_getGroups(stlist);
+  for(gl = gtl; gl; gl = gl->next) {
+    const char *name = mwSametimeGroup_getName(gl->data);
+    g_hash_table_insert(stgroups, (char *) name, gl->data);
+  }
+  g_list_free(gtl);
+
+  /* find all groups which should be pruned from the local list */
+  for(gn = blist->root; gn; gn = gn->next) {
+    GaimGroup *grp = (GaimGroup *) gn;
+    const char *gname, *owner;
+    struct mwSametimeGroup *stgrp;
+
+    if(! GAIM_BLIST_NODE_IS_GROUP(gn)) continue;
+
+    /* group not belonging to this account */
+    if(! gaim_group_on_account(grp, acct))
+      continue;
+
+    /* dynamic group belonging to this account. don't prune contents */
+    owner = gaim_blist_node_get_string(gn, GROUP_KEY_OWNER);
+    if(owner && !strcmp(owner, acct_n))
+       continue;
+
+    /* we actually are synching by this key as opposed to the group
+       title, which can be different things in the st list */
+    gname = gaim_blist_node_get_string(gn, GROUP_KEY_NAME);
+    if(! gname) gname = grp->name;
+
+    stgrp = g_hash_table_lookup(stgroups, gname);
+    if(! stgrp) {
+      /* remove the whole group */
+      DEBUG_INFO("marking group %s for pruning\n", grp->name);
+      g_prune = g_list_prepend(g_prune, grp);
+
+    } else {
+      /* synch the group contents */
+      group_prune(gc, grp, stgrp);
+    }
+  }
+  DEBUG_INFO("done marking groups\n");
+
+  /* don't need this anymore */
+  g_hash_table_destroy(stgroups);
+
+  /* prune all marked groups */
+  while(g_prune) {
+    GaimGroup *grp = g_prune->data;
+    GaimBlistNode *gn = (GaimBlistNode *) grp;
+    const char *owner;
+    gboolean del = TRUE;
+
+    owner = gaim_blist_node_get_string(gn, GROUP_KEY_OWNER);
+    if(owner && strcmp(owner, acct_n)) {
+      /* it's a specialty group belonging to another account with some
+	 of our members in it, so don't fully delete it */
+      del = FALSE;
+    }
+    
+    group_clear(g_prune->data, acct, del);
+    g_prune = g_list_delete_link(g_prune, g_prune);
+  }
+
+  /* done with the pruning, let's merge in the additions */
+  blist_merge(gc, stlist);
+}
+
+
+/** callback passed to the storage service when it's told to load the
+    st list */
+static void fetch_blist_cb(struct mwServiceStorage *srvc,
+			   guint32 result, struct mwStorageUnit *item,
+			   gpointer data) {
+
+  struct mwGaimPluginData *pd = data;
+  struct mwSametimeList *stlist;
+
+  struct mwGetBuffer *b;
+
+  g_return_if_fail(result == ERR_SUCCESS);
+
+  /* check our preferences for loading */
+  if(BLIST_PREF_IS_LOCAL()) {
+    DEBUG_INFO("preferences indicate not to load remote buddy list\n");
+    return;
+  }
+
+  b = mwGetBuffer_wrap(mwStorageUnit_asOpaque(item));
+
+  stlist = mwSametimeList_new();
+  mwSametimeList_get(b, stlist);
+
+  /* merge or synch depending on preferences */
+  if(BLIST_PREF_IS_MERGE() || BLIST_PREF_IS_STORE()) {
+    blist_merge(pd->gc, stlist);
+
+  } else if(BLIST_PREF_IS_SYNCH()) {
+    blist_sync(pd->gc, stlist);
+  }
+
+  mwSametimeList_free(stlist);
+}
+
+
+/** signal triggered when a conversation is opened in Gaim */
+static void conversation_created_cb(GaimConversation *g_conv,
+				    struct mwGaimPluginData *pd) {
+
+  /* we need to tell the IM service to negotiate features for the
+     conversation right away, otherwise it'll wait until the first
+     message is sent before offering NotesBuddy features. Therefore
+     whenever Gaim creates a conversation, we'll immediately open the
+     channel to the other side and figure out what the target can
+     handle. Unfortunately, this makes us vulnerable to Psychic Mode,
+     whereas a more lazy negotiation based on the first message
+     would not */
+
+  GaimConnection *gc;
+  struct mwIdBlock who = { 0, 0 };
+  struct mwConversation *conv;
+
+  gc = gaim_conversation_get_gc(g_conv);
+  if(pd->gc != gc)
+    return; /* not ours */
+
+  if(gaim_conversation_get_type(g_conv) != GAIM_CONV_TYPE_IM)
+    return; /* wrong type */
+
+  who.user = (char *) gaim_conversation_get_name(g_conv);
+  conv = mwServiceIm_getConversation(pd->srvc_im, &who);
+
+  convo_features(conv);
+    
+  if(mwConversation_isClosed(conv))
+    mwConversation_open(conv);
+}
+
+
+static void blist_menu_nab(GaimBlistNode *node, gpointer data) {
+  struct mwGaimPluginData *pd = data;
+  GaimConnection *gc;
+
+  GaimGroup *group = (GaimGroup *) node;
+
+  GString *str;
+  char *tmp;
+
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  g_return_if_fail(gc != NULL);
+
+  g_return_if_fail(GAIM_BLIST_NODE_IS_GROUP(node));
+
+  str = g_string_new(NULL);
+
+  tmp = (char *) gaim_blist_node_get_string(node, GROUP_KEY_NAME);
+
+  g_string_append_printf(str, _("<b>Group Title:</b> %s<br>"), group->name);
+  g_string_append_printf(str, _("<b>Notes Group ID:</b> %s<br>"), tmp);
+
+  tmp = g_strdup_printf(_("Info for Group %s"), group->name);
+
+  gaim_notify_formatted(gc, tmp, _("Notes Address Book Information"),
+			NULL, str->str, NULL, NULL);
+
+  g_free(tmp);
+  g_string_free(str, TRUE);
+}
+
+
+/** The normal blist menu prpl function doesn't get called for groups,
+    so we use the blist-node-extended-menu signal to trigger this
+    handler */
+static void blist_node_menu_cb(GaimBlistNode *node,
+                               GList **menu, struct mwGaimPluginData *pd) {
+  const char *owner;
+  GaimGroup *group;
+  GaimAccount *acct;
+  GaimMenuAction *act;
+
+  /* we only want groups */
+  if(! GAIM_BLIST_NODE_IS_GROUP(node)) return;
+  group = (GaimGroup *) node;
+
+  acct = gaim_connection_get_account(pd->gc);
+  g_return_if_fail(acct != NULL);
+
+  /* better make sure we're connected */
+  if(! gaim_account_is_connected(acct)) return;
+
+#if 0
+  /* if there's anyone in the group for this acct, offer to invite
+     them all to a conference */
+  if(gaim_group_on_account(group, acct)) {
+    act = gaim_menu_action_new(_("Invite Group to Conference..."),
+                               GAIM_CALLBACK(blist_menu_group_invite),
+                               pd, NULL);
+    *menu = g_list_append(*menu, NULL);
+  }
+#endif
+
+  /* check if it's a NAB group for this account */
+  owner = gaim_blist_node_get_string(node, GROUP_KEY_OWNER);
+  if(owner && !strcmp(owner, gaim_account_get_username(acct))) {
+    act = gaim_menu_action_new(_("Get Notes Address Book Info"),
+                               GAIM_CALLBACK(blist_menu_nab), pd, NULL);
+    *menu = g_list_append(*menu, act);
+  }
+}
+
+
+/* lifted this from oldstatus, since HEAD doesn't do this at login
+   anymore. */
+static void blist_init(GaimAccount *acct) {
+  GaimBlistNode *gnode, *cnode, *bnode;
+  GList *add_buds = NULL;
+
+  for(gnode = gaim_get_blist()->root; gnode; gnode = gnode->next) {
+    if(! GAIM_BLIST_NODE_IS_GROUP(gnode)) continue;
+
+    for(cnode = gnode->child; cnode; cnode = cnode->next) {
+      if(! GAIM_BLIST_NODE_IS_CONTACT(cnode))
+	continue;
+      for(bnode = cnode->child; bnode; bnode = bnode->next) {
+	GaimBuddy *b;
+	if(!GAIM_BLIST_NODE_IS_BUDDY(bnode))
+	  continue;
+	
+	b = (GaimBuddy *)bnode;
+	if(b->account == acct) {
+	  add_buds = g_list_append(add_buds, b);
+	}
+      }
+    }
+  }
+  
+  if(add_buds) {
+    gaim_account_add_buddies(acct, add_buds);
+    g_list_free(add_buds);
+  }
+}
+
+
+/** Last thing to happen from a started session */
+static void services_starting(struct mwGaimPluginData *pd) {
+
+  GaimConnection *gc;
+  GaimAccount *acct;
+  struct mwStorageUnit *unit;
+  GaimBuddyList *blist;
+  GaimBlistNode *l;
+
+  gc = pd->gc;
+  acct = gaim_connection_get_account(gc);
+
+  /* grab the buddy list from the server */
+  unit = mwStorageUnit_new(mwStore_AWARE_LIST);
+  mwServiceStorage_load(pd->srvc_store, unit, fetch_blist_cb, pd, NULL); 
+
+  /* find all the NAB groups and subscribe to them */
+  blist = gaim_get_blist();
+  for(l = blist->root; l; l = l->next) {
+    GaimGroup *group = (GaimGroup *) l;
+    enum mwSametimeGroupType gt;
+    const char *owner;
+
+    if(! GAIM_BLIST_NODE_IS_GROUP(l)) continue;
+
+    /* if the group is ownerless, or has an owner and we're not it,
+       skip it */
+    owner = gaim_blist_node_get_string(l, GROUP_KEY_OWNER);
+    if(!owner || strcmp(owner, gaim_account_get_username(acct)))
+      continue;
+
+    gt = gaim_blist_node_get_int(l, GROUP_KEY_TYPE);
+    if(gt == mwSametimeGroup_DYNAMIC)
+      group_add(pd, group);
+  }
+
+  /* set the aware attributes */
+  /* indicate we understand what AV prefs are, but don't support any */
+  mwServiceAware_setAttributeBoolean(pd->srvc_aware,
+				     mwAttribute_AV_PREFS_SET, TRUE);
+  mwServiceAware_unsetAttribute(pd->srvc_aware, mwAttribute_MICROPHONE);
+  mwServiceAware_unsetAttribute(pd->srvc_aware, mwAttribute_SPEAKERS);
+  mwServiceAware_unsetAttribute(pd->srvc_aware, mwAttribute_VIDEO_CAMERA);
+
+  /* ... but we can do file transfers! */
+  mwServiceAware_setAttributeBoolean(pd->srvc_aware,
+				     mwAttribute_FILE_TRANSFER, TRUE);
+
+  blist_init(acct);
+}
+
+
+static void session_loginRedirect(struct mwSession *session,
+				  const char *host) {
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimAccount *account;
+  guint port;
+  const char *current_host;
+
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+  account = gaim_connection_get_account(gc);
+  port = gaim_account_get_int(account, MW_KEY_PORT, MW_PLUGIN_DEFAULT_PORT);
+  current_host = gaim_account_get_string(account, MW_KEY_HOST,
+					 MW_PLUGIN_DEFAULT_HOST);
+
+  if(gaim_account_get_bool(account, MW_KEY_FORCE, FALSE) ||
+     (! strcmp(current_host, host)) ||
+     (gaim_proxy_connect(NULL, account, host, port, connect_cb, pd) == NULL)) {
+
+    /* if we're configured to force logins, or if we're being
+       redirected to the already configured host, or if we couldn't
+       connect to the new host, we'll force the login instead */
+
+    mwSession_forceLogin(session);
+  }
+}
+
+
+static void mw_prpl_set_status(GaimAccount *acct, GaimStatus *status);
+
+
+/** called from mw_session_stateChange when the session's state is
+    mwSession_STARTED. Any finalizing of start-up stuff should go
+    here */
+static void session_started(struct mwGaimPluginData *pd) {
+  GaimStatus *status;
+  GaimAccount *acct;
+
+  /* set out initial status */
+  acct = gaim_connection_get_account(pd->gc);
+  status = gaim_account_get_active_status(acct);
+  mw_prpl_set_status(acct, status);
+  
+  /* start watching for new conversations */
+  gaim_signal_connect(gaim_conversations_get_handle(),
+		      "conversation-created", pd,
+		      GAIM_CALLBACK(conversation_created_cb), pd);
+
+  /* watch for group extended menu items */
+  gaim_signal_connect(gaim_blist_get_handle(),
+		      "blist-node-extended-menu", pd,
+		      GAIM_CALLBACK(blist_node_menu_cb), pd);
+  
+  /* use our services to do neat things */
+  services_starting(pd);
+}
+
+
+static void session_stopping(struct mwGaimPluginData *pd) {
+  /* stop watching the signals from session_started */
+  gaim_signals_disconnect_by_handle(pd);
+}
+
+
+static void mw_session_stateChange(struct mwSession *session,
+				   enum mwSessionState state,
+				   gpointer info) {
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  const char *msg = NULL;
+
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  switch(state) {
+  case mwSession_STARTING:
+    msg = _("Sending Handshake");
+    gaim_connection_update_progress(gc, msg, 2, MW_CONNECT_STEPS);
+    break;
+
+  case mwSession_HANDSHAKE:
+    msg = _("Waiting for Handshake Acknowledgement");
+    gaim_connection_update_progress(gc, msg, 3, MW_CONNECT_STEPS);
+    break;
+
+  case mwSession_HANDSHAKE_ACK:
+    msg = _("Handshake Acknowledged, Sending Login");
+    gaim_connection_update_progress(gc, msg, 4, MW_CONNECT_STEPS);
+    break;
+
+  case mwSession_LOGIN:
+    msg = _("Waiting for Login Acknowledgement");
+    gaim_connection_update_progress(gc, msg, 5, MW_CONNECT_STEPS);
+    break;
+
+  case mwSession_LOGIN_REDIR:
+    msg = _("Login Redirected");
+    gaim_connection_update_progress(gc, msg, 6, MW_CONNECT_STEPS);
+    session_loginRedirect(session, info);
+    break;
+
+  case mwSession_LOGIN_CONT:
+    msg = _("Forcing Login");
+    gaim_connection_update_progress(gc, msg, 7, MW_CONNECT_STEPS);
+
+  case mwSession_LOGIN_ACK:
+    msg = _("Login Acknowledged");
+    gaim_connection_update_progress(gc, msg, 8, MW_CONNECT_STEPS);
+    break;
+
+  case mwSession_STARTED:
+    msg = _("Starting Services");
+    gaim_connection_update_progress(gc, msg, 9, MW_CONNECT_STEPS);
+
+    session_started(pd);
+
+    msg = _("Connected");
+    gaim_connection_update_progress(gc, msg, 10, MW_CONNECT_STEPS);
+    gaim_connection_set_state(gc, GAIM_CONNECTED);
+    break;
+
+  case mwSession_STOPPING:
+
+    session_stopping(pd);
+
+    if(GPOINTER_TO_UINT(info) & ERR_FAILURE) {
+      char *err = mwError(GPOINTER_TO_UINT(info));
+      gaim_connection_error(gc, err);
+      g_free(err);
+    }
+    break;
+
+  case mwSession_STOPPED:
+    break;
+
+  case mwSession_UNKNOWN:
+  default:
+    DEBUG_WARN("session in unknown state\n");
+  }
+}
+
+
+static void mw_session_setPrivacyInfo(struct mwSession *session) {
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimAccount *acct;
+  struct mwPrivacyInfo *privacy;
+  GSList *l, **ll;
+  guint count;
+
+  DEBUG_INFO("privacy information set from server\n");
+
+  g_return_if_fail(session != NULL);
+
+  pd = mwSession_getClientData(session);
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  g_return_if_fail(gc != NULL);
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  privacy = mwSession_getPrivacyInfo(session);
+  count = privacy->count;
+
+  ll = (privacy->deny)? &acct->deny: &acct->permit;
+  for(l = *ll; l; l = l->next) g_free(l->data);
+  g_slist_free(*ll);
+  l = *ll = NULL;
+
+  while(count--) {
+    struct mwUserItem *u = privacy->users + count;
+    l = g_slist_prepend(l, g_strdup(u->id));
+  }
+  *ll = l;
+}
+
+
+static void mw_session_setUserStatus(struct mwSession *session) {
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  struct mwAwareIdBlock idb = { mwAware_USER, NULL, NULL };
+  struct mwUserStatus *stat;
+
+  g_return_if_fail(session != NULL);
+
+  pd = mwSession_getClientData(session);
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  g_return_if_fail(gc != NULL);
+
+  idb.user = mwSession_getProperty(session, mwSession_AUTH_USER_ID);
+  stat = mwSession_getUserStatus(session);
+
+  /* trigger an update of our own status if we're in the buddy list */
+  mwServiceAware_setStatus(pd->srvc_aware, &idb, stat);
+}
+
+
+static void mw_session_admin(struct mwSession *session,
+			     const char *text) {
+  GaimConnection *gc;
+  GaimAccount *acct;
+  const char *host;
+  const char *msg;
+  char *prim;
+
+  gc = session_to_gc(session);
+  g_return_if_fail(gc != NULL);
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  host = gaim_account_get_string(acct, MW_KEY_HOST, NULL);
+
+  msg = _("A Sametime administrator has issued the following announcement"
+	   " on server %s");
+  prim = g_strdup_printf(msg, NSTR(host));
+
+  gaim_notify_message(gc, GAIM_NOTIFY_MSG_INFO,
+		      _("Sametime Administrator Announcement"),
+		      prim, text, NULL, NULL);
+
+  g_free(prim);
+}
+
+
+/** called from read_cb, attempts to read available data from sock and
+    pass it to the session, passing back the return code from the read
+    call for handling in read_cb */
+static int read_recv(struct mwSession *session, int sock) {
+  guchar buf[BUF_LEN];
+  int len;
+
+  len = read(sock, buf, BUF_LEN);
+  if(len > 0) mwSession_recv(session, buf, len);
+
+  return len;
+}
+
+
+/** callback triggered from gaim_input_add, watches the socked for
+    available data to be processed by the session */
+static void read_cb(gpointer data, gint source, GaimInputCondition cond) {
+  struct mwGaimPluginData *pd = data;
+  int ret = 0, err = 0;
+
+  g_return_if_fail(pd != NULL);
+ 
+  ret = read_recv(pd->session, pd->socket);
+
+  /* normal operation ends here */
+  if(ret > 0) return;
+
+  /* fetch the global error value */
+  err = errno;
+
+  /* read problem occurred if we're here, so we'll need to take care of
+     it and clean up internal state */
+
+  if(pd->socket) {
+    close(pd->socket);
+    pd->socket = 0;
+  }
+
+  if(pd->gc->inpa) {
+    gaim_input_remove(pd->gc->inpa);
+    pd->gc->inpa = 0;
+  }
+
+  if(! ret) {
+    DEBUG_INFO("connection reset\n");
+    gaim_connection_error(pd->gc, _("Connection reset"));
+
+  } else if(ret < 0) {
+    char *msg = strerror(err);
+
+    DEBUG_INFO("error in read callback: %s\n", msg);
+
+    msg = g_strdup_printf(_("Error reading from socket: %s"), msg);
+    gaim_connection_error(pd->gc, msg);
+    g_free(msg);
+  }
+}
+
+
+/** Callback passed to gaim_proxy_connect when an account is logged
+    in, and if the session logging in receives a redirect message */
+static void connect_cb(gpointer data, gint source, const gchar *error_message) {
+
+  struct mwGaimPluginData *pd = data;
+  GaimConnection *gc = pd->gc;
+
+  if(source < 0) {
+    /* connection failed */
+
+    if(pd->socket) {
+      /* this is a redirect connect, force login on existing socket */
+      mwSession_forceLogin(pd->session);
+
+    } else {
+      /* this is a regular connect, error out */
+      gaim_connection_error(pd->gc, _("Unable to connect to host"));
+    }
+
+    return;
+  }
+
+  if(pd->socket) {
+    /* stop any existing login attempt */
+    mwSession_stop(pd->session, ERR_SUCCESS);
+  }
+
+  pd->socket = source;
+  gc->inpa = gaim_input_add(source, GAIM_INPUT_READ,
+			    read_cb, pd);
+
+  mwSession_start(pd->session);
+}
+
+
+static void mw_session_announce(struct mwSession *s,
+				struct mwLoginInfo *from,
+				gboolean may_reply,
+				const char *text) {
+  struct mwGaimPluginData *pd;
+  GaimAccount *acct;
+  GaimConversation *conv;
+  GaimBuddy *buddy;
+  char *who = from->user_id;
+  char *msg;
+  
+  pd = mwSession_getClientData(s);
+  acct = gaim_connection_get_account(pd->gc);
+  conv = gaim_find_conversation_with_account(GAIM_CONV_TYPE_IM, who, acct);
+  if(! conv) conv = gaim_conversation_new(GAIM_CONV_TYPE_IM, acct, who);
+
+  buddy = gaim_find_buddy(acct, who);
+  if(buddy) who = (char *) gaim_buddy_get_contact_alias(buddy);
+
+  who = g_strdup_printf(_("Announcement from %s"), who);
+  msg = gaim_markup_linkify(text);
+
+  gaim_conversation_write(conv, who, msg, GAIM_MESSAGE_RECV, time(NULL));
+  g_free(who);
+  g_free(msg);
+}
+
+
+static struct mwSessionHandler mw_session_handler = {
+  .io_write = mw_session_io_write,
+  .io_close = mw_session_io_close,
+  .clear = mw_session_clear,
+  .on_stateChange = mw_session_stateChange,
+  .on_setPrivacyInfo = mw_session_setPrivacyInfo,
+  .on_setUserStatus = mw_session_setUserStatus,
+  .on_admin = mw_session_admin,
+  .on_announce = mw_session_announce,
+};
+
+
+static void mw_aware_on_attrib(struct mwServiceAware *srvc,
+			       struct mwAwareAttribute *attrib) {
+
+  ; /** @todo handle server attributes.  There may be some stuff we
+	actually want to look for, but I'm not aware of anything right
+	now.*/
+}
+
+
+static void mw_aware_clear(struct mwServiceAware *srvc) {
+  ; /* nothing for now */
+}
+
+
+static struct mwAwareHandler mw_aware_handler = {
+  .on_attrib = mw_aware_on_attrib,
+  .clear = mw_aware_clear,
+};
+
+
+static struct mwServiceAware *mw_srvc_aware_new(struct mwSession *s) {
+  struct mwServiceAware *srvc;
+  srvc = mwServiceAware_new(s, &mw_aware_handler);
+  return srvc;
+};
+
+
+static void mw_conf_invited(struct mwConference *conf,
+			    struct mwLoginInfo *inviter,
+			    const char *invitation) {
+  
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+
+  char *c_inviter, *c_name, *c_topic, *c_invitation;
+  GHashTable *ht;
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  ht = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
+
+  c_inviter = g_strdup(inviter->user_id);
+  g_hash_table_insert(ht, CHAT_KEY_CREATOR, c_inviter);
+
+  c_name = g_strdup(mwConference_getName(conf));
+  g_hash_table_insert(ht, CHAT_KEY_NAME, c_name);
+
+  c_topic = g_strdup(mwConference_getTitle(conf));
+  g_hash_table_insert(ht, CHAT_KEY_TOPIC, c_topic);
+
+  c_invitation = g_strdup(invitation);
+  g_hash_table_insert(ht, CHAT_KEY_INVITE, c_invitation);
+
+  DEBUG_INFO("received invitation from '%s' to join ('%s','%s'): '%s'\n",
+	     NSTR(c_inviter), NSTR(c_name),
+	     NSTR(c_topic), NSTR(c_invitation));
+
+  if(! c_topic) c_topic = "(no title)";
+  if(! c_invitation) c_invitation = "(no message)";
+  serv_got_chat_invite(gc, c_topic, c_inviter, c_invitation, ht);
+}
+
+
+/* The following mess helps us relate a mwConference to a GaimConvChat
+   in the various forms by which either may be indicated */
+
+#define CONF_TO_ID(conf)   (GPOINTER_TO_INT(conf))
+#define ID_TO_CONF(pd, id) (conf_find_by_id((pd), (id)))
+
+#define CHAT_TO_ID(chat)   (gaim_conv_chat_get_id(chat))
+#define ID_TO_CHAT(id)     (gaim_find_chat(id))
+
+#define CHAT_TO_CONF(pd, chat)  (ID_TO_CONF((pd), CHAT_TO_ID(chat)))
+#define CONF_TO_CHAT(conf)      (ID_TO_CHAT(CONF_TO_ID(conf)))
+
+
+static struct mwConference *
+conf_find_by_id(struct mwGaimPluginData *pd, int id) {
+
+  struct mwServiceConference *srvc = pd->srvc_conf;
+  struct mwConference *conf = NULL;
+  GList *l, *ll;
+  
+  ll = mwServiceConference_getConferences(srvc);
+  for(l = ll; l; l = l->next) {
+    struct mwConference *c = l->data;
+    GaimConvChat *h = mwConference_getClientData(c);
+
+    if(CHAT_TO_ID(h) == id) {
+      conf = c;
+      break;
+    }
+  }
+  g_list_free(ll);
+  
+  return conf;
+}
+
+
+static void mw_conf_opened(struct mwConference *conf, GList *members) {
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConversation *g_conf;
+
+  const char *n = mwConference_getName(conf);
+  const char *t = mwConference_getTitle(conf);
+
+  DEBUG_INFO("conf %s opened, %u initial members\n",
+	     NSTR(n), g_list_length(members));
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  if(! t) t = "(no title)";
+  g_conf = serv_got_joined_chat(gc, CONF_TO_ID(conf), t);
+
+  mwConference_setClientData(conf, GAIM_CONV_CHAT(g_conf), NULL);
+
+  for(; members; members = members->next) {
+    struct mwLoginInfo *peer = members->data;
+    gaim_conv_chat_add_user(GAIM_CONV_CHAT(g_conf), peer->user_id,
+			    NULL, GAIM_CBFLAGS_NONE, FALSE);
+  }
+}
+
+
+static void mw_conf_closed(struct mwConference *conf, guint32 reason) {
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+
+  const char *n = mwConference_getName(conf);
+  char *msg = mwError(reason);
+
+  DEBUG_INFO("conf %s closed, 0x%08x\n", NSTR(n), reason);
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  serv_got_chat_left(gc, CONF_TO_ID(conf));
+
+  gaim_notify_error(gc, _("Conference Closed"), NULL, msg);
+  g_free(msg);
+}
+
+
+static void mw_conf_peer_joined(struct mwConference *conf,
+				struct mwLoginInfo *peer) {
+
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConvChat *g_conf;
+
+  const char *n = mwConference_getName(conf);
+
+  DEBUG_INFO("%s joined conf %s\n", NSTR(peer->user_id), NSTR(n));
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  g_conf = mwConference_getClientData(conf);
+  g_return_if_fail(g_conf != NULL);
+
+  gaim_conv_chat_add_user(g_conf, peer->user_id,
+			  NULL, GAIM_CBFLAGS_NONE, TRUE);
+}
+
+
+static void mw_conf_peer_parted(struct mwConference *conf,
+				struct mwLoginInfo *peer) {
+  
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConvChat *g_conf;
+
+  const char *n = mwConference_getName(conf);
+
+  DEBUG_INFO("%s left conf %s\n", NSTR(peer->user_id), NSTR(n));
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  g_conf = mwConference_getClientData(conf);
+  g_return_if_fail(g_conf != NULL);
+
+  gaim_conv_chat_remove_user(g_conf, peer->user_id, NULL);
+}
+
+
+static void mw_conf_text(struct mwConference *conf,
+			 struct mwLoginInfo *who, const char *text) {
+  
+  struct mwServiceConference *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  char *esc;
+
+  if(! text) return;
+
+  srvc = mwConference_getService(conf);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  esc = g_markup_escape_text(text, -1);
+  serv_got_chat_in(gc, CONF_TO_ID(conf), who->user_id, 0, esc, time(NULL));
+  g_free(esc);
+}
+
+
+static void mw_conf_typing(struct mwConference *conf,
+			   struct mwLoginInfo *who, gboolean typing) {
+
+  /* gaim really has no good way to expose this to the user. */
+
+  const char *n = mwConference_getName(conf);
+  const char *w = who->user_id;
+
+  if(typing) {
+    DEBUG_INFO("%s in conf %s: <typing>\n", NSTR(w), NSTR(n));
+
+  } else {
+    DEBUG_INFO("%s in conf %s: <stopped typing>\n", NSTR(w), NSTR(n));
+  }
+}
+
+
+static void mw_conf_clear(struct mwServiceConference *srvc) {
+  ;
+}
+
+
+static struct mwConferenceHandler mw_conference_handler = {
+  .on_invited = mw_conf_invited,
+  .conf_opened = mw_conf_opened,
+  .conf_closed = mw_conf_closed,
+  .on_peer_joined = mw_conf_peer_joined,
+  .on_peer_parted = mw_conf_peer_parted,
+  .on_text = mw_conf_text,
+  .on_typing = mw_conf_typing,
+  .clear = mw_conf_clear,
+};
+
+
+static struct mwServiceConference *mw_srvc_conf_new(struct mwSession *s) {
+  struct mwServiceConference *srvc;
+  srvc = mwServiceConference_new(s, &mw_conference_handler);
+  return srvc;
+}
+
+
+/** size of an outgoing file transfer chunk */
+#define MW_FT_LEN  (BUF_LONG * 2)
+
+
+static void ft_incoming_cancel(GaimXfer *xfer) {
+  /* incoming transfer rejected or canceled in-progress */
+  struct mwFileTransfer *ft = xfer->data;
+  if(ft) mwFileTransfer_reject(ft);
+}
+
+
+static void ft_incoming_init(GaimXfer *xfer) {
+  /* incoming transfer accepted */
+  
+  /* - accept the mwFileTransfer
+     - open/create the local FILE "wb"
+     - stick the FILE's fp in xfer->dest_fp
+  */
+
+  struct mwFileTransfer *ft;
+  FILE *fp;
+
+  ft = xfer->data;
+
+  fp = g_fopen(xfer->local_filename, "wb");
+  if(! fp) {
+    mwFileTransfer_cancel(ft);
+    return;
+  }
+
+  xfer->dest_fp = fp;
+  mwFileTransfer_accept(ft);
+}
+
+
+static void mw_ft_offered(struct mwFileTransfer *ft) {
+  /*
+    - create a gaim ft object
+    - offer it
+  */
+
+  struct mwServiceFileTransfer *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimAccount *acct;
+  const char *who;
+  GaimXfer *xfer;
+
+  /* @todo add some safety checks */
+  srvc = mwFileTransfer_getService(ft);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+  acct = gaim_connection_get_account(gc);
+
+  who = mwFileTransfer_getUser(ft)->user;
+
+  DEBUG_INFO("file transfer %p offered\n", ft);
+  DEBUG_INFO(" from: %s\n", NSTR(who));
+  DEBUG_INFO(" file: %s\n", NSTR(mwFileTransfer_getFileName(ft)));
+  DEBUG_INFO(" size: %u\n", mwFileTransfer_getFileSize(ft));
+  DEBUG_INFO(" text: %s\n", NSTR(mwFileTransfer_getMessage(ft)));
+
+  xfer = gaim_xfer_new(acct, GAIM_XFER_RECEIVE, who);
+  if (xfer)
+  {
+	gaim_xfer_ref(xfer);
+	mwFileTransfer_setClientData(ft, xfer, (GDestroyNotify) gaim_xfer_unref);
+	xfer->data = ft;
+
+	gaim_xfer_set_init_fnc(xfer, ft_incoming_init);
+	gaim_xfer_set_cancel_recv_fnc(xfer, ft_incoming_cancel);
+	gaim_xfer_set_request_denied_fnc(xfer, ft_incoming_cancel);
+
+	gaim_xfer_set_filename(xfer, mwFileTransfer_getFileName(ft));
+	gaim_xfer_set_size(xfer, mwFileTransfer_getFileSize(ft));
+	gaim_xfer_set_message(xfer, mwFileTransfer_getMessage(ft));
+
+	gaim_xfer_request(xfer);
+  }
+}
+
+
+static void ft_send(struct mwFileTransfer *ft, FILE *fp) {
+  guchar buf[MW_FT_LEN];
+  struct mwOpaque o = { .data = buf, .len = MW_FT_LEN };
+  guint32 rem;
+  GaimXfer *xfer;
+
+  xfer = mwFileTransfer_getClientData(ft);
+
+  rem = mwFileTransfer_getRemaining(ft);
+  if(rem < MW_FT_LEN) o.len = rem;
+  
+  if(fread(buf, (size_t) o.len, 1, fp)) {
+
+    /* calculate progress and display it */
+    xfer->bytes_sent += o.len;
+    xfer->bytes_remaining -= o.len;
+    gaim_xfer_update_progress(xfer);
+
+    mwFileTransfer_send(ft, &o);
+
+  } else {
+    int err = errno;
+    DEBUG_WARN("problem reading from file %s: %s\n",
+	       NSTR(mwFileTransfer_getFileName(ft)), strerror(err));
+
+    mwFileTransfer_cancel(ft);
+  }
+}
+
+
+static void mw_ft_opened(struct mwFileTransfer *ft) {
+  /*
+    - get gaim ft from client data in ft
+    - set the state to active
+  */
+
+  GaimXfer *xfer;
+
+  xfer = mwFileTransfer_getClientData(ft);
+
+  if(! xfer) {
+    mwFileTransfer_cancel(ft);
+    mwFileTransfer_free(ft);
+    g_return_if_reached();
+  }
+
+  if(gaim_xfer_get_type(xfer) == GAIM_XFER_SEND) {
+    xfer->dest_fp = g_fopen(xfer->local_filename, "rb");
+    ft_send(ft, xfer->dest_fp);
+  }  
+}
+
+
+static void mw_ft_closed(struct mwFileTransfer *ft, guint32 code) {
+  /*
+    - get gaim ft from client data in ft
+    - indicate rejection/cancelation/completion
+    - free the file transfer itself
+  */
+
+  GaimXfer *xfer;
+
+  xfer = mwFileTransfer_getClientData(ft);
+  if(xfer) {
+    xfer->data = NULL;
+
+    if(! mwFileTransfer_getRemaining(ft)) {
+      gaim_xfer_set_completed(xfer, TRUE);
+      gaim_xfer_end(xfer);
+
+    } else if(mwFileTransfer_isCancelLocal(ft)) {
+      /* calling gaim_xfer_cancel_local is redundant, since that's
+	 probably what triggered this function to be called */
+      ;
+
+    } else if(mwFileTransfer_isCancelRemote(ft)) {
+      /* steal the reference for the xfer */
+      mwFileTransfer_setClientData(ft, NULL, NULL);
+      gaim_xfer_cancel_remote(xfer);
+
+      /* drop the stolen reference */
+      gaim_xfer_unref(xfer);
+      return;
+    }
+  }
+
+  mwFileTransfer_free(ft);
+}
+
+
+static void mw_ft_recv(struct mwFileTransfer *ft,
+		       struct mwOpaque *data) {
+  /*
+    - get gaim ft from client data in ft
+    - update transfered percentage
+    - if done, destroy the ft, disassociate from gaim ft
+  */
+
+  GaimXfer *xfer;
+  FILE *fp;
+
+  xfer = mwFileTransfer_getClientData(ft);
+  g_return_if_fail(xfer != NULL);
+
+  fp = xfer->dest_fp;
+  g_return_if_fail(fp != NULL);
+
+  /* we must collect and save our precious data */
+  fwrite(data->data, 1, data->len, fp);
+
+  /* update the progress */
+  xfer->bytes_sent += data->len;
+  xfer->bytes_remaining -= data->len;
+  gaim_xfer_update_progress(xfer);
+
+  /* let the other side know we got it, and to send some more */
+  mwFileTransfer_ack(ft);
+}
+
+
+static void mw_ft_ack(struct mwFileTransfer *ft) {
+  GaimXfer *xfer;
+
+  xfer = mwFileTransfer_getClientData(ft);
+  g_return_if_fail(xfer != NULL);
+  g_return_if_fail(xfer->watcher == 0);
+
+  if(! mwFileTransfer_getRemaining(ft)) {
+    gaim_xfer_set_completed(xfer, TRUE);
+    gaim_xfer_end(xfer);
+
+  } else if(mwFileTransfer_isOpen(ft)) {
+    ft_send(ft, xfer->dest_fp);
+  }
+}
+
+
+static void mw_ft_clear(struct mwServiceFileTransfer *srvc) {
+  ;
+}
+
+
+static struct mwFileTransferHandler mw_ft_handler = {
+  .ft_offered = mw_ft_offered,
+  .ft_opened = mw_ft_opened,
+  .ft_closed = mw_ft_closed,
+  .ft_recv = mw_ft_recv,
+  .ft_ack = mw_ft_ack,
+  .clear = mw_ft_clear,
+};
+
+
+static struct mwServiceFileTransfer *mw_srvc_ft_new(struct mwSession *s) {
+  struct mwServiceFileTransfer *srvc;
+  GHashTable *ft_map;
+
+  ft_map = g_hash_table_new(g_direct_hash, g_direct_equal);
+
+  srvc = mwServiceFileTransfer_new(s, &mw_ft_handler);
+  mwService_setClientData(MW_SERVICE(srvc), ft_map,
+			  (GDestroyNotify) g_hash_table_destroy);
+
+  return srvc;
+}
+
+
+static void convo_data_free(struct convo_data *cd) {
+  GList *l;
+
+  /* clean the queue */
+  for(l = cd->queue; l; l = g_list_delete_link(l, l)) {
+    struct convo_msg *m = l->data;
+    if(m->clear) m->clear(m->data);
+    g_free(m);
+  }
+
+  g_free(cd);
+}
+
+
+/** allocates a convo_data structure and associates it with the
+    conversation in the client data slot */
+static void convo_data_new(struct mwConversation *conv) {
+  struct convo_data *cd;
+
+  g_return_if_fail(conv != NULL);
+
+  if(mwConversation_getClientData(conv))
+    return;
+
+  cd = g_new0(struct convo_data, 1);
+  cd->conv = conv;
+
+  mwConversation_setClientData(conv, cd, (GDestroyNotify) convo_data_free);
+}
+
+
+static GaimConversation *convo_get_gconv(struct mwConversation *conv) {
+  struct mwServiceIm *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimAccount *acct;
+
+  struct mwIdBlock *idb;
+
+  srvc = mwConversation_getService(conv);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+  acct = gaim_connection_get_account(gc);
+
+  idb = mwConversation_getTarget(conv);
+
+  return gaim_find_conversation_with_account(GAIM_CONV_TYPE_IM,
+					     idb->user, acct);
+}
+
+
+static void convo_queue(struct mwConversation *conv,
+			enum mwImSendType type, gconstpointer data) {
+
+  struct convo_data *cd;
+  struct convo_msg *m;
+
+  convo_data_new(conv);
+  cd = mwConversation_getClientData(conv);
+
+  m = g_new0(struct convo_msg, 1);
+  m->type = type;
+
+  switch(type) {
+  case mwImSend_PLAIN:
+    m->data = g_strdup(data);
+    m->clear = g_free;
+    break;
+    
+  case mwImSend_TYPING:
+  default:
+    m->data = (gpointer) data;
+    m->clear = NULL;
+  }
+
+  cd->queue = g_list_append(cd->queue, m);
+}
+
+
+/* Does what it takes to get an error displayed for a conversation */
+static void convo_error(struct mwConversation *conv, guint32 err) {
+  GaimConversation *gconv;
+  char *tmp, *text;
+  struct mwIdBlock *idb;
+  
+  idb = mwConversation_getTarget(conv);
+  
+  tmp = mwError(err);
+  text = g_strconcat(_("Unable to send message: "), tmp, NULL);
+  
+  gconv = convo_get_gconv(conv);
+  if(gconv && !gaim_conv_present_error(idb->user, gconv->account, text)) {
+    
+    g_free(text);
+    text = g_strdup_printf(_("Unable to send message to %s:"),
+			   (idb->user)? idb->user: "(unknown)");
+    gaim_notify_error(gaim_account_get_connection(gconv->account),
+		      NULL, text, tmp);
+  }
+  
+  g_free(tmp);
+  g_free(text);
+}
+
+
+static void convo_queue_send(struct mwConversation *conv) {
+  struct convo_data *cd;
+  GList *l;
+  
+  cd = mwConversation_getClientData(conv);
+
+  for(l = cd->queue; l; l = g_list_delete_link(l, l)) {
+    struct convo_msg *m = l->data;
+
+    mwConversation_send(conv, m->type, m->data);
+
+    if(m->clear) m->clear(m->data);
+    g_free(m);
+  }
+
+  cd->queue = NULL;
+}
+
+
+/**  called when a mw conversation leaves a gaim conversation to
+     inform the gaim conversation that it's unsafe to offer any *cool*
+     features. */
+static void convo_nofeatures(struct mwConversation *conv) {
+  GaimConversation *gconv;
+  GaimConnection *gc;
+
+  gconv = convo_get_gconv(conv);
+  if(! gconv) return;
+
+  gc = gaim_conversation_get_gc(gconv);
+  if(! gc) return;
+
+  gaim_conversation_set_features(gconv, gc->flags);
+}
+
+
+/** called when a mw conversation and gaim conversation come together,
+    to inform the gaim conversation of what features to offer the
+    user */
+static void convo_features(struct mwConversation *conv) {
+  GaimConversation *gconv;
+  GaimConnectionFlags feat;
+
+  gconv = convo_get_gconv(conv);
+  if(! gconv) return;
+
+  feat = gaim_conversation_get_features(gconv);
+
+  if(mwConversation_isOpen(conv)) {
+    if(mwConversation_supports(conv, mwImSend_HTML)) {
+      feat |= GAIM_CONNECTION_HTML;
+    } else {
+      feat &= ~GAIM_CONNECTION_HTML;
+    }
+
+    if(mwConversation_supports(conv, mwImSend_MIME)) {
+      feat &= ~GAIM_CONNECTION_NO_IMAGES;
+    } else {
+      feat |= GAIM_CONNECTION_NO_IMAGES;
+    }
+
+    DEBUG_INFO("conversation features set to 0x%04x\n", feat);
+    gaim_conversation_set_features(gconv, feat);
+
+  } else {
+    convo_nofeatures(conv);
+  }
+}
+
+
+static void mw_conversation_opened(struct mwConversation *conv) {
+  struct mwServiceIm *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimAccount *acct;
+
+  struct convo_dat *cd;
+
+  srvc = mwConversation_getService(conv);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+  acct = gaim_connection_get_account(gc);
+
+  /* set up the queue */
+  cd = mwConversation_getClientData(conv);
+  if(cd) {
+    convo_queue_send(conv);
+  
+    if(! convo_get_gconv(conv)) {
+      mwConversation_free(conv);
+      return;
+    }
+
+  } else {
+    convo_data_new(conv);
+  }
+
+  { /* record the client key for the buddy */
+    GaimBuddy *buddy;
+    struct mwLoginInfo *info;
+    info = mwConversation_getTargetInfo(conv);
+    
+    buddy = gaim_find_buddy(acct, info->user_id);
+    if(buddy) {
+      gaim_blist_node_set_int((GaimBlistNode *) buddy,
+			      BUDDY_KEY_CLIENT, info->type);
+    }
+  }
+
+  convo_features(conv);
+}
+
+
+static void mw_conversation_closed(struct mwConversation *conv,
+				   guint32 reason) {
+
+  struct convo_data *cd;
+
+  g_return_if_fail(conv != NULL);
+
+  /* if there's an error code and a non-typing message in the queue,
+     print an error message to the conversation */
+  cd = mwConversation_getClientData(conv);
+  if(reason && cd && cd->queue) {
+    GList *l;
+    for(l = cd->queue; l; l = l->next) {
+      struct convo_msg *m = l->data;
+      if(m->type != mwImSend_TYPING) {
+	convo_error(conv, reason);
+	break;
+      }
+    }
+  }
+
+#if 0
+  /* don't do this, to prevent the occasional weird sending of
+     formatted messages as plaintext when the other end closes the
+     conversation after we've begun composing the message */
+  convo_nofeatures(conv);
+#endif
+
+  mwConversation_removeClientData(conv);
+}
+
+
+static void im_recv_text(struct mwConversation *conv,
+			 struct mwGaimPluginData *pd,
+			 const char *msg) {
+
+  struct mwIdBlock *idb;
+  char *txt, *esc;
+  const char *t;
+
+  idb = mwConversation_getTarget(conv);
+
+  txt = gaim_utf8_try_convert(msg);
+  t = txt? txt: msg;
+
+  esc = g_markup_escape_text(t, -1);
+  serv_got_im(pd->gc, idb->user, esc, 0, time(NULL));
+  g_free(esc);
+
+  g_free(txt);
+}
+
+
+static void im_recv_typing(struct mwConversation *conv,
+			   struct mwGaimPluginData *pd,
+			   gboolean typing) {
+
+  struct mwIdBlock *idb;
+  idb = mwConversation_getTarget(conv);
+
+  serv_got_typing(pd->gc, idb->user, 0,
+		  typing? GAIM_TYPING: GAIM_NOT_TYPING);
+}
+
+
+static void im_recv_html(struct mwConversation *conv,
+			 struct mwGaimPluginData *pd,
+			 const char *msg) {
+  struct mwIdBlock *idb;
+  char *t1, *t2;
+  const char *t;
+
+  idb = mwConversation_getTarget(conv);
+
+  /* ensure we're receiving UTF8 */
+  t1 = gaim_utf8_try_convert(msg);
+  t = t1? t1: msg;
+
+  /* convert entities to UTF8 so they'll log correctly */
+  t2 = gaim_utf8_ncr_decode(t);
+  t = t2? t2: t;
+
+  serv_got_im(pd->gc, idb->user, t, 0, time(NULL));
+
+  g_free(t1);
+  g_free(t2);
+}
+
+
+static void im_recv_subj(struct mwConversation *conv,
+			 struct mwGaimPluginData *pd,
+			 const char *subj) {
+
+  /** @todo somehow indicate receipt of a conversation subject. It
+      would also be nice if we added a /topic command for the
+      protocol */
+  ;
+}
+
+
+/** generate "cid:908@20582notesbuddy" from "<908@20582notesbuddy>" */
+static char *make_cid(const char *cid) {
+  gsize n;
+  char *c, *d;
+
+  g_return_val_if_fail(cid != NULL, NULL);
+
+  n = strlen(cid);
+  g_return_val_if_fail(n > 2, NULL);
+
+  c = g_strndup(cid+1, n-2);
+  d = g_strdup_printf("cid:%s", c);
+
+  g_free(c);
+  return d;
+}
+
+
+static void im_recv_mime(struct mwConversation *conv,
+			 struct mwGaimPluginData *pd,
+			 const char *data) {
+
+  GHashTable *img_by_cid;
+  GList *images;
+
+  GString *str;
+
+  GaimMimeDocument *doc;
+  const GList *parts;
+
+  img_by_cid = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
+  images = NULL;
+
+  /* don't want the contained string to ever be NULL */
+  str = g_string_new("");
+  
+  doc = gaim_mime_document_parse(data);
+
+  /* handle all the MIME parts */
+  parts = gaim_mime_document_get_parts(doc);
+  for(; parts; parts = parts->next) {
+    GaimMimePart *part = parts->data;
+    const char *type;
+
+    type = gaim_mime_part_get_field(part, "content-type");
+    DEBUG_INFO("MIME part Content-Type: %s\n", NSTR(type));
+
+    if(! type) {
+      ; /* feh */
+      
+    } else if(gaim_str_has_prefix(type, "image")) {
+      /* put images into the image store */
+
+      guchar *d_dat;
+      gsize d_len;
+      char *cid;
+      int img;
+
+      /* obtain and unencode the data */
+      gaim_mime_part_get_data_decoded(part, &d_dat, &d_len);
+      
+      /* look up the content id */
+      cid = (char *) gaim_mime_part_get_field(part, "Content-ID");
+      cid = make_cid(cid);
+
+      /* add image to the gaim image store */
+      img = gaim_imgstore_add(d_dat, d_len, cid);
+      g_free(d_dat);
+
+      /* map the cid to the image store identifier */
+      g_hash_table_insert(img_by_cid, cid, GINT_TO_POINTER(img));
+
+      /* recall the image for dereferencing later */
+      images = g_list_append(images, GINT_TO_POINTER(img));
+      
+    } else if(gaim_str_has_prefix(type, "text")) {
+
+      /* concatenate all the text parts together */
+      guchar *data;
+      gsize len;
+
+      gaim_mime_part_get_data_decoded(part, &data, &len);
+      g_string_append(str, (const char *)data);
+      g_free(data);
+    }
+  }  
+
+  gaim_mime_document_free(doc);
+
+  /* @todo should put this in its own function */
+  { /* replace each IMG tag's SRC attribute with an ID attribute. This
+       actually modifies the contents of str */
+    GData *attribs;
+    char *start, *end;
+    char *tmp = str->str;
+
+    while(*tmp && gaim_markup_find_tag("img", tmp, (const char **) &start,
+				       (const char **) &end, &attribs)) {
+
+      char *alt, *align, *border, *src;
+      int img = 0;
+
+      alt = g_datalist_get_data(&attribs, "alt");
+      align = g_datalist_get_data(&attribs, "align");
+      border = g_datalist_get_data(&attribs, "border");
+      src = g_datalist_get_data(&attribs, "src");
+
+      if(src)
+	img = GPOINTER_TO_INT(g_hash_table_lookup(img_by_cid, src));
+
+      if(img) {
+	GString *atstr;
+	gsize len = (end - start);
+	gsize mov;
+
+	atstr = g_string_new("");
+	if(alt) g_string_append_printf(atstr, " alt=\"%s\"", alt);
+	if(align) g_string_append_printf(atstr, " align=\"%s\"", align);
+	if(border) g_string_append_printf(atstr, " border=\"%s\"", border);
+
+	mov = g_snprintf(start, len, "<img%s id=\"%i\"", atstr->str, img);
+	while(mov < len) start[mov++] = ' ';
+
+	g_string_free(atstr, TRUE);
+      }
+
+      g_datalist_clear(&attribs);
+      tmp = end + 1;
+    }
+  }
+
+  im_recv_html(conv, pd, str->str);
+
+  g_string_free(str, TRUE);
+  
+  /* clean up the cid table */
+  g_hash_table_destroy(img_by_cid);
+
+  /* dereference all the imgages */
+  while(images) {
+    gaim_imgstore_unref(GPOINTER_TO_INT(images->data));
+    images = g_list_delete_link(images, images);
+  }
+}
+
+
+static void mw_conversation_recv(struct mwConversation *conv,
+				 enum mwImSendType type,
+				 gconstpointer msg) {
+  struct mwServiceIm *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+
+  srvc = mwConversation_getService(conv);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+
+  switch(type) {
+  case mwImSend_PLAIN:
+    im_recv_text(conv, pd, msg);
+    break;
+
+  case mwImSend_TYPING:
+    im_recv_typing(conv, pd, !! msg);
+    break;
+
+  case mwImSend_HTML:
+    im_recv_html(conv, pd, msg);
+    break;
+
+  case mwImSend_SUBJECT:
+    im_recv_subj(conv, pd, msg);
+    break;
+
+  case mwImSend_MIME:
+    im_recv_mime(conv, pd, msg);
+    break;
+
+  default:
+    DEBUG_INFO("conversation received strange type, 0x%04x\n", type);
+    ; /* erm... */
+  }
+}
+
+
+static void mw_place_invite(struct mwConversation *conv,
+			    const char *message,
+			    const char *title, const char *name) {
+  struct mwServiceIm *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+
+  struct mwIdBlock *idb;
+  GHashTable *ht;
+
+  srvc = mwConversation_getService(conv);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+
+  idb = mwConversation_getTarget(conv);
+  
+  ht = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
+  g_hash_table_insert(ht, CHAT_KEY_CREATOR, g_strdup(idb->user));
+  g_hash_table_insert(ht, CHAT_KEY_NAME, g_strdup(name));
+  g_hash_table_insert(ht, CHAT_KEY_TOPIC, g_strdup(title));
+  g_hash_table_insert(ht, CHAT_KEY_INVITE, g_strdup(message));
+  g_hash_table_insert(ht, CHAT_KEY_IS_PLACE, g_strdup("")); /* ugh */
+
+  if(! title) title = "(no title)";
+  if(! message) message = "(no message)";
+  serv_got_chat_invite(pd->gc, title, idb->user, message, ht);
+
+  mwConversation_close(conv, ERR_SUCCESS);
+  mwConversation_free(conv);
+}
+
+
+static void mw_im_clear(struct mwServiceIm *srvc) {
+  ;
+}
+
+
+static struct mwImHandler mw_im_handler = {
+  .conversation_opened = mw_conversation_opened,
+  .conversation_closed = mw_conversation_closed,
+  .conversation_recv = mw_conversation_recv,
+  .place_invite = mw_place_invite,
+  .clear = mw_im_clear,
+};
+
+
+static struct mwServiceIm *mw_srvc_im_new(struct mwSession *s) {
+  struct mwServiceIm *srvc;
+  srvc = mwServiceIm_new(s, &mw_im_handler);
+  mwServiceIm_setClientType(srvc, mwImClient_NOTESBUDDY);
+  return srvc;
+}
+
+
+/* The following helps us relate a mwPlace to a GaimConvChat in the
+   various forms by which either may be indicated. Uses some of
+   the similar macros from the conference service above */
+
+#define PLACE_TO_ID(place)   (GPOINTER_TO_INT(place))
+#define ID_TO_PLACE(pd, id)  (place_find_by_id((pd), (id)))
+
+#define CHAT_TO_PLACE(pd, chat)  (ID_TO_PLACE((pd), CHAT_TO_ID(chat)))
+#define PLACE_TO_CHAT(place)     (ID_TO_CHAT(PLACE_TO_ID(place)))
+
+
+static struct mwPlace *
+place_find_by_id(struct mwGaimPluginData *pd, int id) {
+  struct mwServicePlace *srvc = pd->srvc_place;
+  struct mwPlace *place = NULL;
+  GList *l;
+
+  l = (GList *) mwServicePlace_getPlaces(srvc);
+  for(; l; l = l->next) {
+    struct mwPlace *p = l->data;
+    GaimConvChat *h = GAIM_CONV_CHAT(mwPlace_getClientData(p));
+
+    if(CHAT_TO_ID(h) == id) {
+      place = p;
+      break;
+    }
+  }
+
+  return place;
+}
+
+
+static void mw_place_opened(struct mwPlace *place) {
+  struct mwServicePlace *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConversation *gconf;
+
+  GList *members, *l;
+
+  const char *n = mwPlace_getName(place);
+  const char *t = mwPlace_getTitle(place);
+
+  srvc = mwPlace_getService(place);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  members = mwPlace_getMembers(place);
+
+  DEBUG_INFO("place %s opened, %u initial members\n",
+	     NSTR(n), g_list_length(members));
+
+  if(! t) t = "(no title)";
+  gconf = serv_got_joined_chat(gc, PLACE_TO_ID(place), t);
+
+  mwPlace_setClientData(place, gconf, NULL);
+
+  for(l = members; l; l = l->next) {
+    struct mwIdBlock *idb = l->data;
+    gaim_conv_chat_add_user(GAIM_CONV_CHAT(gconf), idb->user,
+			    NULL, GAIM_CBFLAGS_NONE, FALSE);
+  }
+  g_list_free(members);
+}
+
+
+static void mw_place_closed(struct mwPlace *place, guint32 code) {
+  struct mwServicePlace *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+
+  const char *n = mwPlace_getName(place);
+  char *msg = mwError(code);
+
+  DEBUG_INFO("place %s closed, 0x%08x\n", NSTR(n), code);
+
+  srvc = mwPlace_getService(place);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  serv_got_chat_left(gc, PLACE_TO_ID(place));
+
+  gaim_notify_error(gc, _("Place Closed"), NULL, msg);
+  g_free(msg);
+}
+
+
+static void mw_place_peerJoined(struct mwPlace *place,
+				const struct mwIdBlock *peer) {
+  struct mwServicePlace *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConversation *gconf;
+
+  const char *n = mwPlace_getName(place);
+
+  DEBUG_INFO("%s joined place %s\n", NSTR(peer->user), NSTR(n));
+
+  srvc = mwPlace_getService(place);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  gconf = mwPlace_getClientData(place);
+  g_return_if_fail(gconf != NULL);
+
+  gaim_conv_chat_add_user(GAIM_CONV_CHAT(gconf), peer->user,
+			  NULL, GAIM_CBFLAGS_NONE, TRUE);
+}
+
+
+static void mw_place_peerParted(struct mwPlace *place,
+				const struct mwIdBlock *peer) {
+  struct mwServicePlace *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  GaimConversation *gconf;
+
+  const char *n = mwPlace_getName(place);
+
+  DEBUG_INFO("%s left place %s\n", NSTR(peer->user), NSTR(n));
+
+  srvc = mwPlace_getService(place);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  gconf = mwPlace_getClientData(place);
+  g_return_if_fail(gconf != NULL);
+
+  gaim_conv_chat_remove_user(GAIM_CONV_CHAT(gconf), peer->user, NULL);
+}
+
+
+static void mw_place_peerSetAttribute(struct mwPlace *place,
+				      const struct mwIdBlock *peer,
+				      guint32 attr, struct mwOpaque *o) {
+  ;
+}
+
+
+static void mw_place_peerUnsetAttribute(struct mwPlace *place,
+					const struct mwIdBlock *peer,
+					guint32 attr) {
+  ;
+}
+
+
+static void mw_place_message(struct mwPlace *place,
+			     const struct mwIdBlock *who,
+			     const char *msg) {
+  struct mwServicePlace *srvc;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+  char *esc;
+
+  if(! msg) return;
+
+  srvc = mwPlace_getService(place);
+  session = mwService_getSession(MW_SERVICE(srvc));
+  pd = mwSession_getClientData(session);
+  gc = pd->gc;
+
+  esc = g_markup_escape_text(msg, -1);
+  serv_got_chat_in(gc, PLACE_TO_ID(place), who->user, 0, esc, time(NULL));
+  g_free(esc);
+}
+
+
+static void mw_place_clear(struct mwServicePlace *srvc) {
+  ;
+}
+
+
+static struct mwPlaceHandler mw_place_handler = {
+  .opened = mw_place_opened,
+  .closed = mw_place_closed,
+  .peerJoined = mw_place_peerJoined,
+  .peerParted = mw_place_peerParted,
+  .peerSetAttribute = mw_place_peerSetAttribute,
+  .peerUnsetAttribute = mw_place_peerUnsetAttribute,
+  .message = mw_place_message,
+  .clear = mw_place_clear,
+};
+
+
+static struct mwServicePlace *mw_srvc_place_new(struct mwSession *s) {
+  struct mwServicePlace *srvc;
+  srvc = mwServicePlace_new(s, &mw_place_handler);
+  return srvc;
+}
+
+
+static struct mwServiceResolve *mw_srvc_resolve_new(struct mwSession *s) {
+  struct mwServiceResolve *srvc;
+  srvc = mwServiceResolve_new(s);
+  return srvc;
+}
+
+
+static struct mwServiceStorage *mw_srvc_store_new(struct mwSession *s) {
+  struct mwServiceStorage *srvc;
+  srvc = mwServiceStorage_new(s);
+  return srvc;
+}
+
+
+/** allocate and associate a mwGaimPluginData with a GaimConnection */
+static struct mwGaimPluginData *mwGaimPluginData_new(GaimConnection *gc) {
+  struct mwGaimPluginData *pd;
+
+  g_return_val_if_fail(gc != NULL, NULL);
+
+  pd = g_new0(struct mwGaimPluginData, 1);
+  pd->gc = gc;
+  pd->session = mwSession_new(&mw_session_handler);
+  pd->srvc_aware = mw_srvc_aware_new(pd->session);
+  pd->srvc_conf = mw_srvc_conf_new(pd->session);
+  pd->srvc_ft = mw_srvc_ft_new(pd->session);
+  pd->srvc_im = mw_srvc_im_new(pd->session);
+  pd->srvc_place = mw_srvc_place_new(pd->session);
+  pd->srvc_resolve = mw_srvc_resolve_new(pd->session);
+  pd->srvc_store = mw_srvc_store_new(pd->session);
+  pd->group_list_map = g_hash_table_new(g_direct_hash, g_direct_equal);
+  pd->sock_buf = gaim_circ_buffer_new(0);
+
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_aware));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_conf));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_ft));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_im));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_place));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_resolve));
+  mwSession_addService(pd->session, MW_SERVICE(pd->srvc_store));
+
+  mwSession_addCipher(pd->session, mwCipher_new_RC2_40(pd->session));
+  mwSession_addCipher(pd->session, mwCipher_new_RC2_128(pd->session));
+
+  mwSession_setClientData(pd->session, pd, NULL);
+  gc->proto_data = pd;
+
+  return pd;
+}
+
+
+static void mwGaimPluginData_free(struct mwGaimPluginData *pd) {
+  g_return_if_fail(pd != NULL);
+
+  pd->gc->proto_data = NULL;
+
+  mwSession_removeService(pd->session, mwService_AWARE);
+  mwSession_removeService(pd->session, mwService_CONFERENCE);
+  mwSession_removeService(pd->session, mwService_FILE_TRANSFER);
+  mwSession_removeService(pd->session, mwService_IM);
+  mwSession_removeService(pd->session, mwService_PLACE);
+  mwSession_removeService(pd->session, mwService_RESOLVE);
+  mwSession_removeService(pd->session, mwService_STORAGE);
+
+  mwService_free(MW_SERVICE(pd->srvc_aware));
+  mwService_free(MW_SERVICE(pd->srvc_conf));
+  mwService_free(MW_SERVICE(pd->srvc_ft));
+  mwService_free(MW_SERVICE(pd->srvc_im));
+  mwService_free(MW_SERVICE(pd->srvc_place));
+  mwService_free(MW_SERVICE(pd->srvc_resolve));
+  mwService_free(MW_SERVICE(pd->srvc_store));
+
+  mwCipher_free(mwSession_getCipher(pd->session, mwCipher_RC2_40));
+  mwCipher_free(mwSession_getCipher(pd->session, mwCipher_RC2_128));
+
+  mwSession_free(pd->session);
+
+  g_hash_table_destroy(pd->group_list_map);
+  gaim_circ_buffer_destroy(pd->sock_buf);
+
+  g_free(pd);
+}
+
+
+static const char *mw_prpl_list_icon(GaimAccount *a, GaimBuddy *b) {
+  /* my little green dude is a chopped up version of the aim running
+     guy.  First, cut off the head and store someplace safe. Then,
+     take the left-half side of the body and throw it away. Make a
+     copy of the remaining body, and flip it horizontally. Now attach
+     the two pieces into an X shape, and drop the head back on the
+     top, being careful to center it. Then, just change the color
+     saturation to bring the red down a bit, and voila! */
+
+  /* then, throw all of that away and use sodipodi to make a new
+     icon. You know, LIKE A REAL MAN. */
+
+  return "meanwhile";
+}
+
+
+static void mw_prpl_list_emblems(GaimBuddy *b,
+				 const char **se, const char **sw,
+				 const char **nw, const char **ne) {
+
+  /* speaking of custom icons, the external icon here is an ugly
+     little example of what happens when I use Gimp */
+
+  GaimPresence *presence;
+  GaimStatus *status;
+  const char *status_id;
+
+  presence = gaim_buddy_get_presence(b);
+  status = gaim_presence_get_active_status(presence);
+  status_id = gaim_status_get_id(status);
+
+  if(! GAIM_BUDDY_IS_ONLINE(b)) {
+    *se = "offline";
+  } else if(!strcmp(status_id, MW_STATE_AWAY)) {
+    *se = "away";
+  } else if(!strcmp(status_id, MW_STATE_BUSY)) {
+    *se = "dnd";
+  }  
+
+  if(buddy_is_external(b)) {
+    /* best assignment ever */
+    *(*se?sw:se) = "external";
+  }
+}
+
+
+static char *mw_prpl_status_text(GaimBuddy *b) {
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+  struct mwAwareIdBlock t = { mwAware_USER, b->name, NULL };
+  const char *ret;
+
+  gc = b->account->gc;
+  pd = gc->proto_data;
+
+  ret = mwServiceAware_getText(pd->srvc_aware, &t);
+  return ret? g_markup_escape_text(ret, -1): NULL;
+}
+
+
+static const char *status_text(GaimBuddy *b) {
+  GaimPresence *presence;
+  GaimStatus *status;
+
+  presence = gaim_buddy_get_presence(b);
+  status = gaim_presence_get_active_status(presence);
+
+  return gaim_status_get_name(status);
+}
+
+
+static gboolean user_supports(struct mwServiceAware *srvc,
+			      const char *who, guint32 feature) {
+
+  const struct mwAwareAttribute *attr;
+  struct mwAwareIdBlock idb = { mwAware_USER, (char *) who, NULL };
+
+  attr = mwServiceAware_getAttribute(srvc, &idb, feature);
+  return (attr != NULL) && mwAwareAttribute_asBoolean(attr);
+}
+
+
+static char *user_supports_text(struct mwServiceAware *srvc, const char *who) {
+  const char *feat[] = {NULL, NULL, NULL, NULL, NULL};
+  const char **f = feat;
+  
+  if(user_supports(srvc, who, mwAttribute_AV_PREFS_SET)) {
+    gboolean mic, speak, video;
+    
+    mic = user_supports(srvc, who, mwAttribute_MICROPHONE);
+    speak = user_supports(srvc, who, mwAttribute_SPEAKERS);
+    video = user_supports(srvc, who, mwAttribute_VIDEO_CAMERA);
+    
+    if(mic) *f++ = _("Microphone");
+    if(speak) *f++ = _("Speakers");
+    if(video) *f++ = _("Video Camera");
+  }
+  
+  if(user_supports(srvc, who, mwAttribute_FILE_TRANSFER))
+    *f++ = _("File Transfer");
+  
+  return (*feat)? g_strjoinv(", ", (char **)feat): NULL;
+  /* jenni loves siege */
+}
+
+
+static void mw_prpl_tooltip_text(GaimBuddy *b, GaimNotifyUserInfo *user_info, gboolean full) {
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+  struct mwAwareIdBlock idb = { mwAware_USER, b->name, NULL };
+
+  const char *message;
+  const char *status;
+  char *tmp;
+
+  gc = b->account->gc;
+  pd = gc->proto_data;
+
+  message = mwServiceAware_getText(pd->srvc_aware, &idb);
+  status = status_text(b);
+
+  if(message != NULL && gaim_utf8_strcasecmp(status, message)) {
+    tmp = g_markup_escape_text(message, -1);
+	gaim_notify_user_info_add_pair(user_info, status, tmp);
+    g_free(tmp);
+
+  } else {
+	gaim_notify_user_info_add_pair(user_info, _("Status"), status);
+  }
+
+  if(full) {
+    tmp = user_supports_text(pd->srvc_aware, b->name);
+    if(tmp) {
+	  gaim_notify_user_info_add_pair(user_info, _("Supports"), tmp);
+      g_free(tmp);
+    }
+
+    if(buddy_is_external(b)) {
+	  gaim_notify_user_info_add_pair(user_info, NULL, _("External User"));
+    }
+  }
+}
+
+
+static GList *mw_prpl_status_types(GaimAccount *acct) {
+  GList *types = NULL;
+  GaimStatusType *type;
+
+  type = gaim_status_type_new(GAIM_STATUS_AVAILABLE, MW_STATE_ACTIVE,
+			      NULL, TRUE);
+  gaim_status_type_add_attr(type, MW_STATE_MESSAGE, _("Message"),
+			    gaim_value_new(GAIM_TYPE_STRING));
+  types = g_list_append(types, type);
+
+  type = gaim_status_type_new(GAIM_STATUS_AWAY, MW_STATE_AWAY,
+			      NULL, TRUE);
+  gaim_status_type_add_attr(type, MW_STATE_MESSAGE, _("Message"),
+			    gaim_value_new(GAIM_TYPE_STRING));
+  types = g_list_append(types, type);
+  
+  type = gaim_status_type_new(GAIM_STATUS_UNAVAILABLE, MW_STATE_BUSY,
+			      _("Do Not Disturb"), TRUE);
+  gaim_status_type_add_attr(type, MW_STATE_MESSAGE, _("Message"),
+			    gaim_value_new(GAIM_TYPE_STRING));
+  types = g_list_append(types, type);
+  
+  type = gaim_status_type_new(GAIM_STATUS_OFFLINE, MW_STATE_OFFLINE,
+			      NULL, TRUE);
+  types = g_list_append(types, type);
+
+  return types;
+}
+
+
+static void conf_create_prompt_cancel(GaimBuddy *buddy,
+				      GaimRequestFields *fields) {
+  ; /* nothing to do */
+}
+
+
+static void conf_create_prompt_join(GaimBuddy *buddy,
+				    GaimRequestFields *fields) {
+  GaimAccount *acct;
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+  struct mwServiceConference *srvc;
+
+  GaimRequestField *f;
+
+  const char *topic, *invite;
+  struct mwConference *conf;
+  struct mwIdBlock idb = { NULL, NULL };
+
+  acct = buddy->account;
+  gc = gaim_account_get_connection(acct);
+  pd = gc->proto_data;
+  srvc = pd->srvc_conf;
+
+  f = gaim_request_fields_get_field(fields, CHAT_KEY_TOPIC);
+  topic = gaim_request_field_string_get_value(f);
+
+  f = gaim_request_fields_get_field(fields, CHAT_KEY_INVITE);
+  invite = gaim_request_field_string_get_value(f);
+
+  conf = mwConference_new(srvc, topic);
+  mwConference_open(conf);
+
+  idb.user = buddy->name;
+  mwConference_invite(conf, &idb, invite);
+}
+
+
+static void blist_menu_conf_create(GaimBuddy *buddy, const char *msg) {
+
+  GaimRequestFields *fields;
+  GaimRequestFieldGroup *g;
+  GaimRequestField *f;
+
+  GaimAccount *acct;
+  GaimConnection *gc;
+
+  const char *msgA;
+  const char *msgB;
+  char *msg1;
+  
+  g_return_if_fail(buddy != NULL);
+
+  acct = buddy->account;
+  g_return_if_fail(acct != NULL);
+
+  gc = gaim_account_get_connection(acct);
+  g_return_if_fail(gc != NULL);
+  
+  fields = gaim_request_fields_new();
+
+  g = gaim_request_field_group_new(NULL);
+  gaim_request_fields_add_group(fields, g);
+  
+  f = gaim_request_field_string_new(CHAT_KEY_TOPIC, _("Topic"), NULL, FALSE);
+  gaim_request_field_group_add_field(g, f);
+
+  f = gaim_request_field_string_new(CHAT_KEY_INVITE, _("Message"), msg, FALSE);
+  gaim_request_field_group_add_field(g, f);
+  
+  msgA = _("Create conference with user");
+  msgB = _("Please enter a topic for the new conference, and an invitation"
+	   " message to be sent to %s");
+  msg1 = g_strdup_printf(msgB, buddy->name);
+
+  gaim_request_fields(gc, _("New Conference"),
+		      msgA, msg1, fields,
+		      _("Create"), G_CALLBACK(conf_create_prompt_join),
+		      _("Cancel"), G_CALLBACK(conf_create_prompt_cancel),
+		      buddy);
+  g_free(msg1);
+}
+
+
+static void conf_select_prompt_cancel(GaimBuddy *buddy,
+				      GaimRequestFields *fields) {
+  ;
+}
+
+
+static void conf_select_prompt_invite(GaimBuddy *buddy,
+				      GaimRequestFields *fields) {
+  GaimRequestField *f;
+  const GList *l;
+  const char *msg;
+  
+  f = gaim_request_fields_get_field(fields, CHAT_KEY_INVITE);
+  msg = gaim_request_field_string_get_value(f);
+
+  f = gaim_request_fields_get_field(fields, "conf");
+  l = gaim_request_field_list_get_selected(f);
+
+  if(l) {
+    gpointer d = gaim_request_field_list_get_data(f, l->data);
+    
+    if(GPOINTER_TO_INT(d) == 0x01) {
+      blist_menu_conf_create(buddy, msg);
+
+    } else {
+      struct mwIdBlock idb = { buddy->name, NULL };
+      mwConference_invite(d, &idb, msg);
+    }
+  }
+}
+
+
+static void blist_menu_conf_list(GaimBuddy *buddy,
+				 GList *confs) {
+  
+  GaimRequestFields *fields;
+  GaimRequestFieldGroup *g;
+  GaimRequestField *f;
+
+  GaimAccount *acct;
+  GaimConnection *gc;
+
+  const char *msgA;
+  const char *msgB;
+  char *msg;
+
+  acct = buddy->account;
+  g_return_if_fail(acct != NULL);
+
+  gc = gaim_account_get_connection(acct);
+  g_return_if_fail(gc != NULL);
+
+  fields = gaim_request_fields_new();
+  
+  g = gaim_request_field_group_new(NULL);
+  gaim_request_fields_add_group(fields, g);
+
+  f = gaim_request_field_list_new("conf", _("Available Conferences"));
+  gaim_request_field_list_set_multi_select(f, FALSE);
+  for(; confs; confs = confs->next) {
+    struct mwConference *c = confs->data;
+    gaim_request_field_list_add(f, mwConference_getTitle(c), c);
+  }
+  gaim_request_field_list_add(f, _("Create New Conference..."),
+			      GINT_TO_POINTER(0x01));
+  gaim_request_field_group_add_field(g, f);
+  
+  f = gaim_request_field_string_new(CHAT_KEY_INVITE, "Message", NULL, FALSE);
+  gaim_request_field_group_add_field(g, f);
+  
+  msgA = _("Invite user to a conference");
+  msgB = _("Select a conference from the list below to send an invite to"
+	   " user %s. Select \"Create New Conference\" if you'd like to"
+	   " create a new conference to invite this user to.");
+  msg = g_strdup_printf(msgB, buddy->name);
+
+  gaim_request_fields(gc, _("Invite to Conference"),
+		      msgA, msg, fields,
+		      _("Invite"), G_CALLBACK(conf_select_prompt_invite),
+		      _("Cancel"), G_CALLBACK(conf_select_prompt_cancel),
+		      buddy);
+  g_free(msg);
+}
+
+
+static void blist_menu_conf(GaimBlistNode *node, gpointer data) {
+  GaimBuddy *buddy = (GaimBuddy *) node;
+  GaimAccount *acct;
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+  GList *l;
+
+  g_return_if_fail(node != NULL);
+  g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));
+
+  acct = buddy->account;
+  g_return_if_fail(acct != NULL);
+
+  gc = gaim_account_get_connection(acct);
+  g_return_if_fail(gc != NULL);
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  /*
+    - get a list of all conferences on this session
+    - if none, prompt to create one, and invite buddy to it
+    - else, prompt to select a conference or create one
+  */
+
+  l = mwServiceConference_getConferences(pd->srvc_conf);
+  if(l) {
+    blist_menu_conf_list(buddy, l);
+    g_list_free(l);
+
+  } else {
+    blist_menu_conf_create(buddy, NULL);
+  }
+}
+
+
+#if 0
+static void blist_menu_announce(GaimBlistNode *node, gpointer data) {
+  GaimBuddy *buddy = (GaimBuddy *) node;
+  GaimAccount *acct;
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+  struct mwSession *session;
+  char *rcpt_name;
+  GList *rcpt;
+
+  g_return_if_fail(node != NULL);
+  g_return_if_fail(GAIM_BLIST_NODE_IS_BUDDY(node));
+
+  acct = buddy->account;
+  g_return_if_fail(acct != NULL);
+
+  gc = gaim_account_get_connection(acct);
+  g_return_if_fail(gc != NULL);
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  rcpt_name = g_strdup_printf("@U %s", buddy->name);
+  rcpt = g_list_prepend(NULL, rcpt_name);
+
+  session = pd->session;
+  mwSession_sendAnnounce(session, FALSE,
+			 "This is a TEST announcement. Please ignore.",
+			 rcpt);
+
+  g_list_free(rcpt);
+  g_free(rcpt_name);
+}
+#endif
+
+
+static GList *mw_prpl_blist_node_menu(GaimBlistNode *node) {
+  GList *l = NULL;
+  GaimMenuAction *act;
+
+  if(! GAIM_BLIST_NODE_IS_BUDDY(node))
+    return l;
+
+  l = g_list_append(l, NULL);
+
+  act = gaim_menu_action_new(_("Invite to Conference..."),
+                             GAIM_CALLBACK(blist_menu_conf), NULL, NULL);
+  l = g_list_append(l, act);
+
+#if 0
+  act = gaim_menu_action_new(_("Send TEST Announcement"),
+			     GAIM_CALLBACK(blist_menu_announce), NULL, NULL);
+  l = g_list_append(l, act);
+#endif
+
+  /** note: this never gets called for a GaimGroup, have to use the
+      blist-node-extended-menu signal for that. The function
+      blist_node_menu_cb is assigned to this signal in the function
+      services_starting */
+
+  return l;
+}
+
+
+static GList *mw_prpl_chat_info(GaimConnection *gc) {
+  GList *l = NULL;
+  struct proto_chat_entry *pce;
+  
+  pce = g_new0(struct proto_chat_entry, 1);
+  pce->label = _("Topic:");
+  pce->identifier = CHAT_KEY_TOPIC;
+  l = g_list_append(l, pce);
+  
+  return l;
+}
+
+
+static GHashTable *mw_prpl_chat_info_defaults(GaimConnection *gc,
+					      const char *name) {
+  GHashTable *table;
+
+  g_return_val_if_fail(gc != NULL, NULL);
+
+  table = g_hash_table_new_full(g_str_hash, g_str_equal,
+				NULL, g_free);
+
+  g_hash_table_insert(table, CHAT_KEY_NAME, g_strdup(name));
+  g_hash_table_insert(table, CHAT_KEY_INVITE, NULL);
+
+  return table;
+}
+
+
+static void mw_prpl_login(GaimAccount *acct);
+
+
+static void prompt_host_cancel_cb(GaimConnection *gc) {
+  gaim_connection_error(gc, _("No Sametime Community Server specified"));
+}
+
+
+static void prompt_host_ok_cb(GaimConnection *gc, const char *host) {
+  if(host && *host) {
+    GaimAccount *acct = gaim_connection_get_account(gc);
+    gaim_account_set_string(acct, MW_KEY_HOST, host);
+    mw_prpl_login(acct);
+
+  } else {
+    prompt_host_cancel_cb(gc);
+  }
+}
+
+
+static void prompt_host(GaimConnection *gc) {
+  GaimAccount *acct;
+  const char *msgA;
+  char *msg;
+  
+  acct = gaim_connection_get_account(gc);
+  msgA = _("No host or IP address has been configured for the"
+	  " Meanwhile account %s. Please enter one below to"
+	  " continue logging in.");
+  msg = g_strdup_printf(msgA, NSTR(gaim_account_get_username(acct)));
+  
+  gaim_request_input(gc, _("Meanwhile Connection Setup"),
+		     _("No Sametime Community Server Specified"), msg,
+		     MW_PLUGIN_DEFAULT_HOST, FALSE, FALSE, NULL,
+		     _("Connect"), G_CALLBACK(prompt_host_ok_cb),
+		     _("Cancel"), G_CALLBACK(prompt_host_cancel_cb),
+		     gc);
+
+  g_free(msg);
+}
+
+
+static void mw_prpl_login(GaimAccount *account) {
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+
+  char *user, *pass, *host;
+  guint port;
+
+  gc = gaim_account_get_connection(account);
+  pd = mwGaimPluginData_new(gc);
+
+  /* while we do support images, the default is to not offer it */
+  gc->flags |= GAIM_CONNECTION_NO_IMAGES;
+
+  user = g_strdup(gaim_account_get_username(account));
+  pass = g_strdup(gaim_account_get_password(account));
+
+  host = strrchr(user, ':');
+  if(host) {
+    /* annoying user split from 1.2.0, need to undo it */
+    *host++ = '\0';
+    gaim_account_set_string(account, MW_KEY_HOST, host);
+    gaim_account_set_username(account, user);
+    
+  } else {
+    host = (char *) gaim_account_get_string(account, MW_KEY_HOST,
+					    MW_PLUGIN_DEFAULT_HOST);
+  }
+
+  if(! host || ! *host) {
+    /* somehow, we don't have a host to connect to. Well, we need one
+       to actually continue, so let's ask the user directly. */
+    prompt_host(gc);
+    return;
+  }
+
+  port = gaim_account_get_int(account, MW_KEY_PORT, MW_PLUGIN_DEFAULT_PORT);
+
+  DEBUG_INFO("user: '%s'\n", user);
+  DEBUG_INFO("host: '%s'\n", host);
+  DEBUG_INFO("port: %u\n", port);
+
+  mwSession_setProperty(pd->session, mwSession_NO_SECRET,
+			(char *) no_secret, NULL);
+  mwSession_setProperty(pd->session, mwSession_AUTH_USER_ID, user, g_free);
+  mwSession_setProperty(pd->session, mwSession_AUTH_PASSWORD, pass, g_free);
+
+  if(gaim_account_get_bool(account, MW_KEY_FAKE_IT, FALSE)) {
+    guint client, major, minor;
+
+    /* if we're faking the login, let's also fake the version we're
+       reporting. Let's also allow the actual values to be specified */
+
+    client = gaim_account_get_int(account, MW_KEY_CLIENT, mwLogin_BINARY);
+    major = gaim_account_get_int(account, MW_KEY_MAJOR, 0x001e);
+    minor = gaim_account_get_int(account, MW_KEY_MINOR, 0x001d);
+
+    DEBUG_INFO("client id: 0x%04x\n", client);
+    DEBUG_INFO("client major: 0x%04x\n", major);
+    DEBUG_INFO("client minor: 0x%04x\n", minor);
+  
+    mwSession_setProperty(pd->session, mwSession_CLIENT_TYPE_ID,
+			  GUINT_TO_POINTER(client), NULL);
+    
+    mwSession_setProperty(pd->session, mwSession_CLIENT_VER_MAJOR,
+			  GUINT_TO_POINTER(major), NULL);
+
+    mwSession_setProperty(pd->session, mwSession_CLIENT_VER_MINOR,
+			  GUINT_TO_POINTER(minor), NULL);
+  }
+
+  gaim_connection_update_progress(gc, _("Connecting"), 1, MW_CONNECT_STEPS);
+
+  if (gaim_proxy_connect(gc, account, host, port, connect_cb, pd) == NULL) {
+    gaim_connection_error(gc, _("Unable to connect to host"));
+  }
+}
+
+
+static void mw_prpl_close(GaimConnection *gc) {
+  struct mwGaimPluginData *pd;
+
+  g_return_if_fail(gc != NULL);
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  /* get rid of the blist save timeout */
+  if(pd->save_event) {
+    gaim_timeout_remove(pd->save_event);
+    pd->save_event = 0;
+    blist_store(pd);
+  }
+
+  /* stop the session */
+  mwSession_stop(pd->session, 0x00);
+
+  /* no longer necessary */
+  gc->proto_data = NULL;
+
+  /* stop watching the socket */
+  if(gc->inpa) {
+    gaim_input_remove(gc->inpa);
+    gc->inpa = 0;
+  }
+
+  /* clean up the rest */
+  mwGaimPluginData_free(pd);
+}
+
+
+static int mw_rand() {
+  static int seed = 0;
+
+  /* for diversity, not security. don't touch */
+  srand(time(NULL) ^ seed);
+  seed = rand();
+
+  return seed;
+}
+
+
+/** generates a random-ish content id string */
+static char *im_mime_content_id() {
+  return g_strdup_printf("%03x@%05xmeanwhile",
+			 mw_rand() & 0xfff, mw_rand() & 0xfffff);
+}
+
+
+/** generates a multipart/related content type with a random-ish
+    boundary value */
+static char *im_mime_content_type() {
+  return g_strdup_printf("multipart/related; boundary=related_MW%03x_%04x",
+                         mw_rand() & 0xfff, mw_rand() & 0xffff);
+}
+
+
+/** determine content type from extension. Not so happy about this,
+    but I don't want to actually write image type detection */
+static char *im_mime_img_content_type(GaimStoredImage *img) {
+  const char *fn = gaim_imgstore_get_filename(img);
+  const char *ct = NULL;
+
+  ct = strrchr(fn, '.');
+  if(! ct) {
+    ct = "image";
+
+  } else if(! strcmp(".png", ct)) {
+    ct = "image/png";
+
+  } else if(! strcmp(".jpg", ct)) {
+    ct = "image/jpeg";
+
+  } else if(! strcmp(".jpeg", ct)) {
+    ct = "image/jpeg";
+
+  } else if(! strcmp(".gif", ct)) {
+    ct = "image/gif";
+
+  } else {
+    ct = "image";
+  }
+
+  return g_strdup_printf("%s; name=\"%s\"", ct, fn);
+}
+
+
+static char *im_mime_img_content_disp(GaimStoredImage *img) {
+  const char *fn = gaim_imgstore_get_filename(img);
+  return g_strdup_printf("attachment; filename=\"%s\"", fn);
+}
+
+
+/** turn an IM with embedded images into a multi-part mime document */
+static char *im_mime_convert(GaimConnection *gc,
+			     struct mwConversation *conv,
+			     const char *message) {
+  GString *str;
+  GaimMimeDocument *doc;
+  GaimMimePart *part;
+
+  GData *attr;
+  char *tmp, *start, *end;
+
+  str = g_string_new(NULL);
+
+  doc = gaim_mime_document_new();
+
+  gaim_mime_document_set_field(doc, "Mime-Version", "1.0");
+  gaim_mime_document_set_field(doc, "Content-Disposition", "inline");
+
+  tmp = im_mime_content_type();
+  gaim_mime_document_set_field(doc, "Content-Type", tmp);
+  g_free(tmp);
+
+  tmp = (char *) message;
+  while(*tmp && gaim_markup_find_tag("img", tmp, (const char **) &start,
+				     (const char **) &end, &attr)) {
+    char *id;
+    GaimStoredImage *img = NULL;
+    
+    gsize len = (start - tmp);
+
+    /* append the in-between-tags text */
+    if(len) g_string_append_len(str, tmp, len);
+
+    /* find the imgstore data by the id tag */
+    id = g_datalist_get_data(&attr, "id");
+    if(id && *id)
+      img = gaim_imgstore_get(atoi(id));
+
+    if(img) {
+      char *cid;
+      gpointer data;
+      size_t size;
+
+      part = gaim_mime_part_new(doc);
+
+      data = im_mime_img_content_disp(img);
+      gaim_mime_part_set_field(part, "Content-Disposition", data);
+      g_free(data);
+
+      data = im_mime_img_content_type(img);
+      gaim_mime_part_set_field(part, "Content-Type", data);
+      g_free(data);
+
+      cid = im_mime_content_id();
+      data = g_strdup_printf("<%s>", cid);
+      gaim_mime_part_set_field(part, "Content-ID", data);
+      g_free(data);
+
+      gaim_mime_part_set_field(part, "Content-transfer-encoding", "base64");
+
+      /* obtain and base64 encode the image data, and put it in the
+	 mime part */
+      data = gaim_imgstore_get_data(img);
+      size = gaim_imgstore_get_size(img);
+      data = gaim_base64_encode(data, (gsize) size);
+      gaim_mime_part_set_data(part, data);
+      g_free(data);
+
+      /* append the modified tag */
+      g_string_append_printf(str, "<img src=\"cid:%s\">", cid);
+      g_free(cid);
+      
+    } else {
+      /* append the literal image tag, since we couldn't find a
+	 relative imgstore object */
+      gsize len = (end - start) + 1;
+      g_string_append_len(str, start, len);
+    }
+
+    g_datalist_clear(&attr);
+    tmp = end + 1;
+  }
+
+  /* append left-overs */
+  g_string_append(str, tmp);
+
+  /* add the text/html part */
+  part = gaim_mime_part_new(doc);
+  gaim_mime_part_set_field(part, "Content-Disposition", "inline");
+
+  tmp = gaim_utf8_ncr_encode(str->str);
+  gaim_mime_part_set_field(part, "Content-Type", "text/html");
+  gaim_mime_part_set_field(part, "Content-Transfer-Encoding", "7bit");
+  gaim_mime_part_set_data(part, tmp);
+  g_free(tmp);
+
+  g_string_free(str, TRUE);
+
+  str = g_string_new(NULL);
+  gaim_mime_document_write(doc, str);
+  tmp = str->str;
+  g_string_free(str, FALSE);
+
+  return tmp;
+}
+
+
+static int mw_prpl_send_im(GaimConnection *gc,
+			   const char *name,
+			   const char *message,
+			   GaimMessageFlags flags) {
+
+  struct mwGaimPluginData *pd;
+  struct mwIdBlock who = { (char *) name, NULL };
+  struct mwConversation *conv;
+
+  g_return_val_if_fail(gc != NULL, 0);
+  pd = gc->proto_data;
+
+  g_return_val_if_fail(pd != NULL, 0);
+
+  conv = mwServiceIm_getConversation(pd->srvc_im, &who);
+
+  /* this detection of features to determine how to send the message
+     (plain, html, or mime) is flawed because the other end of the
+     conversation could close their channel at any time, rendering any
+     existing formatting in an outgoing message innapropriate. The end
+     result is that it may be possible that the other side of the
+     conversation will receive a plaintext message with html contents,
+     which is bad. I'm not sure how to fix this correctly. */
+
+  if(strstr(message, "<img ") || strstr(message, "<IMG "))
+    flags |= GAIM_MESSAGE_IMAGES;
+
+  if(mwConversation_isOpen(conv)) {
+    char *tmp;
+    int ret;
+
+    if((flags & GAIM_MESSAGE_IMAGES) &&
+       mwConversation_supports(conv, mwImSend_MIME)) {
+      /* send a MIME message */
+
+      tmp = im_mime_convert(gc, conv, message);
+      ret = mwConversation_send(conv, mwImSend_MIME, tmp);
+      g_free(tmp);
+      
+    } else if(mwConversation_supports(conv, mwImSend_HTML)) {
+      /* send an HTML message */
+
+      char *ncr;
+      ncr = gaim_utf8_ncr_encode(message);
+      tmp = gaim_strdup_withhtml(ncr);
+      g_free(ncr);
+
+      ret = mwConversation_send(conv, mwImSend_HTML, tmp);
+      g_free(tmp);
+
+    } else {
+      /* default to text */
+      tmp = gaim_markup_strip_html(message);
+      ret = mwConversation_send(conv, mwImSend_PLAIN, tmp);
+      g_free(tmp);
+    }
+    
+    return !ret;
+
+  } else {
+
+    /* queue up the message safely as plain text */
+    char *tmp = gaim_markup_strip_html(message);
+    convo_queue(conv, mwImSend_PLAIN, tmp);
+    g_free(tmp);
+
+    if(! mwConversation_isPending(conv))
+      mwConversation_open(conv);
+
+    return 1;
+  }
+}
+
+
+static unsigned int mw_prpl_send_typing(GaimConnection *gc,
+					const char *name,
+					GaimTypingState state) {
+  
+  struct mwGaimPluginData *pd;
+  struct mwIdBlock who = { (char *) name, NULL };
+  struct mwConversation *conv;
+
+  gpointer t = GINT_TO_POINTER(!! state);
+
+  g_return_val_if_fail(gc != NULL, 0);
+  pd = gc->proto_data;
+
+  g_return_val_if_fail(pd != NULL, 0);
+
+  conv = mwServiceIm_getConversation(pd->srvc_im, &who);
+
+  if(mwConversation_isOpen(conv)) {
+    mwConversation_send(conv, mwImSend_TYPING, t);
+    
+  } else if((state == GAIM_TYPING) || (state == GAIM_TYPED)) {
+    /* only open a channel for sending typing notification, not for
+       when typing has stopped. There's no point in re-opening a
+       channel just to tell someone that this side isn't typing. */
+    
+    convo_queue(conv, mwImSend_TYPING, t);
+    
+    if(! mwConversation_isPending(conv)) {
+      mwConversation_open(conv);
+    }
+  }
+
+  return 0;
+}
+
+
+static const char *mw_client_name(guint16 type) {
+  switch(type) {
+  case mwLogin_LIB:
+    return "Lotus Binary Library";
+    
+  case mwLogin_JAVA_WEB:
+    return "Lotus Java Client Applet";
+
+  case mwLogin_BINARY:
+    return "Lotus Sametime Connect";
+
+  case mwLogin_JAVA_APP:
+    return "Lotus Java Client Application";
+
+  case mwLogin_LINKS:
+    return "Lotus Sametime Links";
+
+  case mwLogin_NOTES_6_5:
+  case mwLogin_NOTES_6_5_3:
+  case mwLogin_NOTES_7_0_beta:
+  case mwLogin_NOTES_7_0:
+    return "Lotus Notes Client";
+
+  case mwLogin_ICT:
+  case mwLogin_ICT_1_7_8_2:
+  case mwLogin_ICT_SIP:
+    return "IBM Community Tools";
+
+  case mwLogin_NOTESBUDDY_4_14:
+  case mwLogin_NOTESBUDDY_4_15:
+  case mwLogin_NOTESBUDDY_4_16:
+    return "Alphaworks NotesBuddy";
+
+  case 0x1305:
+  case 0x1306:
+  case 0x1307:
+    return "Lotus Sametime Connect 7.5";
+
+  case mwLogin_SANITY:
+    return "Sanity";
+
+  case mwLogin_ST_PERL:
+    return "ST-Send-Message";
+
+  case mwLogin_TRILLIAN:
+  case mwLogin_TRILLIAN_IBM:
+    return "Trillian";
+
+  case mwLogin_MEANWHILE:
+    return "Meanwhile";
+
+  default:
+    return NULL;
+  }
+}
+
+
+static void mw_prpl_get_info(GaimConnection *gc, const char *who) {
+
+  struct mwAwareIdBlock idb = { mwAware_USER, (char *) who, NULL };
+
+  struct mwGaimPluginData *pd;
+  GaimAccount *acct;
+  GaimBuddy *b;
+  GaimNotifyUserInfo *user_info;
+  char *tmp;
+  const char *tmp2;
+
+  g_return_if_fail(who != NULL);
+  g_return_if_fail(*who != '\0');
+
+  pd = gc->proto_data;
+
+  acct = gaim_connection_get_account(gc);
+  b = gaim_find_buddy(acct, who);
+  user_info = gaim_notify_user_info_new();
+
+  if(gaim_str_has_prefix(who, "@E ")) {
+	gaim_notify_user_info_add_pair(user_info, _("External User"), NULL);
+  }
+
+  gaim_notify_user_info_add_pair(user_info, _("User ID"), who);
+
+  if(b) {
+    guint32 type;
+
+    if(b->server_alias) {
+		gaim_notify_user_info_add_pair(user_info, _("Full Name"), b->server_alias);
+    }
+
+    type = gaim_blist_node_get_int((GaimBlistNode *) b, BUDDY_KEY_CLIENT);
+    if(type) {
+	  tmp = g_strdup(mw_client_name(type));
+	  if (!tmp)
+		tmp = g_strdup_printf(_("Unknown (0x%04x)<br>"), type);
+
+	  gaim_notify_user_info_add_pair(user_info, _("Last Known Client"), tmp);
+		
+	  g_free(tmp);
+    }
+  }
+  
+  tmp = user_supports_text(pd->srvc_aware, who);
+  if(tmp) {
+	gaim_notify_user_info_add_pair(user_info, _("Supports"), tmp);
+	g_free(tmp);
+  }
+
+  if(b) {
+	gaim_notify_user_info_add_pair(user_info, _("Status"), status_text(b));
+
+	/* XXX Is this adding a status message in its own section rather than with the "Status" label? */
+    tmp2 = mwServiceAware_getText(pd->srvc_aware, &idb);
+    if(tmp2) {
+      tmp = g_markup_escape_text(tmp2, -1);
+	  gaim_notify_user_info_add_section_break(user_info);
+	  gaim_notify_user_info_add_pair(user_info, NULL, tmp);
+      g_free(tmp);
+    }
+  }
+
+  /* @todo emit a signal to allow a plugin to override the display of
+     this notification, so that it can create its own */
+
+  gaim_notify_userinfo(gc, who, user_info, NULL, NULL);
+  gaim_notify_user_info_destroy(user_info);
+}
+ 
+ 
+static void mw_prpl_set_status(GaimAccount *acct, GaimStatus *status) {
+  GaimConnection *gc;
+  const char *state;
+  char *message = NULL;
+  struct mwSession *session;
+  struct mwUserStatus stat;
+  
+  g_return_if_fail(acct != NULL);
+  gc = gaim_account_get_connection(acct);
+  
+  state = gaim_status_get_id(status);
+  
+  DEBUG_INFO("Set status to %s\n", gaim_status_get_name(status));
+  
+  g_return_if_fail(gc != NULL);
+  
+  session = gc_to_session(gc);
+  g_return_if_fail(session != NULL);
+  
+  /* get a working copy of the current status */
+  mwUserStatus_clone(&stat, mwSession_getUserStatus(session));
+  
+  /* determine the state */
+  if(! strcmp(state, MW_STATE_ACTIVE)) {
+    stat.status = mwStatus_ACTIVE;
+    
+  } else if(! strcmp(state, MW_STATE_AWAY)) {
+    stat.status = mwStatus_AWAY;
+    
+  } else if(! strcmp(state, MW_STATE_BUSY)) {
+    stat.status = mwStatus_BUSY;
+  }
+  
+  /* determine the message */
+  message = (char *) gaim_status_get_attr_string(status, MW_STATE_MESSAGE);
+  
+  if(message) {
+    /* all the possible non-NULL values of message up to this point
+       are const, so we don't need to free them */
+    message = gaim_markup_strip_html(message);
+  }
+  
+  /* out with the old */
+  g_free(stat.desc);
+  
+  /* in with the new */
+  stat.desc = (char *) message;
+  
+  mwSession_setUserStatus(session, &stat);
+  mwUserStatus_clear(&stat);
+}
+
+ 
+static void mw_prpl_set_idle(GaimConnection *gc, int t) {
+  struct mwSession *session;
+  struct mwUserStatus stat;
+ 
+
+  session = gc_to_session(gc);
+  g_return_if_fail(session != NULL);
+
+  mwUserStatus_clone(&stat, mwSession_getUserStatus(session));
+
+  if(t) {
+    time_t now = time(NULL);
+    stat.time = now - t;
+
+  } else {
+    stat.time = 0;
+  }
+
+  if(t > 0 && stat.status == mwStatus_ACTIVE) {
+    /* we were active and went idle, so change the status to IDLE. */
+    stat.status = mwStatus_IDLE;
+
+  } else if(t == 0 && stat.status == mwStatus_IDLE) {
+    /* we only become idle automatically, so change back to ACTIVE */
+    stat.status = mwStatus_ACTIVE;
+  }
+
+  mwSession_setUserStatus(session, &stat);
+  mwUserStatus_clear(&stat);
+}
+
+
+static void notify_im(GaimConnection *gc, GList *row, void *user_data) {
+  GaimAccount *acct;
+  GaimConversation *conv;
+  char *id;
+
+  acct = gaim_connection_get_account(gc);
+  id = g_list_nth_data(row, 1);
+  conv = gaim_find_conversation_with_account(GAIM_CONV_TYPE_IM, id, acct);
+  if(! conv) conv = gaim_conversation_new(GAIM_CONV_TYPE_IM, acct, id);
+  gaim_conversation_present(conv);
+}
+
+
+static void notify_add(GaimConnection *gc, GList *row, void *user_data) {
+  BuddyAddData *data = user_data;
+  char *group_name = NULL;
+  
+  if (data && data->group) {
+    group_name = data->group->name;
+  }
+
+  gaim_blist_request_add_buddy(gaim_connection_get_account(gc),
+			       g_list_nth_data(row, 1), group_name,
+			       g_list_nth_data(row, 0));
+}
+
+
+static void notify_close(gpointer data) {
+  if (data) {
+    g_free(data);
+  }
+}
+
+
+static void multi_resolved_query(struct mwResolveResult *result,
+				 GaimConnection *gc, gpointer data) {
+  GList *l;
+  const char *msgA;
+  const char *msgB;
+  char *msg;
+
+  GaimNotifySearchResults *sres;
+  GaimNotifySearchColumn *scol;
+
+  sres = gaim_notify_searchresults_new();
+
+  scol = gaim_notify_searchresults_column_new(_("User Name"));
+  gaim_notify_searchresults_column_add(sres, scol);
+
+  scol = gaim_notify_searchresults_column_new(_("Sametime ID"));
+  gaim_notify_searchresults_column_add(sres, scol);
+
+  gaim_notify_searchresults_button_add(sres, GAIM_NOTIFY_BUTTON_IM,
+				       notify_im);
+
+  gaim_notify_searchresults_button_add(sres, GAIM_NOTIFY_BUTTON_ADD,
+				       notify_add);
+
+  for(l = result->matches; l; l = l->next) {
+    struct mwResolveMatch *match = l->data;
+    GList *row = NULL;
+        
+    DEBUG_INFO("multi resolve: %s, %s\n",
+	       NSTR(match->id), NSTR(match->name));
+
+    if(!match->id || !match->name)
+      continue;
+    
+    row = g_list_append(row, g_strdup(match->name));
+    row = g_list_append(row, g_strdup(match->id));
+    gaim_notify_searchresults_row_add(sres, row);
+  }
+
+  msgA = _("An ambiguous user ID was entered");
+  msgB = _("The identifier '%s' may possibly refer to any of the following"
+	   " users. Please select the correct user from the list below to"
+	   " add them to your buddy list.");
+  msg = g_strdup_printf(msgB, result->name);
+
+  gaim_notify_searchresults(gc, _("Select User"),
+			    msgA, msg, sres, notify_close, data);
+
+  g_free(msg);
+}
+
+
+static void add_buddy_resolved(struct mwServiceResolve *srvc,
+			       guint32 id, guint32 code, GList *results,
+			       gpointer b) {
+
+  struct mwResolveResult *res = NULL;
+  BuddyAddData *data = b;
+  GaimBuddy *buddy = NULL;
+  GaimConnection *gc;
+  struct mwGaimPluginData *pd;
+
+  if (data) {
+    buddy = data->buddy;
+  }
+
+  gc = gaim_account_get_connection(buddy->account);
+  pd = gc->proto_data;
+
+  if(results)
+    res = results->data;
+
+  if(!code && res && res->matches) {
+    if(g_list_length(res->matches) == 1) {
+      struct mwResolveMatch *match = res->matches->data;
+      
+      /* only one? that might be the right one! */
+      if(strcmp(res->name, match->id)) {
+	/* uh oh, the single result isn't identical to the search
+	   term, better safe then sorry, so let's make sure it's who
+	   the user meant to add */
+	gaim_blist_remove_buddy(buddy);
+	multi_resolved_query(res, gc, data);
+	
+      } else {
+
+	/* same person, set the server alias */
+	gaim_blist_server_alias_buddy(buddy, match->name);
+	gaim_blist_node_set_string((GaimBlistNode *) buddy,
+				   BUDDY_KEY_NAME, match->name);
+
+	/* subscribe to awareness */
+	buddy_add(pd, buddy);
+
+	blist_schedule(pd);
+
+        g_free(data);
+      }
+      
+    } else {
+      /* prompt user if more than one match was returned */
+      gaim_blist_remove_buddy(buddy);
+      multi_resolved_query(res, gc, data);
+    }
+    
+    return;
+  }
+
+#if 0
+  /* fall-through indicates that we couldn't find a matching user in
+     the resolve service (ether error or zero results), so we remove
+     this buddy */
+
+  /* note: I can't really think of a good reason to alter the buddy
+     list in any way. There has been at least one report where the
+     resolve service isn't returning correct results anyway, so let's
+     just leave them in the list. I'm just going to if0 this section
+     out unless I can think of a very good reason to do this. -siege */
+
+  DEBUG_INFO("no such buddy in community\n");
+  gaim_blist_remove_buddy(buddy);
+  blist_schedule(pd);
+
+  if(res && res->name) {
+    /* compose and display an error message */
+    const char *msgA;
+    const char *msgB;
+    char *msg;
+
+    msgA = _("Unable to add user: user not found");
+
+    msgB = _("The identifier '%s' did not match any users in your"
+	     " Sametime community. This entry has been removed from"
+	     " your buddy list.");
+    msg = g_strdup_printf(msgB, NSTR(res->name));
+
+    gaim_notify_error(gc, _("Unable to add user"), msgA, msg);
+
+    g_free(msg);
+  }
+#endif
+}
+
+
+static void mw_prpl_add_buddy(GaimConnection *gc,
+			      GaimBuddy *buddy,
+			      GaimGroup *group) {
+
+  struct mwGaimPluginData *pd;
+  struct mwServiceResolve *srvc;
+  GList *query;
+  enum mwResolveFlag flags;
+  guint32 req;
+
+  BuddyAddData *data;
+
+  data = g_new0(BuddyAddData, 1);
+  data->buddy = buddy;
+  data->group = group;
+
+  pd = gc->proto_data;
+  srvc = pd->srvc_resolve;
+
+  /* catch external buddies. They won't be in the resolve service */
+  if(buddy_is_external(buddy)) {
+    buddy_add(pd, buddy);
+    return;
+  }
+
+  query = g_list_prepend(NULL, buddy->name);
+  flags = mwResolveFlag_FIRST | mwResolveFlag_USERS;
+
+  req = mwServiceResolve_resolve(srvc, query, flags, add_buddy_resolved,
+				 data, NULL);
+  g_list_free(query);
+
+  if(req == SEARCH_ERROR) {
+    gaim_blist_remove_buddy(buddy);
+    blist_schedule(pd);
+  }
+}
+
+
+static void foreach_add_buddies(GaimGroup *group, GList *buddies,
+				struct mwGaimPluginData *pd) {
+  struct mwAwareList *list;
+
+  list = list_ensure(pd, group);
+  mwAwareList_addAware(list, buddies);
+  g_list_free(buddies);
+}
+
+
+static void mw_prpl_add_buddies(GaimConnection *gc,
+				GList *buddies,
+				GList *groups) {
+
+  struct mwGaimPluginData *pd;
+  GHashTable *group_sets;
+  struct mwAwareIdBlock *idbs, *idb;
+
+  pd = gc->proto_data;
+
+  /* map GaimGroup:GList of mwAwareIdBlock */
+  group_sets = g_hash_table_new(g_direct_hash, g_direct_equal);
+
+  /* bunch of mwAwareIdBlock allocated at once, free'd at once */
+  idb = idbs = g_new(struct mwAwareIdBlock, g_list_length(buddies));
+
+  /* first pass collects mwAwareIdBlock lists for each group */
+  for(; buddies; buddies = buddies->next) {
+    GaimBuddy *b = buddies->data;
+    GaimGroup *g;
+    const char *fn;
+    GList *l;
+
+    /* nab the saved server alias and stick it on the buddy */
+    fn = gaim_blist_node_get_string((GaimBlistNode *) b, BUDDY_KEY_NAME);
+    gaim_blist_server_alias_buddy(b, fn);
+
+    /* convert GaimBuddy into a mwAwareIdBlock */
+    idb->type = mwAware_USER;
+    idb->user = (char *) b->name;
+    idb->community = NULL;
+
+    /* put idb into the list associated with the buddy's group */
+    g = gaim_buddy_get_group(b);
+    l = g_hash_table_lookup(group_sets, g);
+    l = g_list_prepend(l, idb++);
+    g_hash_table_insert(group_sets, g, l);
+  }
+
+  /* each group's buddies get added in one shot, and schedule the blist
+     for saving */
+  g_hash_table_foreach(group_sets, (GHFunc) foreach_add_buddies, pd);
+  blist_schedule(pd);
+
+  /* cleanup */
+  g_hash_table_destroy(group_sets);
+  g_free(idbs);
+}
+
+
+static void mw_prpl_remove_buddy(GaimConnection *gc,
+				 GaimBuddy *buddy, GaimGroup *group) {
+
+  struct mwGaimPluginData *pd;
+  struct mwAwareIdBlock idb = { mwAware_USER, buddy->name, NULL };
+  struct mwAwareList *list;
+
+  GList *rem = g_list_prepend(NULL, &idb);
+
+  pd = gc->proto_data;
+  group = gaim_buddy_get_group(buddy);
+  list = list_ensure(pd, group);
+
+  mwAwareList_removeAware(list, rem);
+  blist_schedule(pd);
+
+  g_list_free(rem);
+}
+
+
+static void privacy_fill(struct mwPrivacyInfo *priv,
+			 GSList *members) {
+  
+  struct mwUserItem *u;
+  guint count;
+
+  count = g_slist_length(members);
+  DEBUG_INFO("privacy_fill: %u members\n", count);
+
+  priv->count = count;
+  priv->users = g_new0(struct mwUserItem, count);
+
+  while(count--) {
+    u = priv->users + count;
+    u->id = members->data;
+    members = members->next;
+  }
+}
+
+
+static void mw_prpl_set_permit_deny(GaimConnection *gc) {
+  GaimAccount *acct;
+  struct mwGaimPluginData *pd;
+  struct mwSession *session;
+
+  struct mwPrivacyInfo privacy = {
+    .deny = FALSE,
+    .count = 0,
+    .users = NULL,
+  };
+
+  g_return_if_fail(gc != NULL);
+
+  acct = gaim_connection_get_account(gc);
+  g_return_if_fail(acct != NULL);
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  session = pd->session;
+  g_return_if_fail(session != NULL);
+
+  switch(acct->perm_deny) {
+  case GAIM_PRIVACY_DENY_USERS:
+    DEBUG_INFO("GAIM_PRIVACY_DENY_USERS\n");
+    privacy_fill(&privacy, acct->deny);
+    privacy.deny = TRUE;
+    break;
+
+  case GAIM_PRIVACY_ALLOW_ALL:
+    DEBUG_INFO("GAIM_PRIVACY_ALLOW_ALL\n");
+    privacy.deny = TRUE;
+    break;
+
+  case GAIM_PRIVACY_ALLOW_USERS:
+    DEBUG_INFO("GAIM_PRIVACY_ALLOW_USERS\n");
+    privacy_fill(&privacy, acct->permit);
+    privacy.deny = FALSE;
+    break;
+
+  case GAIM_PRIVACY_DENY_ALL:
+    DEBUG_INFO("GAIM_PRIVACY_DENY_ALL\n");
+    privacy.deny = FALSE;
+    break;
+    
+  default:
+    DEBUG_INFO("acct->perm_deny is 0x%x\n", acct->perm_deny);
+    return;
+  }
+
+  mwSession_setPrivacyInfo(session, &privacy);
+  g_free(privacy.users);
+}
+
+
+static void mw_prpl_add_permit(GaimConnection *gc, const char *name) {
+  mw_prpl_set_permit_deny(gc);
+}
+
+
+static void mw_prpl_add_deny(GaimConnection *gc, const char *name) {
+  mw_prpl_set_permit_deny(gc);
+}
+
+
+static void mw_prpl_rem_permit(GaimConnection *gc, const char *name) {
+  mw_prpl_set_permit_deny(gc);
+}
+
+
+static void mw_prpl_rem_deny(GaimConnection *gc, const char *name) {
+  mw_prpl_set_permit_deny(gc);
+}
+
+
+static struct mwConference *conf_find(struct mwServiceConference *srvc,
+				      const char *name) {
+  GList *l, *ll;
+  struct mwConference *conf = NULL;
+
+  ll = mwServiceConference_getConferences(srvc);
+  for(l = ll; l; l = l->next) {
+    struct mwConference *c = l->data;
+    if(! strcmp(name, mwConference_getName(c))) {
+      conf = c;
+      break;
+    }
+  }
+  g_list_free(ll);
+
+  return conf;
+}
+
+
+static void mw_prpl_join_chat(GaimConnection *gc,
+			      GHashTable *components) {
+
+  struct mwGaimPluginData *pd;
+  char *c, *t;
+  
+  pd = gc->proto_data;
+
+  c = g_hash_table_lookup(components, CHAT_KEY_NAME);
+  t = g_hash_table_lookup(components, CHAT_KEY_TOPIC);
+  
+  if(g_hash_table_lookup(components, CHAT_KEY_IS_PLACE)) {
+    /* use place service */
+    struct mwServicePlace *srvc;
+    struct mwPlace *place = NULL;
+
+    srvc = pd->srvc_place;
+    place = mwPlace_new(srvc, c, t);
+    mwPlace_open(place);
+     
+  } else {
+    /* use conference service */
+    struct mwServiceConference *srvc;
+    struct mwConference *conf = NULL;
+
+    srvc = pd->srvc_conf;
+    if(c) conf = conf_find(srvc, c);
+
+    if(conf) {
+      DEBUG_INFO("accepting conference invitation\n");
+      mwConference_accept(conf);
+      
+    } else {
+      DEBUG_INFO("creating new conference\n");
+      conf = mwConference_new(srvc, t);
+      mwConference_open(conf);
+    }
+  }
+}
+
+
+static void mw_prpl_reject_chat(GaimConnection *gc,
+				GHashTable *components) {
+
+  struct mwGaimPluginData *pd;
+  struct mwServiceConference *srvc;
+  char *c;
+  
+  pd = gc->proto_data;
+  srvc = pd->srvc_conf;
+
+  if(g_hash_table_lookup(components, CHAT_KEY_IS_PLACE)) {
+    ; /* nothing needs doing */
+
+  } else {
+    /* reject conference */
+    c = g_hash_table_lookup(components, CHAT_KEY_NAME);
+    if(c) {
+      struct mwConference *conf = conf_find(srvc, c);
+      if(conf) mwConference_reject(conf, ERR_SUCCESS, "Declined");
+    }
+  }
+}
+
+
+static char *mw_prpl_get_chat_name(GHashTable *components) {
+  return g_hash_table_lookup(components, CHAT_KEY_NAME);
+}
+
+
+static void mw_prpl_chat_invite(GaimConnection *gc,
+				int id,
+				const char *invitation,
+				const char *who) {
+
+  struct mwGaimPluginData *pd;
+  struct mwConference *conf;
+  struct mwPlace *place;
+  struct mwIdBlock idb = { (char *) who, NULL };
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  conf = ID_TO_CONF(pd, id);
+
+  if(conf) {
+    mwConference_invite(conf, &idb, invitation);
+    return;
+  }
+
+  place = ID_TO_PLACE(pd, id);
+  g_return_if_fail(place != NULL);
+
+  /* @todo: use the IM service for invitation */
+  mwPlace_legacyInvite(place, &idb, invitation);
+}
+
+
+static void mw_prpl_chat_leave(GaimConnection *gc,
+			       int id) {
+
+  struct mwGaimPluginData *pd;
+  struct mwConference *conf;
+
+  pd = gc->proto_data;
+
+  g_return_if_fail(pd != NULL);
+  conf = ID_TO_CONF(pd, id);
+
+  if(conf) {
+    mwConference_destroy(conf, ERR_SUCCESS, "Leaving");
+
+  } else {
+    struct mwPlace *place = ID_TO_PLACE(pd, id);
+    g_return_if_fail(place != NULL);
+
+    mwPlace_destroy(place, ERR_SUCCESS);
+  }
+}
+
+
+static void mw_prpl_chat_whisper(GaimConnection *gc,
+				 int id,
+				 const char *who,
+				 const char *message) {
+
+  mw_prpl_send_im(gc, who, message, 0);
+}
+
+
+static int mw_prpl_chat_send(GaimConnection *gc,
+			     int id,
+			     const char *message,
+			     GaimMessageFlags flags) {
+
+  struct mwGaimPluginData *pd;
+  struct mwConference *conf;
+  char *msg;
+  int ret;
+
+  pd = gc->proto_data;
+
+  g_return_val_if_fail(pd != NULL, 0);
+  conf = ID_TO_CONF(pd, id);
+
+  msg = gaim_markup_strip_html(message);
+
+  if(conf) {
+    ret = ! mwConference_sendText(conf, message);
+
+  } else {
+    struct mwPlace *place = ID_TO_PLACE(pd, id);
+    g_return_val_if_fail(place != NULL, 0);
+
+    ret = ! mwPlace_sendText(place, message);
+  }
+
+  g_free(msg);
+  return ret;
+}
+
+
+static void mw_prpl_keepalive(GaimConnection *gc) {
+  struct mwSession *session;
+
+  g_return_if_fail(gc != NULL);
+
+  session = gc_to_session(gc);
+  g_return_if_fail(session != NULL);
+
+  mwSession_sendKeepalive(session);
+}
+
+
+static void mw_prpl_alias_buddy(GaimConnection *gc,
+				const char *who,
+				const char *alias) {
+
+  struct mwGaimPluginData *pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  /* it's a change to the buddy list, so we've gotta reflect that in
+     the server copy */
+
+  blist_schedule(pd);
+}
+
+
+static void mw_prpl_group_buddy(GaimConnection *gc,
+				const char *who,
+				const char *old_group,
+				const char *new_group) {
+
+  struct mwAwareIdBlock idb = { mwAware_USER, (char *) who, NULL };
+  GList *gl = g_list_prepend(NULL, &idb);
+
+  struct mwGaimPluginData *pd = gc->proto_data;
+  GaimGroup *group;
+  struct mwAwareList *list;
+
+  /* add who to new_group's aware list */
+  group = gaim_find_group(new_group);
+  list = list_ensure(pd, group);
+  mwAwareList_addAware(list, gl);
+
+  /* remove who from old_group's aware list */
+  group = gaim_find_group(old_group);
+  list = list_ensure(pd, group);
+  mwAwareList_removeAware(list, gl);
+
+  g_list_free(gl);
+
+  /* schedule the changes to be saved */
+  blist_schedule(pd);
+}
+
+
+static void mw_prpl_rename_group(GaimConnection *gc,
+				 const char *old,
+				 GaimGroup *group,
+				 GList *buddies) {
+
+  struct mwGaimPluginData *pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+
+  /* it's a change in the buddy list, so we've gotta reflect that in
+     the server copy. Also, having this function should prevent all
+     those buddies from being removed and re-added. We don't really
+     give a crap what the group is named in Gaim other than to record
+     that as the group name/alias */
+
+  blist_schedule(pd);
+}
+
+
+static void mw_prpl_buddy_free(GaimBuddy *buddy) {
+  /* I don't think we have any cleanup for buddies yet */
+  ;
+}
+
+
+static void mw_prpl_convo_closed(GaimConnection *gc, const char *who) {
+  struct mwGaimPluginData *pd = gc->proto_data;
+  struct mwServiceIm *srvc;
+  struct mwConversation *conv;
+  struct mwIdBlock idb = { (char *) who, NULL };
+
+  g_return_if_fail(pd != NULL);
+
+  srvc = pd->srvc_im;
+  g_return_if_fail(srvc != NULL);
+
+  conv = mwServiceIm_findConversation(srvc, &idb);
+  if(! conv) return;
+
+  if(mwConversation_isOpen(conv))
+    mwConversation_free(conv);
+}
+
+
+static const char *mw_prpl_normalize(const GaimAccount *account,
+				     const char *id) {
+
+  /* code elsewhere assumes that the return value points to different
+     memory than the passed value, but it won't free the normalized
+     data. wtf? */
+
+  static char buf[BUF_LEN];
+  strncpy(buf, id, sizeof(buf));
+  return buf;
+}
+
+
+static void mw_prpl_remove_group(GaimConnection *gc, GaimGroup *group) {
+  struct mwGaimPluginData *pd;
+  struct mwAwareList *list;
+
+  pd = gc->proto_data;
+  g_return_if_fail(pd != NULL);
+  g_return_if_fail(pd->group_list_map != NULL);
+
+  list = g_hash_table_lookup(pd->group_list_map, group);
+
+  if(list) {
+    g_hash_table_remove(pd->group_list_map, list);
+    g_hash_table_remove(pd->group_list_map, group);
+    mwAwareList_free(list);
+
+    blist_schedule(pd);
+  }
+}
+
+
+static gboolean mw_prpl_can_receive_file(GaimConnection *gc,
+					 const char *who) {
+  struct mwGaimPluginData *pd;
+  struct mwServiceAware *srvc;
+  GaimAccount *acct;
+
+  g_return_val_if_fail(gc != NULL, FALSE);
+
+  pd = gc->proto_data;
+  g_return_val_if_fail(pd != NULL, FALSE);
+
+  srvc = pd->srvc_aware;
+  g_return_val_if_fail(srvc != NULL, FALSE);
+  
+  acct = gaim_connection_get_account(gc);
+  g_return_val_if_fail(acct != NULL, FALSE);
+
+  return gaim_find_buddy(acct, who) &&
+    user_supports(srvc, who, mwAttribute_FILE_TRANSFER);
+}
+
+
+static void ft_outgoing_init(GaimXfer *xfer) {
+  GaimAccount *acct;
+  GaimConnection *gc;
+
+  struct mwGaimPluginData *pd;
+  struct mwServiceFileTransfer *srvc;
+  struct mwFileTransfer *ft;
+
+  const char *filename;
+  gsize filesize;
+  FILE *fp;
+
+  struct mwIdBlock idb = { NULL, NULL };
+
+  DEBUG_INFO("ft_outgoing_init\n");
+
+  acct = gaim_xfer_get_account(xfer);
+  gc = gaim_account_get_connection(acct);
+  pd = gc->proto_data;
+  srvc = pd->srvc_ft;
+
+  filename = gaim_xfer_get_local_filename(xfer);
+  filesize = gaim_xfer_get_size(xfer);
+  idb.user = xfer->who;
+
+  gaim_xfer_update_progress(xfer);
+
+  /* test that we can actually send the file */
+  fp = g_fopen(filename, "rb");
+  if(! fp) {
+    char *msg = g_strdup_printf(_("Error reading file %s: \n%s\n"),
+				filename, strerror(errno));
+    gaim_xfer_error(gaim_xfer_get_type(xfer), acct, xfer->who, msg);
+    g_free(msg);
+    return;
+  }
+  fclose(fp);
+
+  {
+    char *tmp = strrchr(filename, G_DIR_SEPARATOR);
+    if(tmp++) filename = tmp;
+  }
+  
+  ft = mwFileTransfer_new(srvc, &idb, NULL, filename, filesize);
+
+  gaim_xfer_ref(xfer);
+  mwFileTransfer_setClientData(ft, xfer, (GDestroyNotify) gaim_xfer_unref);
+  xfer->data = ft;
+
+  mwFileTransfer_offer(ft);
+}
+
+
+static void ft_outgoing_cancel(GaimXfer *xfer) {
+  struct mwFileTransfer *ft = xfer->data;
+  
+  DEBUG_INFO("ft_outgoing_cancel called\n");
+
+  if(ft) mwFileTransfer_cancel(ft);
+}
+
+
+static GaimXfer *mw_prpl_new_xfer(GaimConnection *gc, const char *who) {
+  GaimAccount *acct;
+  GaimXfer *xfer;
+
+  acct = gaim_connection_get_account(gc);
+
+  xfer = gaim_xfer_new(acct, GAIM_XFER_SEND, who);
+  if (xfer)
+  {
+    gaim_xfer_set_init_fnc(xfer, ft_outgoing_init);
+    gaim_xfer_set_cancel_send_fnc(xfer, ft_outgoing_cancel);
+  }
+
+  return xfer;
+}
+
+static void mw_prpl_send_file(GaimConnection *gc,
+			      const char *who, const char *file) {
+
+  GaimXfer *xfer = mw_prpl_new_xfer(gc, who);
+
+  if(file) {
+    DEBUG_INFO("file != NULL\n");
+    gaim_xfer_request_accepted(xfer, file);
+
+  } else {
+    DEBUG_INFO("file == NULL\n");
+    gaim_xfer_request(xfer);
+  }
+}
+
+
+static GaimPluginProtocolInfo mw_prpl_info = {
+  .options                   = OPT_PROTO_IM_IMAGE,
+  .user_splits               = NULL, /*< set in mw_plugin_init */
+  .protocol_options          = NULL, /*< set in mw_plugin_init */
+  .icon_spec                 = NO_BUDDY_ICONS,
+  .list_icon                 = mw_prpl_list_icon,
+  .list_emblems              = mw_prpl_list_emblems,
+  .status_text               = mw_prpl_status_text,
+  .tooltip_text              = mw_prpl_tooltip_text,
+  .status_types              = mw_prpl_status_types,
+  .blist_node_menu           = mw_prpl_blist_node_menu,
+  .chat_info                 = mw_prpl_chat_info,
+  .chat_info_defaults        = mw_prpl_chat_info_defaults,
+  .login                     = mw_prpl_login,
+  .close                     = mw_prpl_close,
+  .send_im                   = mw_prpl_send_im,
+  .set_info                  = NULL,
+  .send_typing               = mw_prpl_send_typing,
+  .get_info                  = mw_prpl_get_info,
+  .set_status                = mw_prpl_set_status,
+  .set_idle                  = mw_prpl_set_idle,
+  .change_passwd             = NULL,
+  .add_buddy                 = mw_prpl_add_buddy,
+  .add_buddies               = mw_prpl_add_buddies,
+  .remove_buddy              = mw_prpl_remove_buddy,
+  .remove_buddies            = NULL,
+  .add_permit                = mw_prpl_add_permit,
+  .add_deny                  = mw_prpl_add_deny,
+  .rem_permit                = mw_prpl_rem_permit,
+  .rem_deny                  = mw_prpl_rem_deny,
+  .set_permit_deny           = mw_prpl_set_permit_deny,
+  .join_chat                 = mw_prpl_join_chat,
+  .reject_chat               = mw_prpl_reject_chat,
+  .get_chat_name             = mw_prpl_get_chat_name,
+  .chat_invite               = mw_prpl_chat_invite,
+  .chat_leave                = mw_prpl_chat_leave,
+  .chat_whisper              = mw_prpl_chat_whisper,
+  .chat_send                 = mw_prpl_chat_send,
+  .keepalive                 = mw_prpl_keepalive,
+  .register_user             = NULL,
+  .get_cb_info               = NULL,
+  .get_cb_away               = NULL,
+  .alias_buddy               = mw_prpl_alias_buddy,
+  .group_buddy               = mw_prpl_group_buddy,
+  .rename_group              = mw_prpl_rename_group,
+  .buddy_free                = mw_prpl_buddy_free,
+  .convo_closed              = mw_prpl_convo_closed,
+  .normalize                 = mw_prpl_normalize,
+  .set_buddy_icon            = NULL,
+  .remove_group              = mw_prpl_remove_group,
+  .get_cb_real_name          = NULL,
+  .set_chat_topic            = NULL,
+  .find_blist_chat           = NULL,
+  .roomlist_get_list         = NULL,
+  .roomlist_expand_category  = NULL,
+  .can_receive_file          = mw_prpl_can_receive_file,
+  .send_file                 = mw_prpl_send_file,
+  .new_xfer                  = mw_prpl_new_xfer,
+  .offline_message           = NULL,
+  .whiteboard_prpl_ops       = NULL,
+  .send_raw                  = NULL
+};
+
+
+static GaimPluginPrefFrame *
+mw_plugin_get_plugin_pref_frame(GaimPlugin *plugin) {
+  GaimPluginPrefFrame *frame;
+  GaimPluginPref *pref;
+
+  frame = gaim_plugin_pref_frame_new();
+  
+  pref = gaim_plugin_pref_new_with_label(_("Remotely Stored Buddy List"));
+  gaim_plugin_pref_frame_add(frame, pref);
+  
+
+  pref = gaim_plugin_pref_new_with_name(MW_PRPL_OPT_BLIST_ACTION);
+  gaim_plugin_pref_set_label(pref, _("Buddy List Storage Mode"));
+
+  gaim_plugin_pref_set_type(pref, GAIM_PLUGIN_PREF_CHOICE);
+  gaim_plugin_pref_add_choice(pref, _("Local Buddy List Only"),
+			      GINT_TO_POINTER(blist_choice_LOCAL));
+  gaim_plugin_pref_add_choice(pref, _("Merge List from Server"),
+			      GINT_TO_POINTER(blist_choice_MERGE));
+  gaim_plugin_pref_add_choice(pref, _("Merge and Save List to Server"),
+			      GINT_TO_POINTER(blist_choice_STORE));
+  gaim_plugin_pref_add_choice(pref, _("Synchronize List with Server"),
+			      GINT_TO_POINTER(blist_choice_SYNCH));
+
+  gaim_plugin_pref_frame_add(frame, pref);
+
+  return frame;
+}
+
+
+static GaimPluginUiInfo mw_plugin_ui_info = {
+  .get_plugin_pref_frame = mw_plugin_get_plugin_pref_frame,
+};
+
+
+static void st_import_action_cb(GaimConnection *gc, char *filename) {
+  struct mwSametimeList *l;
+
+  FILE *file;
+  char buf[BUF_LEN];
+  size_t len;
+
+  GString *str;
+
+  file = g_fopen(filename, "r");
+  g_return_if_fail(file != NULL);
+
+  str = g_string_new(NULL);
+  while( (len = fread(buf, 1, BUF_LEN, file)) ) {
+    g_string_append_len(str, buf, len);
+  }
+
+  fclose(file);
+
+  l = mwSametimeList_load(str->str);
+  g_string_free(str, TRUE);
+
+  blist_merge(gc, l);
+  mwSametimeList_free(l);
+}
+
+
+/** prompts for a file to import blist from */
+static void st_import_action(GaimPluginAction *act) {
+  GaimConnection *gc;
+  GaimAccount *account;
+  char *title;
+
+  gc = act->context;
+  account = gaim_connection_get_account(gc);
+  title = g_strdup_printf(_("Import Sametime List for Account %s"),
+			  gaim_account_get_username(account));
+
+  gaim_request_file(gc, title, NULL, FALSE,
+		    G_CALLBACK(st_import_action_cb), NULL,
+		    gc);
+
+  g_free(title);
+}
+
+
+static void st_export_action_cb(GaimConnection *gc, char *filename) {
+  struct mwSametimeList *l;
+  char *str;
+  FILE *file;
+
+  file = g_fopen(filename, "w");
+  g_return_if_fail(file != NULL);
+
+  l = mwSametimeList_new();
+  blist_export(gc, l);
+  str = mwSametimeList_store(l);
+  mwSametimeList_free(l);
+
+  fprintf(file, "%s", str);
+  fclose(file);
+
+  g_free(str);
+}
+
+
+/** prompts for a file to export blist to */
+static void st_export_action(GaimPluginAction *act) {
+  GaimConnection *gc;
+  GaimAccount *account;
+  char *title;
+
+  gc = act->context;
+  account = gaim_connection_get_account(gc);
+  title = g_strdup_printf(_("Export Sametime List for Account %s"),
+			  gaim_account_get_username(account));
+
+  gaim_request_file(gc, title, NULL, TRUE,
+		    G_CALLBACK(st_export_action_cb), NULL,
+		    gc);
+
+  g_free(title);
+}
+
+
+static void remote_group_multi_cleanup(gpointer ignore,
+				       GaimRequestFields *fields) {
+  
+  GaimRequestField *f;
+  const GList *l;
+
+  f = gaim_request_fields_get_field(fields, "group");
+  l = gaim_request_field_list_get_items(f);
+
+  for(; l; l = l->next) {
+    const char *i = l->data;
+    struct named_id *res;
+
+    res = gaim_request_field_list_get_data(f, i);
+
+    g_free(res->id);
+    g_free(res->name);
+    g_free(res);
+  }
+}
+
+
+static void remote_group_done(struct mwGaimPluginData *pd,
+			      const char *id, const char *name) {
+  GaimConnection *gc;
+  GaimAccount *acct;
+  GaimGroup *group;
+  GaimBlistNode *gn;
+  const char *owner;
+
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  acct = gaim_connection_get_account(gc);
+  
+  /* collision checking */
+  group = gaim_find_group(name);
+  if(group) {
+    const char *msgA;
+    const char *msgB;
+    char *msg;
+
+    msgA = _("Unable to add group: group exists");
+    msgB = _("A group named '%s' already exists in your buddy list.");
+    msg = g_strdup_printf(msgB, name);
+
+    gaim_notify_error(gc, _("Unable to add group"), msgA, msg);
+
+    g_free(msg);
+    return;
+  }
+
+  group = gaim_group_new(name);
+  gn = (GaimBlistNode *) group;
+
+  owner = gaim_account_get_username(acct);
+
+  gaim_blist_node_set_string(gn, GROUP_KEY_NAME, id);
+  gaim_blist_node_set_int(gn, GROUP_KEY_TYPE, mwSametimeGroup_DYNAMIC);
+  gaim_blist_node_set_string(gn, GROUP_KEY_OWNER, owner);
+  gaim_blist_add_group(group, NULL);
+
+  group_add(pd, group);
+  blist_schedule(pd);
+}
+
+
+static void remote_group_multi_cb(struct mwGaimPluginData *pd,
+				  GaimRequestFields *fields) {
+  GaimRequestField *f;
+  const GList *l;
+
+  f = gaim_request_fields_get_field(fields, "group");
+  l = gaim_request_field_list_get_selected(f);
+
+  if(l) {
+    const char *i = l->data;
+    struct named_id *res;
+
+    res = gaim_request_field_list_get_data(f, i);
+    remote_group_done(pd, res->id, res->name);
+  }
+
+  remote_group_multi_cleanup(NULL, fields);
+}
+
+
+static void remote_group_multi(struct mwResolveResult *result,
+			       struct mwGaimPluginData *pd) {
+
+  GaimRequestFields *fields;
+  GaimRequestFieldGroup *g;
+  GaimRequestField *f;
+  GList *l;
+  const char *msgA;
+  const char *msgB;
+  char *msg;
+
+  GaimConnection *gc = pd->gc;
+
+  fields = gaim_request_fields_new();
+
+  g = gaim_request_field_group_new(NULL);
+  gaim_request_fields_add_group(fields, g);
+
+  f = gaim_request_field_list_new("group", _("Possible Matches"));
+  gaim_request_field_list_set_multi_select(f, FALSE);
+  gaim_request_field_set_required(f, TRUE);
+
+  for(l = result->matches; l; l = l->next) {
+    struct mwResolveMatch *match = l->data;
+    struct named_id *res = g_new0(struct named_id, 1);
+
+    res->id = g_strdup(match->id);
+    res->name = g_strdup(match->name);
+
+    gaim_request_field_list_add(f, res->name, res);
+  }
+
+  gaim_request_field_group_add_field(g, f);
+
+  msgA = _("Notes Address Book group results");
+  msgB = _("The identifier '%s' may possibly refer to any of the following"
+	  " Notes Address Book groups. Please select the correct group from"
+	  " the list below to add it to your buddy list.");
+  msg = g_strdup_printf(msgB, result->name);
+
+  gaim_request_fields(gc, _("Select Notes Address Book"),
+		      msgA, msg, fields,
+		      _("Add Group"), G_CALLBACK(remote_group_multi_cb),
+		      _("Cancel"), G_CALLBACK(remote_group_multi_cleanup),
+		      pd);
+
+  g_free(msg);
+}
+
+
+static void remote_group_resolved(struct mwServiceResolve *srvc,
+				  guint32 id, guint32 code, GList *results,
+				  gpointer b) {
+
+  struct mwResolveResult *res = NULL;
+  struct mwSession *session;
+  struct mwGaimPluginData *pd;
+  GaimConnection *gc;
+
+  session = mwService_getSession(MW_SERVICE(srvc));
+  g_return_if_fail(session != NULL);
+
+  pd = mwSession_getClientData(session);
+  g_return_if_fail(pd != NULL);
+
+  gc = pd->gc;
+  g_return_if_fail(gc != NULL);
+  
+  if(!code && results) {
+    res = results->data;
+
+    if(res->matches) {
+      remote_group_multi(res, pd);
+      return;
+    }
+  }
+
+  if(res && res->name) {
+    const char *msgA;
+    const char *msgB;
+    char *msg;
+
+    msgA = _("Unable to add group: group not found");
+
+    msgB = _("The identifier '%s' did not match any Notes Address Book"
+	    " groups in your Sametime community.");
+    msg = g_strdup_printf(msgB, res->name);
+
+    gaim_notify_error(gc, _("Unable to add group"), msgA, msg);
+
+    g_free(msg);
+  }
+}
+
+
+static void remote_group_action_cb(GaimConnection *gc, const char *name) {
+  struct mwGaimPluginData *pd;
+  struct mwServiceResolve *srvc;
+  GList *query;
+  enum mwResolveFlag flags;
+  guint32 req;
+
+  pd = gc->proto_data;
+  srvc = pd->srvc_resolve;
+
+  query = g_list_prepend(NULL, (char *) name);
+  flags = mwResolveFlag_FIRST | mwResolveFlag_GROUPS;
+  
+  req = mwServiceResolve_resolve(srvc, query, flags, remote_group_resolved,
+				 NULL, NULL);
+  g_list_free(query);
+
+  if(req == SEARCH_ERROR) {
+    /** @todo display error */
+  }
+}
+
+
+static void remote_group_action(GaimPluginAction *act) {
+  GaimConnection *gc;
+  const char *msgA;
+  const char *msgB;
+
+  gc = act->context;
+
+  msgA = _("Notes Address Book Group");
+  msgB = _("Enter the name of a Notes Address Book group in the field below"
+	  " to add the group and its members to your buddy list.");
+
+  gaim_request_input(gc, _("Add Group"), msgA, msgB, NULL,
+		     FALSE, FALSE, NULL,
+		     _("Add"), G_CALLBACK(remote_group_action_cb),
+		     _("Cancel"), NULL,
+		     gc);
+}
+
+
+static void search_notify(struct mwResolveResult *result,
+			  GaimConnection *gc) {
+  GList *l;
+  const char *msgA;
+  const char *msgB;
+  char *msg1;
+  char *msg2;
+
+  GaimNotifySearchResults *sres;
+  GaimNotifySearchColumn *scol;
+
+  sres = gaim_notify_searchresults_new();
+
+  scol = gaim_notify_searchresults_column_new(_("User Name"));
+  gaim_notify_searchresults_column_add(sres, scol);
+
+  scol = gaim_notify_searchresults_column_new(_("Sametime ID"));
+  gaim_notify_searchresults_column_add(sres, scol);
+
+  gaim_notify_searchresults_button_add(sres, GAIM_NOTIFY_BUTTON_IM,
+				       notify_im);
+
+  gaim_notify_searchresults_button_add(sres, GAIM_NOTIFY_BUTTON_ADD,
+				       notify_add);
+
+  for(l = result->matches; l; l = l->next) {
+    struct mwResolveMatch *match = l->data;
+    GList *row = NULL;
+
+    if(!match->id || !match->name)
+      continue;
+    
+    row = g_list_append(row, g_strdup(match->name));
+    row = g_list_append(row, g_strdup(match->id));
+    gaim_notify_searchresults_row_add(sres, row);
+  }
+
+  msgA = _("Search results for '%s'");
+  msgB = _("The identifier '%s' may possibly refer to any of the following"
+	   " users. You may add these users to your buddy list or send them"
+	   " messages with the action buttons below.");
+
+  msg1 = g_strdup_printf(msgA, result->name);
+  msg2 = g_strdup_printf(msgB, result->name);
+
+  gaim_notify_searchresults(gc, _("Search Results"),
+			    msg1, msg2, sres, notify_close, NULL);
+
+  g_free(msg1);
+  g_free(msg2);
+}
+
+
+static void search_resolved(struct mwServiceResolve *srvc,
+			    guint32 id, guint32 code, GList *results,
+			    gpointer b) {
+
+  GaimConnection *gc = b;
+  struct mwResolveResult *res = NULL;
+
+  if(results) res = results->data;
+
+  if(!code && res && res->matches) {
+    search_notify(res, gc);
+
+  } else {
+    const char *msgA;
+    const char *msgB;
+    char *msg;
+
+    msgA = _("No matches");
+    msgB = _("The identifier '%s' did not match any users in your"
+	     " Sametime community.");
+    msg = g_strdup_printf(msgB, NSTR(res->name));
+
+    gaim_notify_error(gc, _("No Matches"), msgA, msg);
+
+    g_free(msg);
+  }
+}
+
+
+static void search_action_cb(GaimConnection *gc, const char *name) {
+  struct mwGaimPluginData *pd;
+  struct mwServiceResolve *srvc;
+  GList *query;
+  enum mwResolveFlag flags;
+  guint32 req;
+
+  pd = gc->proto_data;
+  srvc = pd->srvc_resolve;
+  
+  query = g_list_prepend(NULL, (char *) name);
+  flags = mwResolveFlag_FIRST | mwResolveFlag_USERS;
+
+  req = mwServiceResolve_resolve(srvc, query, flags, search_resolved,
+				 gc, NULL);
+  g_list_free(query);
+
+  if(req == SEARCH_ERROR) {
+    /** @todo display error */
+  }
+}
+
+
+static void search_action(GaimPluginAction *act) {
+  GaimConnection *gc;
+  const char *msgA;
+  const char *msgB;
+
+  gc = act->context;
+
+  msgA = _("Search for a user");
+  msgB = _("Enter a name or partial ID in the field below to search"
+	   " for matching users in your Sametime community.");
+
+  gaim_request_input(gc, _("User Search"), msgA, msgB, NULL,
+		     FALSE, FALSE, NULL,
+		     _("Search"), G_CALLBACK(search_action_cb),
+		     _("Cancel"), NULL,
+		     gc);
+}
+
+
+static GList *mw_plugin_actions(GaimPlugin *plugin, gpointer context) {
+  GaimPluginAction *act;
+  GList *l = NULL;
+
+  act = gaim_plugin_action_new(_("Import Sametime List..."),
+			       st_import_action);
+  l = g_list_append(l, act);
+
+  act = gaim_plugin_action_new(_("Export Sametime List..."),
+			       st_export_action);
+  l = g_list_append(l, act);
+
+  act = gaim_plugin_action_new(_("Add Notes Address Book Group..."),
+			       remote_group_action);
+  l = g_list_append(l, act);
+
+  act = gaim_plugin_action_new(_("User Search..."),
+			       search_action);
+  l = g_list_append(l, act);
+
+  return l;
+}
+
+
+static gboolean mw_plugin_load(GaimPlugin *plugin) {
+  return TRUE;
+}
+
+
+static gboolean mw_plugin_unload(GaimPlugin *plugin) {
+  return TRUE;
+}
+
+
+static void mw_plugin_destroy(GaimPlugin *plugin) {
+  g_log_remove_handler(G_LOG_DOMAIN, log_handler[0]);
+  g_log_remove_handler("meanwhile", log_handler[1]);
+}
+
+
+static GaimPluginInfo mw_plugin_info = {
+  .magic           = GAIM_PLUGIN_MAGIC,
+  .major_version   = GAIM_MAJOR_VERSION,
+  .minor_version   = GAIM_MINOR_VERSION,
+  .type            = GAIM_PLUGIN_PROTOCOL,
+  .ui_requirement  = NULL,
+  .flags           = 0,
+  .dependencies    = NULL,
+  .priority        = GAIM_PRIORITY_DEFAULT,
+  .id              = PLUGIN_ID,
+  .name            = PLUGIN_NAME,
+  .version         = VERSION,
+  .summary         = PLUGIN_SUMMARY,
+  .description     = PLUGIN_DESC,
+  .author          = PLUGIN_AUTHOR,
+  .homepage        = PLUGIN_HOMEPAGE,
+  .load            = mw_plugin_load,
+  .unload          = mw_plugin_unload,
+  .destroy         = mw_plugin_destroy,
+  .ui_info         = NULL,
+  .extra_info      = &mw_prpl_info,
+  .prefs_info      = &mw_plugin_ui_info,
+  .actions         = mw_plugin_actions,
+};
+
+
+static void mw_log_handler(const gchar *domain, GLogLevelFlags flags,
+			   const gchar *msg, gpointer data) {
+
+  if(! (msg && *msg)) return;
+
+  /* handle g_log requests via gaim's built-in debug logging */
+  if(flags & G_LOG_LEVEL_ERROR) {
+    gaim_debug_error(domain, "%s\n", msg);
+
+  } else if(flags & G_LOG_LEVEL_WARNING) {
+    gaim_debug_warning(domain, "%s\n", msg);
+
+  } else {
+    gaim_debug_info(domain, "%s\n", msg);
+  }
+}
+
+
+static void mw_plugin_init(GaimPlugin *plugin) {
+  GaimAccountOption *opt;
+  GList *l = NULL;
+
+  GLogLevelFlags logflags =
+    G_LOG_LEVEL_MASK | G_LOG_FLAG_FATAL | G_LOG_FLAG_RECURSION;
+
+  /* set up the preferences */
+  gaim_prefs_add_none(MW_PRPL_OPT_BASE);
+  gaim_prefs_add_int(MW_PRPL_OPT_BLIST_ACTION, BLIST_CHOICE_DEFAULT);
+
+  /* remove dead preferences */
+  gaim_prefs_remove(MW_PRPL_OPT_PSYCHIC);
+  gaim_prefs_remove(MW_PRPL_OPT_SAVE_DYNAMIC);
+
+  /* host to connect to */
+  opt = gaim_account_option_string_new(_("Server"), MW_KEY_HOST,
+				       MW_PLUGIN_DEFAULT_HOST);
+  l = g_list_append(l, opt);
+
+  /* port to connect to */
+  opt = gaim_account_option_int_new(_("Port"), MW_KEY_PORT,
+				    MW_PLUGIN_DEFAULT_PORT);
+  l = g_list_append(l, opt);
+
+  { /* copy the old force login setting from prefs if it's
+       there. Don't delete the preference, since there may be more
+       than one account that wants to check for it. */
+    gboolean b = FALSE;
+    const char *label = _("Force login (ignore server redirects)");
+
+    if(gaim_prefs_exists(MW_PRPL_OPT_FORCE_LOGIN))
+      b = gaim_prefs_get_bool(MW_PRPL_OPT_FORCE_LOGIN);
+
+    opt = gaim_account_option_bool_new(label, MW_KEY_FORCE, b);
+    l = g_list_append(l, opt);
+  }
+
+  /* pretend to be Sametime Connect */
+  opt = gaim_account_option_bool_new(_("Hide client identity"),
+				     MW_KEY_FAKE_IT, FALSE);
+  l = g_list_append(l, opt);
+
+  mw_prpl_info.protocol_options = l;
+  l = NULL;
+
+  /* forward all our g_log messages to gaim. Generally all the logging
+     calls are using gaim_log directly, but the g_return macros will
+     get caught here */
+  log_handler[0] = g_log_set_handler(G_LOG_DOMAIN, logflags,
+				     mw_log_handler, NULL);
+  
+  /* redirect meanwhile's logging to gaim's */
+  log_handler[1] = g_log_set_handler("meanwhile", logflags,
+				     mw_log_handler, NULL);
+}
+
+
+GAIM_INIT_PLUGIN(sametime, mw_plugin_init, mw_plugin_info);
+/* The End. */
+