libpurple/protocols/facebook/facebook.c

Thu, 03 Dec 2020 04:34:04 -0600

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 03 Dec 2020 04:34:04 -0600
changeset 40634
4d3018b00ad4
parent 40618
4726bf0ff977
child 40697
81f81f5d2f39
permissions
-rw-r--r--

Fix the PurpleProtocolClient interface and split it out to its own file.

Testing Done:
Compile and unit tests.

Reviewed at https://reviews.imfreedom.org/r/193/

/* purple
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 */

#include <glib/gi18n-lib.h>

#include <purple.h>

#include "api.h"
#include "data.h"
#include "facebook.h"
#include "http.h"
#include "util.h"

static GSList *fb_cmds = NULL;
static PurpleProtocol *fb_protocol = NULL;

static void
fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data);

static PurpleGroup *
fb_get_group(gboolean friend)
{
	PurpleBlistNode *n;
	PurpleBlistNode *node;
	PurpleGroup *grp;
	const gchar *title;

	if (friend) {
		title = _("Facebook Friends");
	} else {
		title = _("Facebook Non-Friends");
	}

	grp = purple_blist_find_group(title);

	if (G_UNLIKELY(grp == NULL)) {
		grp = purple_group_new(title);
		node = NULL;

		for (n = purple_blist_get_default_root(); n != NULL;
		     n = n->next) {
			node = n;
		}

		/* Append to the end of the buddy list */
		purple_blist_add_group(grp, node);

		if (!friend) {
			node = PURPLE_BLIST_NODE(grp);
			purple_blist_node_set_bool(node, "collapsed", TRUE);
		}
	}

	return grp;
}

static void
fb_buddy_add_nonfriend(PurpleAccount *acct, FbApiUser *user)
{
	gchar uid[FB_ID_STRMAX];
	PurpleBuddy *bdy;
	PurpleGroup *grp;

	FB_ID_TO_STR(user->uid, uid);
	bdy = purple_buddy_new(acct, uid, user->name);
	grp = fb_get_group(FALSE);

	purple_buddy_set_server_alias(bdy, user->name);
	purple_blist_add_buddy(bdy, NULL, grp, NULL);
}

static void
fb_cb_api_auth(FbApi *api, gpointer data)
{
	FbData *fata = data;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);

	purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4);
	fb_data_save(fata);
	fb_api_contacts(api);
}

static void
fb_cb_api_connect(FbApi *api, gpointer data)
{
	FbData *fata = data;
	PurpleAccount *acct;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);

	fb_data_save(fata);
	purple_connection_set_state(gc, PURPLE_CONNECTION_CONNECTED);

	if (purple_account_get_bool(acct, "show-unread", TRUE)) {
		fb_api_unread(api);
	}
}

static void
fb_cb_api_contact(FbApi *api, FbApiUser *user, gpointer data)
{
	FbData *fata = data;
	gchar uid[FB_ID_STRMAX];
	GSList *msgs;
	PurpleAccount *acct;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	FB_ID_TO_STR(user->uid, uid);

	if (purple_blist_find_buddy(acct, uid) == NULL) {
		fb_buddy_add_nonfriend(acct, user);
	}

	msgs = fb_data_take_messages(fata, user->uid);

	if (msgs != NULL) {
		fb_cb_api_messages(api, msgs, fata);
		g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
	}
}

static gboolean
fb_cb_sync_contacts(gpointer data)
{
	FbApi *api;
	FbData *fata = data;

	api = fb_data_get_api(fata);
	fb_data_clear_timeout(fata, "sync-contacts", FALSE);
	fb_api_contacts(api);
	return FALSE;
}

static void
fb_cb_icon(FbDataImage *img, GError *error)
{
	const gchar *csum;
	const gchar *name;
	const gchar *str;
	FbHttpParams *params;
	gsize size;
	guint8 *image;
	PurpleAccount *acct;
	PurpleBuddy *bdy;

	bdy = fb_data_image_get_data(img);
	acct = purple_buddy_get_account(bdy);
	name = purple_buddy_get_name(bdy);

	if (G_UNLIKELY(error != NULL)) {
		fb_util_debug_warning("Failed to retrieve icon for %s: %s",
		                      name, error->message);
		return;
	}

	str = fb_data_image_get_url(img);
	params = fb_http_params_new_parse(str, TRUE);
	csum = fb_http_params_get_str(params, "oh", NULL);

	image = fb_data_image_dup_image(img, &size);
	purple_buddy_icons_set_for_user(acct, name, image, size, csum);
	fb_http_params_free(params);
}

static void
fb_sync_contacts_add_timeout(FbData *fata)
{
	gint sync;
	PurpleConnection *gc;
	PurpleAccount *acct;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);

	sync = purple_account_get_int(acct, "sync-interval", 5);

	if (sync < 1) {
		purple_account_set_int(acct, "sync-interval", 1);
		sync = 1;
	}

	sync *= 60;
	fb_data_add_timeout(fata, "sync-contacts", sync, fb_cb_sync_contacts,
	                    fata);
}

