diff options
Diffstat (limited to 'facebook/facebook-api.c')
-rw-r--r-- | facebook/facebook-api.c | 3585 |
1 files changed, 2563 insertions, 1022 deletions
diff --git a/facebook/facebook-api.c b/facebook/facebook-api.c index dfb4e9a..7d91b05 100644 --- a/facebook/facebook-api.c +++ b/facebook/facebook-api.c @@ -15,350 +15,969 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ +#include <bitlbee.h> #include <stdarg.h> #include <string.h> -#include <sha1.h> #include "facebook-api.h" +#include "facebook-http.h" +#include "facebook-json.h" +#include "facebook-marshal.h" #include "facebook-thrift.h" +#include "facebook-util.h" -/** - * Gets the error domain for #fb_api. - * - * @return The #GQuark of the error domain. - **/ -GQuark fb_api_error_quark(void) +typedef struct _FbApiData FbApiData; + +enum +{ + PROP_0, + + PROP_CID, + PROP_DID, + PROP_MID, + PROP_STOKEN, + PROP_TOKEN, + PROP_UID, + + PROP_N +}; + +struct _FbApiPrivate +{ + FbHttp *http; + FbMqtt *mqtt; + GHashTable *data; + + FbId uid; + gint64 sid; + guint64 mid; + gchar *cid; + gchar *did; + gchar *stoken; + gchar *token; + + GHashTable *mids; + gboolean invisible; + guint unread; + +}; + +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 *writeid); + +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; + + 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); + } + + g_object_unref(priv->http); + g_object_unref(priv->mqtt); + + g_hash_table_destroy(priv->data); + g_hash_table_destroy(priv->mids); + + g_free(priv->cid); + g_free(priv->did); + g_free(priv->stoken); + g_free(priv->token); +} + +static void +fb_api_class_init(FbApiClass *klass) { - static GQuark q; + 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; - if (G_UNLIKELY(q == 0)) + priv = G_TYPE_INSTANCE_GET_PRIVATE(api, FB_TYPE_API, FbApiPrivate); + api->priv = priv; + + priv->http = fb_http_new(NULL); + priv->mqtt = fb_mqtt_new(); + priv->data = g_hash_table_new_full(g_direct_hash, g_direct_equal, + NULL, NULL); + priv->mids = g_hash_table_new_full(g_int64_hash, g_int64_equal, + g_free, 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; } -/** - * Creates a new #json_value for JSON contents of the #fb_api. This - * function is special in that it handles all errors, unlike the parent - * function #fb_json_new(). The returned #json_value should be freed - * with #json_value_free() when no longer needed. - * - * @param api The #fb_api. - * @param data The data. - * @param size The size of the data. - * @param json The return location for the json_value. - * - * @return TRUE if the data was parsed without error, otherwise FALSE. - **/ -static gboolean fb_api_json_new(fb_api_t *api, const gchar *data, gsize size, - json_value **json) +static void +fb_api_data_set(FbApi *api, gpointer handle, gpointer data, + GDestroyNotify func) { - json_value *jv; - const gchar *msg; - gint64 code; + FbApiData *fata; + FbApiPrivate *priv = api->priv; - jv = fb_json_new(data, size, &api->err); + fata = g_new0(FbApiData, 1); + fata->data = data; + fata->func = func; + g_hash_table_replace(priv->data, handle, fata); +} - if (G_UNLIKELY(api->err != NULL)) { - fb_api_error(api, 0, NULL); - return FALSE; +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; } - if (fb_json_int_chk(jv, "error_code", &code)) { - if (!fb_json_str_chk(jv, "error_msg", &msg)) - msg = "Generic Error"; + 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; - fb_api_error(api, FB_API_ERROR_GENERAL, "%s", msg); - json_value_free(jv); + if (G_UNLIKELY(size == 0)) { + fb_api_error(api, FB_API_ERROR_GENERAL, "Empty JSON data"); return FALSE; - } else if (fb_json_str_chk(jv, "errorCode", &msg)) { - if ((g_ascii_strcasecmp(msg, "ERROR_QUEUE_NOT_FOUND") == 0) || - (g_ascii_strcasecmp(msg, "ERROR_QUEUE_LOST") == 0)) - { - g_free(api->stoken); - api->stoken = NULL; + } + + fb_util_debug_info("Parsing JSON: %.*s", (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 ((g_strcmp0(str, "OAuthException") == 0) || (code == 401)) { + errc = FB_API_ERROR_AUTH; + success = FALSE; + + g_free(priv->stoken); + priv->stoken = NULL; + + g_free(priv->token); + priv->token = NULL; + } + + str = fb_json_values_next_str(values, NULL); + + if ((g_strcmp0(str, "ERROR_QUEUE_NOT_FOUND") == 0) || + (g_strcmp0(str, "ERROR_QUEUE_LOST") == 0)) + { + 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; } + } - fb_api_error(api, FB_API_ERROR_GENERAL, "%s", msg); - json_value_free(jv); + 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; } - *json = jv; + if (node != NULL) { + *node = root; + } else { + json_node_free(root); + } + return TRUE; } -/** - * Checks an #fb_http_req for errors. - * - * @param api The #fb_api. - * @param req The #fb_http_req. - * - * @return TRUE if no errors were found, otherwise FALSE. - **/ -static gboolean fb_api_http_chk(fb_api_t *api, fb_http_req_t *req) +static gboolean +fb_api_http_chk(FbApi *api, FbHttpRequest *req, JsonNode **root) { - if (G_UNLIKELY(api->err != NULL)) { - fb_api_error(api, 0, NULL); - return FALSE; + const gchar *data; + GError *err; + gsize size; + + data = fb_http_request_get_data(req, &size); + err = fb_http_request_take_error(req); + + if ((err == NULL) && (root == NULL)) { + return TRUE; } - if (req->err != NULL) { - api->err = g_error_copy(req->err); - fb_api_error(api, 0, NULL); - return FALSE; + /* Rudimentary check to prevent wrongful error parsing */ + if ((size < 2) || (data[0] != '{') || (data[size - 1] != '}')) { + FB_API_ERROR_EMIT(api, err, return FALSE); } - return TRUE; -} + if (!fb_api_json_chk(api, data, size, root)) { + if (G_UNLIKELY(err != NULL)) { + g_error_free(err); + } -/** - * Creates a new #fb_http_req for a #fb_api request. - * - * @param api The #fb_api. - * @param host The host. - * @param path The path. - * @param func The #fb_http_func. - * @param class The class. - * @param name The friendly name. - * @param method The method. - **/ -static fb_http_req_t *fb_api_req_new(fb_api_t *api, const gchar *host, - const gchar *path, fb_http_func_t func, - const gchar *class, const gchar *name, - const gchar *method) -{ - fb_http_req_t *req; - - req = fb_http_req_new(api->http, host, 443, path, func, api); - req->flags = FB_HTTP_REQ_FLAG_POST | FB_HTTP_REQ_FLAG_SSL; - - fb_http_req_params_set(req, - FB_HTTP_PAIR("api_key", FB_API_KEY), - FB_HTTP_PAIR("fb_api_caller_class", class), - FB_HTTP_PAIR("fb_api_req_friendly_name", name), - FB_HTTP_PAIR("method", method), - FB_HTTP_PAIR("client_country_code", "US"), - FB_HTTP_PAIR("format", "json"), - FB_HTTP_PAIR("locale", "en_US"), - NULL - ); + return FALSE; + } - return req; + FB_API_ERROR_EMIT(api, err, return FALSE); + return TRUE; } -/** - * Sends a #fb_http_req for a #fb_api. This computes the signature for - * the request and sets the "sig" parameter. This sets the OAuth header - * for authorization. - * - * @param api The #fb_api. - * @param req The #fb_http_req. - **/ -static void fb_api_req_send(fb_api_t *api, fb_http_req_t *req) -{ - GString *gstr; - GList *keys; - GList *l; +static FbHttpRequest * +fb_api_http_req(FbApi *api, const gchar *url, const gchar *name, + const gchar *method, FbHttpValues *values, + FbHttpFunc func) +{ const gchar *key; const gchar *val; - gchar *hash; - gchar *auth; - - /* Ensure an old signature is not computed */ - g_hash_table_remove(req->params, "sig"); - - gstr = g_string_sized_new(128); - keys = g_hash_table_get_keys(req->params); + FbApiPrivate *priv = api->priv; + FbHttpRequest *req; + FbHttpValues *hdrs; + FbHttpValues *prms; + gchar *data; + GList *keys; + GList *l; + GString *gstr; + + fb_http_values_set_str(values, "api_key", FB_API_KEY); + fb_http_values_set_str(values, "device_id", priv->did); + fb_http_values_set_str(values, "fb_api_req_friendly_name", name); + fb_http_values_set_str(values, "format", "json"); + fb_http_values_set_str(values, "method", method); + + data = fb_util_locale_str(); + fb_http_values_set_str(values, "locale", data); + g_free(data); + + req = fb_http_request_new(priv->http, url, TRUE, func, api); + fb_http_values_remove(values, "sig"); + + gstr = g_string_new(NULL); + keys = fb_http_values_get_keys(values); 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(req->params, key); + val = fb_http_values_get_str(values, key, NULL); g_string_append_printf(gstr, "%s=%s", key, val); } g_string_append(gstr, FB_API_SECRET); - hash = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, gstr->len); + data = g_compute_checksum_for_string(G_CHECKSUM_MD5, gstr->str, gstr->len); + fb_http_values_set_str(values, "sig", data); - fb_http_req_params_set(req, - FB_HTTP_PAIR("sig", hash), - NULL - ); - - g_free(hash); - g_list_free(keys); g_string_free(gstr, TRUE); + g_list_free(keys); + g_free(data); - if (api->token != NULL) { - auth = g_strdup_printf("OAuth %s", api->token); - fb_http_req_headers_set(req, - FB_HTTP_PAIR("Authorization", auth), - NULL - ); - g_free(auth); + if (priv->token != NULL) { + hdrs = fb_http_request_get_headers(req); + fb_http_values_set_strf(hdrs, "Authorization", "OAuth %s", priv->token); } - fb_http_req_send(req); + prms = fb_http_request_get_params(req); + fb_http_values_consume(prms, values); + fb_http_request_send(req); + return req; } -/** - * Implemented #fb_http_func for simple boolean error checking. - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_http_bool(fb_http_req_t *req, gpointer data) +static FbHttpRequest * +fb_api_http_query(FbApi *api, gint64 query, JsonBuilder *builder, + FbHttpFunc func) { - fb_api_t *api = data; + const gchar *name; + FbHttpValues *prms; + gchar *json; + + switch (query) { + case FB_API_QUERY_CONTACT: + name = "FetchContactQuery"; + 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_values_new(); + json = fb_json_bldr_close(builder, JSON_NODE_OBJECT, NULL); + fb_http_values_set_strf(prms, "query_id", "%" G_GINT64_FORMAT, query); + fb_http_values_set_str(prms, "query_params", json); + g_free(json); - if (!fb_api_http_chk(api, req)) + return fb_api_http_req(api, FB_API_URL_GQL, name, "get", prms, func); +} + +static void +fb_api_cb_http_bool(FbHttpRequest *req, gpointer data) +{ + const gchar *hata; + FbApi *api = data; + + if (!fb_api_http_chk(api, req, NULL)) { return; + } - if (bool2int(req->body) == 0) + hata = fb_http_request_get_data(req, NULL); + + if (bool2int((gchar *) hata)) { 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; + g_signal_emit_by_name(api, "error", error); +} -/** - * Implements #fb_mqtt_funcs->error(). - * - * @param mqtt The #fb_mqtt. - * @param err The #GError. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_mqtt_error(fb_mqtt_t *mqtt, GError *err, gpointer data) +static void +fb_api_cb_mqtt_open(FbMqtt *mqtt, gpointer data) { - fb_api_t *api = data; + const GByteArray *bytes; + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbThrift *thft; + GByteArray *cytes; - if (api->err == NULL) { - api->err = g_error_copy(err); - fb_api_error(api, 0, NULL); - } -} + static guint8 flags = FB_MQTT_CONNECT_FLAG_USER | + FB_MQTT_CONNECT_FLAG_PASS | + FB_MQTT_CONNECT_FLAG_CLR; -/** - * Implements #fb_mqtt_funcs->open(). - * - * @param mqtt The #fb_mqtt. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_mqtt_open(fb_mqtt_t *mqtt, gpointer data) -{ - fb_api_t *api = data; - gchar *msg; - - static guint8 flags = - FB_MQTT_CONNECT_FLAG_USER | - FB_MQTT_CONNECT_FLAG_PASS | - FB_MQTT_CONNECT_FLAG_CLR; - - msg = g_strdup_printf("{" - "\"u\":\"%" FB_ID_FORMAT "\"," - "\"a\":\"" FB_API_AGENT "\"," - "\"mqtt_sid\":%s," - "\"d\":\"%s\"," - "\"chat_on\":true," - "\"no_auto_fg\":true," - "\"fg\":false," - "\"pf\":\"jz\"," - "\"nwt\":1," - "\"nwst\":0" - "}", api->uid, api->mid, api->cuid); - - fb_mqtt_connect(mqtt, - flags, /* Flags */ - api->cid, /* Client identifier */ - msg, /* Will message */ - api->token, /* Username */ - NULL); - - g_free(msg); -} - -/** - * Implemented #fb_http_func for the sequence identifier. - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_seqid(fb_http_req_t *req, gpointer data) -{ - fb_api_t *api = data; - json_value *json; - json_value *jv; - const gchar *str; + thft = fb_thrift_new(NULL, 0); - if (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { - return; - } + /* Write the client identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 1); + fb_thrift_write_str(thft, priv->cid); - /* Scattered values lead to a gnarly conditional... */ - if (!fb_json_val_chk(json, "data", json_array, &jv) || + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRUCT, 4); - /* Obtain the first array element */ - (jv->u.array.length != 1) || - ((jv = jv->u.array.values[0]) == NULL) || + /* Write the user identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 5); + fb_thrift_write_i64(thft, priv->uid); - /* Check the name */ - !fb_json_str_chk(jv, "name", &str) || - (g_ascii_strcasecmp(str, "thread_list_ids") != 0) || + /* Write the information string */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 6); + fb_thrift_write_str(thft, ""); - /* Obtain the sequence identifier */ - !fb_json_val_chk(jv, "fql_result_set", json_array, &jv) || - (jv->u.array.length != 1) || - !fb_json_str_chk(jv->u.array.values[0], "sync_sequence_id", &str)) - { - fb_api_error(api, FB_API_ERROR, "Failed to obtain SequenceID"); - goto finish; + /* Write the UNKNOWN ("cp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 7); + fb_thrift_write_i64(thft, 23); + + /* Write the UNKNOWN ("ecp"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 8); + fb_thrift_write_i64(thft, 26); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 9); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("no_auto_fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 10); + fb_thrift_write_bool(thft, TRUE); + + /* Write the visibility state */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 11); + fb_thrift_write_bool(thft, !priv->invisible); + + /* Write the device identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_STRING, 12); + fb_thrift_write_str(thft, priv->did); + + /* Write the UNKNOWN ("fg"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_BOOL, 13); + fb_thrift_write_bool(thft, TRUE); + + /* Write the UNKNOWN ("nwt"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 14); + fb_thrift_write_i32(thft, 1); + + /* Write the UNKNOWN ("nwst"?) */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I32, 15); + fb_thrift_write_i32(thft, 0); + + /* Write the MQTT identifier */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_I64, 16); + fb_thrift_write_i64(thft, priv->mid); + + /* Write the UNKNOWN */ + fb_thrift_write_field(thft, FB_THRIFT_TYPE_LIST, 18); + 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, 19); + 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_zcompress(bytes); + + fb_util_debug_hexdump(FB_UTIL_DEBUG_LEVEL_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) +{ + 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; } - if (api->stoken == NULL) { - fb_api_publish(api, "/messenger_sync_create_queue", "{" - "\"device_params\":{}," - "\"encoding\":\"JSON\"," - "\"max_deltas_able_to_process\":1250," - "\"initial_titan_sequence_id\":%s," - "\"sync_api_version\":2," - "\"delta_batch_size\":125," - "\"device_id\":\"%s\"" - "}", str, api->cuid); + 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); + +} - goto finish; +static void +fb_api_cb_seqid(FbHttpRequest *req, 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, req, &root)) { + return; } - fb_api_publish(api, "/messenger_sync_get_diffs", "{" - "\"encoding\":\"JSON\"," - "\"last_seq_id\":%s," - "\"max_deltas_able_to_process\":1250," - "\"sync_api_version\":2," - "\"sync_token\":\"%s\"," - "\"delta_batch_size\":125" - "}", str, api->stoken); + 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_FUNC(api, connect); + 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); -finish: - json_value_free(json); + fb_api_connect_queue(api); + g_object_unref(values); + json_node_free(root); } -/** - * Implements #fb_mqtt_funcs->connack(). - * - * @param mqtt The #fb_mqtt. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_mqtt_connack(fb_mqtt_t *mqtt, gpointer data) +static void +fb_api_cb_mqtt_connect(FbMqtt *mqtt, gpointer data) { - fb_api_t *api = data; - fb_http_req_t *req; + 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); - fb_api_publish(api, "/foreground_state", "{" - "\"foreground\": true," - "\"keepalive_timeout\": %d" - "}", 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, @@ -378,165 +997,438 @@ static void fb_api_cb_mqtt_connack(fb_mqtt_t *mqtt, gpointer data) /* Notifications seem to lead to some sort of sending rate limit */ fb_mqtt_unsubscribe(mqtt, "/orca_message_notifications", NULL); - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_FQL, - fb_api_cb_seqid, - "com.facebook.orca.protocol.methods.u", - "fetchThreadList", - "GET"); + if (priv->sid == 0) { + /* See fb_api_thread_list() for key mapping */ + 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; + ); - static const gchar *query = "{" - "\"thread_list_ids\":\"" - "SELECT sync_sequence_id " - "FROM unified_thread " - "WHERE folder='inbox' " - "ORDER BY sync_sequence_id " - "DESC LIMIT 1\"" - "}"; + if (!fb_json_values_next_bool(values, TRUE)) { + fb_api_error(api, FB_API_ERROR_GENERAL, + "Failed to mark thread as read"); + } - fb_http_req_params_set(req, FB_HTTP_PAIR("q", query), NULL); - fb_api_req_send(api, req); + g_object_unref(values); + json_node_free(root); } -/** - * Handles typing notifications which are to be published to the user. - * - * @param api The #fb_api. - * @param pload The message payload. - **/ -static void fb_api_cb_publish_tn(fb_api_t *api, const GByteArray *pload) -{ - json_value *json; - fb_api_typing_t typg; - const gchar *str; - gint64 uid; - gint64 state; - - if (!fb_api_json_new(api, (gchar*) pload->data, pload->len, &json)) +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" + } + }; + + 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); + 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_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; + } - if (!fb_json_str_chk(json, "type", &str) || - (g_ascii_strcasecmp(str, "typ") != 0)) - { - goto finish; + 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); + 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 (!fb_json_int_chk(json, "sender_fbid", &uid) || - !fb_json_int_chk(json, "state", &state)) - { - fb_api_error(api, FB_API_ERROR, "Failed to obtain typing state"); - goto finish; + 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); } - typg.uid = uid; - typg.state = state; - FB_API_FUNC(api, typing, &typg); + g_slist_free_full(events, (GDestroyNotify) fb_api_event_free); + g_object_unref(values); + json_node_free(root); -finish: - json_value_free(json); } -/** - * Handles messages which are to be published to the user. - * - * @param api The #fb_api. - * @param pload The message payload. - **/ -static void fb_api_cb_publish_ms(fb_api_t *api, const GByteArray *pload) -{ - GSList *msgs; - fb_api_msg_t msg; - fb_thrift_t *thft; - json_value *json; - json_value *jv; - json_value *jx; - json_value *jy; - json_value *jz; - const gchar *str; - gint64 in; - guint i; - - thft = fb_thrift_new((GByteArray*) pload, 0, TRUE); +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 gchar * +fb_api_xma_parse(FbApi *api, const gchar *body, JsonNode *root, GError **error) +{ + const gchar *str; + const gchar *url; + FbHttpValues *prms; + FbJsonValues *values; + gchar *text; + GError *err = NULL; + + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.story_attachment.target.__type__.name"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.story_attachment.url"); + 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); + url = fb_json_values_next_str(values, NULL); + + if (g_strcmp0(str, "ExternalUrl") == 0) { + prms = fb_http_values_new(); + fb_http_values_parse(prms, url, TRUE); + text = fb_http_values_dup_str(prms, "u", NULL); + fb_http_values_free(prms); + } 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; + FbId uid; + FbJsonValues *values; + FbThrift *thft; + gchar *stoken; + GError *err = NULL; + GSList *msgs = NULL; + guint size; + JsonNode *root; + JsonNode *node; + + thft = fb_thrift_new(pload, 0); fb_thrift_read_str(thft, NULL); - i = thft->pos; - fb_thrift_free(thft); + size = fb_thrift_get_pos(thft); + g_object_unref(thft); - g_return_if_fail(i < pload->len); + g_return_if_fail(size < pload->len); + data = (gchar *) pload->data + size; + size = pload->len - size; - if (!fb_api_json_new(api, (gchar*) pload->data + i, pload->len - i, &json)) + if (!fb_api_json_chk(api, data, size, &root)) { return; + } - msgs = NULL; + 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); - if (fb_json_str_chk(json, "syncToken", &str)) { - g_free(api->stoken); - api->stoken = g_strdup(str); - FB_API_FUNC(api, connect); - goto finish; + 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; } - if (!fb_json_val_chk(json, "deltas", json_array, &jv)) - goto finish; + 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_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); + + if (g_hash_table_remove(priv->mids, &id)) { + continue; + } + + fb_api_message_reset(&msg, FALSE); + 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); - for (i = 0; i < jv->u.array.length; i++) { - jx = jv->u.array.values[i]; + if (uid == priv->uid) { + msg.flags |= FB_API_MESSAGE_FLAG_SELF; + msg.uid = oid; + } else { + msg.uid = uid; + } - if (!fb_json_val_chk(jx, "deltaNewMessage", json_object, &jy) || - !fb_json_val_chk(jy, "messageMetadata", json_object, &jz) || - !fb_json_int_chk(jz, "actorFbId", &in) || - (in == api->uid)) - { + if (msg.uid == 0) { continue; } - msg.uid = in; - msg.tid = 0; + body = fb_json_values_next_str(values, NULL); - if (fb_json_val_chk(jz, "threadKey", json_object, &jz) && - fb_json_int_chk(jz, "threadFbId", &in)) - { - msg.tid = in; + if (body != NULL) { + dmsg = fb_api_message_dup(&msg, FALSE); + dmsg->text = g_strdup(body); + msgs = g_slist_prepend(msgs, dmsg); } - if (fb_json_str_chk(jy, "body", &str)) { - msg.text = str; - msgs = g_slist_prepend(msgs, g_memdup(&msg, sizeof msg)); + id = fb_json_values_next_int(values, 0); + + if (id != 0) { + dmsg = fb_api_message_dup(&msg, FALSE); + fb_api_sticker(api, id, dmsg); } - if (fb_json_val_chk(jy, "attachments", json_array, &jy) && - (jy->u.array.length > 0)) - { - msg.text = "* Non-Displayable Attachments *"; - msgs = g_slist_prepend(msgs, g_memdup(&msg, sizeof msg)); + 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; } } - msgs = g_slist_reverse(msgs); - FB_API_FUNC(api, message, msgs); + 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); + } -finish: - g_slist_free_full(msgs, g_free); - json_value_free(json); + g_slist_free_full(msgs, (GDestroyNotify) fb_api_message_free); + g_object_unref(values); + json_node_free(root); } -/** - * Handles a presence states which are to be published to the user. - * - * @param api The #fb_api. - * @param pload The message payload. - **/ -static void fb_api_cb_publish_p(fb_api_t *api, const GByteArray *pload) -{ - fb_thrift_t *thft; - fb_thrift_type_t type; - fb_api_pres_t pres; - GSList *press; - gint64 i64; - gint32 i32; - guint size; - guint i; +static void +fb_api_cb_publish_p(FbApi *api, GByteArray *pload) +{ + FbApiPresence *pres; + FbThrift *thft; + FbThriftType type; + gint32 i32; + gint64 i64; + GSList *press; + guint i; + guint size; /* Start at 1 to skip the NULL byte */ - thft = fb_thrift_new((GByteArray*) pload, 1, TRUE); + thft = fb_thrift_new(pload, 1); press = NULL; /* Skip the full list boolean field */ @@ -563,28 +1455,34 @@ static void fb_api_cb_publish_p(fb_api_t *api, const GByteArray *pload) g_warn_if_fail(type == FB_THRIFT_TYPE_I32); fb_thrift_read_i32(thft, &i32); - pres.uid = i64; - pres.active = i32 != 0; - press = g_slist_prepend(press, g_memdup(&pres, sizeof pres)); - FB_UTIL_DEBUGLN("Presence: %" FB_ID_FORMAT " (%d)", i64, i32 != 0); + 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); /* Skip the last active timestamp field */ - if (!fb_thrift_read_field(thft, &type, NULL)) + if (!fb_thrift_read_field(thft, &type, NULL)) { continue; + } g_warn_if_fail(type == FB_THRIFT_TYPE_I64); fb_thrift_read_i64(thft, NULL); /* Skip the active client bits field */ - if (!fb_thrift_read_field(thft, &type, NULL)) + if (!fb_thrift_read_field(thft, &type, NULL)) { continue; + } g_warn_if_fail(type == FB_THRIFT_TYPE_I16); fb_thrift_read_i16(thft, NULL); /* Skip the VoIP compatibility bits field */ - if (!fb_thrift_read_field(thft, &type, NULL)) + if (!fb_thrift_read_field(thft, &type, NULL)) { continue; + } g_warn_if_fail(type == FB_THRIFT_TYPE_I64); fb_thrift_read_i64(thft, NULL); @@ -595,27 +1493,32 @@ static void fb_api_cb_publish_p(fb_api_t *api, const GByteArray *pload) /* Read the field stop */ fb_thrift_read_stop(thft); - fb_thrift_free(thft); + g_object_unref(thft); press = g_slist_reverse(press); - FB_API_FUNC(api, presence, press); - g_slist_free_full(press, g_free); + g_signal_emit_by_name(api, "presences", press); + g_slist_free_full(press, (GDestroyNotify) fb_api_presence_free); } -/** - * Implements #fb_mqtt_funcs->publish((). - * - * @param mqtt The #fb_mqtt. - * @param topic The message topic. - * @param pload The message payload. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_mqtt_publish(fb_mqtt_t *mqtt, const gchar *topic, - const GByteArray *pload, gpointer data) -{ - fb_api_t *api = data; +static void +fb_api_cb_mqtt_publish(FbMqtt *mqtt, const gchar *topic, GByteArray *pload, + gpointer data) +{ + FbApi *api = data; + gboolean comp; GByteArray *bytes; - gboolean comp; + 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_mercury}, + {"/orca_typing_notifications", fb_api_cb_publish_typing}, + {"/t_ms", fb_api_cb_publish_ms}, + {"/t_p", fb_api_cb_publish_p} + }; comp = fb_util_zcompressed(pload); @@ -630,811 +1533,1449 @@ static void fb_api_cb_mqtt_publish(fb_mqtt_t *mqtt, const gchar *topic, bytes = (GByteArray*) pload; } - fb_util_hexdump(bytes, 2, "Reading message (topic: %s):", topic); + fb_util_debug_hexdump(FB_UTIL_DEBUG_LEVEL_INFO, bytes, + "Reading message (topic: %s)", + topic); - if (g_ascii_strcasecmp(topic, "/orca_typing_notifications") == 0) - fb_api_cb_publish_tn(api, bytes); - else if (g_ascii_strcasecmp(topic, "/t_ms") == 0) - fb_api_cb_publish_ms(api, bytes); - else if (g_ascii_strcasecmp(topic, "/t_p") == 0) - fb_api_cb_publish_p(api, bytes); + 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)) + if (G_LIKELY(comp)) { g_byte_array_free(bytes, TRUE); + } } -/** - * Creates a new #fb_api. The returned #fb_api should be freed with - * #fb_api_free() when no longer needed. - * - * @param funcs The #fb_api_funcs. - * @param data The user-defined data or NULL. - * @param cid The client identifier or NULL. - * @param mid The MQTT identifier or NULL. - * @param cuid The client unique identifier or NULL. - * - * @return The #fb_api or NULL on error. - **/ -fb_api_t *fb_api_new(const fb_api_funcs_t *funcs, gpointer data) +FbApi * +fb_api_new(void) { - fb_api_t *api; - - static const fb_mqtt_funcs_t muncs = { - .error = fb_api_cb_mqtt_error, - .open = fb_api_cb_mqtt_open, - .connack = fb_api_cb_mqtt_connack, - .publish = fb_api_cb_mqtt_publish - }; - - g_return_val_if_fail(funcs != NULL, NULL); - - api = g_new0(fb_api_t, 1); - memcpy(&api->funcs, funcs, sizeof *funcs); - api->data = data; - api->http = fb_http_new(FB_API_AGENT); - api->mqtt = fb_mqtt_new(&muncs, api); + FbApi *api; + FbApiPrivate *priv; + + api = g_object_new(FB_TYPE_API, NULL); + priv = api->priv; + + 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; } -/** - * Rehashes the internal settings of a #fb_api. - * - * @param api The #fb_api. - **/ -void fb_api_rehash(fb_api_t *api) +void +fb_api_rehash(FbApi *api) { - sha1_state_t sha; - guint8 rb[50]; + FbApiPrivate *priv; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; - if (api->cid == NULL) { - random_bytes(rb, sizeof rb); - api->cid = g_compute_checksum_for_data(G_CHECKSUM_MD5, rb, sizeof rb); + if (priv->cid == NULL) { + priv->cid = fb_util_randstr(32); } - if (api->mid == 0) - api->mid = g_strdup_printf("%" G_GUINT32_FORMAT, g_random_int()); + if (priv->did == NULL) { + priv->did = fb_util_uuid(); + } - if (api->cuid == NULL) { - sha1_init(&sha); - random_bytes(rb, sizeof rb); - sha1_append(&sha, rb, sizeof rb); - api->cuid = sha1_random_uuid(&sha); + if (priv->mid == 0) { + priv->mid = g_random_int(); } - if (strlen(api->cid) > 20) { - api->cid = g_realloc_n(api->cid , 21, sizeof *api->cid); - api->cid[20] = 0; + if (strlen(priv->cid) > 20) { + priv->cid = g_realloc_n(priv->cid , 21, sizeof *priv->cid); + priv->cid[20] = 0; } } -/** - * Frees all memory used by a #fb_api. - * - * @param api The #fb_api. - **/ -void fb_api_free(fb_api_t *api) +gboolean +fb_api_is_invisible(FbApi *api) { - if (G_UNLIKELY(api == NULL)) - return; + FbApiPrivate *priv; - if (api->err != NULL) - g_error_free(api->err); + g_return_val_if_fail(FB_IS_API(api), FALSE); + priv = api->priv; - fb_mqtt_free(api->mqtt); - fb_http_free(api->http); - - g_free(api->cuid); - g_free(api->mid); - g_free(api->cid); - g_free(api->stoken); - g_free(api->token); - g_free(api); + return priv->invisible; } -/** - * Handles an error within an #fb_api. This sets #fb_api->err and calls - * the error function. If the fmt argument is NULL, then #fb_api->err - * is handled. - * - * @param api The #fb_api. - * @param err The #fb_api_error. - * @param fmt The format string or NULL. - * @param ... The arguments of the format string. - **/ -void fb_api_error(fb_api_t *api, fb_api_error_t err, const gchar *fmt, ...) +void +fb_api_error(FbApi *api, FbApiError error, const gchar *format, ...) { - gchar *str; - va_list ap; + GError *err; + va_list ap; - g_return_if_fail(api != NULL); + g_return_if_fail(FB_IS_API(api)); - if (fmt != NULL) { - va_start(ap, fmt); - str = g_strdup_vprintf(fmt, ap); - va_end(ap); + va_start(ap, format); + err = g_error_new_valist(FB_API_ERROR, error, format, ap); + va_end(ap); - g_clear_error(&api->err); - g_set_error_literal(&api->err, FB_API_ERROR, err, str); - g_free(str); - } + 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); - if (api->err != NULL) - FB_API_FUNC(api, error, api->err); + g_signal_emit_by_name(api, "error", error); + g_error_free(error); } -/** - * Implemented #fb_http_func for #fb_api_auth(). - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_auth(fb_http_req_t *req, gpointer data) +static void +fb_api_cb_attach(FbHttpRequest *req, gpointer data) { - fb_api_t *api = data; - json_value *json; const gchar *str; - gint64 in; - - if (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { + 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, req, &root)) { return; } - if (!fb_json_int_chk(json, "uid", &in) || - !fb_json_str_chk(json, "access_token", &str)) - { - fb_api_error(api, FB_API_ERROR_GENERAL, "Failed to obtain user info"); - goto finish; + 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, req); + 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(api->token); - api->token = g_strdup(str); + g_free(name); + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); - api->uid = in; - FB_API_FUNC(api, auth); + 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); -finish: - json_value_free(json); } -/** - * Sends a authentication request. - * - * @param api The #fb_api. - * @param user The username (email). - * @param pass The password. - **/ -void fb_api_auth(fb_api_t *api, const gchar *user, const gchar *pass) +static void +fb_api_attach(FbApi *api, FbId aid, const gchar *msgid, FbApiMessage *msg) { - fb_http_req_t *req; + FbHttpRequest *req; + FbHttpValues *prms; - g_return_if_fail(api != NULL); + prms = fb_http_values_new(); + fb_http_values_set_str(prms, "mid", msgid); + fb_http_values_set_strf(prms, "aid", "%" FB_ID_FORMAT, aid); - req = fb_api_req_new(api, FB_API_BHOST, FB_API_PATH_AUTH, - fb_api_cb_auth, - "com.facebook.auth.protocol.d", - "authenticate", - "auth.login"); + req = fb_api_http_req(api, FB_API_URL_ATTACH, "getAttachment", + "messaging.getAttachment", prms, + fb_api_cb_attach); + fb_api_data_set(api, req, msg, (GDestroyNotify) fb_api_message_free); +} - fb_http_req_params_set(req, - FB_HTTP_PAIR("email", user), - FB_HTTP_PAIR("password", pass), - NULL +static void +fb_api_cb_auth(FbHttpRequest *req, gpointer data) +{ + FbApi *api = data; + FbApiPrivate *priv = api->priv; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; + + if (!fb_api_http_chk(api, req, &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; ); - fb_api_req_send(api, req); + 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); } -/** - * Implemented #fb_http_func for #fb_api_contacts(). - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_contacts(fb_http_req_t *req, gpointer data) -{ - fb_api_t *api = data; - GSList *users; - fb_api_user_t user; - json_value *json; - json_value *jv; - json_value *jx; - json_value *jy; - json_value *jz; - const gchar *str; - const gchar *uid; - const gchar *name; - guint i; - - if (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { - return; +void +fb_api_auth(FbApi *api, const gchar *user, const gchar *pass) +{ + FbHttpValues *prms; + + prms = fb_http_values_new(); + fb_http_values_set_str(prms, "email", user); + fb_http_values_set_str(prms, "password", pass); + fb_api_http_req(api, FB_API_URL_AUTH, "authenticate", "auth.login", prms, + fb_api_cb_auth); +} + +static gboolean +fb_api_contact_parse(FbApi *api, FbApiUser *user, JsonNode *root, + GError **error) +{ + const gchar *str; + FbApiPrivate *priv = api->priv; + FbHttpValues *prms; + FbJsonValues *values; + 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, + "$.id"); + fb_json_values_add(values, FB_JSON_TYPE_STR, FALSE, + "$.represented_profile.friendship_status"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.structured_name.text"); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, + "$.hugePictureUrl.uri"); + 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) { + str = fb_json_values_next_str(values, "0"); + } else { + fb_json_values_next_str(values, NULL); } - users = NULL; + user->uid = FB_ID_FROM_STR(str); + str = fb_json_values_next_str(values, NULL); - if (!fb_json_val_chk(json, "viewer", json_object, &jv) || - !fb_json_val_chk(jv, "messenger_contacts", json_object, &jv) || - !fb_json_val_chk(jv, "nodes", json_array, &jv)) + if ((str != NULL) && + (g_strcmp0(str, "ARE_FRIENDS") != 0) && + (user->uid != priv->uid)) { - fb_api_error(api, FB_API_ERROR_GENERAL, "Failed to parse contacts"); - goto finish; + g_object_unref(values); + return FALSE; } - for (i = 0; i < jv->u.array.length; i++) { - jx = jv->u.array.values[i]; + user->name = fb_json_values_next_str_dup(values, NULL); + user->icon = fb_json_values_next_str_dup(values, NULL); + + prms = fb_http_values_new(); + fb_http_values_parse(prms, user->icon, TRUE); + user->csum = fb_http_values_dup_str(prms, "oh", &err); + fb_http_values_free(prms); - /* Scattered values lead to a gnarly conditional... */ - if (!fb_json_val_chk(jx, "represented_profile", json_object, &jy) || + g_object_unref(values); + return TRUE; +} - /* Check the contact type is "user" */ - !fb_json_val_chk(jy, "__type__", json_object, &jz) || - !fb_json_str_chk(jz, "name", &str) || - (g_ascii_strcasecmp(str, "user") != 0) || +static void +fb_api_cb_contact(FbHttpRequest *req, gpointer data) +{ + FbApi *api = data; + FbApiUser user; + GError *err = NULL; + JsonNode *node; + JsonNode *root; - /* Check the contact is a friend */ - !fb_json_str_chk(jy, "friendship_status", &str) || - (g_ascii_strcasecmp(str, "ARE_FRIENDS") != 0) || + if (!fb_api_http_chk(api, req, &root)) { + return; + } - /* Obtain the contact user identifier */ - !fb_json_str_chk(jy, "id", &uid) || + node = fb_json_node_get_nth(root, 0); - /* Obtain the name of the user */ - !fb_json_val_chk(jx, "structured_name", json_object, &jy) || - !fb_json_str_chk(jy, "text", &name)) - { - continue; - } + if (node == NULL) { + fb_api_error(api, FB_API_ERROR_GENERAL, + "Failed to obtain contact information"); + json_node_free(root); + return; + } - user.uid = FB_ID_FROM_STR(uid); + fb_api_user_reset(&user, FALSE); - if (user.uid != api->uid) { - user.name = name; - users = g_slist_prepend(users, g_memdup(&user, sizeof user)); + if (!fb_api_contact_parse(api, &user, node, &err)) { + if (G_LIKELY(err == NULL)) { + fb_api_error(api, FB_API_ERROR_GENERAL, + "Failed to parse contact information"); + } else { + fb_api_error_emit(api, err); } + } else { + g_signal_emit_by_name(api, "contact", &user); } - FB_API_FUNC(api, contacts, users); + fb_api_user_reset(&user, TRUE); + json_node_free(root); +} -finish: - g_slist_free_full(users, g_free); - json_value_free(json); +void +fb_api_contact(FbApi *api, FbId uid) +{ + JsonBuilder *bldr; + + /* Object key mapping: + * 0: contact_id + * 1: big_img_size + * 2: huge_img_size + * 3: small_img_size + * 4: low_res_cover_size + * 6: media_type + */ + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_strf(bldr, "0", "%" FB_ID_FORMAT, uid); + fb_api_http_query(api, FB_API_QUERY_CONTACT, bldr, fb_api_cb_contact); } -/** - * Sends a contacts request. - * - * @param api The #fb_api. - **/ -void fb_api_contacts(fb_api_t *api) +static void +fb_api_cb_contacts(FbHttpRequest *req, gpointer data) { - fb_http_req_t *req; + FbApi *api = data; + FbApiUser *duser; + FbApiUser user; + FbJsonValues *values; + gboolean complete; + gchar *writeid = NULL; + GError *err = NULL; + GSList *users = NULL; + guint count = 0; + JsonNode *node; + JsonNode *root; + + if (!fb_api_http_chk(api, req, &root)) { + return; + } - g_return_if_fail(api != NULL); + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.graph_api_write_id"); + fb_json_values_set_array(values, FALSE, "$.viewer.messenger_contacts" + ".nodes"); - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_GQL, - fb_api_cb_contacts, - "com.facebook.contacts.service.d", - "FetchContactsFullQuery", - "get"); + while (fb_json_values_update(values, &err)) { + g_free(writeid); + count++; - fb_http_req_params_set(req, - FB_HTTP_PAIR("query_id", FB_API_QRYID_CONTACTS), - FB_HTTP_PAIR("query_params", "{}"), - NULL - ); + writeid = fb_json_values_next_str_dup(values, NULL); + node = fb_json_values_get_root(values); + fb_api_user_reset(&user, FALSE); + + if (fb_api_contact_parse(api, &user, node, &err)) { + duser = fb_api_user_dup(&user, FALSE); + users = g_slist_prepend(users, duser); + } else { + fb_api_user_reset(&user, TRUE); + } + + if (G_UNLIKELY(err != NULL)) { + break; + } + } + + complete = (writeid == NULL) || (count < FB_API_CONTACTS_COUNT); + + if (G_UNLIKELY(err == NULL)) { + g_signal_emit_by_name(api, "contacts", users, complete); + + if (!complete) { + fb_api_contacts_after(api, writeid); + } + } else { + fb_api_error_emit(api, err); + } - fb_api_req_send(api, req); + g_free(writeid); + g_slist_free_full(users, (GDestroyNotify) fb_api_user_free); + g_object_unref(values); + json_node_free(root); } -/** - * Connects the #fb_api to the remote services. This is mainly for - * connecting and setting up the internal #fb_mqtt. - * - * @param The #fb_api. - **/ -void fb_api_connect(fb_api_t *api) +void +fb_api_contacts(FbApi *api) { - g_return_if_fail(api != NULL); - - fb_mqtt_open(api->mqtt, FB_MQTT_HOST, FB_MQTT_PORT); + JsonBuilder *bldr; + + /* Object key mapping: + * 0: profile_types + * 1: limit + * 2: big_img_size + * 3: huge_img_size + * 4: small_img_size + * 5: low_res_cover_size + * 6: media_type + */ + + 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); } -/** - * Disconnects the #fb_api from the remote services. This is mainly for - * disconnecting the internal #fb_mqtt. This will close the internal - * #fb_mqtt via #fb_mqtt_close(). - * - * @param The #fb_api. - **/ -void fb_api_disconnect(fb_api_t *api) +static void +fb_api_contacts_after(FbApi *api, const gchar *writeid) { - g_return_if_fail(api != NULL); + JsonBuilder *bldr; + + /* Object key mapping: + * 0: profile_types + * 1: after + * 2: limit + * 3: big_img_size + * 4: huge_img_size + * 5: small_img_size + * 6: low_res_cover_size + * 7: media_type + */ + + if (g_str_has_prefix(writeid, "contact_")) { + writeid += 8; + } + + 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_mqtt_disconnect(api->mqtt); + fb_json_bldr_add_str(bldr, "1", writeid); + 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); } -/** - * Sends a message to a user. - * - * @param api The #fb_api. - * @param id The #fb_id of the user. - * @param thread TRUE to send to a thread identifier, otherwise FALSE. - * @param msg The message. - **/ -void fb_api_message(fb_api_t *api, fb_id_t id, gboolean thread, - const gchar *msg) -{ - guint64 msgid; - const gchar *tpfx; - gchar *escaped; +void +fb_api_connect(FbApi *api, gboolean invisible) +{ + FbApiPrivate *priv; - g_return_if_fail(api != NULL); - g_return_if_fail(msg != NULL); + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; - msgid = FB_API_MSGID(g_get_real_time() / 1000, g_random_int()); - tpfx = thread ? "tfbid_" : ""; + priv->invisible = invisible; + fb_mqtt_open(priv->mqtt, FB_MQTT_HOST, FB_MQTT_PORT); +} - escaped = fb_json_str_escape(msg); +void +fb_api_disconnect(FbApi *api) +{ + FbApiPrivate *priv; - fb_api_publish(api, "/send_message2", "{" - "\"body\":\"%s\"," - "\"to\":\"%s%" FB_ID_FORMAT "\"," - "\"sender_fbid\":\"%" FB_ID_FORMAT "\"," - "\"msgid\":%" G_GUINT64_FORMAT - "}", escaped, tpfx, id, api->uid, msgid); + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; - g_free(escaped); + fb_mqtt_disconnect(priv->mqtt); } -/** - * Publishes a string based message to the MQTT service. This enables - * compression of the message via zlib. - * - * @param api The #fb_api. - * @param topic The message topic. - * @param fmt The format string. - * @param ... The format arguments. - **/ -void fb_api_publish(fb_api_t *api, const gchar *topic, const gchar *fmt, ...) +void +fb_api_message(FbApi *api, FbId id, gboolean thread, const gchar *text) { + const gchar *tpfx; + FbApiPrivate *priv; + FbId *dmid; + FbId mid; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + g_return_if_fail(text != NULL); + priv = api->priv; + + mid = FB_API_MSGID(g_get_real_time() / 1000, g_random_int()); + tpfx = thread ? "tfbid_" : ""; + + dmid = g_memdup(&mid, sizeof mid); + g_hash_table_replace(priv->mids, dmid, dmid); + + bldr = fb_json_bldr_new(JSON_NODE_OBJECT); + fb_json_bldr_add_str(bldr, "body", 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_publish(FbApi *api, const gchar *topic, const gchar *format, ...) +{ + FbApiPrivate *priv; GByteArray *bytes; GByteArray *cytes; - gchar *msg; - va_list ap; + gchar *msg; + va_list ap; - g_return_if_fail(api != NULL); + g_return_if_fail(FB_IS_API(api)); g_return_if_fail(topic != NULL); - g_return_if_fail(fmt != NULL); + g_return_if_fail(format != NULL); + priv = api->priv; - va_start(ap, fmt); - msg = g_strdup_vprintf(fmt, ap); + 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_zcompress(bytes); - fb_util_hexdump(bytes, 2, "Writing message:"); - fb_mqtt_publish(api->mqtt, topic, cytes); + fb_util_debug_hexdump(FB_UTIL_DEBUG_LEVEL_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); } -/** - * Implemented #fb_http_func for #fb_api_thread_create(). - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_thread_create(fb_http_req_t *req, gpointer data) +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) { - fb_api_t *api = data; - json_value *json; const gchar *str; - fb_id_t tid; + 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 (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { + if (G_UNLIKELY(err != NULL)) { + g_propagate_error(error, err); + } + + g_object_unref(values); + return msgs; +} + +static void +fb_api_cb_unread_msgs(FbHttpRequest *req, 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, req, &root)) { return; } - if (!fb_json_str_chk(json, "thread_fbid", &str)) { - fb_api_error(api, FB_API_ERROR, "Failed to create thread"); - goto finish; + 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); - FB_API_FUNC(api, thread_create, tid); + 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, 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; + + 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); -finish: - json_value_free(json); + 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); } -/** - * Sends a thread creation request. - * - * @param api The #fb_api. - * @param uids The #GSList of #fb_id. - **/ -void fb_api_thread_create(fb_api_t *api, GSList *uids) -{ - fb_http_req_t *req; - GSList *cids; - GSList *l; - GString *to; - fb_id_t *uid; - - g_return_if_fail(api != NULL); - g_warn_if_fail((uids != NULL) && (uids->next != NULL)); - - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_THRDS, - fb_api_cb_thread_create, - "ccom.facebook.orca.send.service.l", - "createThread", - "POST"); - - to = g_string_new(NULL); - cids = g_slist_copy(uids); - cids = g_slist_prepend(cids, &api->uid); - - for (l = cids; l != NULL; l = l->next) { - uid = l->data; +static void +fb_api_cb_unread(FbHttpRequest *req, 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, req, &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 (to->len > 0) - g_string_append_c(to, ','); + if (id == NULL) { + id = fb_json_values_next_str(values, "0"); + } - g_string_append_printf(to, "{" - "\"type\":\"id\"," - "\"id\":\"%" FB_ID_FORMAT "\"" - "}", *uid); + /* See fb_api_thread_info() for key mapping */ + 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); } - g_string_prepend_c(to, '['); - g_string_append_c(to, ']'); + if (G_UNLIKELY(err != NULL)) { + fb_api_error_emit(api, err); + } - fb_http_req_params_set(req, - FB_HTTP_PAIR("to", to->str), - NULL - ); + 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; - fb_api_req_send(api, req); - g_string_free(to, TRUE); - g_slist_free(cids); + if (priv->unread < 1) { + return; + } + + /* See fb_api_thread_list() for key mapping */ + 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); } -/** - * Implemented #fb_http_func for #fb_api_thread(). - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_thread_info(fb_http_req_t *req, gpointer data) -{ - fb_api_t *api = data; - fb_api_thread_t thrd; - fb_api_user_t user; - json_value *json; - json_value *jv; - json_value *jx; - gpointer mptr; - const gchar *str; - guint i; - - if (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { +static void +fb_api_cb_sticker(FbHttpRequest *req, 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, req, &root)) { return; } - /* Scattered values lead to a gnarly conditional... */ - if (!fb_json_val_chk(json, "data", json_array, &jv) || + 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); - /* Obtain the first array element */ - (jv->u.array.length != 1) || - ((jv = jv->u.array.values[0]) == NULL) || + FB_API_ERROR_EMIT(api, err, + g_object_unref(values); + json_node_free(root); + return; + ); - /* Check the name */ - !fb_json_str_chk(jv, "name", &str) || - (g_ascii_strcasecmp(str, "threads") != 0) || + msg = fb_api_data_take(api, req); + msg->flags |= FB_API_MESSAGE_FLAG_IMAGE; + msg->text = fb_json_values_next_str_dup(values, NULL); + msgs = g_slist_prepend(msgs, msg); - /* Obtain result array */ - !fb_json_val_chk(jv, "fql_result_set", json_array, &jv) || - (jv->u.array.length != 1) || - ((jv = jv->u.array.values[0]) == NULL) || + 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); +} - /* Obtain the thread identifier */ - !fb_json_str_chk(jv, "thread_fbid", &str)) - { - fb_api_error(api, FB_API_ERROR, "Failed to fetch thread info"); - goto finish; +static void +fb_api_sticker(FbApi *api, FbId sid, FbApiMessage *msg) +{ + JsonBuilder *bldr; + FbHttpRequest *req; + + /* Object key mapping: + * 0: sticker_ids + * 1: media_type + * 2: preview_size + * 3: scaling_factor + * 4: animated_media_type + */ + + 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); + + req = fb_api_http_query(api, FB_API_QUERY_STICKER, bldr, + fb_api_cb_sticker); + fb_api_data_set(api, req, 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; } - thrd.tid = FB_ID_FROM_STR(str); - thrd.topic = NULL; - thrd.users = NULL; + if ((g_slist_length(thrd->users) < 2) || !haself) { + fb_api_thread_reset(thrd, TRUE); + g_object_unref(values); + return FALSE; + } - if (fb_json_str_chk(jv, "name", &str) && (strlen(str) > 0)) - thrd.topic = str; + g_object_unref(values); + return TRUE; +} - if (fb_json_val_chk(jv, "participants", json_array, &jv)) { - for (i = 0; i < jv->u.array.length; i++) { - jx = jv->u.array.values[i]; +static void +fb_api_cb_thread(FbHttpRequest *req, gpointer data) +{ + FbApi *api = data; + FbApiThread thrd; + GError *err = NULL; + JsonNode *node; + JsonNode *root; - if (!fb_json_str_chk(jx, "user_id", &str)) - continue; + if (!fb_api_http_chk(api, req, &root)) { + return; + } - user.uid = FB_ID_FROM_STR(str); + node = fb_json_node_get_nth(root, 0); - if (fb_json_str_chk(jx, "name", &str) && (user.uid != api->uid)) { - user.name = str; - mptr = g_memdup(&user, sizeof user); - thrd.users = g_slist_prepend(thrd.users, mptr); - } + 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_FUNC(api, thread_info, &thrd); - g_slist_free_full(thrd.users, g_free); + fb_api_thread_reset(&thrd, TRUE); + json_node_free(root); +} -finish: - json_value_free(json); +void +fb_api_thread(FbApi *api, FbId tid) +{ + JsonBuilder *bldr; + + /* Object key mapping: + * 0: thread_ids + * 1: verification_type + * 2: hash_key + * 3: small_preview_size + * 4: large_preview_size + * 5: item_count + * 6: event_count + * 7: full_screen_height + * 8: full_screen_width + * 9: medium_preview_size + * 10: fetch_users_separately + * 11: include_message_info + * 12: msg_count + * 13: include_full_user_info + * 14: profile_pic_large_size + * 15: profile_pic_medium_size + * 16: profile_pic_small_size + */ + + 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); } -/** - * Sends a thread info request. - * - * @param api The #fb_api. - * @param tid The thread #fb_id. - **/ -void fb_api_thread_info(fb_api_t *api, fb_id_t tid) +static void +fb_api_cb_thread_create(FbHttpRequest *req, gpointer data) { - fb_http_req_t *req; - gchar *query; + const gchar *str; + FbApi *api = data; + FbId tid; + FbJsonValues *values; + GError *err = NULL; + JsonNode *root; - g_return_if_fail(api != NULL); + if (!fb_api_http_chk(api, req, &root)) { + return; + } - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_FQL, - fb_api_cb_thread_info, - "com.facebook.orca.protocol.methods.u", - "fetchThreadList", - "GET"); + values = fb_json_values_new(root); + fb_json_values_add(values, FB_JSON_TYPE_STR, TRUE, "$.thread_fbid"); + fb_json_values_update(values, &err); - query = g_strdup_printf("{" - "\"threads\":\"" - "SELECT thread_fbid, participants, name " - "FROM unified_thread " - "WHERE thread_fbid='%" FB_ID_FORMAT "' " - "LIMIT 1\"" - "}", tid); + 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); - fb_http_req_params_set(req, FB_HTTP_PAIR("q", query), NULL); - fb_api_req_send(api, req); - g_free(query); + g_object_unref(values); + json_node_free(root); } -/** - * Sends a thread invite request. - * - * @param api The #fb_api. - * @param tid The thread #fb_id. - * @param uid The user #fb_id. - **/ -void fb_api_thread_invite(fb_api_t *api, fb_id_t tid, fb_id_t uid) -{ - fb_http_req_t *req; - gchar *stid; - gchar *to; - - g_return_if_fail(api != NULL); - - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_PARTS, - fb_api_cb_http_bool, - "com.facebook.orca.protocol.a", - "addMembers", - "POST"); - - stid = g_strdup_printf("t_id.%" FB_ID_FORMAT, tid); - to = g_strdup_printf("[{" - "\"type\":\"id\"," - "\"id\":\"%" FB_ID_FORMAT "\"" - "}]", uid); - - fb_http_req_params_set(req, - FB_HTTP_PAIR("id", stid), - FB_HTTP_PAIR("to", to), - NULL - ); +void +fb_api_thread_create(FbApi *api, GSList *uids) +{ + FbApiPrivate *priv; + FbHttpValues *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); + } - fb_api_req_send(api, req); - g_free(stid); - g_free(to); + json = fb_json_bldr_close(bldr, JSON_NODE_ARRAY, NULL); + prms = fb_http_values_new(); + fb_http_values_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); } -/** - * Frees all memory used by a #fb_api_thread. - * - * @param thrd The #fb_api_thread. - **/ -static void fb_api_cb_threads_free(fb_api_thread_t *thrd) +void +fb_api_thread_invite(FbApi *api, FbId tid, FbId uid) { - g_slist_free_full(thrd->users, g_free); - g_free(thrd); + FbHttpValues *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_values_new(); + fb_http_values_set_str(prms, "to", json); + fb_http_values_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); } -/** - * Implemented #fb_http_func for #fb_api_threads(). - * - * @param req The #fb_http_req. - * @param data The user-defined data, which is #fb_api. - **/ -static void fb_api_cb_thread_list(fb_http_req_t *req, gpointer data) -{ - fb_api_t *api = data; - fb_api_thread_t thrd; - fb_api_user_t user; - GSList *thrds; - json_value *json; - json_value *jv; - json_value *jx; - json_value *jy; - gpointer mptr; - const gchar *str; - guint i; - guint j; - - if (!fb_api_http_chk(api, req) || - !fb_api_json_new(api, req->body, req->body_size, &json)) - { +void +fb_api_thread_remove(FbApi *api, FbId tid, FbId uid) +{ + FbApiPrivate *priv; + FbHttpValues *prms; + gchar *json; + JsonBuilder *bldr; + + g_return_if_fail(FB_IS_API(api)); + priv = api->priv; + + prms = fb_http_values_new(); + fb_http_values_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_values_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) +{ + FbHttpValues *prms; + + prms = fb_http_values_new(); + fb_http_values_set_str(prms, "name", topic); + fb_http_values_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(FbHttpRequest *req, 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, req, &root)) { return; } - thrds = NULL; + arr = fb_json_node_get_arr(root, "$.viewer.message_threads.nodes", + &err); + FB_API_ERROR_EMIT(api, err, + json_node_free(root); + return; + ); - /* Scattered values lead to a gnarly conditional... */ - if (!fb_json_val_chk(json, "data", json_array, &jv) || + elms = json_array_get_elements(arr); - /* Obtain the first array element */ - (jv->u.array.length != 1) || - ((jv = jv->u.array.values[0]) == NULL) || + for (l = elms; l != NULL; l = l->next) { + fb_api_thread_reset(&thrd, FALSE); - /* Check the name */ - !fb_json_str_chk(jv, "name", &str) || - (g_ascii_strcasecmp(str, "threads") != 0) || + 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); + } - /* Obtain result array */ - !fb_json_val_chk(jv, "fql_result_set", json_array, &jv)) - { - fb_api_error(api, FB_API_ERROR, "Failed to fetch thread list"); - goto finish; + if (G_UNLIKELY(err != NULL)) { + break; + } } - for (i = 0; i < jv->u.array.length; i++) { - jx = jv->u.array.values[i]; + 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); + } - if (!fb_json_str_chk(jx, "thread_fbid", &str)) - continue; + g_slist_free_full(thrds, (GDestroyNotify) fb_api_thread_free); + g_list_free(elms); + json_array_unref(arr); + json_node_free(root); +} - thrd.tid = FB_ID_FROM_STR(str); - thrd.topic = NULL; - thrd.users = NULL; +void +fb_api_threads(FbApi *api) +{ + JsonBuilder *bldr; + + /* Object key mapping: + * 0: folder_tag + * 1: thread_count + * 2: include_thread_info + * 3: verification_type + * 4: hash_key + * 5: small_preview_size + * 6: large_preview_size + * 7: item_count + * 8: event_count + * 9: full_screen_height + * 10: full_screen_width + * 11: medium_preview_size + * 12: fetch_users_separately + * 13: include_message_info + * 14: msg_count + * 15: <UNKNOWN> + * 16: profile_pic_large_size + * 17: profile_pic_medium_size + * 18: profile_pic_small_size + */ + + 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); +} - if (fb_json_str_chk(jx, "name", &str) && (strlen(str) > 0)) - thrd.topic = str; +void +fb_api_typing(FbApi *api, FbId uid, gboolean state) +{ + gchar *json; + JsonBuilder *bldr; - if (!fb_json_val_chk(jx, "participants", json_array, &jx)) { - thrds = g_slist_prepend(thrds, g_memdup(&thrd, sizeof thrd)); - continue; - } + 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); - for (j = 0; j < jx->u.array.length; j++) { - jy = jx->u.array.values[j]; + json = fb_json_bldr_close(bldr, JSON_NODE_OBJECT, NULL); + fb_api_publish(api, "/typing", "%s", json); + g_free(json); +} - if (!fb_json_str_chk(jy, "user_id", &str)) - continue; +FbApiEvent * +fb_api_event_dup(const FbApiEvent *event) +{ + if (event == NULL) { + return g_new0(FbApiEvent, 1); + } - user.uid = FB_ID_FROM_STR(str); + return g_memdup(event, sizeof *event); +} - if (fb_json_str_chk(jy, "name", &str) && (user.uid != api->uid)) { - user.name = str; - mptr = g_memdup(&user, sizeof user); - thrd.users = g_slist_prepend(thrd.users, mptr); - } - } +void +fb_api_event_reset(FbApiEvent *event) +{ + g_return_if_fail(event != NULL); + memset(event, 0, sizeof *event); +} + +void +fb_api_event_free(FbApiEvent *event) +{ + if (G_LIKELY(event != NULL)) { + g_free(event); + } +} + +FbApiMessage * +fb_api_message_dup(const FbApiMessage *msg, gboolean deep) +{ + FbApiMessage *ret; - thrds = g_slist_prepend(thrds, g_memdup(&thrd, sizeof thrd)); + if (msg == NULL) { + return g_new0(FbApiMessage, 1); } - thrds = g_slist_reverse(thrds); - FB_API_FUNC(api, thread_list, thrds); + ret = g_memdup(msg, sizeof *msg); + + if (deep) { + ret->text = g_strdup(msg->text); + } -finish: - g_slist_free_full(thrds, (GDestroyNotify) fb_api_cb_threads_free); - json_value_free(json); + return ret; } -/** - * Sends a thread list request. - * - * @param api The #fb_api. - * @param limit The thread count limit. - **/ -void fb_api_thread_list(fb_api_t *api, guint limit) -{ - fb_http_req_t *req; - gchar *query; - - g_return_if_fail(api != NULL); - - req = fb_api_req_new(api, FB_API_GHOST, FB_API_PATH_FQL, - fb_api_cb_thread_list, - "com.facebook.orca.protocol.methods.u", - "fetchThreadList", - "GET"); - - query = g_strdup_printf("{" - "\"threads\":\"" - "SELECT thread_fbid, participants, name " - "FROM unified_thread " - "WHERE folder='inbox' " - "ORDER BY timestamp DESC " - "LIMIT %u\"" - "}", limit); - - fb_http_req_params_set(req, FB_HTTP_PAIR("q", query), NULL); - fb_api_req_send(api, req); - g_free(query); -} - -/** - * Sends a thread topic request. - * - * @param api The #fb_api. - * @param tid The thread #fb_id. - * @param topic The topic message. - **/ -void fb_api_thread_topic(fb_api_t *api, fb_id_t tid, const gchar *topic) +void +fb_api_message_reset(FbApiMessage *msg, gboolean deep) { - fb_http_req_t *req; - gchar *stid; + g_return_if_fail(msg != NULL); - g_return_if_fail(api != NULL); + if (deep) { + g_free(msg->text); + } - req = fb_api_req_new(api, FB_API_HOST, FB_API_PATH_TOPIC, - fb_api_cb_http_bool, - "com.facebook.orca.protocol.a", - "setThreadName", - "messaging.setthreadname"); + memset(msg, 0, sizeof *msg); +} - stid = g_strdup_printf("t_id.%" FB_ID_FORMAT, tid); +void +fb_api_message_free(FbApiMessage *msg) +{ + if (G_LIKELY(msg != NULL)) { + g_free(msg->text); + g_free(msg); + } +} - fb_http_req_params_set(req, - FB_HTTP_PAIR("tid", stid), - FB_HTTP_PAIR("name", topic), - NULL - ); +FbApiPresence * +fb_api_presence_dup(const FbApiPresence *pres) +{ + if (pres == NULL) { + return g_new0(FbApiPresence, 1); + } - fb_api_req_send(api, req); - g_free(stid); + return g_memdup(pres, sizeof *pres); } -/** - * Sends a typing state to a user. - * - * @param api The #fb_api. - * @param uid The #fb_id. - * @param state TRUE if the user is typing, otherwise FALSE. - **/ -void fb_api_typing(fb_api_t *api, fb_id_t uid, gboolean state) -{ - g_return_if_fail(api != NULL); - - fb_api_publish(api, "/typing", "{" - "\"to\":\"%" FB_ID_FORMAT "\"," - "\"state\":%d" - "}", uid, state != 0); +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; + + if (thrd == NULL) { + return g_new0(FbApiThread, 1); + } + + ret = g_memdup(thrd, sizeof *thrd); + + if (deep) { + ret->topic = g_strdup(thrd->topic); + ret->users = g_slist_copy_deep(thrd->users, + (GCopyFunc) fb_api_user_dup, + GINT_TO_POINTER(deep)); + } + + 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); + } } |