/* 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 <json-glib/json-glib.h>
#include <stdarg.h>
#include <string.h>

#include "glibcompat.h"

#include "api.h"
#include "http.h"
#include "json.h"
#include "marshal.h"
#include "thrift.h"
#include "util.h"

typedef struct _FbApiData FbApiData;

enum
{
	PROP_0,

	PROP_CID,
	PROP_DID,
	PROP_MID,
	PROP_STOKEN,
	PROP_TOKEN,
	PROP_UID,

	PROP_N
};

struct _FbApiPrivate
{
	FbMqtt *mqtt;
	FbHttpConns *cons;
	PurpleConnection *gc;
	GHashTable *data;
	gboolean retrying;

	FbId uid;
	gint64 sid;
	guint64 mid;
	gchar *cid;
	gchar *did;
	gchar *stoken;
	gchar *token;

	GQueue *msgs;
	gboolean invisible;
	guint unread;
	FbId lastmid;
};

struct _FbApiData
{
	gpointer data;
	GDestroyNotify func;
};

static void
fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg);

static void
fb_api_contacts_after(FbApi *api, const gchar *cursor);

static void
fb_api_message_send(FbApi *api, FbApiMessage *msg);

static void
fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg);

G_DEFINE_TYPE(FbApi, fb_api, G_TYPE_OBJECT);

static void
fb_api_set_property(GObject *obj, guint prop, const GValue *val,
                    GParamSpec *pspec)
{
	FbApiPrivate *priv = FB_API(obj)->priv;

	switch (prop) {
	case PROP_CID:
		g_free(priv->cid);
		priv->cid = g_value_dup_string(val);
		break;
	case PROP_DID:
		g_free(priv->did);
		priv->did = g_value_dup_string(val);
		break;
	case PROP_MID:
		priv->mid = g_value_get_uint64(val);
		break;
	case PROP_STOKEN:
		g_free(priv->stoken);
		priv->stoken = g_value_dup_string(val);
		break;
	case PROP_TOKEN:
		g_free(priv->token);
		priv->token = g_value_dup_string(val);
		break;
	case PROP_UID:
		priv->uid = g_value_get_int64(val);
		break;

	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec);
		break;
	}
}

static void
fb_api_get_property(GObject *obj, guint prop, GValue *val, GParamSpec *pspec)
{
	FbApiPrivate *priv = FB_API(obj)->priv;

	switch (prop) {
	case PROP_CID:
		g_value_set_string(val, priv->cid);
		break;
	case PROP_DID:
		g_value_set_string(val, priv->did);
		break;
	case PROP_MID:
		g_value_set_uint64(val, priv->mid);
		break;
	case PROP_STOKEN:
		g_value_set_string(val, priv->stoken);
		break;
	case PROP_TOKEN:
		g_value_set_string(val, priv->token);
		break;
	case PROP_UID:
		g_value_set_int64(val, priv->uid);
		break;

	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop, pspec);
		break;
	}
}


static void
fb_api_dispose(GObject *obj)
{
	FbApiData *fata;
	FbApiPrivate *priv = FB_API(obj)->priv;
	GHashTableIter iter;

	fb_http_conns_cancel_all(priv->cons);
	g_hash_table_iter_init(&iter, priv->data);

	while (g_hash_table_iter_next(&iter, NULL, (gpointer) &fata)) {
		fata->func(fata->data);
		g_free(fata);
	}

	if (G_UNLIKELY(priv->mqtt != NULL)) {
		g_object_unref(priv->mqtt);
	}

	fb_http_conns_free(priv->cons);
	g_hash_table_destroy(priv->data);
	g_queue_free_full(priv->msgs, (GDestroyNotify) fb_api_message_free);

	g_free(priv->cid);
	g_free(priv->did);
	g_free(priv->stoken);
	g_free(priv->token);
}

static void
fb_api_class_init(FbApiClass *klass)
{
	GObjectClass *gklass = G_OBJECT_CLASS(klass);
	GParamSpec *props[PROP_N] = {NULL};

	gklass->set_property = fb_api_set_property;
	gklass->get_property = fb_api_get_property;
	gklass->dispose = fb_api_dispose;
	g_type_class_add_private(klass, sizeof (FbApiPrivate));

	/**
	 * FbApi:cid:
	 *
	 * The client identifier for MQTT. This value should be saved
	 * and loaded for persistence.
	 */
	props[PROP_CID] = g_param_spec_string(
		"cid",
		"Client ID",
		"Client identifier for MQTT",
		NULL,
		G_PARAM_READWRITE);

	/**
	 * FbApi:did:
	 *
	 * The device identifier for the MQTT message queue. This value
	 * should be saved and loaded for persistence.
	 */
	props[PROP_DID] = g_param_spec_string(
		"did",
		"Device ID",
		"Device identifier for the MQTT message queue",
		NULL,
		G_PARAM_READWRITE);

	/**
	 * FbApi:mid:
	 *
	 * The MQTT identifier. This value should be saved and loaded
	 * for persistence.
	 */
	props[PROP_MID] = g_param_spec_uint64(
		"mid",
		"MQTT ID",
		"MQTT identifier",
		0, G_MAXUINT64, 0,
		G_PARAM_READWRITE);

	/**
	 * FbApi:stoken:
	 *
	 * The synchronization token for the MQTT message queue. This
	 * value should be saved and loaded for persistence.
	 */
	props[PROP_STOKEN] = g_param_spec_string(
		"stoken",
		"Sync Token",
		"Synchronization token for the MQTT message queue",
		NULL,
		G_PARAM_READWRITE);

	/**
	 * FbApi:token:
	 *
	 * The access token for authentication. This value should be
	 * saved and loaded for persistence.
	 */
	props[PROP_TOKEN] = g_param_spec_string(
		"token",
		"Access Token",
		"Access token for authentication",
		NULL,
		G_PARAM_READWRITE);

	/**
	 * FbApi:uid:
	 *
	 * The #FbId of the user of the #FbApi.
	 */
	props[PROP_UID] = g_param_spec_int64(
		"uid",
		"User ID",
		"User identifier",
		0, G_MAXINT64, 0,
		G_PARAM_READWRITE);
	g_object_class_install_properties(gklass, PROP_N, props);

	/**
	 * FbApi::auth:
	 * @api: The #FbApi.
	 *
	 * Emitted upon the successful completion of the authentication
	 * process. This is emitted as a result of #fb_api_auth().
	 */
	g_signal_new("auth",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__VOID,
	             G_TYPE_NONE,
	             0);

	/**
	 * FbApi::connect:
	 * @api: The #FbApi.
	 *
	 * Emitted upon the successful completion of the connection
	 * process. This is emitted as a result of #fb_api_connect().
	 */
	g_signal_new("connect",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__VOID,
	             G_TYPE_NONE,
	             0);

	/**
	 * FbApi::contact:
	 * @api: The #FbApi.
	 * @user: The #FbApiUser.
	 *
	 * Emitted upon the successful reply of a contact request. This
	 * is emitted as a result of #fb_api_contact().
	 */
	g_signal_new("contact",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::contacts:
	 * @api: The #FbApi.
	 * @users: The #GSList of #FbApiUser's.
	 * @complete: #TRUE if the list is fetched, otherwise #FALSE.
	 *
	 * Emitted upon the successful reply of a contacts request.
	 * This is emitted as a result of #fb_api_contacts(). This can
	 * be emitted multiple times before the entire contacts list
	 * has been fetched. Use @complete for detecting the completion
	 * status of the list fetch.
	 */
	g_signal_new("contacts",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER_BOOLEAN,
	             G_TYPE_NONE,
	             2, G_TYPE_POINTER, G_TYPE_BOOLEAN);

	/**
	 * FbApi::error:
	 * @api: The #FbApi.
	 * @error: The #GError.
	 *
	 * Emitted whenever an error is hit within the #FbApi. This
	 * should disconnect the #FbApi with #fb_api_disconnect().
	 */
	g_signal_new("error",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__OBJECT,
	             G_TYPE_NONE,
	             1, G_TYPE_ERROR);

	/**
	 * FbApi::events:
	 * @api: The #FbApi.
	 * @events: The #GSList of #FbApiEvent's.
	 *
	 * Emitted upon incoming events from the stream.
	 */
	g_signal_new("events",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::messages:
	 * @api: The #FbApi.
	 * @msgs: The #GSList of #FbApiMessage's.
	 *
	 * Emitted upon incoming messages from the stream.
	 */
	g_signal_new("messages",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::presences:
	 * @api: The #FbApi.
	 * @press: The #GSList of #FbApiPresence's.
	 *
	 * Emitted upon incoming presences from the stream.
	 */
	g_signal_new("presences",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::thread:
	 * @api: The #FbApi.
	 * @thrd: The #FbApiThread.
	 *
	 * Emitted upon the successful reply of a thread request. This
	 * is emitted as a result of #fb_api_thread().
	 */
	g_signal_new("thread",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::thread-create:
	 * @api: The #FbApi.
	 * @tid: The thread #FbId.
	 *
	 * Emitted upon the successful reply of a thread creation
	 * request. This is emitted as a result of
	 * #fb_api_thread_create().
	 */
	g_signal_new("thread-create",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__INT64,
	             G_TYPE_NONE,
	             1, FB_TYPE_ID);

	/**
	 * FbApi::threads:
	 * @api: The #FbApi.
	 * @thrds: The #GSList of #FbApiThread's.
	 *
	 * Emitted upon the successful reply of a threads request. This
	 * is emitted as a result of #fb_api_threads().
	 */
	g_signal_new("threads",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);

	/**
	 * FbApi::typing:
	 * @api: The #FbApi.
	 * @typg: The #FbApiTyping.
	 *
	 * Emitted upon an incoming typing state from the stream.
	 */
	g_signal_new("typing",
	             G_TYPE_FROM_CLASS(klass),
	             G_SIGNAL_ACTION,
	             0,
	             NULL, NULL,
	             fb_marshal_VOID__POINTER,
	             G_TYPE_NONE,
	             1, G_TYPE_POINTER);
}

static void
fb_api_init(FbApi *api)
{
	FbApiPrivate *priv;

	priv = G_TYPE_INSTANCE_GET_PRIVATE(api, FB_TYPE_API, FbApiPrivate);
	api->priv = priv;

	priv->cons = fb_http_conns_new();
	priv->msgs = g_queue_new();
	priv->data = g_hash_table_new_full(g_direct_hash, g_direct_equal,
	                                   NULL, NULL);
}

GQuark
fb_api_error_quark(void)
{
	static GQuark q = 0;

	if (G_UNLIKELY(q == 0)) {
		q = g_quark_from_static_string("fb-api-error-quark");
	}

	return q;
}

static void
fb_api_data_set(FbApi *api, gpointer handle, gpointer data,
                GDestroyNotify func)
{
	FbApiData *fata;
	FbApiPrivate *priv = api->priv;

	fata = g_new0(FbApiData, 1);
	fata->data = data;
	fata->func = func;
	g_hash_table_replace(priv->data, handle, fata);
}

static gpointer
fb_api_data_take(FbApi *api, gconstpointer handle)
{
	FbApiData *fata;
	FbApiPrivate *priv = api->priv;
	gpointer data;

	fata = g_hash_table_lookup(priv->data, handle);

	if (fata == NULL) {
		return NULL;
	}

	data = fata->data;
	g_hash_table_remove(priv->data, handle);
	g_free(fata);
	return data;
}

static gboolean
fb_api_json_chk(FbApi *api, gconstpointer data, gssize size, JsonNode **node)
{
	const gchar *str;
	FbApiError errc = FB_API_ERROR_GENERAL;
	FbApiPrivate *priv;
	FbJsonValues *values;
	gboolean success = TRUE;
	gchar *msg;
	GError *err = NULL;
	gint64 code;
	guint i;
	JsonNode *root;

	static const gchar *exprs[] = {
		"$.error.message",
		"$.error.summary",
		"$.error_msg",
		"$.errorCode",
		"$.failedSend.errorMessage",
	};

	g_return_val_if_fail(FB_IS_API(api), FALSE);
	priv = api->priv;

	if (G_UNLIKELY(size == 0)) {
		fb_api_error(api, FB_API_ERROR_GENERAL, _("Empty JSON data"));
		return FALSE;
	}

	fb_util_debug(FB_UTIL_DEBUG_INFO, "Parsing JSON: %.*s\n",
	              (gint) size, (const gchar *) data);

	root = fb_json_node_new(data, size, &err);
	FB_API_ERROR_EMIT(api, err, return FALSE);

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.error_code");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.error.type");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.errorCode");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return FALSE
	);

	code = fb_json_values_next_int(values, 0);
	str = fb_json_values_next_str(values, NULL);

	if (purple_strequal(str, "OAuthException") || (code == 401)) {
		errc = FB_API_ERROR_AUTH;
		success = FALSE;

		g_free(priv->stoken);
		priv->stoken = NULL;

		g_free(priv->token);
		priv->token = NULL;
	}

	/* 509 is used for "invalid attachment id" */
	if (code == 509) {
		errc = FB_API_ERROR_NONFATAL;
		success = FALSE;
	}

	str = fb_json_values_next_str(values, NULL);

	if (purple_strequal(str, "ERROR_QUEUE_NOT_FOUND") ||
	    purple_strequal(str, "ERROR_QUEUE_LOST"))
	{
		errc = FB_API_ERROR_QUEUE;
		success = FALSE;

		g_free(priv->stoken);
		priv->stoken = NULL;
	}

	g_object_unref(values);

	for (msg = NULL, i = 0; i < G_N_ELEMENTS(exprs); i++) {
		msg = fb_json_node_get_str(root, exprs[i], NULL);

		if (msg != NULL) {
			success = FALSE;
			break;
		}
	}

	if (!success && (msg == NULL)) {
		msg = g_strdup(_("Unknown error"));
	}

	if (msg != NULL) {
		fb_api_error(api, errc, "%s", msg);
		json_node_free(root);
		g_free(msg);
		return FALSE;
	}

	if (node != NULL) {
		*node = root;
	} else {
		json_node_free(root);
	}

	return TRUE;
}

static gboolean
fb_api_http_chk(FbApi *api, PurpleHttpConnection *con, PurpleHttpResponse *res,
                JsonNode **root)
{
	const gchar *data;
	const gchar *msg;
	FbApiPrivate *priv = api->priv;
	gchar *emsg;
	GError *err = NULL;
	gint code;
	gsize size;

	if (fb_http_conns_is_canceled(priv->cons)) {
		return FALSE;
	}

	msg = purple_http_response_get_error(res);
	code = purple_http_response_get_code(res);
	data = purple_http_response_get_data(res, &size);
	fb_http_conns_remove(priv->cons, con);

	if (msg != NULL) {
		emsg = g_strdup_printf("%s (%d)", msg, code);
	} else {
		emsg = g_strdup_printf("%d", code);
	}

	fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Response (%p):", con);
	fb_util_debug(FB_UTIL_DEBUG_INFO, "  Response Error: %s", emsg);
	g_free(emsg);

	if (G_LIKELY(size > 0)) {
		fb_util_debug(FB_UTIL_DEBUG_INFO, "  Response Data: %.*s",
		              (gint) size, data);
	}

	if (fb_http_error_chk(res, &err) && (root == NULL)) {
		return TRUE;
	}

	/* Rudimentary check to prevent wrongful error parsing */
	if ((size < 2) || (data[0] != '{') || (data[size - 1] != '}')) {
		FB_API_ERROR_EMIT(api, err, return FALSE);
	}

	if (!fb_api_json_chk(api, data, size, root)) {
		if (G_UNLIKELY(err != NULL)) {
			g_error_free(err);
		}

		return FALSE;
	}

	FB_API_ERROR_EMIT(api, err, return FALSE);
	return TRUE;
}

static PurpleHttpConnection *
fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
                const gchar *method, FbHttpParams *params,
		PurpleHttpCallback callback)
{
	FbApiPrivate *priv = api->priv;
	gchar *data;
	gchar *key;
	gchar *val;
	GList *keys;
	GList *l;
	GString *gstr;
	PurpleHttpConnection *ret;
	PurpleHttpRequest *req;

	fb_http_params_set_str(params, "api_key", FB_API_KEY);
	fb_http_params_set_str(params, "device_id", priv->did);
	fb_http_params_set_str(params, "fb_api_req_friendly_name", name);
	fb_http_params_set_str(params, "format", "json");
	fb_http_params_set_str(params, "method", method);

	val = fb_util_get_locale();
	fb_http_params_set_str(params, "locale", val);
	g_free(val);

	req = purple_http_request_new(url);
	purple_http_request_set_max_len(req, -1);
	purple_http_request_set_method(req, "POST");

	/* Ensure an old signature is not computed */
	g_hash_table_remove(params, "sig");

	gstr = g_string_new(NULL);
	keys = g_hash_table_get_keys(params);
	keys = g_list_sort(keys, (GCompareFunc) g_ascii_strcasecmp);

	for (l = keys; l != NULL; l = l->next) {
		key = l->data;
		val = g_hash_table_lookup(params, key);
		g_string_append_printf(gstr, "%s=%s", key, val);
	}

	g_string_append(gstr, FB_API_SECRET);
	data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str,
	                                     gstr->len);
	fb_http_params_set_str(params, "sig", data);
	g_string_free(gstr, TRUE);
	g_list_free(keys);
	g_free(data);

	if (priv->token != NULL) {
		data = g_strdup_printf("OAuth %s", priv->token);
		purple_http_request_header_set(req, "Authorization", data);
		g_free(data);
	}

	purple_http_request_header_set(req, "User-Agent", FB_API_AGENT);

	data = fb_http_params_close(params, NULL);
	purple_http_request_set_contents(req, data, -1);
	ret = purple_http_request(priv->gc, req, callback, api);
	fb_http_conns_add(priv->cons, ret);
	purple_http_request_unref(req);

	fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Request (%p):", ret);
	fb_util_debug(FB_UTIL_DEBUG_INFO, "  Request URL: %s", url);
	fb_util_debug(FB_UTIL_DEBUG_INFO, "  Request Data: %s", data);

	g_free(data);
	return ret;
}