static void
fb_cb_api_contacts(FbApi *api, GSList *users, gboolean complete, gpointer data)
{
	const gchar *alias;
	const gchar *csum;
	FbApiUser *user;
	FbData *fata = data;
	FbId muid;
	gchar uid[FB_ID_STRMAX];
	GSList *l;
	GValue val = G_VALUE_INIT;
	PurpleAccount *acct;
	PurpleBuddy *bdy;
	PurpleConnection *gc;
	PurpleConnectionState state;
	PurpleGroup *grp;
	PurpleGroup *grpn;
	PurpleStatus *status;
	PurpleStatusPrimitive pstat;
	PurpleStatusType *type;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	grp = fb_get_group(TRUE);
	grpn = fb_get_group(FALSE);
	alias = purple_account_get_private_alias(acct);
	state = purple_connection_get_state(gc);

	g_value_init(&val, FB_TYPE_ID);
	g_object_get_property(G_OBJECT(api), "uid", &val);
	muid = g_value_get_int64(&val);
	g_value_unset(&val);

	for (l = users; l != NULL; l = l->next) {
		user = l->data;
		FB_ID_TO_STR(user->uid, uid);

		if (G_UNLIKELY(user->uid == muid)) {
			if (G_UNLIKELY(alias != NULL)) {
				continue;
			}

			purple_account_set_private_alias(acct, user->name);
			continue;
		}

		bdy = purple_blist_find_buddy(acct, uid);

		if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) {
			purple_blist_remove_buddy(bdy);
			bdy = NULL;
		}

		if (bdy == NULL) {
			bdy = purple_buddy_new(acct, uid, NULL);
			purple_blist_add_buddy(bdy, NULL, grp, NULL);
		}

		purple_buddy_set_server_alias(bdy, user->name);
		csum = purple_buddy_icons_get_checksum_for_user(bdy);

		if (!purple_strequal(csum, user->csum)) {
			fb_data_image_add(fata, user->icon, fb_cb_icon,
			                  bdy, NULL);
		}
	}

	fb_data_image_queue(fata);

	if (!complete) {
		return;
	}

	if (state != PURPLE_CONNECTION_CONNECTED) {
		status = purple_account_get_active_status(acct);
		type = purple_status_get_status_type(status);
		pstat = purple_status_type_get_primitive(type);

		purple_connection_update_progress(gc, _("Connecting"), 3, 4);
		fb_api_connect(api, pstat == PURPLE_STATUS_INVISIBLE);
	}

	fb_sync_contacts_add_timeout(fata);
}

static void
fb_cb_api_contacts_delta(FbApi *api, GSList *added, GSList *removed, gpointer data)
{
	FbApiUser *user;
	FbData *fata = data;
	gchar uid[FB_ID_STRMAX];
	GSList *l;
	PurpleAccount *acct;
	PurpleBuddy *bdy;
	PurpleConnection *gc;
	PurpleGroup *grp;
	PurpleGroup *grpn;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	grp = fb_get_group(TRUE);
	grpn = fb_get_group(FALSE);

	for (l = added; l != NULL; l = l->next) {
		user = l->data;
		FB_ID_TO_STR(user->uid, uid);

		bdy = purple_blist_find_buddy(acct, uid);

		if ((bdy != NULL) && (purple_buddy_get_group(bdy) == grpn)) {
			purple_blist_remove_buddy(bdy);
		}

		bdy = purple_buddy_new(acct, uid, NULL);
		purple_blist_add_buddy(bdy, NULL, grp, NULL);

		purple_buddy_set_server_alias(bdy, user->name);
	}

	for (l = removed; l != NULL; l = l->next) {
		bdy = purple_blist_find_buddy(acct, l->data);

		if (bdy != NULL) {
			purple_blist_remove_buddy(bdy);
		}
	}

	fb_sync_contacts_add_timeout(fata);
}

static void
fb_cb_api_error(FbApi *api, GError *error, gpointer data)
{
	FbData *fata = data;
	PurpleConnection *gc;
	PurpleConnectionError errc;

	gc = fb_data_get_connection(fata);

	if (error->domain == G_IO_ERROR) {
		purple_connection_g_error(gc, error);
		return;
	}

	if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_QUEUE)) {
		/* Save the reset data */
		fb_data_save(fata);
	}

	if ((error->domain == FB_HTTP_ERROR) &&
	    (error->code >= 400) &&
	    (error->code <= 500))
	{
		errc = PURPLE_CONNECTION_ERROR_OTHER_ERROR;
	} else if (g_error_matches(error, FB_API_ERROR, FB_API_ERROR_AUTH)) {
		errc = PURPLE_CONNECTION_ERROR_AUTHENTICATION_FAILED;
	} else {
		errc = PURPLE_CONNECTION_ERROR_NETWORK_ERROR;
	}


	if (!g_error_matches(error, FB_API_ERROR, FB_API_ERROR_NONFATAL)) {
		purple_connection_error(gc, errc, error->message);
	}
}

static void
fb_cb_api_events(FbApi *api, GSList *events, gpointer data)
{
	FbData *fata = data;
	FbApiEvent *event;
	gchar uid[FB_ID_STRMAX];
	gchar tid[FB_ID_STRMAX];
	GHashTable *fetch;
	GHashTableIter iter;
	GSList *l;
	PurpleAccount *acct;
	PurpleChatConversation *chat;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	fetch = g_hash_table_new(fb_id_hash, fb_id_equal);

	for (l = events; l != NULL; l = l->next) {
		event = l->data;

		FB_ID_TO_STR(event->tid, tid);
		chat = purple_conversations_find_chat_with_account(tid, acct);

		if (chat == NULL) {
			continue;
		}

		FB_ID_TO_STR(event->uid, uid);

		switch (event->type) {
		case FB_API_EVENT_TYPE_THREAD_TOPIC:
			purple_chat_conversation_set_topic(chat, uid,
			                                   event->text);
			break;

		case FB_API_EVENT_TYPE_THREAD_USER_ADDED:
			if (purple_blist_find_buddy(acct, uid) == NULL) {
				if (event->text) {
					FbApiUser *user = fb_api_user_dup(NULL, FALSE);
					user->uid = event->uid;
					user->name = g_strdup(event->text);

					fb_buddy_add_nonfriend(acct, user);

					fb_api_user_free(user);
				} else {
					g_hash_table_insert(fetch, &event->tid, event);
					break;
				}
			}

			purple_chat_conversation_add_user(chat, uid, NULL, 0,
			                                  TRUE);
			break;

		case FB_API_EVENT_TYPE_THREAD_USER_REMOVED:
			purple_chat_conversation_remove_user(chat, uid, event->text);
			break;
		}
	}

	g_hash_table_iter_init(&iter, fetch);

	while (g_hash_table_iter_next(&iter, NULL, (gpointer) &event)) {
		fb_api_thread(api, event->tid);
	}

	g_hash_table_destroy(fetch);
}

