Sophie

Sophie

distrib > * > 2008.0 > x86_64 > by-pkgid > 8f030c6fd819087af1e8e268b798094e > files > 1

pidgin-2.2.1-1mdv2008.0.src.rpm

/*
 * Facebook plugin
 *
 * Copyright (C) 2007 Neaveru Freedland.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA.
 */

#define FACEBOOK_PLUGIN_ID "core-neaveru-facebook"

#include "internal.h"
#include "cipher.h"
#include "debug.h"
#include "signals.h"
#include "version.h"

#include "notify.h"
#include "xmlnode.h"
#include "util.h"
#include "request.h"

//CONFIGURABLE:
#define FACEBOOK_APP_DEFAULT_API_KEY "e4dfa9bd876978055a06f8aed5307164"
#define FACEBOOK_APP_DEFAULT_SECRET "9e4081d6b78379f2e593e41d3560696c"
#define FACEBOOK_APP_KEY_EDITABLE 1 //Change to 0 if you don't want a user to be able to change the key

#define PREFS_BASE		"/plugins/core/facebook"
#define PREF_TIMEOUT	PREFS_BASE "/request_timer"
#define PREF_ONLOAD		PREFS_BASE "/autostart"
#define PREF_API_KEY	PREFS_BASE "/api_key"
#define PREF_SECRET		PREFS_BASE "/secret"

#define FACEBOOK_DEBUG_ID "facebook"
#define FACEBOOK_REQUEST_HOST "api.facebook.com"
#define FACEBOOK_REQUEST_PAGE "/restserver.php"
#define FACEBOOK_REQUEST_URL "http://" FACEBOOK_REQUEST_HOST FACEBOOK_REQUEST_PAGE
#define FACEBOOK_REQUEST_HEADER \
	"POST " FACEBOOK_REQUEST_PAGE " HTTP/1.1\r\n" \
	"HOST: " FACEBOOK_REQUEST_HOST "\r\n" \
	"CONNECTION: close\r\n" \
	"CONTENT-TYPE: application/x-www-form-urlencoded\r\n" \
	"CONTENT-LENGTH: %d\r\n\r\n%s"

#define FACEBOOK_APP_VERSION "1.0"
#define FACEBOOK_ERROR_NODE_NAME "error_response"

static void facebook_request_auth_get_session_notify_cb(gpointer data);

static char*	facebook_secret = NULL; //starts as app secret, but changes once we get a session
static char* 	facebook_auth_token = NULL; //http://api.facebook.com
static char*	facebook_session_key = NULL; //http://api.facebook.com
static char*	facebook_uid = NULL; //the current users uid

static GHashTable*		facebook_hijacked_tooltips_table; //will store our hijacked tooltip functions
static GHashTable*		facebook_friends_table; //store our friends

static guint	facebook_request_timer_handle;

typedef void (*facebook_xml_parse_callback) (xmlnode *node); //CB for parsed data returned from facebook
typedef gboolean (*facebook_xml_parse_error_callback) (char *error_code, char *error_msg, xmlnode *node); //error returned from fb

//the previous ops->notify_userinfo function, see below
static void *(*hijacked_notify_userinfo) (PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info);

//Will hold the information we collect for our facebook friends
typedef struct _facebook_friend
{
	char *uid; //facebook user id, unique per user (should be a long?)
	char *status; //user's status, like an away
	char *first_name;
	char *last_name;
} facebook_friend;

//Will hold callback data for facebook requests
typedef struct _facebook_request_callback_data
{
	char *method;
	facebook_xml_parse_callback success_cb; //cb when success
	facebook_xml_parse_error_callback error_cb; //cb when error
} facebook_request_callback_data;

//Wrapper to get a child node's data
//free me
static char *
xmlnode_get_child_data(xmlnode *node, char *child_name)
{
	xmlnode *child;
	if (!node)
		return g_strdup("");

	child = xmlnode_get_child(node, child_name);

	if (child)
		return xmlnode_get_data(child);

	return g_strdup("");
}


static const char *
facebook_get_api_key()
{
#if FACEBOOK_APP_KEY_EDITABLE 
	return purple_prefs_get_string(PREF_API_KEY);
#else
	return FACEBOOK_APP_DEFAULT_API_KEY;
#endif
}