static PurpleHttpConnection *
fb_api_http_query(FbApi *api, gint64 query, JsonBuilder *builder,
                  PurpleHttpCallback hcb)
{
	const gchar *name;
	FbHttpParams *prms;
	gchar *json;

	switch (query) {
	case FB_API_QUERY_CONTACT:
		name = "UsersQuery";
		break;
	case FB_API_QUERY_CONTACTS:
		name = "FetchContactsFullQuery";
		break;
	case FB_API_QUERY_CONTACTS_AFTER:
		name = "FetchContactsFullWithAfterQuery";
		break;
	case FB_API_QUERY_STICKER:
		name = "FetchStickersWithPreviewsQuery";
		break;
	case FB_API_QUERY_THREAD:
		name = "ThreadQuery";
		break;
	case FB_API_QUERY_THREADS:
		name = "ThreadListQuery";
		break;
	case FB_API_QUERY_XMA:
		name = "XMAQuery";
		break;
	default:
		g_return_val_if_reached(NULL);
		return NULL;
	}

	prms = fb_http_params_new();
	json = fb_json_bldr_close(builder, JSON_NODE_OBJECT, NULL);

	fb_http_params_set_strf(prms, "query_id", "%" G_GINT64_FORMAT, query);
	fb_http_params_set_str(prms, "query_params", json);
	g_free(json);

	return fb_api_http_req(api, FB_API_URL_GQL, name, "get", prms, hcb);
}