static void
fb_cb_image(FbDataImage *img, GError *error)
{
	const gchar *url;
	FbApi *api;
	FbApiMessage *msg;
	FbData *fata;
	gsize size;
	GSList *msgs = NULL;
	guint id;
	guint8 *image;
	PurpleImage *pimg;

	fata = fb_data_image_get_fata(img);
	msg = fb_data_image_get_data(img);

	if (G_UNLIKELY(error != NULL)) {
		url = fb_data_image_get_url(img);
		fb_util_debug_warning("Failed to retrieve image %s: %s",
		                      url, error->message);
		return;
	}

	api = fb_data_get_api(fata);
	image = fb_data_image_dup_image(img, &size);
	pimg = purple_image_new_from_data(image, size);
	id = purple_image_store_add_weak(pimg);

	g_free(msg->text);
	msg->text = g_strdup_printf("<img src=\""
	                            PURPLE_IMAGE_STORE_PROTOCOL
	                            "%u\">", id);
	msg->flags |= FB_API_MESSAGE_FLAG_DONE;

	msgs = g_slist_prepend(msgs, msg);
	fb_cb_api_messages(api, msgs, fata);
	g_slist_free(msgs);
}

static void
fb_cb_api_messages(FbApi *api, GSList *msgs, gpointer data)
{
	const gchar *text;
	FbApiMessage *msg;
	FbData *fata = data;
	gboolean isself;
	gboolean mark;
	gboolean open;
	gboolean self;
	gchar *html;
	gchar tid[FB_ID_STRMAX];
	gchar uid[FB_ID_STRMAX];
	gint id;
	gint64 tstamp;
	GSList *l;
	PurpleAccount *acct;
	PurpleChatConversation *chat;
	PurpleConnection *gc;
	PurpleMessageFlags flags;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	mark = purple_account_get_bool(acct, "mark-read", TRUE);
	open = purple_account_get_bool(acct, "group-chat-open", TRUE);
	self = purple_account_get_bool(acct, "show-self", TRUE);

	for (l = msgs; l != NULL; l = l->next) {
		msg = l->data;
		FB_ID_TO_STR(msg->uid, uid);

		if (purple_blist_find_buddy(acct, uid) == NULL) {
			msg = fb_api_message_dup(msg, TRUE);
			fb_data_add_message(fata, msg);
			fb_api_contact(api, msg->uid);
			continue;
		}

		isself = (msg->flags & FB_API_MESSAGE_FLAG_SELF) != 0;

		if (isself && !self) {
			continue;
		}

		flags = isself ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV;
		tstamp = msg->tstamp / 1000;

		if (msg->flags & FB_API_MESSAGE_FLAG_IMAGE) {
			if (!(msg->flags & FB_API_MESSAGE_FLAG_DONE)) {
				msg = fb_api_message_dup(msg, TRUE);
				fb_data_image_add(fata, msg->text, fb_cb_image,
				                  msg, (GDestroyNotify)
				                       fb_api_message_free);
				fb_data_image_queue(fata);
				continue;
			}

			flags |= PURPLE_MESSAGE_IMAGES;
			text = msg->text;
			html = NULL;
		} else {
			html = purple_markup_escape_text(msg->text, -1);
			text = html;
		}

		if (msg->tid == 0) {
			if (mark && !isself) {
				fb_data_set_unread(fata, msg->uid, TRUE);
			}

			fb_util_serv_got_im(gc, uid, text, flags, tstamp);
			g_free(html);
			continue;
		}

		FB_ID_TO_STR(msg->tid, tid);
		chat = purple_conversations_find_chat_with_account(tid, acct);

		if (chat == NULL) {
			if (!open) {
				g_free(html);
				continue;
			}

			id = fb_id_hash(&msg->tid);
			purple_serv_got_joined_chat(gc, id, tid);
			fb_api_thread(api, msg->tid);
		} else {
			id = purple_chat_conversation_get_id(chat);
		}

		if (mark && !isself) {
			fb_data_set_unread(fata, msg->tid, TRUE);
		}

		fb_util_serv_got_chat_in(gc, id, uid, text, flags, tstamp);
		g_free(html);
	}
}

static void
fb_cb_api_presences(FbApi *api, GSList *press, gpointer data)
{
	const gchar *statid;
	FbApiPresence *pres;
	FbData *fata = data;
	gchar uid[FB_ID_STRMAX];
	GSList *l;
	PurpleAccount *acct;
	PurpleConnection *gc;
	PurpleStatusPrimitive pstat;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);

	for (l = press; l != NULL; l = l->next) {
		pres = l->data;

		if (pres->active) {
			pstat = PURPLE_STATUS_AVAILABLE;
		} else {
			pstat = PURPLE_STATUS_OFFLINE;
		}

		FB_ID_TO_STR(pres->uid, uid);
		statid = purple_primitive_get_id_from_type(pstat);
		purple_protocol_got_user_status(acct, uid, statid, NULL);
	}
}

static void
fb_cb_api_thread(FbApi *api, FbApiThread *thrd, gpointer data)
{
	const gchar *name;
	FbApiUser *user;
	FbData *fata = data;
	gboolean active;
	gchar tid[FB_ID_STRMAX];
	gchar uid[FB_ID_STRMAX];
	gint id;
	GSList *l;
	PurpleAccount *acct;
	PurpleChatConversation *chat;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	id = fb_id_hash(&thrd->tid);
	FB_ID_TO_STR(thrd->tid, tid);

	chat = purple_conversations_find_chat_with_account(tid, acct);

	if ((chat == NULL) || purple_chat_conversation_has_left(chat)) {
		chat = purple_serv_got_joined_chat(gc, id, tid);
		active = FALSE;
	} else {
		/* If there are no users in the group chat, including
		 * the local user, then the group chat has yet to be
		 * setup by this function. As a result, any group chat
		 * without users is inactive.
		 */
		active = purple_chat_conversation_get_users_count(chat) > 0;
	}

	if (!active) {
		name = purple_account_get_username(acct);
		purple_chat_conversation_add_user(chat, name, NULL, 0, FALSE);
	}

	purple_chat_conversation_set_topic(chat, NULL, thrd->topic);

	for (l = thrd->users; l != NULL; l = l->next) {
		user = l->data;
		FB_ID_TO_STR(user->uid, uid);

		if (purple_chat_conversation_has_user(chat, uid)) {
			continue;
		}

		if (purple_blist_find_buddy(acct, uid) == NULL) {
			fb_buddy_add_nonfriend(acct, user);
		}

		purple_chat_conversation_add_user(chat, uid, NULL, 0, active);
	}
}