static const char *
facebook_get_secret()
{
	if (facebook_secret)
		return facebook_secret;

#if FACEBOOK_APP_KEY_EDITABLE 
	return purple_prefs_get_string(PREF_SECRET);
#else
	return FACEBOOK_APP_DEFAULT_SECRET;
#endif
}
//Create new facebook friend
static facebook_friend *
facebook_friend_new(char *uid, char *first_name, char *last_name, char *status)
{
	facebook_friend *f;
	if (!uid || !first_name || !last_name)
		return NULL;

	f = (facebook_friend*) malloc(sizeof(facebook_friend));
	f->uid = g_strdup(uid);
	f->first_name = g_strdup(first_name);
	f->last_name = g_strdup(last_name);
	f->status = g_strdup(status ? status : "");

	return f;
}

static char *
facebook_friend_get_uid(facebook_friend *f)
{
	return f->uid;
}

static const char *
facebook_friend_get_status(facebook_friend *f)
{
	return f->status;
}

static const char *
facebook_friend_get_first_name(facebook_friend *f)
{
	return f->first_name;
}

/* Not being used right now
static const char *
facebook_friend_get_last_name(facebook_friend *f)
{
	return f->last_name;
}*/

//Full name = first_name last_name
//free me when done
static char *
facebook_friend_get_full_name(facebook_friend *f)
{
	return g_strdup_printf("%s %s", f->first_name, f->last_name);
}

/* Not being used
//Set the status for the facebook friend
//Note that this frees the previous status, so strdup it if you still need it
static void
facebook_friend_set_status(facebook_friend *f, char *status)
{
	g_free(f->status);
	f->status = g_strdup(status == NULL ? "" : status);
}*/

//For sorting friends
static int facebook_friend_name_cmp(const facebook_friend *a, const facebook_friend * b)
{
	int rv = strcmp(a->first_name, b->first_name);
	if (rv != 0)
		return rv;
	return strcmp(a->last_name, b->last_name);
}

static void
facebook_friend_free(facebook_friend *f)
{
	g_free(f->uid);
	g_free(f->first_name);
	g_free(f->last_name);
	g_free(f->status);
	g_free(f);
}

//Get a facebook friend based on uid
static facebook_friend *
facebook_find_friend(const char *uid)
{
	return (facebook_friend *) g_hash_table_lookup(facebook_friends_table, uid);
}

//Save the friend into our table
//Note, this uses replace, so save any previous friend data or it gets destroyed
static void
purple_facebook_save_facebook_friend(facebook_friend *f)
{
	//We're just going to do a replace instead of a status update
	//because maybe they updated their name etc
	g_hash_table_replace(facebook_friends_table, facebook_friend_get_uid(f), f);
}


//Gets the buddy's facebook uid or null
static const char *
purple_buddy_get_facebook_friend_uid(PurpleBuddy *b)
{
	return purple_blist_node_get_string((PurpleBlistNode *)b, "facebook_uid");
}

//Sets the facebook friend uid for a buddy
static void
purple_buddy_set_facebook_friend_uid(PurpleBuddy *b, char *uid)
{
	purple_blist_node_set_string((PurpleBlistNode *)b, "facebook_uid", uid);
}

//Sets the facebook friend for a buddy
//This is just a wrapper for set facebook uid
static void
purple_buddy_set_facebook_friend(PurpleBuddy *b, facebook_friend *f)
{
	purple_buddy_set_facebook_friend_uid(b, facebook_friend_get_uid(f));
}

static facebook_friend *
purple_buddy_get_facebook_friend(PurpleBuddy *b)
{
	const char *uid = purple_buddy_get_facebook_friend_uid(b);

	if (uid == NULL)
		return NULL;

	return facebook_find_friend(uid);
}



//Creates a parameter to be sent through the http request
//free me when done
static char *
create_param(const char *name, const char *value)
{
	return g_strdup_printf("%s=%s", name, value);
}

//Wrapper to create a call_id param, api.facebook.com
static char *
create_call_id_param()
{
	GTimeVal time;
	char call_id[255];
	g_get_current_time(&time);
	sprintf(call_id, "%ld%06ld", time.tv_sec, time.tv_usec);
	return create_param("call_id", call_id);
}


//Used to sort parameters, just a wrapper for strcmp
static int
param_sort(const void *p1, const void *p2)
{
	return strcmp(*(char **) p1, *(char **) p2);
}

//creates a base 16 encoded md5 'encrypted' key
//free me
static char *
hexed_md5(unsigned char *text, int len)
{
	const int md5len = 16;
	PurpleCipher *cipher;
	PurpleCipherContext *context;
	guchar md5[md5len];

	cipher = purple_ciphers_find_cipher("md5");
	context = purple_cipher_context_new(cipher, NULL);
	purple_cipher_context_append(context, text, len);
	purple_cipher_context_digest(context, md5len, md5, NULL);
	purple_cipher_context_destroy(context);
	return purple_base16_encode(md5, md5len);
}