static void
fb_api_cb_http_bool(PurpleHttpConnection *con, PurpleHttpResponse *res,
                    gpointer data)
{
	const gchar *hata;
	FbApi *api = data;

	if (!fb_api_http_chk(api, con, res, NULL)) {
		return;
	}

	hata = purple_http_response_get_data(res, NULL);

	if (!purple_strequal(hata, "true")) {
		fb_api_error(api, FB_API_ERROR,
		             _("Failed generic API operation"));
	}
}

static void
fb_api_cb_mqtt_error(FbMqtt *mqtt, GError *error, gpointer data)
{
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;

	if (!priv->retrying) {
		priv->retrying = TRUE;
		fb_util_debug_info("Attempting to reconnect the MQTT stream...");
		fb_api_connect(api, priv->invisible);
	} else {
		g_signal_emit_by_name(api, "error", error);
	}
}

static void
fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data)
{
	const GByteArray *bytes;
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;
	FbThrift *thft;
	GByteArray *cytes;
	GError *err = NULL;

	static guint8 flags = FB_MQTT_CONNECT_FLAG_USER |
	                      FB_MQTT_CONNECT_FLAG_PASS |
	                      FB_MQTT_CONNECT_FLAG_CLR;

	thft = fb_thrift_new(NULL, 0);

	/* Write the client identifier */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 1, 0);
	fb_thrift_write_str(thft, priv->cid);

	fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRUCT, 4, 1);

	/* Write the user identifier */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 1, 0);
	fb_thrift_write_i64(thft, priv->uid);

	/* Write the information string */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 2, 1);
	fb_thrift_write_str(thft, "");

	/* Write the UNKNOWN ("cp"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 3, 2);
	fb_thrift_write_i64(thft, 23);

	/* Write the UNKNOWN ("ecp"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 4, 3);
	fb_thrift_write_i64(thft, 26);

	/* Write the UNKNOWN */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 5, 4);
	fb_thrift_write_i32(thft, 1);

	/* Write the UNKNOWN ("no_auto_fg"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 6, 5);
	fb_thrift_write_bool(thft, TRUE);

	/* Write the visibility state */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 7, 6);
	fb_thrift_write_bool(thft, !priv->invisible);

	/* Write the device identifier */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 8, 7);
	fb_thrift_write_str(thft, priv->did);

	/* Write the UNKNOWN ("fg"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 9, 8);
	fb_thrift_write_bool(thft, TRUE);

	/* Write the UNKNOWN ("nwt"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 10, 9);
	fb_thrift_write_i32(thft, 1);

	/* Write the UNKNOWN ("nwst"?) */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 11, 10);
	fb_thrift_write_i32(thft, 0);

	/* Write the MQTT identifier */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 12, 11);
	fb_thrift_write_i64(thft, priv->mid);

	/* Write the UNKNOWN */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_LIST, 14, 12);
	fb_thrift_write_list(thft, FB_THRIFT_TYPE_I32, 0);
	fb_thrift_write_stop(thft);

	/* Write the token */
	fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 15, 14);
	fb_thrift_write_str(thft, priv->token);

	/* Write the STOP for the struct */
	fb_thrift_write_stop(thft);

	bytes = fb_thrift_get_bytes(thft);
	cytes = fb_util_zlib_deflate(bytes, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(thft);
		return;
	);

	fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes, "Writing connect");
	fb_mqtt_connect(mqtt, flags, cytes);

	g_byte_array_free(cytes, TRUE);
	g_object_unref(thft);
}

static void
fb_api_connect_queue(FbApi *api)
{
	FbApiMessage *msg;
	FbApiPrivate *priv = api->priv;
	gchar *json;
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_int(bldr, "delta_batch_size", 125);
	fb_json_bldr_add_int(bldr, "max_deltas_able_to_process", 1250);
	fb_json_bldr_add_int(bldr, "sync_api_version", 3);
	fb_json_bldr_add_str(bldr, "encoding", "JSON");

	if (priv->stoken == NULL) {
		fb_json_bldr_add_int(bldr, "initial_titan_sequence_id",
		                     priv->sid);
		fb_json_bldr_add_str(bldr, "device_id", priv->did);
		fb_json_bldr_add_int(bldr, "entity_fbid", priv->uid);

		fb_json_bldr_obj_begin(bldr, "queue_params");
		fb_json_bldr_add_str(bldr, "buzz_on_deltas_enabled", "false");

		fb_json_bldr_obj_begin(bldr, "graphql_query_hashes");
		fb_json_bldr_add_str(bldr, "xma_query_id",
		                     G_STRINGIFY(FB_API_QUERY_XMA));
		fb_json_bldr_obj_end(bldr);

		fb_json_bldr_obj_begin(bldr, "graphql_query_params");
		fb_json_bldr_obj_begin(bldr, G_STRINGIFY(FB_API_QUERY_XMA));
		fb_json_bldr_add_str(bldr, "xma_id", "<ID>");
		fb_json_bldr_obj_end(bldr);
		fb_json_bldr_obj_end(bldr);
		fb_json_bldr_obj_end(bldr);

		json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
		fb_api_publish(api, "/messenger_sync_create_queue", "%s",
		               json);
		g_free(json);
		return;
	}

	fb_json_bldr_add_int(bldr, "last_seq_id", priv->sid);
	fb_json_bldr_add_str(bldr, "sync_token", priv->stoken);

	json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
	fb_api_publish(api, "/messenger_sync_get_diffs", "%s", json);
	g_signal_emit_by_name(api, "connect");
	g_free(json);

	if (!g_queue_is_empty(priv->msgs)) {
		msg = g_queue_peek_head(priv->msgs);
		fb_api_message_send(api, msg);
	}

	if (priv->retrying) {
		priv->retrying = FALSE;
		fb_util_debug_info("Reconnected the MQTT stream");
	}
}

static void
fb_api_cb_seqid(PurpleHttpConnection *con, PurpleHttpResponse *res,
                gpointer data)
{
	const gchar *str;
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.viewer.message_threads.sync_sequence_id");
	fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE,
	                   "$.viewer.message_threads.unread_count");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	str = fb_json_values_next_str(values, "0");
	priv->sid = g_ascii_strtoll(str, NULL, 10);
	priv->unread = fb_json_values_next_int(values, 0);

	fb_api_connect_queue(api);
	g_object_unref(values);
	json_node_free(root);
}

static void
fb_api_cb_mqtt_connect(FbMqtt *mqtt, gpointer data)
{
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;
	gchar *json;
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_bool(bldr, "foreground", TRUE);
	fb_json_bldr_add_int(bldr, "keepalive_timeout", FB_MQTT_KA);

	json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
	fb_api_publish(api, "/foreground_state", "%s", json);
	g_free(json);

	fb_mqtt_subscribe(mqtt,
		"/inbox", 0,
		"/mercury", 0,
		"/messaging_events", 0,
		"/orca_presence", 0,
		"/orca_typing_notifications", 0,
		"/pp", 0,
		"/t_ms", 0,
		"/t_p", 0,
		"/t_rtc", 0,
		"/webrtc", 0,
		"/webrtc_response", 0,
		NULL
	);

	/* Notifications seem to lead to some sort of sending rate limit */
	fb_mqtt_unsubscribe(mqtt, "/orca_message_notifications", NULL);

	if (priv->sid == 0) {
		bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
		fb_json_bldr_add_str(bldr, "1", "0");
		fb_api_http_query(api, FB_API_QUERY_THREADS, bldr,
		                  fb_api_cb_seqid);
	} else {
		fb_api_connect_queue(api);
	}
}

static void
fb_api_cb_publish_mark(FbApi *api, GByteArray *pload)
{
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_json_chk(api, pload->data, pload->len, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE, "$.succeeded");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	if (!fb_json_values_next_bool(values, TRUE)) {
		fb_api_error(api, FB_API_ERROR_GENERAL,
		             _("Failed to mark thread as read"));
	}

	g_object_unref(values);
	json_node_free(root);
}

static GSList *
fb_api_event_parse(FbApi *api, FbApiEvent *event, GSList *events,
                   JsonNode *root, GError **error)
{
	const gchar *str;
	FbApiEvent *devent;
	FbJsonValues *values;
	GError *err = NULL;
	guint i;

	static const struct {
		FbApiEventType type;
		const gchar *expr;
	} evtypes[] = {
		{
			FB_API_EVENT_TYPE_THREAD_USER_ADDED,
			"$.log_message_data.added_participants"
		}, {
			FB_API_EVENT_TYPE_THREAD_USER_REMOVED,
			"$.log_message_data.removed_participants"
		}
	};

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.log_message_type");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.author");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.log_message_data.name");
	fb_json_values_update(values, &err);

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
		g_object_unref(values);
		return events;
	}

	str = fb_json_values_next_str(values, NULL);

	if (g_strcmp0(str, "log:thread-name") == 0) {
		str = fb_json_values_next_str(values, "");
		str = strrchr(str, ':');

		if (str != NULL) {
			devent = fb_api_event_dup(event, FALSE);
			devent->type = FB_API_EVENT_TYPE_THREAD_TOPIC;
			devent->uid = FB_ID_FROM_STR(str + 1);
			devent->text = fb_json_values_next_str_dup(values, NULL);
			events = g_slist_prepend(events, devent);
		}
	}

	g_object_unref(values);

	for (i = 0; i < G_N_ELEMENTS(evtypes); i++) {
		values = fb_json_values_new(root);
		fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$");
		fb_json_values_set_array(values, FALSE, evtypes[i].expr);

		while (fb_json_values_update(values, &err)) {
			str = fb_json_values_next_str(values, "");
			str = strrchr(str, ':');

			if (str != NULL) {
				devent = fb_api_event_dup(event, FALSE);
				devent->type = evtypes[i].type;
				devent->uid = FB_ID_FROM_STR(str + 1);
				events = g_slist_prepend(events, devent);
			}
		}

		g_object_unref(values);

		if (G_UNLIKELY(err != NULL)) {
			g_propagate_error(error, err);
			break;
		}
	}

	return events;
}