static void
fb_cb_api_thread_create(FbApi *api, FbId tid, gpointer data)
{
	FbData *fata = data;
	gchar sid[FB_ID_STRMAX];
	GHashTable *table;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	FB_ID_TO_STR(tid, sid);

	table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
	g_hash_table_insert(table, "name", g_strdup(sid));
	purple_serv_join_chat(gc, table);
	g_hash_table_destroy(table);
}

static void
fb_cb_api_thread_kicked(FbApi *api, FbApiThread *thrd, gpointer data)
{
	FbData *fata = data;
	gchar tid[FB_ID_STRMAX];
	PurpleAccount *acct;
	PurpleConnection *gc;
	PurpleChatConversation *chat;

	FB_ID_TO_STR(thrd->tid, tid);

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	chat = purple_conversations_find_chat_with_account(tid, acct);

	if (chat == NULL) {
		PurpleRequestCommonParameters *cpar;

		cpar = purple_request_cpar_from_connection(gc);
		purple_notify_error(gc,
				    _("Join a Chat"),
				    _("Failed to Join Chat"),
				    _("You have been removed from this chat"),
				    cpar);
		return;
	}

	purple_conversation_write_system_message(PURPLE_CONVERSATION(chat),
		_("You have been removed from this chat"), 0);

	purple_serv_got_chat_left(gc, purple_chat_conversation_get_id(chat));
}

static void
fb_cb_api_threads(FbApi *api, GSList *thrds, gpointer data)
{
	const gchar *alias;
	FbApiUser *user;
	FbData *fata = data;
	gchar tid[FB_ID_STRMAX];
	gchar uid[FB_ID_STRMAX];
	GSList *l;
	GSList *m;
	GString *gstr;
	FbApiThread *thrd;
	PurpleAccount *acct;
	PurpleBuddy *bdy;
	PurpleConnection *gc;
	PurpleRoomlist *list;
	PurpleRoomlistRoom *room;

	list = fb_data_get_roomlist(fata);

	if (G_UNLIKELY(list == NULL)) {
		return;
	}

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	gstr = g_string_new(NULL);

	for (l = thrds; l != NULL; l = l->next) {
		thrd = l->data;
		FB_ID_TO_STR(thrd->tid, tid);
		g_string_truncate(gstr, 0);

		for (m = thrd->users; m != NULL; m = m->next) {
			user = m->data;
			FB_ID_TO_STR(user->uid, uid);
			bdy = purple_blist_find_buddy(acct, uid);

			if (bdy != NULL) {
				alias = purple_buddy_get_alias(bdy);
			} else {
				alias = user->name;
			}

			if (gstr->len > 0) {
				g_string_append(gstr, ", ");
			}

			g_string_append(gstr, alias);
		}

		room = purple_roomlist_room_new(PURPLE_ROOMLIST_ROOMTYPE_ROOM,
		                                tid, NULL);
		purple_roomlist_room_add_field(list, room, thrd->topic);
		purple_roomlist_room_add_field(list, room, gstr->str);
		purple_roomlist_room_add(list, room);
	}

	purple_roomlist_set_in_progress(list, FALSE);
	fb_data_set_roomlist(fata, NULL);
	g_string_free(gstr, TRUE);
}

static void
fb_cb_api_typing(FbApi *api, FbApiTyping *typg, gpointer data)
{
	FbData *fata = data;
	gchar uid[FB_ID_STRMAX];
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	FB_ID_TO_STR(typg->uid, uid);

	if (typg->state) {
		purple_serv_got_typing(gc, uid, 0, PURPLE_IM_TYPING);
	} else {
		purple_serv_got_typing_stopped(gc, uid);
	}
}

static void
fb_mark_read(FbData *fata, FbId id, gboolean thread)
{
	FbApi *api;
	PurpleAccount *acct;
	PurpleConnection *gc;

	gc = fb_data_get_connection(fata);
	acct = purple_connection_get_account(gc);
	api = fb_data_get_api(fata);

	if (!fb_data_get_unread(fata, id) ||
	    (purple_account_get_bool(acct, "mark-read-available", FALSE) &&
	     fb_api_is_invisible(api)))
	{
		return;
	}

	fb_data_set_unread(fata, id, FALSE);
	fb_api_read(api, id, thread);
}

static gboolean
fb_cb_conv_read(gpointer data)
{
	const gchar *name;
	FbData *fata;
	FbId id;
	gchar *tname;
	PurpleConnection *gc;
	PurpleConversation *conv = data;

	gc = purple_conversation_get_connection(conv);
	fata = purple_connection_get_protocol_data(gc);
	name = purple_conversation_get_name(conv);
	id = FB_ID_FROM_STR(name);

	tname = g_strconcat("conv-read-", name, NULL);
	fb_data_clear_timeout(fata, tname, FALSE);
	g_free(tname);

	if (purple_conversation_has_focus(conv)) {
		fb_mark_read(fata, id, PURPLE_IS_CHAT_CONVERSATION(conv));
	}
	return FALSE;
}

static void
fb_cb_conv_updated(PurpleConversation *conv, PurpleConversationUpdateType type,
                   gpointer data)
{
	const gchar *name;
	const gchar *pid;
	FbData *fata = data;
	gchar *tname;
	PurpleAccount *acct;

	acct = purple_conversation_get_account(conv);
	pid = purple_account_get_protocol_id(acct);

	if ((type == PURPLE_CONVERSATION_UPDATE_UNSEEN) &&
	    purple_strequal(pid, FB_PROTOCOL_ID) &&
	    purple_account_get_bool(acct, "mark-read", TRUE))
	{
		/* Use event loop for purple_conversation_has_focus() */
		name = purple_conversation_get_name(conv);
		tname = g_strconcat("conv-read-", name, NULL);
		fb_data_add_timeout(fata, tname, 0, fb_cb_conv_read, conv);
		g_free(tname);
	}
}