//Creates our signature, before it gets md5ed and hexed
static char *
create_premd5_sig(char **params, int n_param)
{
	int i;
	char *rv;
	char **new_params = malloc((n_param + 2) * sizeof(char *));

	for (i = 0; i < n_param; i++)
		new_params[i] = params[i];

	new_params[i++] = (char *) facebook_get_secret();

	new_params[i] = NULL;

	rv = g_strjoinv("", new_params);
	g_free(new_params);
	return rv;
}

//Our signature, look at facebook api
//Note that params must be sorted!
static char *
create_sig(char **params, int n_param)
{
	char *premd5sig, *sig;
	premd5sig = create_premd5_sig(params, n_param);
	
	sig = hexed_md5((unsigned char *) premd5sig, strlen(premd5sig));

	g_free(premd5sig);
	return sig;
}

//Create our post request parameter string
//ie, api_key=ABC&method=auth.createToken&...
static char *
create_facebook_request_param_string(char *method, char **params, int n_param)
{
	const int added_param_cnt_pre_sig = 3;
	const int added_param_cnt_post_sig = 4;
	const int total_params = n_param + added_param_cnt_post_sig;

	//new params will contain all of our parameters and will be NULL terminated, for sorting
	char **new_params = (char **) malloc(sizeof(char*) * (total_params + 1));
	char *param_string;
	char *sig;
	int i = 0;

	char *param_method, *param_version, *param_api_key, *param_sig;;

	for (i = 0; i < n_param; i++)
		new_params[i] = params[i];

	//These will need to be freed
	//this should probably check the method and add session where needed
	new_params[i++] = param_method = create_param("method", method);
	new_params[i++] = param_version = create_param("v", FACEBOOK_APP_VERSION);
	new_params[i++] = param_api_key = create_param("api_key", facebook_get_api_key());

	//Sort
	qsort(new_params, n_param + added_param_cnt_pre_sig, sizeof(char *), param_sort);

	//Finally create and add the sig
	sig = create_sig(new_params, n_param + added_param_cnt_pre_sig);
	new_params[i++] = param_sig = create_param("sig", sig);

	new_params[i] = NULL;

	//Join and we're done
	param_string = g_strjoinv("&", new_params);

	g_free(param_method);
	g_free(param_version);
	g_free(param_api_key);
	g_free(param_sig);
	g_free(sig);

	g_free(new_params);

	return param_string;
}

//This function takes care of any unhandled facebook request errors
static void
send_facebook_request_unhandled_error(char *error_code, char *error_msg, xmlnode *node)
{
	char *node_str;

	node_str = xmlnode_to_str(node, NULL);

	purple_debug_error(FACEBOOK_DEBUG_ID, "Unhandled error, code (%d), message (%d), data (%s)\n",
		error_code, error_msg, node_str);

	g_free(node_str);
}

static void 
send_facebook_request_cb(PurpleUtilFetchUrlData *url_data, gpointer cb_data,
	const gchar *url_text, gsize len, const gchar *error_message)
{
	xmlnode *node = NULL;
	facebook_request_callback_data *callback_data = (facebook_request_callback_data *) cb_data;

	purple_debug_info(FACEBOOK_DEBUG_ID, "Received request data, method %s\n", callback_data->method);

	if (!len)
	{
		//Error receiving data?
		purple_debug_error(FACEBOOK_DEBUG_ID, "Error sending out facebook request: %s\n", error_message);
	} else {
		//Create a node starting from the first < received to the last > received
		char *start = index(url_text, '<');
		char *end = rindex(url_text, '>') + 1;
		node = xmlnode_from_str(start, end - start);

		if (!node)
		{
			//malformed response by facebook, this shouldn't happen
			purple_debug_error(FACEBOOK_DEBUG_ID, 
				"Error creating node from text: (%s)\n", url_text);
		} else if (!strcmp(node->name, FACEBOOK_ERROR_NODE_NAME)) {
			//We got a valid response from facebook, but it was an error message
			char *error_code = xmlnode_get_child_data(node, "error_code"); //Is this an int?
			char *error_msg = xmlnode_get_child_data(node, "error_msg");
			if (callback_data->error_cb == NULL || !callback_data->error_cb(error_code, error_msg, node))
			{
				//If no error handler or error handler didn't handle it, call generic function
				send_facebook_request_unhandled_error(error_code, error_msg, node);
			}
			g_free(error_code);
			g_free(error_msg);
		} else {
			//Got valid data, call the handler
			if (callback_data->success_cb != NULL)
				callback_data->success_cb(node);
		}
		xmlnode_free(node);
	}

	g_free(callback_data->method);
	g_free(callback_data);

}