static void
fb_api_cb_publish_mercury(FbApi *api, GByteArray *pload)
{
	const gchar *str;
	FbApiEvent event;
	FbJsonValues *values;
	GError *err = NULL;
	GSList *events = NULL;
	JsonNode *root;
	JsonNode *node;

	if (!fb_api_json_chk(api, pload->data, pload->len, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.thread_fbid");
	fb_json_values_set_array(values, FALSE, "$.actions");

	while (fb_json_values_update(values, &err)) {
		fb_api_event_reset(&event, FALSE);
		str = fb_json_values_next_str(values, "0");
		event.tid = FB_ID_FROM_STR(str);

		node = fb_json_values_get_root(values);
		events = fb_api_event_parse(api, &event, events, node, &err);
	}

	if (G_LIKELY(err == NULL)) {
		events = g_slist_reverse(events);
		g_signal_emit_by_name(api, "events", events);
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(events, (GDestroyNotify) fb_api_event_free);
	g_object_unref(values);
	json_node_free(root);

}

static void
fb_api_cb_publish_typing(FbApi *api, GByteArray *pload)
{
	const gchar *str;
	FbApiPrivate *priv = api->priv;
	FbApiTyping typg;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_json_chk(api, pload->data, pload->len, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.type");
	fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.sender_fbid");
	fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.state");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	str = fb_json_values_next_str(values, NULL);

	if (g_ascii_strcasecmp(str, "typ") == 0) {
		typg.uid = fb_json_values_next_int(values, 0);

		if (typg.uid != priv->uid) {
			typg.state = fb_json_values_next_int(values, 0);
			g_signal_emit_by_name(api, "typing", &typg);
		}
	}

	g_object_unref(values);
	json_node_free(root);
}

static void
fb_api_cb_publish_ms_r(FbApi *api, GByteArray *pload)
{
	FbApiMessage *msg;
	FbApiPrivate *priv = api->priv;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_json_chk(api, pload->data, pload->len, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.succeeded");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	if (fb_json_values_next_bool(values, TRUE)) {
		/* Pop and free the successful message */
		msg = g_queue_pop_head(priv->msgs);
		fb_api_message_free(msg);

		if (!g_queue_is_empty(priv->msgs)) {
			msg = g_queue_peek_head(priv->msgs);
			fb_api_message_send(api, msg);
		}
	} else {
		fb_api_error(api, FB_API_ERROR_GENERAL,
					 "Failed to send message");
	}

	g_object_unref(values);
	json_node_free(root);
}

static gchar *
fb_api_xma_parse(FbApi *api, const gchar *body, JsonNode *root, GError **error)
{
	const gchar *str;
	const gchar *url;
	FbHttpParams *params;
	FbJsonValues *values;
	gchar *text;
	GError *err = NULL;

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.story_attachment.target.__type__.name");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.story_attachment.url");
	fb_json_values_update(values, &err);

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
		g_object_unref(values);
		return NULL;
	}

	str = fb_json_values_next_str(values, NULL);
	url = fb_json_values_next_str(values, NULL);

	if ((str == NULL) || (url == NULL)) {
		text = g_strdup(_("<Unsupported Attachment>"));
		g_object_unref(values);
		return text;
	}

	if (purple_strequal(str, "ExternalUrl")) {
		params = fb_http_params_new_parse(url, TRUE);
		text = fb_http_params_dup_str(params, "u", NULL);
		fb_http_params_free(params);
	} else {
		text = g_strdup(url);
	}

	if (fb_http_urlcmp(body, text, FALSE)) {
		g_free(text);
		g_object_unref(values);
		return NULL;
	}

	g_object_unref(values);
	return text;
}

static GSList *
fb_api_message_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg,
                            GSList *msgs, const gchar *body, JsonNode *root,
                            GError **error)
{
	const gchar *str;
	FbApiMessage *dmsg;
	FbId id;
	FbJsonValues *values;
	gchar *xma;
	GError *err = NULL;
	JsonNode *node;
	JsonNode *xode;

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.xmaGraphQL");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE, "$.fbid");
	fb_json_values_set_array(values, FALSE, "$.deltaNewMessage"
	                                         ".attachments");

	while (fb_json_values_update(values, &err)) {
		str = fb_json_values_next_str(values, NULL);

		if (str == NULL) {
			id = fb_json_values_next_int(values, 0);
			dmsg = fb_api_message_dup(msg, FALSE);
			fb_api_attach(api, id, mid, dmsg);
			continue;
		}

		node = fb_json_node_new(str, -1, &err);

		if (G_UNLIKELY(err != NULL)) {
			break;
		}

		xode = fb_json_node_get_nth(node, 0);
		xma = fb_api_xma_parse(api, body, xode, &err);

		if (xma != NULL) {
			dmsg = fb_api_message_dup(msg, FALSE);
			dmsg->text = xma;
			msgs = g_slist_prepend(msgs, dmsg);
		}

		json_node_free(node);

		if (G_UNLIKELY(err != NULL)) {
			break;
		}
	}

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
	}

	g_object_unref(values);
	return msgs;
}

static void
fb_api_cb_publish_ms(FbApi *api, GByteArray *pload)
{
	const gchar *body;
	const gchar *data;
	const gchar *str;
	FbApiMessage *dmsg;
	FbApiMessage msg;
	FbApiPrivate *priv = api->priv;
	FbId id;
	FbId oid;
	FbJsonValues *values;
	FbThrift *thft;
	gchar *stoken;
	GError *err = NULL;
	GSList *msgs = NULL;
	guint size;
	JsonNode *root;
	JsonNode *node;

	/* Read identifier string (for Facebook employees) */
	thft = fb_thrift_new(pload, 0);
	fb_thrift_read_str(thft, NULL);
	size = fb_thrift_get_pos(thft);
	g_object_unref(thft);

	g_return_if_fail(size < pload->len);
	data = (gchar *) pload->data + size;
	size = pload->len - size;

	if (!fb_api_json_chk(api, data, size, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.lastIssuedSeqId");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.syncToken");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	priv->sid = fb_json_values_next_int(values, 0);
	stoken = fb_json_values_next_str_dup(values, NULL);
	g_object_unref(values);

	if (G_UNLIKELY(stoken != NULL)) {
		g_free(priv->stoken);
		priv->stoken = stoken;
		g_signal_emit_by_name(api, "connect");
		json_node_free(root);
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.messageMetadata.offlineThreadingId");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.messageMetadata.actorFbId");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.messageMetadata"
	                    ".threadKey.otherUserFbId");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.messageMetadata"
	                    ".threadKey.threadFbId");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.messageMetadata.timestamp");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.deltaNewMessage.body");
	fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
	                   "$.deltaNewMessage.stickerId");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.deltaNewMessage.messageMetadata.messageId");
	fb_json_values_set_array(values, TRUE, "$.deltas");

	while (fb_json_values_update(values, &err)) {
		id = fb_json_values_next_int(values, 0);

		/* Ignore everything but new messages */
		if (id == 0) {
			continue;
		}

		/* Ignore sequential duplicates */
		if (id == priv->lastmid) {
			fb_util_debug_info("Ignoring duplicate %" FB_ID_FORMAT, id);
			continue;
		}

		priv->lastmid = id;
		fb_api_message_reset(&msg, FALSE);
		msg.uid = fb_json_values_next_int(values, 0);
		oid = fb_json_values_next_int(values, 0);
		msg.tid = fb_json_values_next_int(values, 0);
		msg.tstamp = fb_json_values_next_int(values, 0);

		if (msg.uid == priv->uid) {
			msg.flags |= FB_API_MESSAGE_FLAG_SELF;

			if (msg.tid == 0) {
				msg.uid = oid;
			}
		}

		body = fb_json_values_next_str(values, NULL);

		if (body != NULL) {
			dmsg = fb_api_message_dup(&msg, FALSE);
			dmsg->text = g_strdup(body);
			msgs = g_slist_prepend(msgs, dmsg);
		}

		id = fb_json_values_next_int(values, 0);

		if (id != 0) {
			dmsg = fb_api_message_dup(&msg, FALSE);
			fb_api_sticker(api, id, dmsg);
		}

		str = fb_json_values_next_str(values, NULL);

		if (str == NULL) {
			continue;
		}

		node = fb_json_values_get_root(values);
		msgs = fb_api_message_parse_attach(api, str, &msg, msgs, body,
		                                   node, &err);

		if (G_UNLIKELY(err != NULL)) {
			break;
		}
	}

	if (G_LIKELY(err == NULL)) {
		msgs = g_slist_reverse(msgs);
		g_signal_emit_by_name(api, "messages", msgs);
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
	g_object_unref(values);
	json_node_free(root);
}