static void
fb_cb_conv_deleting(PurpleConversation *conv, gpointer data)
{
	const gchar *name;
	const gchar *pid;
	FbData *fata = data;
	gchar *tname;
	PurpleAccount *acct;

	acct = purple_conversation_get_account(conv);
	pid = purple_account_get_protocol_id(acct);

	if (!purple_strequal(pid, FB_PROTOCOL_ID)) {
		return;
	}

	name = purple_conversation_get_name(conv);
	tname = g_strconcat("conv-read-", name, NULL);
	fb_data_clear_timeout(fata, tname, TRUE);
	g_free(tname);
}

static void
fb_blist_chat_create(GSList *buddies, gpointer data)
{
	const gchar *name;
	FbApi *api;
	FbData *fata = data;
	FbId *did;
	FbId uid;
	GSList *l;
	GSList *uids = NULL;
	PurpleConnection *gc;
	PurpleRequestCommonParameters *cpar;

	gc = fb_data_get_connection(fata);
	api = fb_data_get_api(fata);

	if (g_slist_length(buddies) < 2) {
		cpar = purple_request_cpar_from_connection(gc);
		purple_notify_error(gc,
		                    _("Initiate Chat"),
		                    _("Failed to Initiate Chat"),
		                    _("At least two initial chat participants"
		                      " are required."),
				    cpar);
		return;
	}

	for (l = buddies; l != NULL; l = l->next) {
		name = purple_buddy_get_name(l->data);
		uid = FB_ID_FROM_STR(name);
		did = g_memdup(&uid, sizeof uid);
		uids = g_slist_prepend(uids, did);
	}

	fb_api_thread_create(api, uids);
	g_slist_free_full(uids, g_free);
}

static void
fb_blist_chat_init(PurpleBlistNode *node, gpointer data)
{
	FbData *fata = data;
	GSList *select = NULL;
	PurpleConnection *gc;

	if (!PURPLE_IS_BUDDY(node)) {
		return;
	}

	gc = fb_data_get_connection(fata);
	select = g_slist_prepend(select, PURPLE_BUDDY(node));

	fb_util_request_buddy(gc,
	                      _("Initiate Chat"),
	                      _("Initial Chat Participants"),
	                      _("Select at least two initial participants."),
	                      select, TRUE,
			      G_CALLBACK(fb_blist_chat_create), NULL,
			      fata);
	g_slist_free(select);
}

static void
fb_login(PurpleAccount *acct)
{
	const gchar *pass;
	const gchar *user;
	FbApi *api;
	FbData *fata;
	gpointer convh;
	PurpleConnection *gc;
	GProxyResolver *resolver;
	GError *error = NULL;

	gc = purple_account_get_connection(acct);

	resolver = purple_proxy_get_proxy_resolver(acct, &error);
	if (resolver == NULL) {
		fb_util_debug_error("Unable to get account proxy resolver: %s",
		                    error->message);
		purple_connection_take_error(gc, error);
		return;
	}

	fata = fb_data_new(gc, resolver);
	api = fb_data_get_api(fata);
	convh = purple_conversations_get_handle();
	purple_connection_set_protocol_data(gc, fata);

	g_signal_connect(api,
	                 "auth",
	                 G_CALLBACK(fb_cb_api_auth),
	                 fata);
	g_signal_connect(api,
	                 "connect",
	                 G_CALLBACK(fb_cb_api_connect),
	                 fata);
	g_signal_connect(api,
	                 "contact",
	                 G_CALLBACK(fb_cb_api_contact),
	                 fata);
	g_signal_connect(api,
	                 "contacts",
	                 G_CALLBACK(fb_cb_api_contacts),
	                 fata);
	g_signal_connect(api,
	                 "contacts-delta",
	                 G_CALLBACK(fb_cb_api_contacts_delta),
	                 fata);
	g_signal_connect(api,
	                 "error",
	                 G_CALLBACK(fb_cb_api_error),
	                 fata);
	g_signal_connect(api,
	                 "events",
	                 G_CALLBACK(fb_cb_api_events),
	                 fata);
	g_signal_connect(api,
	                 "messages",
	                 G_CALLBACK(fb_cb_api_messages),
	                 fata);
	g_signal_connect(api,
	                 "presences",
	                 G_CALLBACK(fb_cb_api_presences),
	                 fata);
	g_signal_connect(api,
	                 "thread",
	                 G_CALLBACK(fb_cb_api_thread),
	                 fata);
	g_signal_connect(api,
	                 "thread-create",
	                 G_CALLBACK(fb_cb_api_thread_create),
	                 fata);
	g_signal_connect(api,
	                 "thread-kicked",
	                 G_CALLBACK(fb_cb_api_thread_kicked),
	                 fata);
	g_signal_connect(api,
	                 "threads",
	                 G_CALLBACK(fb_cb_api_threads),
	                 fata);
	g_signal_connect(api,
	                 "typing",
	                 G_CALLBACK(fb_cb_api_typing),
	                 fata);

	purple_signal_connect(convh,
	                      "conversation-updated",
	                      gc,
	                      G_CALLBACK(fb_cb_conv_updated),
	                      fata);
	purple_signal_connect(convh,
	                      "deleting-conversation",
	                      gc,
	                      G_CALLBACK(fb_cb_conv_deleting),
	                      fata);

	if (!fb_data_load(fata) || !purple_account_get_remember_password(acct)) {
		user = purple_account_get_username(acct);
		pass = purple_connection_get_password(gc);
		purple_connection_update_progress(gc, _("Authenticating"),
		                                  1, 4);
		fb_api_auth(api, user, pass);
		return;
	}

	purple_connection_update_progress(gc, _("Fetching contacts"), 2, 4);
	fb_api_contacts(api);
}

static void
fb_close(PurpleConnection *gc)
{
	FbApi *api;
	FbData *fata;

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);

	fb_data_save(fata);
	fb_api_disconnect(api);
	g_object_unref(fata);

	purple_connection_set_protocol_data(gc, NULL);
	purple_signals_disconnect_by_handle(gc);
}