//Send a request to facebook
//success_cb will be called if its a successful call
//error_cb will be called if error xml is received
//params is an array of create_param()'s
//n_param is the count of params
//Note: this should be updated to automatically add session param when needed
static void send_facebook_request(char *method,
	facebook_xml_parse_callback success_cb, facebook_xml_parse_error_callback error_cb,
	char **params, int n_param)
{
	char *param_string, *request;

	//This will be send to the send_facebook_request_cb()
	//must free
	facebook_request_callback_data *callback_data = 
		(facebook_request_callback_data *) malloc(sizeof(facebook_request_callback_data));

	callback_data->method = g_strdup(method);
	callback_data->success_cb = success_cb;
	callback_data->error_cb = error_cb;

	//Create our request
	param_string = create_facebook_request_param_string(method, params, n_param);
	request = g_strdup_printf(FACEBOOK_REQUEST_HEADER, (int)strlen(param_string), param_string);
	purple_debug_info(FACEBOOK_DEBUG_ID, "Created request: (%s)\n", param_string);

	//util.h is my friend
	purple_util_fetch_url_request(
		FACEBOOK_REQUEST_URL, //URL
		TRUE, //full url
		NULL, //user agent
		TRUE, //HTTP 1.1
		request, //headers to send
		FALSE, //response includes headers
		send_facebook_request_cb, //callback
		callback_data //user data
		);

	g_free(param_string);
	g_free(request);
}

/*** Start: custom fql request, gets friends & status ***/
static void
facebook_request_fql_friends_status_cb(xmlnode *node)
{
	xmlnode *user_node;

	//Loop through the nodes and create & save the friends
	for (user_node = xmlnode_get_child(node, "user"); user_node; user_node = xmlnode_get_next_twin(user_node))
	{
		xmlnode *status_node;
		char *uid, *status_message, *first_name, *last_name;
		facebook_friend *f;

		status_node 	= xmlnode_get_child(user_node, "status");

		uid				= xmlnode_get_child_data(user_node, "uid");
		first_name		= xmlnode_get_child_data(user_node, "first_name");
		last_name		= xmlnode_get_child_data(user_node, "last_name");
		status_message	= xmlnode_get_child_data(status_node, "message");

		f = facebook_friend_new(uid, first_name, last_name, status_message);
		if (!f)
		{
			purple_debug_error(FACEBOOK_DEBUG_ID, 
				"Couldn't create facebook friend uid (%s) first_name (%s) last_name (%s)\n",
				uid, first_name, last_name);
		} else {
			purple_facebook_save_facebook_friend(f);
		}

		g_free(uid);
		g_free(first_name);
		g_free(last_name);
	    g_free(status_message);
	}
}

static gboolean
facebook_request_fql_friends_status()
{

	//Selects all of the users friends as well as the actual user
	const int param_count = 3;
	int i = 0;
	char *params[param_count];
	char *query;
	
	if (!facebook_uid || !facebook_session_key)
		return FALSE;

	query = g_strdup_printf(
		"SELECT uid, first_name, last_name, status.message FROM user WHERE uid IN (SELECT uid2 FROM friend WHERE uid1 = %s) OR uid = %s",
		facebook_uid, facebook_uid);

	params[i++] = create_param("session_key", facebook_session_key);
	params[i++] = create_call_id_param();
	params[i++] = create_param("query", query);

	send_facebook_request("fql.query", facebook_request_fql_friends_status_cb, NULL, params, 3);

	for (i = 0; i < param_count; i++)
		g_free(params[i]);
	g_free(query);

	return TRUE;
}
/*** End: custom fql request ***/

static gboolean
facebook_request_send_timer(gpointer data)
{
	return facebook_request_fql_friends_status();
}

static void
change_facebook_request_timer(int timeout)
{
	if (facebook_request_timer_handle)
		purple_timeout_remove(facebook_request_timer_handle);

	facebook_request_timer_handle = purple_timeout_add(timeout * 1000 * 60, facebook_request_send_timer, NULL);
}