static void
fb_api_cb_publish_pt(FbThrift *thft, GSList **press, GError **error)
{
	FbApiPresence *pres;
	FbThriftType type;
	gint16 id;
	gint32 i32;
	gint64 i64;
	guint i;
	guint size = 0;

	/* Read identifier string (for Facebook employees) */
	FB_API_TCHK(fb_thrift_read_str(thft, NULL));

	/* Read the full list boolean field */
	FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0));
	FB_API_TCHK(type == FB_THRIFT_TYPE_BOOL);
	FB_API_TCHK(id == 1);
	FB_API_TCHK(fb_thrift_read_bool(thft, NULL));

	/* Read the list field */
	FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id));
	FB_API_TCHK(type == FB_THRIFT_TYPE_LIST);
	FB_API_TCHK(id == 2);

	/* Read the list */
	FB_API_TCHK(fb_thrift_read_list(thft, &type, &size));
	FB_API_TCHK(type == FB_THRIFT_TYPE_STRUCT);

	for (i = 0; i < size; i++) {
		/* Read the user identifier field */
		FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0));
		FB_API_TCHK(type == FB_THRIFT_TYPE_I64);
		FB_API_TCHK(id == 1);
		FB_API_TCHK(fb_thrift_read_i64(thft, &i64));

		/* Read the active field */
		FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id));
		FB_API_TCHK(type == FB_THRIFT_TYPE_I32);
		FB_API_TCHK(id == 2);
		FB_API_TCHK(fb_thrift_read_i32(thft, &i32));

		pres = fb_api_presence_dup(NULL);
		pres->uid = i64;
		pres->active = i32 != 0;
		*press = g_slist_prepend(*press, pres);

		fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d)",
		                   i64, i32 != 0);

		while (id <= 5) {
			if (fb_thrift_read_isstop(thft)) {
				break;
			}

			FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, id));

			switch (id) {
			case 3:
				/* Read the last active timestamp field */
				FB_API_TCHK(type == FB_THRIFT_TYPE_I64);
				FB_API_TCHK(fb_thrift_read_i64(thft, NULL));
				break;

			case 4:
				/* Read the active client bits field */
				FB_API_TCHK(type == FB_THRIFT_TYPE_I16);
				FB_API_TCHK(fb_thrift_read_i16(thft, NULL));
				break;

			case 5:
				/* Read the VoIP compatibility bits field */
				FB_API_TCHK(type == FB_THRIFT_TYPE_I64);
				FB_API_TCHK(fb_thrift_read_i64(thft, NULL));
				break;

			default:
				FB_API_TCHK(FALSE);
				break;
			}
		}

		/* Read the field stop */
		FB_API_TCHK(fb_thrift_read_stop(thft));
	}

	/* Read the field stop */
	FB_API_TCHK(fb_thrift_read_stop(thft));
}

static void
fb_api_cb_publish_p(FbApi *api, GByteArray *pload)
{
	FbThrift *thft;
	GError *err = NULL;
	GSList *press = NULL;

	thft = fb_thrift_new(pload, 0);
	fb_api_cb_publish_pt(thft, &press, &err);
	g_object_unref(thft);

	if (G_LIKELY(err == NULL)) {
		g_signal_emit_by_name(api, "presences", press);
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(press, (GDestroyNotify) fb_api_presence_free);
}

static void
fb_api_cb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, GByteArray *pload,
                       gpointer data)
{
	FbApi *api = data;
	gboolean comp;
	GByteArray *bytes;
	GError *err = NULL;
	guint i;

	static const struct {
		const gchar *topic;
		void (*func) (FbApi *api, GByteArray *pload);
	} parsers[] = {
		{"/mark_thread_response", fb_api_cb_publish_mark},
		{"/mercury", fb_api_cb_publish_mercury},
		{"/orca_typing_notifications", fb_api_cb_publish_typing},
		{"/send_message_response", fb_api_cb_publish_ms_r},
		{"/t_ms", fb_api_cb_publish_ms},
		{"/t_p", fb_api_cb_publish_p}
	};

	comp = fb_util_zlib_test(pload);

	if (G_LIKELY(comp)) {
		bytes = fb_util_zlib_inflate(pload, &err);
		FB_API_ERROR_EMIT(api, err, return);
	} else {
		bytes = (GByteArray *) pload;
	}

	fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes,
	                      "Reading message (topic: %s)",
			      topic);

	for (i = 0; i < G_N_ELEMENTS(parsers); i++) {
		if (g_ascii_strcasecmp(topic, parsers[i].topic) == 0) {
			parsers[i].func(api, bytes);
			break;
		}
	}

	if (G_LIKELY(comp)) {
		g_byte_array_free(bytes, TRUE);
	}
}

FbApi *
fb_api_new(PurpleConnection *gc)
{
	FbApi *api;
	FbApiPrivate *priv;

	api = g_object_new(FB_TYPE_API, NULL);
	priv = api->priv;

	priv->gc = gc;
	priv->mqtt = fb_mqtt_new(gc);

	g_signal_connect(priv->mqtt,
	                 "connect",
	                 G_CALLBACK(fb_api_cb_mqtt_connect),
	                 api);
	g_signal_connect(priv->mqtt,
	                 "error",
	                 G_CALLBACK(fb_api_cb_mqtt_error),
	                 api);
	g_signal_connect(priv->mqtt,
	                 "open",
	                 G_CALLBACK(fb_api_cb_mqtt_open),
	                 api);
	g_signal_connect(priv->mqtt,
	                 "publish",
	                 G_CALLBACK(fb_api_cb_mqtt_publish),
	                 api);

	return api;
}

void
fb_api_rehash(FbApi *api)
{
	FbApiPrivate *priv;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	if (priv->cid == NULL) {
		priv->cid = fb_util_rand_alnum(32);
	}

	if (priv->did == NULL) {
		priv->did = purple_uuid_random();
	}

	if (priv->mid == 0) {
		priv->mid = g_random_int();
	}

	if (strlen(priv->cid) > 20) {
		priv->cid = g_realloc_n(priv->cid , 21, sizeof *priv->cid);
		priv->cid[20] = 0;
	}
}

gboolean
fb_api_is_invisible(FbApi *api)
{
	FbApiPrivate *priv;

	g_return_val_if_fail(FB_IS_API(api), FALSE);
	priv = api->priv;

	return priv->invisible;
}

void
fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...)
{
	GError *err;
	va_list ap;

	g_return_if_fail(FB_IS_API(api));

	va_start(ap, format);
	err = g_error_new_valist(FB_API_ERROR, error, format, ap);
	va_end(ap);

	fb_api_error_emit(api, err);
}

void
fb_api_error_emit(FbApi *api, GError *error)
{
	g_return_if_fail(FB_IS_API(api));
	g_return_if_fail(error != NULL);

	g_signal_emit_by_name(api, "error", error);
	g_error_free(error);
}

static void
fb_api_cb_attach(PurpleHttpConnection *con, PurpleHttpResponse *res,
                 gpointer data)
{
	const gchar *str;
	FbApi *api = data;
	FbApiMessage *msg;
	FbJsonValues *values;
	gchar *name;
	GError *err = NULL;
	GSList *msgs = NULL;
	guint i;
	JsonNode *root;

	static const gchar *imgexts[] = {".jpg", ".png", ".gif"};

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.filename");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.redirect_uri");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	msg = fb_api_data_take(api, con);
	str = fb_json_values_next_str(values, NULL);
	name = g_ascii_strdown(str, -1);

	for (i = 0; i < G_N_ELEMENTS(imgexts); i++) {
		if (g_str_has_suffix(name, imgexts[i])) {
			msg->flags |= FB_API_MESSAGE_FLAG_IMAGE;
			break;
		}
	}

	g_free(name);
	msg->text = fb_json_values_next_str_dup(values, NULL);
	msgs = g_slist_prepend(msgs, msg);

	g_signal_emit_by_name(api, "messages", msgs);
	g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
	g_object_unref(values);
	json_node_free(root);

}

static void
fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg)
{
	FbHttpParams *prms;
	PurpleHttpConnection *http;

	prms = fb_http_params_new();
	fb_http_params_set_str(prms, "mid", msgid);
	fb_http_params_set_strf(prms, "aid", "%" FB_ID_FORMAT, aid);

	http = fb_api_http_req(api, FB_API_URL_ATTACH, "getAttachment",
	                       "messaging.getAttachment", prms,
			       fb_api_cb_attach);
	fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free);
}

static void
fb_api_cb_auth(PurpleHttpConnection *con, PurpleHttpResponse *res,
               gpointer data)
{
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.access_token");
	fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.uid");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	g_free(priv->token);
	priv->token = fb_json_values_next_str_dup(values, NULL);
	priv->uid = fb_json_values_next_int(values, 0);

	g_signal_emit_by_name(api, "auth");
	g_object_unref(values);
	json_node_free(root);
}

void
fb_api_auth(FbApi *api, const gchar *user, const gchar *pass)
{
	FbHttpParams *prms;

	prms = fb_http_params_new();
	fb_http_params_set_str(prms, "email", user);
	fb_http_params_set_str(prms, "password", pass);
	fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login",
	                prms, fb_api_cb_auth);
}

static gchar *
fb_api_user_icon_checksum(gchar *icon)
{
	gchar *csum;
	FbHttpParams *prms;

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

	prms = fb_http_params_new_parse(icon, TRUE);
	csum = fb_http_params_dup_str(prms, "oh", NULL);
	fb_http_params_free(prms);

	if (G_UNLIKELY(csum == NULL)) {
		/* Revert to the icon URL as the unique checksum */
		csum = g_strdup(icon);
	}

	return csum;
}

static void
fb_api_cb_contact(PurpleHttpConnection *con, PurpleHttpResponse *res,
                  gpointer data)
{
	const gchar *str;
	FbApi *api = data;
	FbApiUser user;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *node;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	node = fb_json_node_get_nth(root, 0);

	if (node == NULL) {
		fb_api_error(api, FB_API_ERROR_GENERAL,
		             _("Failed to obtain contact information"));
		json_node_free(root);
		return;
	}

	values = fb_json_values_new(node);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.name");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.profile_pic_large.uri");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	fb_api_user_reset(&user, FALSE);
	str = fb_json_values_next_str(values, "0");
	user.uid = FB_ID_FROM_STR(str);
	user.name = fb_json_values_next_str_dup(values, NULL);
	user.icon = fb_json_values_next_str_dup(values, NULL);

	user.csum = fb_api_user_icon_checksum(user.icon);

	g_signal_emit_by_name(api, "contact", &user);
	fb_api_user_reset(&user, TRUE);
	g_object_unref(values);
	json_node_free(root);
}

