pidgin/pidgin

1896a80ff8e3
Route GLib debug logging directly to the Finch debug window

Instead of flowing through purple debug, this merges some bits of the existing GLib log handler, and the purple debug printer.

Testing Done:
Open the Debug window an see some `GLib-*` outputs.

Reviewed at https://reviews.imfreedom.org/r/1057/
/* purple
*
* Purple is the legal property of its developers, whose names are too numerous
* to list here. Please refer to the COPYRIGHT file distributed with this
* source distribution.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
*/
#include <glib/gi18n-lib.h>
#include <json-glib/json-glib.h>
#include <libsoup/soup.h>
#include <stdarg.h>
#include <string.h>
#include "libpurple/glibcompat.h"
#include "api.h"
#include "http.h"
#include "json.h"
#include "thrift.h"
#include "util.h"
enum
{
PROP_0,
PROP_CID,
PROP_DID,
PROP_MID,
PROP_STOKEN,
PROP_TOKEN,
PROP_UID,
PROP_N
};
typedef struct
{
FbMqtt *mqtt;
SoupSession *cons;
PurpleConnection *gc;
gboolean retrying;
FbId uid;
gint64 sid;
guint64 mid;
gchar *cid;
gchar *did;
gchar *stoken;
gchar *token;
GQueue *msgs;
gboolean invisible;
guint unread;
FbId lastmid;
gchar *contacts_delta;
} FbApiPrivate;
/**
* FbApi:
*
* Represents a Facebook Messenger connection.
*/
struct _FbApi
{
GObject parent;
FbApiPrivate *priv;
};
static void fb_api_error_literal(FbApi *api, FbApiError error,
const gchar *msg);
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);
void
fb_api_contacts_delta(FbApi *api, const gchar *delta_cursor);
G_DEFINE_TYPE_WITH_PRIVATE(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)
{
FbApiPrivate *priv = FB_API(obj)->priv;
soup_session_abort(priv->cons);
if (G_UNLIKELY(priv->mqtt != NULL)) {
g_object_unref(priv->mqtt);
}
g_object_unref(priv->cons);
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);
g_free(priv->contacts_delta);
}
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;
/**
* 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, NULL,
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, NULL,
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, NULL,
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, NULL,
G_TYPE_NONE,
2, G_TYPE_POINTER, G_TYPE_BOOLEAN);
/**
* FbApi::contacts-delta:
* @api: The #FbApi.
* @added: The #GSList of added #FbApiUser's.
* @removed: The #GSList of strings with removed user ids.
*
* Like 'contacts', but only the deltas.
*/
g_signal_new("contacts-delta",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_ACTION,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
2, G_TYPE_POINTER, G_TYPE_POINTER);
/**
* 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, NULL,
G_TYPE_NONE,
1, G_TYPE_POINTER);
/**
* 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, NULL,
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, NULL,
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, NULL,
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, NULL,
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, NULL,
G_TYPE_NONE,
1, FB_TYPE_ID);
/**
* FbApi::thread-kicked:
* @api: The #FbApi.
* @thrd: The #FbApiThread.
*
* Emitted upon the reply of a thread request when the user is no longer
* part of that thread. This is emitted as a result of #fb_api_thread().
*/
g_signal_new("thread-kicked",
G_TYPE_FROM_CLASS(klass),
G_SIGNAL_ACTION,
0,
NULL, NULL, NULL,
G_TYPE_NONE,
1, G_TYPE_POINTER);
/**
* 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, NULL,
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, NULL,
G_TYPE_NONE,
1, G_TYPE_POINTER);
}
static void
fb_api_init(FbApi *api)
{
FbApiPrivate *priv = fb_api_get_instance_private(api);
api->priv = priv;
priv->msgs = g_queue_new();
}
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 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_literal(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_literal(api, errc, 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, SoupMessage *res, JsonNode **root)
{
const gchar *data;
const gchar *msg;
GError *err = NULL;
gint code;
gsize size;
msg = res->reason_phrase;
code = res->status_code;
data = res->response_body->data;
size = res->response_body->length;
fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Response (%p):", res);
if (msg != NULL) {
fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Error: %s (%d)", msg,
code);
} else {
fb_util_debug(FB_UTIL_DEBUG_INFO, " Response Error: %d", code);
}
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 SoupMessage *
fb_api_http_req(FbApi *api, const gchar *url, const gchar *name,
const gchar *method, FbHttpParams *params,
SoupSessionCallback callback)
{
FbApiPrivate *priv = api->priv;
gchar *data;
gchar *key;
gchar *val;
GList *keys;
GList *l;
GString *gstr;
SoupMessage *msg;
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);
/* 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);
msg = soup_form_request_new_from_hash("POST", url, params);
fb_http_params_free(params);
if (priv->token != NULL) {
data = g_strdup_printf("OAuth %s", priv->token);
soup_message_headers_replace(msg->request_headers, "Authorization",
data);
g_free(data);
}
soup_session_queue_message(priv->cons, msg, callback, api);
fb_util_debug(FB_UTIL_DEBUG_INFO, "HTTP Request (%p):", msg);
fb_util_debug(FB_UTIL_DEBUG_INFO, " Request URL: %s", url);
return msg;
}
static SoupMessage *
fb_api_http_query(FbApi *api, gint64 query, JsonBuilder *builder,
SoupSessionCallback 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_CONTACTS_DELTA:
name = "FetchContactsDeltaQuery";
break;
case FB_API_QUERY_STICKER:
name = "FetchStickersWithPreviewsQuery";
break;
case FB_API_QUERY_THREAD:
name = "ThreadQuery";
break;
case FB_API_QUERY_SEQ_ID:
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(G_GNUC_UNUSED SoupSession *session, SoupMessage *res,
gpointer data)
{
FbApi *api = data;
if (!fb_api_http_chk(api, res, NULL)) {
return;
}
if (!purple_strequal(res->response_body->data, "true")) {
fb_api_error_literal(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, FB_API_MQTT_AGENT);
/* 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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, res, &root)) {
return;
}
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.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);
if (priv->sid == 0) {
fb_api_error_literal(api, FB_API_ERROR_GENERAL,
_("Failed to get sync_sequence_id"));
} else {
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_SEQ_ID, 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_literal(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_literal(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);
if (g_str_has_prefix(url, FB_API_FBRPC_PREFIX)) {
text = fb_http_params_dup_str(params, "target_url", NULL);
} else {
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, "$.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 GSList *
fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error);
static GSList *
fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error);
static void
fb_api_cb_publish_mst(FbThrift *thft, GError **error)
{
if (fb_thrift_read_isstop(thft)) {
FB_API_TCHK(fb_thrift_read_stop(thft));
} else {
FbThriftType type;
gint16 id;
FB_API_TCHK(fb_thrift_read_field(thft, &type, &id, 0));
FB_API_TCHK(type == FB_THRIFT_TYPE_STRING);
// FB_API_TCHK(id == 2);
FB_API_TCHK(fb_thrift_read_str(thft, NULL));
FB_API_TCHK(fb_thrift_read_stop(thft));
}
}
static void
fb_api_cb_publish_ms(FbApi *api, GByteArray *pload)
{
const gchar *data;
FbApiPrivate *priv = api->priv;
FbJsonValues *values;
FbThrift *thft;
gchar *stoken;
GError *err = NULL;
GList *elms, *l;
GSList *msgs = NULL;
GSList *events = NULL;
guint size;
JsonNode *root;
JsonNode *node;
JsonArray *arr;
static const struct {
const gchar *member;
FbApiEventType type;
gboolean is_message;
} event_types[] = {
{"deltaNewMessage", 0, 1},
{"deltaThreadName", FB_API_EVENT_TYPE_THREAD_TOPIC, 0},
{"deltaParticipantsAddedToGroupThread", FB_API_EVENT_TYPE_THREAD_USER_ADDED, 0},
{"deltaParticipantLeftGroupThread", FB_API_EVENT_TYPE_THREAD_USER_REMOVED, 0},
};
/* Read identifier string (for Facebook employees) */
thft = fb_thrift_new(pload, 0);
fb_api_cb_publish_mst(thft, &err);
size = fb_thrift_get_pos(thft);
g_object_unref(thft);
FB_API_ERROR_EMIT(api, err,
return;
);
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;
}
arr = fb_json_node_get_arr(root, "$.deltas", NULL);
elms = json_array_get_elements(arr);
for (l = elms; l != NULL; l = l->next) {
guint i = 0;
JsonObject *o = json_node_get_object(l->data);
for (i = 0; i < G_N_ELEMENTS(event_types); i++) {
if ((node = json_object_get_member(o, event_types[i].member))) {
if (event_types[i].is_message) {
msgs = fb_api_cb_publish_ms_new_message(
api, node, msgs, &err
);
} else {
events = fb_api_cb_publish_ms_event(
api, node, events, event_types[i].type, &err
);
}
}
}
if (G_UNLIKELY(err != NULL)) {
break;
}
}
g_list_free(elms);
json_array_unref(arr);
if (G_LIKELY(err == NULL)) {
if (msgs) {
msgs = g_slist_reverse(msgs);
g_signal_emit_by_name(api, "messages", msgs);
}
if (events) {
events = g_slist_reverse(events);
g_signal_emit_by_name(api, "events", events);
}
} else {
fb_api_error_emit(api, err);
}
g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free);
g_slist_free_full(events, (GDestroyNotify) fb_api_event_free);
json_node_free(root);
}
static GSList *
fb_api_cb_publish_ms_new_message(FbApi *api, JsonNode *root, GSList *msgs, GError **error)
{
const gchar *body;
const gchar *str;
GError *err = NULL;
FbApiPrivate *priv = api->priv;
FbApiMessage *dmsg;
FbApiMessage msg;
FbId id;
FbId oid;
FbJsonValues *values;
JsonNode *node;
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata.offlineThreadingId");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata.actorFbId");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata"
".threadKey.otherUserFbId");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata"
".threadKey.threadFbId");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata.timestamp");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.body");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.stickerId");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.messageMetadata.messageId");
if (fb_json_values_update(values, &err)) {
id = fb_json_values_next_int(values, 0);
/* Ignore everything but new messages */
if (id == 0) {
goto beach;
}
/* Ignore sequential duplicates */
if (id == priv->lastmid) {
fb_util_debug_info("Ignoring duplicate %" FB_ID_FORMAT, id);
goto beach;
}
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) {
goto beach;
}
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)) {
g_propagate_error(error, err);
goto beach;
}
}
beach:
g_object_unref(values);
return msgs;
}
static GSList *
fb_api_cb_publish_ms_event(FbApi *api, JsonNode *root, GSList *events, FbApiEventType type, GError **error)
{
FbApiEvent *event;
FbJsonValues *values = NULL;
FbJsonValues *values_inner = NULL;
GError *err = NULL;
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata.threadKey.threadFbId");
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.messageMetadata.actorFbId");
switch (type) {
case FB_API_EVENT_TYPE_THREAD_TOPIC:
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.name");
break;
case FB_API_EVENT_TYPE_THREAD_USER_ADDED:
values_inner = fb_json_values_new(root);
fb_json_values_add(values_inner, FB_JSON_TYPE_INT, FALSE,
"$.userFbId");
/* use the text field for the full name */
fb_json_values_add(values_inner, FB_JSON_TYPE_STR, FALSE,
"$.fullName");
fb_json_values_set_array(values_inner, FALSE,
"$.addedParticipants");
break;
case FB_API_EVENT_TYPE_THREAD_USER_REMOVED:
fb_json_values_add(values, FB_JSON_TYPE_INT, FALSE,
"$.leftParticipantFbId");
/* use the text field for the kick message */
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.messageMetadata.adminText");
break;
}
fb_json_values_update(values, &err);
event = fb_api_event_dup(NULL, FALSE);
event->type = type;
event->tid = fb_json_values_next_int(values, 0);
event->uid = fb_json_values_next_int(values, 0);
if (type == FB_API_EVENT_TYPE_THREAD_TOPIC) {
event->text = fb_json_values_next_str_dup(values, NULL);
} else if (type == FB_API_EVENT_TYPE_THREAD_USER_REMOVED) {
/* overwrite actor with subject */
event->uid = fb_json_values_next_int(values, 0);
event->text = fb_json_values_next_str_dup(values, NULL);
} else if (type == FB_API_EVENT_TYPE_THREAD_USER_ADDED) {
while (fb_json_values_update(values_inner, &err)) {
FbApiEvent *devent = fb_api_event_dup(event, FALSE);
devent->uid = fb_json_values_next_int(values_inner, 0);
devent->text = fb_json_values_next_str_dup(values_inner, NULL);
events = g_slist_prepend(events, devent);
}
fb_api_event_free(event);
event = NULL;
g_object_unref(values_inner);
}
g_object_unref(values);
if (G_UNLIKELY(err != NULL)) {
g_propagate_error(error, err);
} else if (event) {
events = g_slist_prepend(events, event);
}
return events;
}
static void
fb_api_cb_publish_pt(FbThrift *thft, GSList **presences, GError **error)
{
FbApiPresence *api_presence;
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));
api_presence = fb_api_presence_dup(NULL);
api_presence->uid = i64;
api_presence->active = i32 != 0;
*presences = g_slist_prepend(*presences, api_presence);
fb_util_debug_info("Presence: %" FB_ID_FORMAT " (%d) id: %d",
i64, i32 != 0, id);
while (id <= 6) {
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;
case 6:
/* Unknown new field */
FB_API_TCHK(type == FB_THRIFT_TYPE_I64);
FB_API_TCHK(fb_thrift_read_i64(thft, NULL));
break;
default:
/* Try to read unknown fields as varint */
FB_API_TCHK(type == FB_THRIFT_TYPE_I16 ||
type == FB_THRIFT_TYPE_I32 ||
type == FB_THRIFT_TYPE_I64);
FB_API_TCHK(fb_thrift_read_i64(thft, NULL));
break;
}
}
/* Read the field stop */
FB_API_TCHK(fb_thrift_read_stop(thft));
}
/* Read the field stop */
if (fb_thrift_read_isstop(thft)) {
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 *presences = NULL;
thft = fb_thrift_new(pload, 0);
fb_api_cb_publish_pt(thft, &presences, &err);
g_object_unref(thft);
if (G_LIKELY(err == NULL)) {
g_signal_emit_by_name(api, "presences", presences);
} else {
fb_api_error_emit(api, err);
}
g_slist_free_full(presences, (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, GProxyResolver *resolver)
{
FbApi *api;
FbApiPrivate *priv;
api = g_object_new(FB_TYPE_API, NULL);
priv = api->priv;
priv->gc = gc;
priv->cons = soup_session_new_with_options(
"proxy-resolver", resolver,
"user-agent", FB_API_AGENT,
NULL);
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 = g_uuid_string_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;
}
static void
fb_api_error_literal(FbApi *api, FbApiError error, const gchar *msg)
{
GError *err;
g_return_if_fail(FB_IS_API(api));
err = g_error_new_literal(FB_API_ERROR, error, msg);
fb_api_error_emit(api, err);
}
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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, 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 = g_object_steal_data(G_OBJECT(res), "fb-api-msg");
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;
SoupMessage *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);
g_object_set_data_full(G_OBJECT(http), "fb-api-msg", msg,
(GDestroyNotify)fb_api_message_free);
}
static void
fb_api_cb_auth(G_GNUC_UNUSED SoupSession *session, SoupMessage *res,
gpointer data)
{
FbApi *api = data;
FbApiPrivate *priv = api->priv;
FbJsonValues *values;
GError *err = NULL;
JsonNode *root;
if (!fb_api_http_chk(api, 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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, res, &root)) {
return;
}
node = fb_json_node_get_nth(root, 0);
if (node == NULL) {
fb_api_error_literal(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 GSList *
fb_api_cb_contacts_nodes(FbApi *api, JsonNode *root, GSList *users)
{
const gchar *str;
FbApiPrivate *priv = api->priv;
FbApiUser *user;
FbId uid;
FbJsonValues *values;
gboolean is_array;
GError *err = NULL;
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");
is_array = (JSON_NODE_TYPE(root) == JSON_NODE_ARRAY);
if (is_array) {
fb_json_values_set_array(values, FALSE, "$");
}
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))
{
if (!is_array) {
break;
}
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);
if (!is_array) {
break;
}
}
g_object_unref(values);
return users;
}
/* base64(contact:<our id>:<their id>:<whatever>) */
static GSList *
fb_api_cb_contacts_parse_removed(FbApi *api, JsonNode *node, GSList *users)
{
gsize len;
char **split;
char *decoded = (char *) g_base64_decode(json_node_get_string(node), &len);
g_return_val_if_fail(decoded[len] == '\0', users);
g_return_val_if_fail(len == strlen(decoded), users);
g_return_val_if_fail(g_str_has_prefix(decoded, "contact:"), users);
split = g_strsplit_set(decoded, ":", 4);
if (g_strv_length(split) != 4) {
g_strfreev(split);
g_return_val_if_reached(users);
}
users = g_slist_prepend(users, g_strdup(split[2]));
g_strfreev(split);
g_free(decoded);
return users;
}
static void
fb_api_cb_contacts(G_GNUC_UNUSED SoupSession *session, SoupMessage *res,
gpointer data)
{
const gchar *cursor;
const gchar *delta_cursor;
FbApi *api = data;
FbApiPrivate *priv = api->priv;
FbJsonValues *values;
gboolean complete;
gboolean is_delta;
GError *err = NULL;
GList *l;
GSList *users = NULL;
JsonNode *root;
JsonNode *croot;
JsonNode *node;
if (!fb_api_http_chk(api, res, &root)) {
return;
}
croot = fb_json_node_get(root, "$.viewer.messenger_contacts.deltas", NULL);
is_delta = (croot != NULL);
if (!is_delta) {
croot = fb_json_node_get(root, "$.viewer.messenger_contacts", NULL);
node = fb_json_node_get(croot, "$.nodes", NULL);
users = fb_api_cb_contacts_nodes(api, node, users);
json_node_free(node);
} else {
GSList *added = NULL;
GSList *removed = NULL;
JsonArray *arr = fb_json_node_get_arr(croot, "$.nodes", NULL);
GList *elms = json_array_get_elements(arr);
for (l = elms; l != NULL; l = l->next) {
if ((node = fb_json_node_get(l->data, "$.added", NULL))) {
added = fb_api_cb_contacts_nodes(api, node, added);
json_node_free(node);
}
if ((node = fb_json_node_get(l->data, "$.removed", NULL))) {
removed = fb_api_cb_contacts_parse_removed(api, node, removed);
json_node_free(node);
}
}
g_signal_emit_by_name(api, "contacts-delta", added, removed);
g_slist_free_full(added, (GDestroyNotify) fb_api_user_free);
g_slist_free_full(removed, g_free);
g_list_free(elms);
json_array_unref(arr);
}
values = fb_json_values_new(croot);
fb_json_values_add(values, FB_JSON_TYPE_BOOL, FALSE,
"$.page_info.has_next_page");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.page_info.delta_cursor");
fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE,
"$.page_info.end_cursor");
fb_json_values_update(values, NULL);
complete = !fb_json_values_next_bool(values, FALSE);
delta_cursor = fb_json_values_next_str(values, NULL);
cursor = fb_json_values_next_str(values, NULL);
if (G_UNLIKELY(err == NULL)) {
if (is_delta || complete) {
g_free(priv->contacts_delta);
priv->contacts_delta = g_strdup(is_delta ? cursor : delta_cursor);
}
if (users) {
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(croot);
json_node_free(root);
}
void
fb_api_contacts(FbApi *api)
{
FbApiPrivate *priv;
JsonBuilder *bldr;
g_return_if_fail(FB_IS_API(api));
priv = api->priv;
if (priv->contacts_delta) {
fb_api_contacts_delta(api, priv->contacts_delta);
return;
}
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_contacts_delta(FbApi *api, const gchar *delta_cursor)
{
JsonBuilder *bldr;
bldr = fb_json_bldr_new(JSON_NODE_OBJECT);
fb_json_bldr_add_str(bldr, "0", delta_cursor);
fb_json_bldr_arr_begin(bldr, "1");
fb_json_bldr_add_str(bldr, NULL, "user");
fb_json_bldr_arr_end(bldr);
fb_json_bldr_add_str(bldr, "2", G_STRINGIFY(FB_API_CONTACTS_COUNT));
fb_api_http_query(api, FB_API_QUERY_CONTACTS_DELTA, 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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, res, &root)) {
return;
}
node = fb_json_node_get_nth(root, 0);
if (node == NULL) {
fb_api_error_literal(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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, 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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, 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 = g_object_steal_data(G_OBJECT(res), "fb-api-msg");
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;
SoupMessage *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);
g_object_set_data_full(G_OBJECT(http), "fb-api-msg", 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;
guint num_users = 0;
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);
num_users++;
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 (num_users < 2 || !haself) {
g_object_unref(values);
return FALSE;
}
g_object_unref(values);
return TRUE;
}
static void
fb_api_cb_thread(G_GNUC_UNUSED SoupSession *session, SoupMessage *res,
gpointer data)
{
FbApi *api = data;
FbApiThread thrd;
GError *err = NULL;
JsonNode *node;
JsonNode *root;
if (!fb_api_http_chk(api, res, &root)) {
return;
}
node = fb_json_node_get_nth(root, 0);
if (node == NULL) {
fb_api_error_literal(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)) {
if (thrd.tid) {
g_signal_emit_by_name(api, "thread-kicked", &thrd);
} else {
fb_api_error_literal(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(G_GNUC_UNUSED SoupSession *session, SoupMessage *res,
gpointer data)
{
const gchar *str;
FbApi *api = data;
FbId tid;
FbJsonValues *values;
GError *err = NULL;
JsonNode *root;
if (!fb_api_http_chk(api, res, &root)) {
return;
}
values = fb_json_values_new(root);
fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.id");
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, "recipients", json);
fb_api_http_req(api, FB_API_URL_THREADS, "createGroup", "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_%" 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_%" 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_int(prms, "tid", 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(G_GNUC_UNUSED SoupSession *session, SoupMessage *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, 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_memdup2(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_memdup2(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 *presence)
{
if (presence == NULL) {
return g_new0(FbApiPresence, 1);
}
return g_memdup2(presence, sizeof *presence);
}
void
fb_api_presence_reset(FbApiPresence *presence)
{
g_return_if_fail(presence != NULL);
memset(presence, 0, sizeof *presence);
}
void
fb_api_presence_free(FbApiPresence *presence)
{
if (G_LIKELY(presence != NULL)) {
g_free(presence);
}
}
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_memdup2(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_memdup2(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_memdup2(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);
}
}