/*** Start: facebook.auth.getSession ***/
static gboolean
facebook_request_auth_get_session_error_cb(char *error_code, char *error_msg, xmlnode *node)
{
	if (!strcmp(error_code, "100"))
	{
		//Invalid parameter... generally means user didn't authenticate
		purple_notify_message(NULL, PURPLE_NOTIFY_MSG_ERROR,
			"Could not authenticate", "Could not authenticate",
			"Couldn't authenticate with the facebook server. \nClick on Get Session to try again",
			NULL, NULL);
		return TRUE;
	} else {
		return FALSE;
	}
}
static void
facebook_request_auth_get_session_cb(xmlnode *node)
{
	char *new_uid, *new_session_key, *new_secret;
	new_uid 		= xmlnode_get_child_data(node, "uid");
	new_session_key	= xmlnode_get_child_data(node, "session_key");
	new_secret		= xmlnode_get_child_data(node, "secret");

	if (new_uid && new_session_key && new_secret)
	{
		g_free(facebook_uid);
		g_free(facebook_session_key);
		g_free(facebook_secret);

		facebook_uid = new_uid;
		facebook_session_key = new_session_key;
		facebook_secret = new_secret;
	}

	change_facebook_request_timer(purple_prefs_get_int(PREF_TIMEOUT));
	facebook_request_fql_friends_status();
}
static void
facebook_request_auth_get_session()
{
	char* auth_token = create_param("auth_token", facebook_auth_token);
	char* params[] = {auth_token};
	send_facebook_request("auth.getSession",
		facebook_request_auth_get_session_cb, facebook_request_auth_get_session_error_cb,
		params, 1);
	g_free(auth_token);

}
/*** End: facebook.auth.getSession ***/

static void
facebook_request_auth_get_session_notify_cb(gpointer data)
{
	facebook_request_auth_get_session();
}

/* Asks to visit the facebook login page */
static void
request_facebook_login(void)
{

	char *link, *message;
	g_return_if_fail(facebook_auth_token != NULL);

	link = g_strdup_printf(
		"http://www.facebook.com/login.php?api_key=%s"
		"&v=" FACEBOOK_APP_VERSION 
		"&auth_token=%s",
		facebook_get_api_key(),
		facebook_auth_token);

	message = g_strdup_printf(
		"Please visit the following to enable:<BR/>"
		"<a href=\"%s\">%s</a><BR/>Then click Close",
		link, link);

	//This should be an action, OK/Cancel type of thing
	purple_notify_formatted(NULL, "Enable facebook plugin",
		"Enable facebook plugin", "",
		message, facebook_request_auth_get_session_notify_cb,
		NULL);

	g_free(link);
	g_free(message);
}

/*** Start: facebook.auth.createToken ***/
static gboolean
facebook_request_auth_create_token_error_cb(char *error_code, char *error_msg, xmlnode *node)
{
	if (!strcmp(error_code, "101") || !strcmp(error_code, "104"))
	{
		//Invalid api key or invalid signature
		char *message = g_strdup_printf("Facebook error: %s", error_msg);
		purple_notify_message(NULL, PURPLE_NOTIFY_MSG_ERROR,
			"Could not create auth token", "Could not create auth token",
			message,
			NULL, NULL);
		g_free(message);
		return TRUE; //handled
	} else {
		//unhandled
		return FALSE;
	}
}

static void
facebook_request_auth_create_token_cb(xmlnode *node)
{
	if (facebook_auth_token)
		g_free(facebook_auth_token);
	facebook_auth_token = xmlnode_get_data(node);

	request_facebook_login();
	purple_debug_misc("facebook",
		"Authentication link: http://www.facebook.com/login.php?api_key=%s&v=%s&auth_token=%s\n",
		facebook_get_api_key(), facebook_get_secret(), facebook_auth_token);
}

static void
facebook_request_auth_create_token()
{
	if (facebook_secret)
		g_free(facebook_secret);
	facebook_secret = NULL;

	send_facebook_request("auth.createToken", 
		facebook_request_auth_create_token_cb, facebook_request_auth_create_token_error_cb,
		NULL, 0);
}
/*** End: facebook.auth.createToken ***/


static void
facebook_friend_chosen_cb(PurpleBlistNode *node, PurpleRequestFields *fields)
{
	PurpleRequestField *field = purple_request_fields_get_field(fields, "uid");
	const GList *l;

	g_return_if_fail(node != NULL);
	g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node));

	for (l = purple_request_field_list_get_selected(field); l != NULL; l = l->next)
	{
		purple_buddy_set_facebook_friend((PurpleBuddy *) node, 
			(facebook_friend*) purple_request_field_list_get_data(field, l->data));
	}

}

static void
facebook_getsession_action(PurplePluginAction *action)
{
	facebook_request_auth_create_token();
}

static void
facebook_refresh_action(PurplePluginAction *action)
{
	if (facebook_session_key)
		facebook_request_fql_friends_status();
	else
		purple_notify_message(NULL, PURPLE_NOTIFY_MSG_ERROR,
			"Not connected to facebook server", "Not connected to facebook server",
			"You must be connected to the facebook server to get data.\n"
			"Click on Get Session to connect",
			NULL, NULL);
}