void
fb_api_contact(FbApi *api, FbId uid)
{
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_arr_begin(bldr, "0");
	fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid);
	fb_json_bldr_arr_end(bldr);

	fb_json_bldr_add_str(bldr, "1", "true");
	fb_api_http_query(api, FB_API_QUERY_CONTACT, bldr, fb_api_cb_contact);
}

static void
fb_api_cb_contacts(PurpleHttpConnection *con, PurpleHttpResponse *res,
                   gpointer data)
{
	const gchar *cursor;
	const gchar *str;
	FbApi *api = data;
	FbApiPrivate *priv = api->priv;
	FbApiUser *user;
	FbId uid;
	FbJsonValues *values;
	gboolean complete;
	GError *err = NULL;
	GSList *users = NULL;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.represented_profile.id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.represented_profile.friendship_status");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.structured_name.text");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.hugePictureUrl.uri");
	fb_json_values_set_array(values, FALSE, "$.viewer.messenger_contacts"
	                                         ".nodes");

	while (fb_json_values_update(values, &err)) {
		str = fb_json_values_next_str(values, "0");
		uid = FB_ID_FROM_STR(str);
		str = fb_json_values_next_str(values, NULL);

		if ((!purple_strequal(str, "ARE_FRIENDS") &&
		    (uid != priv->uid)) || (uid == 0))
		{
			continue;
		}

		user = fb_api_user_dup(NULL, FALSE);
		user->uid = uid;
		user->name = fb_json_values_next_str_dup(values, NULL);
		user->icon = fb_json_values_next_str_dup(values, NULL);

		user->csum = fb_api_user_icon_checksum(user->icon);

		users = g_slist_prepend(users, user);
	}

	g_object_unref(values);

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
                       "$.viewer.messenger_contacts.page_info.end_cursor");
	fb_json_values_update(values, NULL);

	cursor = fb_json_values_next_str(values, NULL);

	if (G_UNLIKELY(err == NULL)) {
		complete = (cursor == NULL);
		g_signal_emit_by_name(api, "contacts", users, complete);

		if (!complete) {
			fb_api_contacts_after(api, cursor);
		}
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(users, (GDestroyNotify) fb_api_user_free);
	g_object_unref(values);
	json_node_free(root);
}

void
fb_api_contacts(FbApi *api)
{
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_arr_begin(bldr, "0");
	fb_json_bldr_add_str(bldr, NULL, "user");
	fb_json_bldr_arr_end(bldr);

	fb_json_bldr_add_str(bldr, "1", G_STRINGIFY(FB_API_CONTACTS_COUNT));
	fb_api_http_query(api, FB_API_QUERY_CONTACTS, bldr,
	                  fb_api_cb_contacts);
}

static void
fb_api_contacts_after(FbApi *api, const gchar *cursor)
{
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_arr_begin(bldr, "0");
	fb_json_bldr_add_str(bldr, NULL, "user");
	fb_json_bldr_arr_end(bldr);

	fb_json_bldr_add_str(bldr, "1", cursor);
	fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT));
	fb_api_http_query(api, FB_API_QUERY_CONTACTS_AFTER, bldr,
	                  fb_api_cb_contacts);
}

void
fb_api_connect(FbApi *api, gboolean invisible)
{
	FbApiPrivate *priv;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	priv->invisible = invisible;
	fb_mqtt_open(priv->mqtt, FB_MQTT_HOST, FB_MQTT_PORT);
}

void
fb_api_disconnect(FbApi *api)
{
	FbApiPrivate *priv;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	fb_mqtt_disconnect(priv->mqtt);
}

static void
fb_api_message_send(FbApi *api, FbApiMessage *msg)
{
	const gchar *tpfx;
	FbApiPrivate *priv = api->priv;
	FbId id;
	FbId mid;
	gchar *json;
	JsonBuilder *bldr;

	mid = FB_API_MSGID(g_get_real_time() / 1000, g_random_int());
	priv->lastmid = mid;

	if (msg->tid != 0) {
		tpfx = "tfbid_";
		id = msg->tid;
	} else {
		tpfx = "";
		id = msg->uid;
	}

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_str(bldr, "body", msg->text);
	fb_json_bldr_add_strf(bldr, "msgid", "%" FB_ID_FORMAT, mid);
	fb_json_bldr_add_strf(bldr, "sender_fbid", "%" FB_ID_FORMAT, priv->uid);
	fb_json_bldr_add_strf(bldr, "to", "%s%" FB_ID_FORMAT, tpfx, id);

	json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
	fb_api_publish(api, "/send_message2", "%s", json);
	g_free(json);
}

void
fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text)
{
	FbApiMessage *msg;
	FbApiPrivate *priv;
	gboolean empty;

	g_return_if_fail(FB_IS_API(api));
	g_return_if_fail(text != NULL);
	priv = api->priv;

	msg = fb_api_message_dup(NULL, FALSE);
	msg->text = g_strdup(text);

	if (thread) {
		msg->tid = id;
	} else {
		msg->uid = id;
	}

	empty = g_queue_is_empty(priv->msgs);
	g_queue_push_tail(priv->msgs, msg);

	if (empty && fb_mqtt_connected(priv->mqtt, FALSE)) {
		fb_api_message_send(api, msg);
	}
}

void
fb_api_publish(FbApi *api, const gchar *topic, const gchar *format, ...)
{
	FbApiPrivate *priv;
	GByteArray *bytes;
	GByteArray *cytes;
	gchar *msg;
	GError *err = NULL;
	va_list ap;

	g_return_if_fail(FB_IS_API(api));
	g_return_if_fail(topic != NULL);
	g_return_if_fail(format != NULL);
	priv = api->priv;

	va_start(ap, format);
	msg = g_strdup_vprintf(format, ap);
	va_end(ap);

	bytes = g_byte_array_new_take((guint8 *) msg, strlen(msg));
	cytes = fb_util_zlib_deflate(bytes, &err);

	FB_API_ERROR_EMIT(api, err,
		g_byte_array_free(bytes, TRUE);
		return;
	);

	fb_util_debug_hexdump(FB_UTIL_DEBUG_INFO, bytes,
	                      "Writing message (topic: %s)",
			      topic);

	fb_mqtt_publish(priv->mqtt, topic, cytes);
	g_byte_array_free(cytes, TRUE);
	g_byte_array_free(bytes, TRUE);
}

void
fb_api_read(FbApi *api, FbId id, gboolean thread)
{
	const gchar *key;
	FbApiPrivate *priv;
	gchar *json;
	JsonBuilder *bldr;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_bool(bldr, "state", TRUE);
	fb_json_bldr_add_int(bldr, "syncSeqId", priv->sid);
	fb_json_bldr_add_str(bldr, "mark", "read");

	key = thread ? "threadFbId" : "otherUserFbId";
	fb_json_bldr_add_strf(bldr, key, "%" FB_ID_FORMAT, id);

	json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
	fb_api_publish(api, "/mark_thread", "%s", json);
	g_free(json);
}

static GSList *
fb_api_cb_unread_parse_attach(FbApi *api, const gchar *mid, FbApiMessage *msg,
                              GSList *msgs, JsonNode *root, GError **error)
{
	const gchar *str;
	FbApiMessage *dmsg;
	FbId id;
	FbJsonValues *values;
	GError *err = NULL;

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.attachment_fbid");
	fb_json_values_set_array(values, FALSE, "$.blob_attachments");

	while (fb_json_values_update(values, &err)) {
		str = fb_json_values_next_str(values, NULL);
		id = FB_ID_FROM_STR(str);
		dmsg = fb_api_message_dup(msg, FALSE);
		fb_api_attach(api, id, mid, dmsg);
	}

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
	}

	g_object_unref(values);
	return msgs;
}