static GList *
fb_status_types(PurpleAccount *acct)
{
	PurpleStatusType *type;
	GList *types = NULL;

	type = purple_status_type_new(PURPLE_STATUS_AVAILABLE,
	                              NULL, NULL, TRUE);
	types = g_list_prepend(types, type);

	/* Just a NULL state (as of now) for compatibility */
	type = purple_status_type_new(PURPLE_STATUS_AWAY,
	                              NULL, NULL, TRUE);
	types = g_list_prepend(types, type);

	type = purple_status_type_new(PURPLE_STATUS_INVISIBLE,
	                              NULL, NULL, TRUE);
	types = g_list_prepend(types, type);

	type = purple_status_type_new(PURPLE_STATUS_OFFLINE,
	                              NULL, NULL, TRUE);
	types = g_list_prepend(types, type);

	return g_list_reverse(types);
}

static const char *
fb_list_icon(PurpleAccount *account, PurpleBuddy *buddy)
{
	return "facebook";
}

static void
fb_client_tooltip_text(PurpleProtocolClient *client, PurpleBuddy *buddy,
                       PurpleNotifyUserInfo *info, gboolean full)
{
	const gchar *name;
	PurplePresence *pres;
	PurpleStatus *status;

	pres = purple_buddy_get_presence(buddy);
	status = purple_presence_get_active_status(pres);

	if (!PURPLE_BUDDY_IS_ONLINE(buddy)) {
		/* Prevent doubles statues for Offline buddies */
		/* See: pidgin_get_tooltip_text() in gtkblist.c */
		purple_notify_user_info_remove_last_item(info);
	}

	name = purple_status_get_name(status);
	purple_notify_user_info_add_pair_plaintext(info, _("Status"), name);
}

static GList *
fb_client_blist_node_menu(PurpleProtocolClient *client, PurpleBlistNode *node)
{
	FbData *fata;
	GList *acts = NULL;
	PurpleAccount *acct;
	PurpleConnection *gc;
	PurpleActionMenu *act;

	if (!PURPLE_IS_BUDDY(node)) {
		return NULL;
	}

	acct = purple_buddy_get_account(PURPLE_BUDDY(node));
	gc = purple_account_get_connection(acct);
	fata = purple_connection_get_protocol_data(gc);

	act = purple_action_menu_new(_("Initiate _Chat"),
	                             PURPLE_CALLBACK(fb_blist_chat_init),
	                             fata, NULL);
	acts = g_list_prepend(acts, act);

	return g_list_reverse(acts);
}

static gboolean
fb_client_offline_message(PurpleProtocolClient *client, PurpleBuddy *buddy)
{
	return TRUE;
}

static void
fb_server_set_status(PurpleAccount *acct, PurpleStatus *status)
{
	FbApi *api;
	FbData *fata;
	gboolean invis;
	PurpleConnection *gc;
	PurpleStatusPrimitive pstat;
	PurpleStatusType *type;

	gc = purple_account_get_connection(acct);
	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);

	type = purple_status_get_status_type(status);
	pstat = purple_status_type_get_primitive(type);
	invis = fb_api_is_invisible(api);

	if ((pstat == PURPLE_STATUS_INVISIBLE) && !invis) {
		fb_api_connect(api, TRUE);
	} else if ((pstat != PURPLE_STATUS_OFFLINE) && invis) {
		fb_api_connect(api, FALSE);
	}
}

static gint
fb_im_send(PurpleProtocolIM *im, PurpleConnection *gc, PurpleMessage *msg)
{
	const gchar *name;
	const gchar *text;
	FbApi *api;
	FbData *fata;
	FbId uid;
	gchar *sext;

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);

	name = purple_message_get_recipient(msg);
	uid = FB_ID_FROM_STR(name);

	text = purple_message_get_contents(msg);
	sext = purple_markup_strip_html(text);
	fb_api_message(api, uid, FALSE, sext);
	g_free(sext);
	return 1;
}

static guint
fb_im_send_typing(PurpleProtocolIM *im, PurpleConnection *gc,
                  const gchar *name, PurpleIMTypingState state)
{
	FbApi *api;
	FbData *fata;
	FbId uid;

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);
	uid = FB_ID_FROM_STR(name);

	fb_api_typing(api, uid, state != PURPLE_IM_NOT_TYPING);
	return 0;
}

static GList *
fb_chat_info()
{
	GList *pces = NULL;
	PurpleProtocolChatEntry *pce;

	pce = g_new0(PurpleProtocolChatEntry, 1);
	pce->label = _("Chat _Name:");
	pce->identifier = "name";
	pce->required = TRUE;
	pces = g_list_prepend(pces, pce);

	return g_list_reverse(pces);
}

static GHashTable *
fb_chat_info_defaults(PurpleConnection *gc, const gchar *name)
{
	GHashTable *data;

	data = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
	g_hash_table_insert(data, "name", g_strdup(name));

	return data;
}

static void
fb_chat_join(PurpleConnection *gc, GHashTable *data)
{
	const gchar *name;
	FbApi *api;
	FbData *fata;
	FbId tid;
	gint id;
	PurpleChatConversation *chat;
	PurpleRequestCommonParameters *cpar;

	name = g_hash_table_lookup(data, "name");
	g_return_if_fail(name != NULL);

	if (!FB_ID_IS_STR(name)) {
		cpar = purple_request_cpar_from_connection(gc);
		purple_notify_error(gc,
		                    _("Join a Chat"),
		                    _("Failed to Join Chat"),
		                    _("Invalid Facebook identifier."),
				    cpar);
		return;
	}

	tid = FB_ID_FROM_STR(name);
	id = fb_id_hash(&tid);
	chat = purple_conversations_find_chat(gc, id);

	if ((chat != NULL) && !purple_chat_conversation_has_left(chat)) {
		purple_conversation_present(PURPLE_CONVERSATION(chat));
		return;
	}

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);
	fb_api_thread(api, tid);
}

static gchar *
fb_chat_get_name(GHashTable *data)
{
	const gchar *name;

	name = g_hash_table_lookup(data, "name");
	g_return_val_if_fail(name != NULL, NULL);

	return g_strdup(name);
}