static GList *
actions(PurplePlugin *plugin, gpointer context)
{
	GList *l = NULL;
	PurplePluginAction *act = NULL;

	//This seems to have broke in pidgin 2.0
	//Temporary fix in place
	/*if (facebook_session_key == NULL) //No session, should try to get one
	{
		act = purple_plugin_action_new("Get Session", facebook_getsession_action);
		l = g_list_append(l, act);
	} else {
		act = purple_plugin_action_new("Force refresh", facebook_refresh_action);
		l = g_list_append(l, act);
	}*/
	act = purple_plugin_action_new("Get/Reset Session", facebook_getsession_action);
	l = g_list_append(l, act);
	act = purple_plugin_action_new("Force data refresh", facebook_refresh_action);
	l = g_list_append(l, act);

	return l;
}

static PurpleNotifyUserInfoEntry*
purple_buddy_get_facebook_user_info_entry(PurpleBuddy *b)
{
	const char *status;
	char *status_message;
	PurpleNotifyUserInfoEntry *entry;
	facebook_friend *f = purple_buddy_get_facebook_friend(b);

	if (!f)
		return NULL;

	status = facebook_friend_get_status(f);
	status_message = (status && strcmp(status, "") ?
		g_strdup_printf("%s is %s", facebook_friend_get_first_name(f), facebook_friend_get_status(f)) :
		g_strdup(""));

	entry = purple_notify_user_info_entry_new("Facebook Status", status_message);

	g_free(status_message);

	return entry;
}
static void 
facebook_prpl_tooltip_text(PurpleBuddy *b, PurpleNotifyUserInfo *user_info, gboolean full)
{
	GList *ignore; //sigh
	void (*original_tooltip_text)(PurpleBuddy *b, PurpleNotifyUserInfo *user_info, gboolean full);
	PurplePluginProtocolInfo *prpl_info;
	PurpleNotifyUserInfoEntry *entry = purple_buddy_get_facebook_user_info_entry(b);;

	if (entry)
		ignore = g_list_append(purple_notify_user_info_get_entries(user_info), entry); //yuck

	//Get and call original tooltip text function
	prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(purple_find_prpl(purple_account_get_protocol_id(b->account)));
	original_tooltip_text = g_hash_table_lookup(facebook_hijacked_tooltips_table, prpl_info);

	//Oh I'm clever
	if (!g_ascii_strcasecmp(b->name, "robflynn"))
		purple_notify_user_info_add_pair(user_info, _("Facebook Description"), _("AOL slayer"));
	if (!g_ascii_strcasecmp(b->name, "seanegn"))
		purple_notify_user_info_add_pair(user_info, _("Facebook Status"), _("AOL CEO"));
	if (!g_ascii_strcasecmp(b->name, "chipx86"))
		purple_notify_user_info_add_pair(user_info, _("Facebook Status"), _("Eats AOL CDs for breakfast"));
	if (!g_ascii_strcasecmp(b->name, "neaveru"))
		purple_notify_user_info_add_pair(user_info, _("Facebook Description"), _("Please don't sue me"));
		//seriously, please don't, I have no money, I swear
		//I'm going to call this plugin "AIM ENHANCED, BY AOL"

	if (original_tooltip_text != NULL)
		original_tooltip_text(b, user_info, full);
}

//free our hijacked tooltips
static void 
facebook_hijacked_tooltip_free(gpointer key)
{
	//Set it back!
	((PurplePluginProtocolInfo* ) key)->tooltip_text = g_hash_table_lookup(facebook_hijacked_tooltips_table, key);
}

//gets all the protocol plugins and steal there tooltip making capabilities
static void 
hijack_prpl_info_tooltip_text()
{
	GList *l;
	PurplePlugin *plugin;
	PurplePluginProtocolInfo *prpl_info;

	for (l = purple_plugins_get_protocols(); l != NULL; l = l->next) 
	{
		plugin = (PurplePlugin *)l->data;
		prpl_info = PURPLE_PLUGIN_PROTOCOL_INFO(plugin);

		//We want to save it in a table, so we can call it later
		g_hash_table_insert(facebook_hijacked_tooltips_table, prpl_info, prpl_info->tooltip_text);
		prpl_info->tooltip_text = facebook_prpl_tooltip_text; //YOINK
	}
}

static void facebook_table_add_friend_to_list(gpointer key, gpointer value, gpointer user_data)
{
	GList *ignore;
	ignore = g_list_insert_sorted((GList *) user_data, value, (int (*) (gconstpointer, gconstpointer)) facebook_friend_name_cmp);
}