static void
fb_api_cb_unread_msgs(PurpleHttpConnection *con, PurpleHttpResponse *res,
                      gpointer data)
{
	const gchar *body;
	const gchar *str;
	FbApi *api = data;
	FbApiMessage *dmsg;
	FbApiMessage msg;
	FbId id;
	FbId tid;
	FbJsonValues *values;
	gchar *xma;
	GError *err = NULL;
	GSList *msgs = NULL;
	JsonNode *node;
	JsonNode *root;
	JsonNode *xode;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	node = fb_json_node_get_nth(root, 0);

	if (node == NULL) {
		fb_api_error(api, FB_API_ERROR_GENERAL,
		             _("Failed to obtain unread messages"));
		json_node_free(root);
		return;
	}

	values = fb_json_values_new(node);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.thread_key.thread_fbid");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		return;
	);

	fb_api_message_reset(&msg, FALSE);
	str = fb_json_values_next_str(values, "0");
	tid = FB_ID_FROM_STR(str);
	g_object_unref(values);

	values = fb_json_values_new(node);
	fb_json_values_add(values, FB_JSON_TYPE_BOOL, TRUE, "$.unread");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.message_sender.messaging_actor.id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.message.text");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.timestamp_precise");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.sticker.id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.message_id");
	fb_json_values_set_array(values, FALSE, "$.messages.nodes");

	while (fb_json_values_update(values, &err)) {
		if (!fb_json_values_next_bool(values, FALSE)) {
			continue;
		}

		str = fb_json_values_next_str(values, "0");
		body = fb_json_values_next_str(values, NULL);

		fb_api_message_reset(&msg, FALSE);
		msg.uid = FB_ID_FROM_STR(str);
		msg.tid = tid;

		str = fb_json_values_next_str(values, "0");
		msg.tstamp = g_ascii_strtoll(str, NULL, 10);

		if (body != NULL) {
			dmsg = fb_api_message_dup(&msg, FALSE);
			dmsg->text = g_strdup(body);
			msgs = g_slist_prepend(msgs, dmsg);
		}

		str = fb_json_values_next_str(values, NULL);

		if (str != NULL) {
			dmsg = fb_api_message_dup(&msg, FALSE);
			id = FB_ID_FROM_STR(str);
			fb_api_sticker(api, id, dmsg);
		}

		node = fb_json_values_get_root(values);
		xode = fb_json_node_get(node, "$.extensible_attachment", NULL);

		if (xode != NULL) {
			xma = fb_api_xma_parse(api, body, xode, &err);

			if (xma != NULL) {
				dmsg = fb_api_message_dup(&msg, FALSE);
				dmsg->text = xma;
				msgs = g_slist_prepend(msgs, dmsg);
			}

			json_node_free(xode);

			if (G_UNLIKELY(err != NULL)) {
				break;
			}
		}

		str = fb_json_values_next_str(values, NULL);

		if (str == NULL) {
			continue;
		}

		msgs = fb_api_cb_unread_parse_attach(api, str, &msg, msgs,
		                                     node, &err);

		if (G_UNLIKELY(err != NULL)) {
			break;
		}
	}

	if (G_UNLIKELY(err == NULL)) {
		msgs = g_slist_reverse(msgs);
		g_signal_emit_by_name(api, "messages", msgs);
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
	g_object_unref(values);
	json_node_free(root);
}

static void
fb_api_cb_unread(PurpleHttpConnection *con, PurpleHttpResponse *res,
                 gpointer data)
{
	const gchar *id;
	FbApi *api = data;
	FbJsonValues *values;
	GError *err = NULL;
	gint64 count;
	JsonBuilder *bldr;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_INT, TRUE, "$.unread_count");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.thread_key.other_user_id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.thread_key.thread_fbid");
	fb_json_values_set_array(values, FALSE, "$.viewer.message_threads"
	                                         ".nodes");

	while (fb_json_values_update(values, &err)) {
		count = fb_json_values_next_int(values, -5);

		if (count < 1) {
			continue;
		}

		id = fb_json_values_next_str(values, NULL);

		if (id == NULL) {
			id = fb_json_values_next_str(values, "0");
		}

		bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
		fb_json_bldr_arr_begin(bldr, "0");
		fb_json_bldr_add_str(bldr, NULL, id);
		fb_json_bldr_arr_end(bldr);

		fb_json_bldr_add_str(bldr, "10", "true");
		fb_json_bldr_add_str(bldr, "11", "true");
		fb_json_bldr_add_int(bldr, "12", count);
		fb_json_bldr_add_str(bldr, "13", "false");
		fb_api_http_query(api, FB_API_QUERY_THREAD, bldr,
		                  fb_api_cb_unread_msgs);
	}

	if (G_UNLIKELY(err != NULL)) {
		fb_api_error_emit(api, err);
	}

	g_object_unref(values);
	json_node_free(root);
}

void
fb_api_unread(FbApi *api)
{
	FbApiPrivate *priv;
	JsonBuilder *bldr;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	if (priv->unread < 1) {
		return;
	}

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_str(bldr, "2", "true");
	fb_json_bldr_add_int(bldr, "1", priv->unread);
	fb_json_bldr_add_str(bldr, "12", "true");
	fb_json_bldr_add_str(bldr, "13", "false");
	fb_api_http_query(api, FB_API_QUERY_THREADS, bldr,
	                  fb_api_cb_unread);
}

static void
fb_api_cb_sticker(PurpleHttpConnection *con, PurpleHttpResponse *res,
                  gpointer data)
{
	FbApi *api = data;
	FbApiMessage *msg;
	FbJsonValues *values;
	GError *err = NULL;
	GSList *msgs = NULL;
	JsonNode *node;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	node = fb_json_node_get_nth(root, 0);
	values = fb_json_values_new(node);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.thread_image.uri");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	msg = fb_api_data_take(api, con);
	msg->flags |= FB_API_MESSAGE_FLAG_IMAGE;
	msg->text = fb_json_values_next_str_dup(values, NULL);
	msgs = g_slist_prepend(msgs, msg);

	g_signal_emit_by_name(api, "messages", msgs);
	g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
	g_object_unref(values);
	json_node_free(root);
}

static void
fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg)
{
	JsonBuilder *bldr;
	PurpleHttpConnection *http;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_arr_begin(bldr, "0");
	fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, sid);
	fb_json_bldr_arr_end(bldr);

	http = fb_api_http_query(api, FB_API_QUERY_STICKER, bldr,
	                         fb_api_cb_sticker);
	fb_api_data_set(api, http, msg, (GDestroyNotify) fb_api_message_free);
}

static gboolean
fb_api_thread_parse(FbApi *api, FbApiThread *thrd, JsonNode *root,
                    GError **error)
{
	const gchar *str;
	FbApiPrivate *priv = api->priv;
	FbApiUser *user;
	FbId uid;
	FbJsonValues *values;
	gboolean haself = FALSE;
	GError *err = NULL;

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
	                   "$.thread_key.thread_fbid");
	fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, "$.name");
	fb_json_values_update(values, &err);

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
		g_object_unref(values);
		return FALSE;
	}

	str = fb_json_values_next_str(values, NULL);

	if (str == NULL) {
		g_object_unref(values);
		return FALSE;
	}

	thrd->tid = FB_ID_FROM_STR(str);
	thrd->topic = fb_json_values_next_str_dup(values, NULL);
	g_object_unref(values);

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.messaging_actor.id");
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE,
	                   "$.messaging_actor.name");
	fb_json_values_set_array(values, TRUE, "$.all_participants.nodes");

	while (fb_json_values_update(values, &err)) {
		str = fb_json_values_next_str(values, "0");
		uid = FB_ID_FROM_STR(str);

		if (uid != priv->uid) {
			user = fb_api_user_dup(NULL, FALSE);
			user->uid = uid;
			user->name = fb_json_values_next_str_dup(values, NULL);
			thrd->users = g_slist_prepend(thrd->users, user);
		} else {
			haself = TRUE;
		}
	}

	if (G_UNLIKELY(err != NULL)) {
		g_propagate_error(error, err);
		fb_api_thread_reset(thrd, TRUE);
		g_object_unref(values);
		return FALSE;
	}

	if ((g_slist_length(thrd->users) < 2) || !haself) {
		fb_api_thread_reset(thrd, TRUE);
		g_object_unref(values);
		return FALSE;
	}

	g_object_unref(values);
	return TRUE;
}

static void
fb_api_cb_thread(PurpleHttpConnection *con, PurpleHttpResponse *res,
                      gpointer data)
{
	FbApi *api = data;
	FbApiThread thrd;
	GError *err = NULL;
	JsonNode *node;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	node = fb_json_node_get_nth(root, 0);

	if (node == NULL) {
		fb_api_error(api, FB_API_ERROR_GENERAL,
		             _("Failed to obtain thread information"));
		json_node_free(root);
		return;
	}

	fb_api_thread_reset(&thrd, FALSE);

	if (!fb_api_thread_parse(api, &thrd, node, &err)) {
		if (G_LIKELY(err == NULL)) {
			fb_api_error(api, FB_API_ERROR_GENERAL,
			             _("Failed to parse thread information"));
		} else {
			fb_api_error_emit(api, err);
		}
	} else {
		g_signal_emit_by_name(api, "thread", &thrd);
	}

	fb_api_thread_reset(&thrd, TRUE);
	json_node_free(root);
}

void
fb_api_thread(FbApi *api, FbId tid)
{
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_arr_begin(bldr, "0");
	fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, tid);
	fb_json_bldr_arr_end(bldr);

	fb_json_bldr_add_str(bldr, "10", "false");
	fb_json_bldr_add_str(bldr, "11", "false");
	fb_json_bldr_add_str(bldr, "13", "false");
	fb_api_http_query(api, FB_API_QUERY_THREAD, bldr, fb_api_cb_thread);
}

static void
fb_api_cb_thread_create(PurpleHttpConnection *con, PurpleHttpResponse *res,
                        gpointer data)
{
	const gchar *str;
	FbApi *api = data;
	FbId tid;
	FbJsonValues *values;
	GError *err = NULL;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	values = fb_json_values_new(root);
	fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.thread_fbid");
	fb_json_values_update(values, &err);

	FB_API_ERROR_EMIT(api, err,
		g_object_unref(values);
		json_node_free(root);
		return;
	);

	str = fb_json_values_next_str(values, "0");
	tid = FB_ID_FROM_STR(str);
	g_signal_emit_by_name(api, "thread-create", tid);

	g_object_unref(values);
	json_node_free(root);
}

void
fb_api_thread_create(FbApi *api, GSList *uids)
{
	FbApiPrivate *priv;
	FbHttpParams *prms;
	FbId *uid;
	gchar *json;
	GSList *l;
	JsonBuilder *bldr;

	g_return_if_fail(FB_IS_API(api));
	g_warn_if_fail(g_slist_length(uids) > 1);
	priv = api->priv;

	bldr = fb_json_bldr_new(JSON_NODE_ARRAY);
	fb_json_bldr_obj_begin(bldr, NULL);
	fb_json_bldr_add_str(bldr, "type", "id");
	fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, priv->uid);
	fb_json_bldr_obj_end(bldr);

	for (l = uids; l != NULL; l = l->next) {
		uid = l->data;
		fb_json_bldr_obj_begin(bldr, NULL);
		fb_json_bldr_add_str(bldr, "type", "id");
		fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, *uid);
		fb_json_bldr_obj_end(bldr);
	}

	json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL);
	prms = fb_http_params_new();
	fb_http_params_set_str(prms, "to", json);
	fb_api_http_req(api, FB_API_URL_THREADS, "createThread", "POST",
	                prms, fb_api_cb_thread_create);
	g_free(json);
}