static void
fb_chat_invite(PurpleConnection *gc, gint id, const gchar *msg,
               const gchar *who)
{
	const gchar *name;
	FbApi *api;
	FbData *fata;
	FbId tid;
	FbId uid;
	PurpleChatConversation *chat;
	PurpleRequestCommonParameters *cpar;

	if (!FB_ID_IS_STR(who)) {
		cpar = purple_request_cpar_from_connection(gc);
		purple_notify_error(gc,
		                    _("Invite Buddy Into Chat Room"),
		                    _("Failed to Invite User"),
		                    _("Invalid Facebook identifier."),
				    cpar);
		return;
	}

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);
	chat = purple_conversations_find_chat(gc, id);

	name = purple_conversation_get_name(PURPLE_CONVERSATION(chat));
	tid = FB_ID_FROM_STR(name);
	uid = FB_ID_FROM_STR(who);

	fb_api_thread_invite(api, tid, uid);
}

static gint
fb_chat_send(PurpleConnection *gc, gint id, PurpleMessage *msg)
{
	const gchar *name;
	const gchar *text;
	FbApi *api;
	FbData *fata;
	FbId tid;
	gchar *sext;
	PurpleAccount *acct;
	PurpleChatConversation *chat;

	acct = purple_connection_get_account(gc);
	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);
	chat = purple_conversations_find_chat(gc, id);

	name = purple_conversation_get_name(PURPLE_CONVERSATION(chat));
	tid = FB_ID_FROM_STR(name);

	text = purple_message_get_contents(msg);
	sext = purple_markup_strip_html(text);
	fb_api_message(api, tid, TRUE, sext);
	g_free(sext);

	name = purple_account_get_username(acct);
	purple_serv_got_chat_in(gc, id, name,
				purple_message_get_flags(msg),
	                        purple_message_get_contents(msg),
	                        time(NULL));
	return 0;
}

static void
fb_chat_set_topic(PurpleConnection *gc, gint id, const gchar *topic)
{
	const gchar *name;
	FbApi *api;
	FbData *fata;
	FbId tid;
	PurpleChatConversation *chat;

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);
	chat = purple_conversations_find_chat(gc, id);

	name = purple_conversation_get_name(PURPLE_CONVERSATION(chat));
	tid = FB_ID_FROM_STR(name);
	fb_api_thread_topic(api, tid, topic);
}

static PurpleRoomlist *
fb_roomlist_get_list(PurpleConnection *gc)
{
	FbApi *api;
	FbData *fata;
	GList *flds = NULL;
	PurpleAccount *acct;
	PurpleRoomlist *list;
	PurpleRoomlistField *fld;

	fata = purple_connection_get_protocol_data(gc);
	list = fb_data_get_roomlist(fata);
	g_return_val_if_fail(list == NULL, NULL);

	api = fb_data_get_api(fata);
	acct = purple_connection_get_account(gc);
	list = purple_roomlist_new(acct);
	fb_data_set_roomlist(fata, list);

	fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING,
	                                _("Topic"), "topic", FALSE);
	flds = g_list_prepend(flds, fld);

	fld = purple_roomlist_field_new(PURPLE_ROOMLIST_FIELD_STRING,
	                                _("Users"), "users", FALSE);
	flds = g_list_prepend(flds, fld);

	flds = g_list_reverse(flds);
	purple_roomlist_set_fields(list, flds);

	purple_roomlist_set_in_progress(list, TRUE);
	fb_api_threads(api);
	return list;
}

static void
fb_roomlist_cancel(PurpleRoomlist *list)
{
	FbData *fata;
	PurpleAccount *acct;
	PurpleConnection *gc;
	PurpleRoomlist *cist;

	acct = purple_roomlist_get_account(list);
	gc = purple_account_get_connection(acct);
	fata = purple_connection_get_protocol_data(gc);
	cist = fb_data_get_roomlist(fata);

	if (G_LIKELY(cist == list)) {
		fb_data_set_roomlist(fata, NULL);
	}

	purple_roomlist_set_in_progress(list, FALSE);
	g_object_unref(list);
}

static PurpleCmdRet
fb_cmd_kick(PurpleConversation *conv, const gchar *cmd, gchar **args,
            gchar **error, gpointer data)
{
	const gchar *name;
	FbApi *api;
	FbData *fata;
	FbId tid;
	FbId uid;
	GError *err = NULL;
	PurpleAccount *acct;
	PurpleBuddy *bdy;
	PurpleConnection *gc;
	PurpleChatConversation *chat;

	g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv),
	                     PURPLE_CMD_RET_FAILED);

	gc = purple_conversation_get_connection(conv);
	acct = purple_connection_get_account(gc);
	chat = PURPLE_CHAT_CONVERSATION(conv);
	bdy = fb_util_account_find_buddy(acct, chat, args[0], &err);

	if (err != NULL) {
		*error = g_strdup_printf(_("%s."), err->message);
		g_error_free(err);
		return PURPLE_CMD_RET_FAILED;
	}

	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);

	name = purple_conversation_get_name(conv);
	tid = FB_ID_FROM_STR(name);

	name = purple_buddy_get_name(bdy);
	uid = FB_ID_FROM_STR(name);

	fb_api_thread_remove(api, tid, uid);
	return PURPLE_CMD_RET_OK;
}

static PurpleCmdRet
fb_cmd_leave(PurpleConversation *conv, const gchar *cmd, gchar **args,
             gchar **error, gpointer data)
{
	const gchar *name;
	FbApi *api;
	FbData *fata;
	FbId tid;
	gint id;
	PurpleConnection *gc;
	PurpleChatConversation *chat;

	g_return_val_if_fail(PURPLE_IS_CHAT_CONVERSATION(conv),
	                     PURPLE_CMD_RET_FAILED);

	gc = purple_conversation_get_connection(conv);
	fata = purple_connection_get_protocol_data(gc);
	api = fb_data_get_api(fata);

	chat = PURPLE_CHAT_CONVERSATION(conv);
	id = purple_chat_conversation_get_id(chat);

	name = purple_conversation_get_name(conv);
	tid = FB_ID_FROM_STR(name);

	purple_serv_got_chat_left(gc, id);
	fb_api_thread_remove(api, tid, 0);
	return PURPLE_CMD_RET_OK;
}