//Create a request to assign a friend to a node
static void
node_assign_friend_cb(PurpleBlistNode *node, gpointer data)
{

	PurpleRequestFields *request;
	PurpleRequestFieldGroup *group;
	PurpleRequestField *field;
	//Create a list, with a dummy node, which will always be first
	facebook_friend* dummy;
	GList *l = NULL;
	char *tmp;

	g_return_if_fail(node != NULL);
	g_return_if_fail(PURPLE_BLIST_NODE_IS_BUDDY(node));

	//Create a list with a dummy node (easier for cmp and hash run through function)
	dummy = facebook_friend_new("0", "", "", "");
	l = g_list_append(l, dummy);
	//Populate the list
	g_hash_table_foreach(facebook_friends_table, facebook_table_add_friend_to_list, l);

	group = purple_request_field_group_new(NULL);
	tmp = g_strdup_printf("Who is %s", ((PurpleBuddy *)node)->name);
	field = purple_request_field_list_new("uid", tmp);
	g_free(tmp);


	//Populate the request field with our list, skip the dummy
	for (l = g_list_first(l)->next; l != NULL; l = l->next)
	{
		char *name = facebook_friend_get_full_name((facebook_friend *) (l->data));
		if (l->data != dummy)
			purple_request_field_list_add(field, name, l->data);
		g_free(name);
	}
	purple_request_field_group_add_field(group, field);

	request = purple_request_fields_new();
	purple_request_fields_add_group(request, group);

	purple_request_fields(node,
			N_("Facebook Friend Picker"),
			"",
			NULL,
			request,
			_("_Set"), G_CALLBACK(facebook_friend_chosen_cb),
			_("_Cancel"), NULL,
			((PurpleBuddy *)node)->account, ((PurpleBuddy *)node)->name, NULL,
			node);

	facebook_friend_free(dummy);
}

//Add option to assign a friend to a node
static void
facebook_extended_menu_cb(PurpleBlistNode *node, GList **m)
{
	PurpleMenuAction *bna = NULL;

	if (!PURPLE_BLIST_NODE_IS_BUDDY(node)) return;

	*m = g_list_append(*m, NULL);
	bna = purple_menu_action_new(_("Assign Facebook User"), PURPLE_CALLBACK(node_assign_friend_cb), NULL, NULL);
	*m = g_list_append(*m, bna);
}

static gboolean
plugin_unload(PurplePlugin *plugin)
{
	PurpleNotifyUiOps *ops = purple_notify_get_ui_ops();

	purple_timeout_remove(facebook_request_timer_handle);
	g_free(facebook_secret);
	g_free(facebook_auth_token);
	g_free(facebook_session_key);
	g_free(facebook_uid);

	g_hash_table_destroy(facebook_hijacked_tooltips_table);
	g_hash_table_destroy(facebook_friends_table);

	if (ops)
		ops->notify_userinfo = hijacked_notify_userinfo;

	return TRUE;
}

//Adds facebook status for a user info
static void *
facebook_notify_userinfo(PurpleConnection *gc, const char *who, PurpleNotifyUserInfo *user_info)
{
	GList *l, *start, *ignore;
	PurpleNotifyUserInfoEntry *entry;
	start = purple_notify_user_info_get_entries(user_info);

	//Set l to the first divider in the list
	for (l = start; l != NULL; l = l->next)
	{
		if (purple_notify_user_info_entry_get_type((PurpleNotifyUserInfoEntry *) (l->data)) == PURPLE_NOTIFY_USER_INFO_ENTRY_SECTION_BREAK)
			break;
	}

	entry = purple_buddy_get_facebook_user_info_entry(purple_find_buddy(purple_connection_get_account(gc), who));

	if (entry && start)
	{
		//Note: Since we don't have direct access to user_info, we can't make this the first element
		//in the list
		if (l == start && l->next)
		{
			//if the first entry is a divider (why?), just insert it afterwards
			ignore = g_list_insert_before(start, l->next, entry);
		} else if (l == start) {
			//if the first entry is a divider and no other entries, insert at end
			ignore = g_list_append(start, entry);
		} else {
			//insert entry before first divider
			ignore = g_list_insert_before(start, l, entry);
		}
	}

	//And pretend like nothing happened
	return hijacked_notify_userinfo(gc, who, user_info);
}

static void
hijack_user_info_handler(void)
{
	PurpleNotifyUiOps *ops;
	ops = purple_notify_get_ui_ops();

	if (!ops || !ops->notify_userinfo)
		return;

	hijacked_notify_userinfo = ops->notify_userinfo;
	ops->notify_userinfo = facebook_notify_userinfo;
}