void
fb_api_thread_invite(FbApi *api, FbId tid, FbId uid)
{
	FbHttpParams *prms;
	gchar *json;
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_ARRAY);
	fb_json_bldr_obj_begin(bldr, NULL);
	fb_json_bldr_add_str(bldr, "type", "id");
	fb_json_bldr_add_strf(bldr, "id", "%" FB_ID_FORMAT, uid);
	fb_json_bldr_obj_end(bldr);
	json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL);

	prms = fb_http_params_new();
	fb_http_params_set_str(prms, "to", json);
	fb_http_params_set_strf(prms, "id", "t_id.%" FB_ID_FORMAT, tid);
	fb_api_http_req(api, FB_API_URL_PARTS, "addMembers", "POST",
	                prms, fb_api_cb_http_bool);
	g_free(json);
}

void
fb_api_thread_remove(FbApi *api, FbId tid, FbId uid)
{
	FbApiPrivate *priv;
	FbHttpParams *prms;
	gchar *json;
	JsonBuilder *bldr;

	g_return_if_fail(FB_IS_API(api));
	priv = api->priv;

	prms = fb_http_params_new();
	fb_http_params_set_strf(prms, "id", "t_id.%" FB_ID_FORMAT, tid);

	if (uid == 0) {
		uid = priv->uid;
	}

	if (uid != priv->uid) {
		bldr = fb_json_bldr_new(JSON_NODE_ARRAY);
		fb_json_bldr_add_strf(bldr, NULL, "%" FB_ID_FORMAT, uid);
		json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL);
		fb_http_params_set_str(prms, "to", json);
		g_free(json);
	}

	fb_api_http_req(api, FB_API_URL_PARTS, "removeMembers", "DELETE",
	                prms, fb_api_cb_http_bool);
}

void
fb_api_thread_topic(FbApi *api, FbId tid, const gchar *topic)
{
	FbHttpParams *prms;

	prms = fb_http_params_new();
	fb_http_params_set_str(prms, "name", topic);
	fb_http_params_set_strf(prms, "tid", "t_id.%" FB_ID_FORMAT, tid);
	fb_api_http_req(api, FB_API_URL_TOPIC, "setThreadName",
	                "messaging.setthreadname", prms,
			fb_api_cb_http_bool);
}

static void
fb_api_cb_threads(PurpleHttpConnection *con, PurpleHttpResponse *res,
                  gpointer data)
{
	FbApi *api = data;
	FbApiThread *dthrd;
	FbApiThread thrd;
	GError *err = NULL;
	GList *elms;
	GList *l;
	GSList *thrds = NULL;
	JsonArray *arr;
	JsonNode *root;

	if (!fb_api_http_chk(api, con, res, &root)) {
		return;
	}

	arr = fb_json_node_get_arr(root, "$.viewer.message_threads.nodes",
	                           &err);
	FB_API_ERROR_EMIT(api, err,
		json_node_free(root);
		return;
	);

	elms = json_array_get_elements(arr);

	for (l = elms; l != NULL; l = l->next) {
		fb_api_thread_reset(&thrd, FALSE);

		if (fb_api_thread_parse(api, &thrd, l->data, &err)) {
			dthrd = fb_api_thread_dup(&thrd, FALSE);
			thrds = g_slist_prepend(thrds, dthrd);
		} else {
			fb_api_thread_reset(&thrd, TRUE);
		}

		if (G_UNLIKELY(err != NULL)) {
			break;
		}
	}

	if (G_LIKELY(err == NULL)) {
		thrds = g_slist_reverse(thrds);
		g_signal_emit_by_name(api, "threads", thrds);
	} else {
		fb_api_error_emit(api, err);
	}

	g_slist_free_full(thrds, (GDestroyNotify) fb_api_thread_free);
	g_list_free(elms);
	json_array_unref(arr);
	json_node_free(root);
}

void
fb_api_threads(FbApi *api)
{
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_str(bldr, "2", "true");
	fb_json_bldr_add_str(bldr, "12", "false");
	fb_json_bldr_add_str(bldr, "13", "false");
	fb_api_http_query(api, FB_API_QUERY_THREADS, bldr, fb_api_cb_threads);
}

void
fb_api_typing(FbApi *api, FbId uid, gboolean state)
{
	gchar *json;
	JsonBuilder *bldr;

	bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
	fb_json_bldr_add_int(bldr, "state", state != 0);
	fb_json_bldr_add_strf(bldr, "to", "%" FB_ID_FORMAT, uid);

	json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL);
	fb_api_publish(api, "/typing", "%s", json);
	g_free(json);
}

FbApiEvent *
fb_api_event_dup(const FbApiEvent *event, gboolean deep)
{
	FbApiEvent *ret;

	if (event == NULL) {
		return g_new0(FbApiEvent, 1);
	}

	ret = g_memdup(event, sizeof *event);

	if (deep) {
		ret->text = g_strdup(event->text);
	}

	return ret;
}

void
fb_api_event_reset(FbApiEvent *event, gboolean deep)
{
	g_return_if_fail(event != NULL);

	if (deep) {
		g_free(event->text);
	}

	memset(event, 0, sizeof *event);
}

void
fb_api_event_free(FbApiEvent *event)
{
	if (G_LIKELY(event != NULL)) {
		g_free(event->text);
		g_free(event);
	}
}

FbApiMessage *
fb_api_message_dup(const FbApiMessage *msg, gboolean deep)
{
	FbApiMessage *ret;

	if (msg == NULL) {
		return g_new0(FbApiMessage, 1);
	}

	ret = g_memdup(msg, sizeof *msg);

	if (deep) {
		ret->text = g_strdup(msg->text);
	}

	return ret;
}

void
fb_api_message_reset(FbApiMessage *msg, gboolean deep)
{
	g_return_if_fail(msg != NULL);

	if (deep) {
		g_free(msg->text);
	}

	memset(msg, 0, sizeof *msg);
}

void
fb_api_message_free(FbApiMessage *msg)
{
	if (G_LIKELY(msg != NULL)) {
		g_free(msg->text);
		g_free(msg);
	}
}

FbApiPresence *
fb_api_presence_dup(const FbApiPresence *pres)
{
	if (pres == NULL) {
		return g_new0(FbApiPresence, 1);
	}

	return g_memdup(pres, sizeof *pres);
}

void
fb_api_presence_reset(FbApiPresence *pres)
{
	g_return_if_fail(pres != NULL);
	memset(pres, 0, sizeof *pres);
}

void
fb_api_presence_free(FbApiPresence *pres)
{
	if (G_LIKELY(pres != NULL)) {
		g_free(pres);
	}
}

FbApiThread *
fb_api_thread_dup(const FbApiThread *thrd, gboolean deep)
{
	FbApiThread *ret;
	FbApiUser *user;
	GSList *l;

	if (thrd == NULL) {
		return g_new0(FbApiThread, 1);
	}

	ret = g_memdup(thrd, sizeof *thrd);

	if (deep) {
		ret->users = NULL;

		for (l = thrd->users; l != NULL; l = l->next) {
			user = fb_api_user_dup(l->data, TRUE);
			ret->users = g_slist_prepend(ret->users, user);
		}

		ret->topic = g_strdup(thrd->topic);
		ret->users = g_slist_reverse(ret->users);
	}

	return ret;
}

void
fb_api_thread_reset(FbApiThread *thrd, gboolean deep)
{
	g_return_if_fail(thrd != NULL);

	if (deep) {
		g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free);
		g_free(thrd->topic);
	}

	memset(thrd, 0, sizeof *thrd);
}

void
fb_api_thread_free(FbApiThread *thrd)
{
	if (G_LIKELY(thrd != NULL)) {
		g_slist_free_full(thrd->users, (GDestroyNotify) fb_api_user_free);
		g_free(thrd->topic);
		g_free(thrd);
	}
}

FbApiTyping *
fb_api_typing_dup(const FbApiTyping *typg)
{
	if (typg == NULL) {
		return g_new0(FbApiTyping, 1);
	}

	return g_memdup(typg, sizeof *typg);
}

void
fb_api_typing_reset(FbApiTyping *typg)
{
	g_return_if_fail(typg != NULL);
	memset(typg, 0, sizeof *typg);
}

void
fb_api_typing_free(FbApiTyping *typg)
{
	if (G_LIKELY(typg != NULL)) {
		g_free(typg);
	}
}

FbApiUser *
fb_api_user_dup(const FbApiUser *user, gboolean deep)
{
	FbApiUser *ret;

	if (user == NULL) {
		return g_new0(FbApiUser, 1);
	}

	ret = g_memdup(user, sizeof *user);

	if (deep) {
		ret->name = g_strdup(user->name);
		ret->icon = g_strdup(user->icon);
		ret->csum = g_strdup(user->csum);
	}

	return ret;
}

void
fb_api_user_reset(FbApiUser *user, gboolean deep)
{
	g_return_if_fail(user != NULL);

	if (deep) {
		g_free(user->name);
		g_free(user->icon);
		g_free(user->csum);
	}

	memset(user, 0, sizeof *user);
}

void
fb_api_user_free(FbApiUser *user)
{
	if (G_LIKELY(user != NULL)) {
		g_free(user->name);
		g_free(user->icon);
		g_free(user->csum);
		g_free(user);
	}
}