static void
facebook_protocol_init(FacebookProtocol *self)
{
	PurpleProtocol *protocol = PURPLE_PROTOCOL(self);
	GList *opts = NULL;
	PurpleAccountOption *opt;

	protocol->id      = FB_PROTOCOL_ID;
	protocol->name    = "Facebook";
	protocol->options = OPT_PROTO_CHAT_TOPIC;

	opt = purple_account_option_int_new(_("Buddy list sync interval"),
	                                    "sync-interval", 5);
	opts = g_list_prepend(opts, opt);

	opt = purple_account_option_bool_new(_("Mark messages as read on focus"),
	                                     "mark-read", TRUE);
	opts = g_list_prepend(opts, opt);

	opt = purple_account_option_bool_new(_("Mark messages as read only when available"),
	                                     "mark-read-available", FALSE);
	opts = g_list_prepend(opts, opt);

	opt = purple_account_option_bool_new(_("Show self messages"),
	                                     "show-self", TRUE);
	opts = g_list_prepend(opts, opt);

	opt = purple_account_option_bool_new(_("Show unread messages"),
	                                     "show-unread", TRUE);
	opts = g_list_prepend(opts, opt);

	opt = purple_account_option_bool_new(_("Open new group chats with "
	                                       "incoming messages"),
	                                     "group-chat-open", TRUE);
	opts = g_list_prepend(opts, opt);
	protocol->account_options = g_list_reverse(opts);
}

static void
facebook_protocol_class_init(FacebookProtocolClass *klass)
{
	PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);

	protocol_class->login = fb_login;
	protocol_class->close = fb_close;
	protocol_class->status_types = fb_status_types;
	protocol_class->list_icon = fb_list_icon;
}

static void
facebook_protocol_class_finalize(G_GNUC_UNUSED FacebookProtocolClass *klass)
{
}

static void
facebook_protocol_client_iface_init(PurpleProtocolClientInterface *iface)
{
	iface->tooltip_text    = fb_client_tooltip_text;
	iface->blist_node_menu = fb_client_blist_node_menu;
	iface->offline_message = fb_client_offline_message;
}

static void
facebook_protocol_server_iface_init(PurpleProtocolServerInterface *iface)
{
	iface->set_status = fb_server_set_status;
}

static void
facebook_protocol_im_iface_init(PurpleProtocolIMInterface *iface)
{
	iface->send        = fb_im_send;
	iface->send_typing = fb_im_send_typing;
}

static void
facebook_protocol_chat_iface_init(PurpleProtocolChatInterface *iface)
{
	iface->info          = fb_chat_info;
	iface->info_defaults = fb_chat_info_defaults;
	iface->join          = fb_chat_join;
	iface->get_name      = fb_chat_get_name;
	iface->invite        = fb_chat_invite;
	iface->send          = fb_chat_send;
	iface->set_topic     = fb_chat_set_topic;
}

static void
facebook_protocol_roomlist_iface_init(PurpleProtocolRoomlistInterface *iface)
{
	iface->get_list = fb_roomlist_get_list;
	iface->cancel   = fb_roomlist_cancel;
}

G_DEFINE_DYNAMIC_TYPE_EXTENDED(
        FacebookProtocol, facebook_protocol, PURPLE_TYPE_PROTOCOL, 0,

        G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CLIENT,
                                      facebook_protocol_client_iface_init)
        G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_SERVER,
                                      facebook_protocol_server_iface_init)
        G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_IM,
                                      facebook_protocol_im_iface_init)
        G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CHAT,
                                      facebook_protocol_chat_iface_init)
        G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_ROOMLIST,
                                      facebook_protocol_roomlist_iface_init));

static void
fb_cmds_register(void)
{
	PurpleCmdId id;

	static PurpleCmdFlag cflags =
		PURPLE_CMD_FLAG_CHAT |
		PURPLE_CMD_FLAG_PROTOCOL_ONLY;

	g_return_if_fail(fb_cmds == NULL);

	id = purple_cmd_register("kick", "s", PURPLE_CMD_P_PROTOCOL, cflags,
				 fb_protocol->id, fb_cmd_kick,
				 _("kick: Kick someone from the chat"),
				 NULL);
	fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id));

	id = purple_cmd_register("leave", "", PURPLE_CMD_P_PROTOCOL, cflags,
				 fb_protocol->id, fb_cmd_leave,
				 _("leave: Leave the chat"),
				 NULL);
	fb_cmds = g_slist_prepend(fb_cmds, GUINT_TO_POINTER(id));
}

static void
fb_cmds_unregister_free(gpointer data)
{
	PurpleCmdId id = GPOINTER_TO_UINT(data);
	purple_cmd_unregister(id);
}

static void
fb_cmds_unregister(void)
{
	g_slist_free_full(fb_cmds, fb_cmds_unregister_free);
}

static PurplePluginInfo *
plugin_query(GError **error)
{
	return purple_plugin_info_new(
		"id",          FB_PROTOCOL_ID,
		"name",        "Facebook Protocol",
		"version",     DISPLAY_VERSION,
		"category",    N_("Protocol"),
		"summary",     N_("Facebook Protocol Plugin"),
		"description", N_("Facebook Protocol Plugin"),
		"website",     PURPLE_WEBSITE,
		"abi-version", PURPLE_ABI_VERSION,
		"flags",       PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
		               PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
		NULL
	);
}

static gboolean
plugin_load(PurplePlugin *plugin, GError **error)
{
	facebook_protocol_register_type(G_TYPE_MODULE(plugin));
	fb_protocol = purple_protocols_add(FACEBOOK_TYPE_PROTOCOL, error);

	if (fb_protocol == NULL) {
		return FALSE;
	}

	fb_cmds_register();
	return TRUE;
}

static gboolean
plugin_unload(PurplePlugin *plugin, GError **error)
{
	fb_cmds_unregister();
	return purple_protocols_remove(fb_protocol, error);
}

PURPLE_PLUGIN_INIT(facebook, plugin_query, plugin_load, plugin_unload);

mercurial