static gboolean
plugin_load(PurplePlugin *plugin)
{
	//Some of this should probably be moved to init_plugin

	facebook_auth_token = NULL;
	facebook_session_key = NULL;
	facebook_uid = NULL;
	facebook_secret = NULL;

	facebook_hijacked_tooltips_table = g_hash_table_new_full(NULL, NULL, facebook_hijacked_tooltip_free, NULL);
	facebook_friends_table = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, (void(*)(gpointer)) facebook_friend_free);

	hijack_prpl_info_tooltip_text();
	hijack_user_info_handler();

	if (purple_prefs_get_bool(PREF_ONLOAD))
			facebook_request_auth_create_token();

	purple_signal_connect(purple_blist_get_handle(), "blist-node-extended-menu",
						plugin, PURPLE_CALLBACK(facebook_extended_menu_cb), NULL);

	return TRUE;
}

static void
facebook_timeout_pref_cb(const char *name, PurplePrefType type,
	gconstpointer val, gpointer data)
{
	change_facebook_request_timer((int) val);
}

static PurplePluginPrefFrame *
get_plugin_pref_frame(PurplePlugin *plugin)
{
	PurplePluginPrefFrame *frame;
	PurplePluginPref *ppref;

	frame = purple_plugin_pref_frame_new();

	ppref = purple_plugin_pref_new_with_name_and_label(
		PREF_ONLOAD, _("_Connect on start"));
	purple_plugin_pref_frame_add(frame, ppref);

	ppref = purple_plugin_pref_new_with_name_and_label(
		PREF_TIMEOUT, _("Time to update facebook statuses (mins)"));
	purple_plugin_pref_set_bounds(ppref, 5, 60); //if you want it below 5 minutes... go outside
	purple_plugin_pref_frame_add(frame, ppref);

#if FACEBOOK_APP_KEY_EDITABLE 
	ppref = purple_plugin_pref_new_with_name_and_label(
		PREF_API_KEY, _("Facebook API Key"));
	purple_plugin_pref_frame_add(frame, ppref);

	ppref = purple_plugin_pref_new_with_name_and_label(
		PREF_SECRET, _("Facebook Secret"));
	purple_plugin_pref_frame_add(frame, ppref);
#endif

	return frame;
}

static PurplePluginUiInfo prefs_info =
{
	get_plugin_pref_frame,
	0,   /* page_num (Reserved) */
	NULL, /* frame (Reserved) */
	/* Padding */
	NULL,
	NULL,
	NULL,
	NULL
};

static PurplePluginInfo info =
{
	PURPLE_PLUGIN_MAGIC,
	PURPLE_MAJOR_VERSION,
	PURPLE_MINOR_VERSION,
	PURPLE_PLUGIN_STANDARD,                             /**< type           */
	NULL,                                             /**< ui_requirement */
	0,                                                /**< flags          */
	NULL,                                             /**< dependencies   */
	PURPLE_PRIORITY_DEFAULT,                            /**< priority       */

	FACEBOOK_PLUGIN_ID,                               /**< id             */
	N_("Facebook"),                                   /**< name           */
	VERSION,                                          /**< version        */
	                                                  /**  summary        */
	N_("Purple2 facebook plugin"),
	                                                  /**  description    */
	N_("Integrate facebook with purple"),
	"Neaveru <neaveru@gmail.com>",			  		  /**< author         */
	"http://www.neaveru.com",                         /**< homepage       */

	plugin_load,                                      /**< load           */
	plugin_unload,                                    /**< unload         */
	NULL,                                             /**< destroy        */

	NULL,                                             /**< ui_info        */
	NULL,                                             /**< extra_info     */
	&prefs_info,                                      /**< prefs_info     */
	actions,
	/* padding */
	NULL,
	NULL,
	NULL,
	NULL
};


static void
init_plugin(PurplePlugin *plugin)
{
	purple_prefs_add_none(PREFS_BASE);
	purple_prefs_add_bool(PREF_ONLOAD, TRUE);
	purple_prefs_add_int(PREF_TIMEOUT, 15);
	purple_prefs_connect_callback(NULL, PREF_TIMEOUT, facebook_timeout_pref_cb, NULL); //should this be here?
#if FACEBOOK_APP_KEY_EDITABLE 
	purple_prefs_add_string(PREF_API_KEY, FACEBOOK_APP_DEFAULT_API_KEY);
	purple_prefs_add_string(PREF_SECRET, FACEBOOK_APP_DEFAULT_SECRET);
#endif
}

PURPLE_INIT_PLUGIN(facebook, init_plugin, info)