/**
 * Interface to the matrix client/server API
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
 */

#include "matrix-api.h"

/* std lib */
#include <string.h>

/* json-glib */
#include <json-glib/json-glib.h>
#include <http_parser.h>

/* libpurple */
#include <debug.h>
#include <ntlm.h>
#include <libpurple/version.h>

#include "libmatrix.h"
#include "matrix-json.h"

struct _MatrixApiRequestData {
    PurpleUtilFetchUrlData *purple_data;
    MatrixConnectionData *conn;
    MatrixApiCallback callback;
    MatrixApiErrorCallback error_callback;
    MatrixApiBadResponseCallback bad_response_callback;
    gpointer user_data;
};


/**
 * Default callback if there was an error calling the API. We just put the
 * connection into the "error" state.
 *
 */
void matrix_api_error(MatrixConnectionData *conn, gpointer user_data,
        const gchar *error_message)
{
    if(strcmp(error_message, "cancelled") != 0)
        purple_connection_error_reason(conn->pc,
            PURPLE_CONNECTION_ERROR_NETWORK_ERROR, error_message);
}

/**
 * Default callback if the API returns a non-200 response. We just put the
 * connection into the "error" state.
 */
void matrix_api_bad_response(MatrixConnectionData *ma, gpointer user_data,
        int http_response_code, JsonNode *json_root)
{
    JsonObject *json_obj;
    const gchar *errcode = NULL, *error = NULL;
    gchar *error_message;
    PurpleConnectionError error_reason = PURPLE_CONNECTION_ERROR_OTHER_ERROR;

    if(json_root != NULL) {
        json_obj = matrix_json_node_get_object(json_root);
        errcode = matrix_json_object_get_string_member(json_obj, "errcode");
        error = matrix_json_object_get_string_member(json_obj, "error");
    }

    if(errcode != NULL && error != NULL) {
        error_message = g_strdup_printf("%s: %s: %s",
                _("Error from home server"), errcode, error);
    } else {
        error_message = g_strdup_printf("%s: %i",
                _("Error from home server"), http_response_code);
    }

    /* Use a non-fatal error reason on HTTP 429 and 500 to allow auto-reconnection */
    if (http_response_code == 429 || http_response_code > 500) {
        error_reason = PURPLE_CONNECTION_ERROR_NETWORK_ERROR;
    }

    purple_connection_error_reason(ma->pc,
            error_reason,
            error_message);

    g_free(error_message);
}


/******************************************************************************
 *
 * HTTP response parsing
 */


#define HEADER_PARSING_STATE_LAST_WAS_VALUE 0
#define HEADER_PARSING_STATE_LAST_WAS_FIELD 1

typedef struct {
    int header_parsing_state;
    GString *current_header_name;
    GString *current_header_value;
    gchar *content_type;
    gboolean got_headers;
    JsonParser *json_parser;
    char *body;
    size_t body_len;
} MatrixApiResponseParserData;


/** create a MatrixApiResponseParserData */
static MatrixApiResponseParserData *_response_parser_data_new()
{
    MatrixApiResponseParserData *res = g_new0(MatrixApiResponseParserData, 1);
    res->header_parsing_state = HEADER_PARSING_STATE_LAST_WAS_VALUE;
    res->current_header_name = g_string_new("");
    res->current_header_value = g_string_new("");
    res->json_parser = json_parser_new();
    return res;
}

/** free a MatrixApiResponseParserData */
static void _response_parser_data_free(MatrixApiResponseParserData *data)
{
    if(data == NULL)
        return;

    g_string_free(data->current_header_name, TRUE);
    g_string_free(data->current_header_value, TRUE);
    g_free(data->content_type);

    /* free the JSON parser, and all of the node structures */
    if(data -> json_parser)
        g_object_unref(data -> json_parser);
    g_free(data->body);
    data->body = NULL;

    g_free(data);
}

static void _handle_header_completed(MatrixApiResponseParserData *response_data)
{
    const gchar *name = response_data->current_header_name->str,
            *value = response_data->current_header_value->str;

    if(*name == '\0') {
        /* nothing to do here */
        return;
    }

    if(purple_debug_is_verbose())
        purple_debug_info("matrixprpl", "Handling API response header %s: %s\n",
                name, value);

    if(strcmp(name, "Content-Type") == 0) {
        g_free(response_data->content_type);
        response_data->content_type = g_strdup(value);
    }
}

/**
 * callback from the http parser which handles a header name
 */
static int _handle_header_field(http_parser *http_parser, const char *at,
        size_t length)
{
    MatrixApiResponseParserData *response_data = http_parser->data;

    if (response_data->header_parsing_state ==
            HEADER_PARSING_STATE_LAST_WAS_VALUE) {
        /* starting a new header */
        _handle_header_completed(response_data);

        g_string_truncate(response_data -> current_header_name, 0);
        g_string_truncate(response_data -> current_header_value, 0);
    }

    g_string_append_len(response_data -> current_header_name, at, length);
    response_data->header_parsing_state = HEADER_PARSING_STATE_LAST_WAS_FIELD;
    return 0;
}

/**
 * callback from the http parser which handles a header value
 */
static int _handle_header_value(http_parser *http_parser, const char *at,
        size_t length)
{
    MatrixApiResponseParserData *response_data = http_parser->data;
    g_string_append_len(response_data -> current_header_value, at, length);
    response_data->header_parsing_state = HEADER_PARSING_STATE_LAST_WAS_VALUE;
    return 0;
}


static int _handle_headers_complete(http_parser *http_parser)
{
    MatrixApiResponseParserData *response_data = http_parser->data;
    _handle_header_completed(response_data);
    response_data->got_headers = TRUE;
    return 0;
}


/**
 * callback from the http parser which handles the message body
 * Can be called multiple times as we accumulate chunks.
 */
static int _handle_body(http_parser *http_parser, const char *at,
        size_t length)
{
    MatrixApiResponseParserData *response_data = http_parser->data;
    if(purple_debug_is_verbose())
        purple_debug_info("matrixprpl", "Handling API response body %.*s\n",
                (int)length, at);

    response_data->body = g_realloc(response_data->body,
                                    response_data->body_len + length);
    memcpy(response_data->body + response_data->body_len, at, length);
    response_data->body_len += length;

    return 0;
}

/**
 * callback from the http parser after all chunks have arrived.
 */
static int _handle_message_complete(http_parser *http_parser)
{
    MatrixApiResponseParserData *response_data = http_parser->data;
    GError *err = NULL;

    if(strcmp(response_data->content_type, "application/json") == 0) {
        if(!json_parser_load_from_data(response_data -> json_parser,
                                       response_data->body,
                                       response_data->body_len,
                &err)) {
            purple_debug_info("matrixprpl", "unable to parse JSON: %s\n",
                    err->message);
            g_error_free(err);
            return 1;
        }
    }
    return 0;
}


/**
 * The callback we give to purple_util_fetch_url_request - does some
 * initial processing of the response
 */
static void matrix_api_complete(PurpleUtilFetchUrlData *url_data,
                                gpointer user_data,
                                const gchar *ret_data,
                                gsize ret_len,
                                const gchar *error_message)
{
    MatrixApiRequestData *data = (MatrixApiRequestData *)user_data;
    MatrixApiResponseParserData *response_data = NULL;
    int response_code = -1;
    JsonNode *root = NULL;
    
    if(error_message) {
        purple_debug_warning("matrixprpl", "Error from http request: %s\n",
                error_message);
    } else if (purple_debug_is_verbose()) {
        purple_debug_info("matrixprpl", "Got response: %s\n", ret_data);
    }

    if(!error_message) {
        http_parser http_parser;
        http_parser_settings http_parser_settings;
        enum http_errno http_error;

        memset(&http_parser, 0, sizeof(http_parser));
        memset(&http_parser_settings, 0, sizeof(http_parser_settings));

        response_data = _response_parser_data_new();

        http_parser_settings.on_header_field = _handle_header_field;
        http_parser_settings.on_header_value = _handle_header_value;
        http_parser_settings.on_headers_complete = _handle_headers_complete;
        http_parser_settings.on_body = _handle_body;
        http_parser_settings.on_message_complete = _handle_message_complete;

        http_parser_init(&http_parser, HTTP_RESPONSE);
        http_parser.data = response_data;
        http_parser_execute(&http_parser, &http_parser_settings,
                ret_data, ret_len);

        /* we have to do a separate call to tell the parser that we've got to
         * EOF.
         */
        http_parser_execute(&http_parser, &http_parser_settings, ret_data, 0);

        http_error = HTTP_PARSER_ERRNO(&http_parser);
        if(http_error != HPE_OK) {
            /* invalid response line */
            purple_debug_info("matrixprpl",
                              "Error (%s) parsing HTTP response %s\n",
                              http_errno_description(http_error), ret_data);
            error_message = _("Invalid response from homeserver");
        } else if (!response_data->got_headers) {
            /* this will happen if we hit EOF before the end of the headers */
            purple_debug_info("matrixprpl",
                              "EOF before end of HTTP headers in response %s\n",
                              ret_data);
            error_message = _("Invalid response from homeserver");

        } else {
            response_code = http_parser.status_code;
        }
    }

    if(!error_message) {
        root = json_parser_get_root(response_data -> json_parser);
    }

    if (error_message) {
        purple_debug_info("matrixprpl", "Handling error: %s\n", error_message);
        (data->error_callback)(data->conn, data->user_data, error_message);
    } else if(response_code >= 300) {
        purple_debug_info("matrixprpl", "API gave response %i\n",
                response_code);
        (data->bad_response_callback)(data->conn, data->user_data,
                response_code, root);
    } else if (data->callback) {
        (data->callback)(data->conn, data->user_data, root,
                         response_data->body, response_data->body_len,
                         response_data->content_type );
    }

    _response_parser_data_free(response_data);
    g_free(data);
}

/******************************************************************************
 *
 * API entry points
 */

/*
 * Add proxy authentication headers to a request
 */
static void _add_proxy_auth_headers(GString *request_str, PurpleProxyInfo *gpi)
{
    const char *username, *password;
    char *t1, *t2, *ntlm_type1;
    const gchar *hostname;

    username = purple_proxy_info_get_username(gpi);
    password = purple_proxy_info_get_password(gpi);
    if (username == NULL)
        return;

    hostname = g_get_host_name();

    t1 = g_strdup_printf("%s:%s", username, password ? password : "");
    t2 = purple_base64_encode((const guchar *)t1, strlen(t1));
    g_free(t1);

    ntlm_type1 = purple_ntlm_gen_type1(hostname, "");
    g_string_append_printf(request_str,
            "Proxy-Authorization: Basic %s\r\n"
            "Proxy-Authorization: NTLM %s\r\n"
            "Proxy-Connection: Keep-Alive\r\n",
            t2, ntlm_type1);
    g_free(ntlm_type1);
    g_free(t2);
}


/**
 * parse a URL as much as we need
 *
 * @param url    url to be parsed
 * @param host   returns a pointer to the start of the hostname, or NULL if none
 * @param path   returns a pointer to the start of the path
 */
static void _parse_url(const gchar *url, const gchar **host, const gchar **path)
{
    const gchar *ptr;

    /* find the separator between the host and the path */
    /* first find the end of the scheme */
    ptr = url;
    while(*ptr != ':' && *ptr != '/' && *ptr != '\0')
        ptr++;

    if(*ptr != ':') {
        /* no scheme, so presumably no hostname - it's a relative path */
        *host = NULL;
        *path = ptr;
        return;
    }

    /* the url has a scheme, which implies it also has a hostname */
    ptr++;
    while(*ptr == '/')
        ptr++;
    *host = ptr;
    /* skip the rest of the hostname. The path starts at the next /. */
    while(*ptr != '/' && *ptr != '\0')
        ptr++;
    *path = ptr;
}


/**
 * We have to build our own HTTP requests because:
 *   - libpurple only supports GET
 *   - libpurple's purple_url_parse assumes that the path + querystring is
 *     shorter than 256 bytes.
 *
 *  @returns a GString* which should be freed
 */
static GString *_build_request(PurpleAccount *acct, const gchar *url,
        const gchar *method, const gchar *extra_headers,
        const gchar *body,
        const gchar *extra_data, gsize extra_len)
{
    PurpleProxyInfo *gpi = purple_proxy_get_setup(acct);
    GString *request_str = g_string_new(NULL);
    const gchar *url_host, *url_path;
    gboolean using_http_proxy = FALSE;

    if(gpi != NULL) {
        PurpleProxyType type = purple_proxy_info_get_type(gpi);
        using_http_proxy = (type == PURPLE_PROXY_USE_ENVVAR
                || type == PURPLE_PROXY_HTTP);
    }

    _parse_url(url, &url_host, &url_path);

    /* we only support absolute URLs (with schemes) */
    g_assert(url_host != NULL);

    /* If we are connecting via a proxy, we should put the whole url
     * in the request line. (But synapse chokes if we do that on a direct
     * connection.)
     */
    g_string_append_printf(request_str, "%s %s HTTP/1.1\r\n",
            method, using_http_proxy ? url : url_path);
    g_string_append_printf(request_str, "Host: %.*s\r\n",
            (int)(url_path-url_host), url_host);

    if (extra_headers != NULL)
        g_string_append(request_str, extra_headers);
    g_string_append(request_str, "Connection: close\r\n");
    g_string_append_printf(request_str, "Content-Length: %" G_GSIZE_FORMAT "\r\n",
            extra_len + (body == NULL ? 0 : strlen(body)));

    if(using_http_proxy)
        _add_proxy_auth_headers(request_str, gpi);

    g_string_append(request_str, "\r\n");
    if(body != NULL)
        g_string_append(request_str, body);

    if(extra_data != NULL)
        g_string_append_len(request_str, extra_data, extra_len);

    return request_str;
}


/**
 * Start an HTTP call to the API
 *
 * @param method      HTTP method (eg "GET")
 * @param extra_headers  Extra HTTP headers to add
 * @param body        body of request, or NULL if none
 * @param extra_data  raw binary data to be sent after the body
 * @param extra_len   The length of the raw binary data
 * @param max_len     maximum number of bytes to return from the request. -1 for
 *                    default (512K).
 *
 * @returns handle for the request, or NULL if the request couldn't be started
 *   (eg, invalid hostname). In this case, the error_callback will have
 *   been called already.
 * Note: extra_data/extra_len is only available on libpurple >=2.11.0
 */
static MatrixApiRequestData *matrix_api_start_full(const gchar *url,
        const gchar *method, const gchar *extra_headers,
        const gchar *body,
        const gchar *extra_data, gsize extra_len,
        MatrixConnectionData *conn,
        MatrixApiCallback callback, MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data, gssize max_len)
{
    MatrixApiRequestData *data;
    GString *request;
    PurpleUtilFetchUrlData *purple_data;

    if (error_callback == NULL)
        error_callback = matrix_api_error;
    if (bad_response_callback == NULL)
        bad_response_callback = matrix_api_bad_response;

    /* _build_request assumes the url is absolute, so enforce that here */
    if(!g_str_has_prefix(url, "http://") &&
            !g_str_has_prefix(url, "https://")) {
        gchar *error_msg;
        error_msg = g_strdup_printf(_("Invalid homeserver URL %s"), url);
        error_callback(conn, user_data, error_msg);
        g_free(error_msg);
        return NULL;
    }

#if !PURPLE_VERSION_CHECK(2,11,0)
    if (extra_len) {
        gchar *error_msg;
        error_msg = g_strdup_printf(_("Feature not available on old purple version"));
        error_callback(conn, user_data, error_msg);
        g_free(error_msg);
        return NULL;
    }
#endif

    request = _build_request(conn->pc->account, url, method, extra_headers,
                             body, extra_data, extra_len);

    if(purple_debug_is_unsafe())
        purple_debug_info("matrixprpl", "request %s\n", request->str);


    data = g_new0(MatrixApiRequestData, 1);
    data->conn = conn;
    data->callback = callback;
    data->error_callback = error_callback;
    data->bad_response_callback = bad_response_callback;
    data->user_data = user_data;

#if PURPLE_VERSION_CHECK(2,11,0)
    purple_data = purple_util_fetch_url_request_data_len_with_account(
            conn -> pc -> account,
            url, FALSE, NULL, TRUE, request->str, request->len,
            TRUE, max_len, matrix_api_complete,
            data);
#else
    purple_data = purple_util_fetch_url_request_len_with_account(
            conn -> pc -> account,
            url, FALSE, NULL, TRUE, request->str, TRUE,
            max_len, matrix_api_complete,
            data);
#endif

    if(purple_data == NULL) {
        /* we couldn't start the request. In this case, our callback will
         * already have been called, which will have freed data.
         */
        data = NULL;
    } else {
        data->purple_data = purple_data;
    }

    g_string_free(request, TRUE);
    return data;
}


/**
 * Start an HTTP call to the API; lighter version of matrix_api_start_full
 * since most callers don't need the extras.
 *
 * @param method      HTTP method (eg "GET")
 * @param body        body of request, or NULL if none
 * @param max_len     maximum number of bytes to return from the request. -1 for
 *                    default (512K).
 *
 * @returns handle for the request, or NULL if the request couldn't be started
 *   (eg, invalid hostname). In this case, the error_callback will have
 *   been called already.
 */
static MatrixApiRequestData *matrix_api_start(const gchar *url,
        const gchar *method, const gchar *body,
        MatrixConnectionData *conn,
        MatrixApiCallback callback, MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data, gssize max_len)
{
    return matrix_api_start_full(url, method, NULL, body, NULL, 0, conn,
            callback, error_callback, bad_response_callback,
            user_data, max_len);
}


void matrix_api_cancel(MatrixApiRequestData *data)
{
    if(data -> purple_data != NULL)
        purple_util_fetch_url_cancel(data -> purple_data);
    data -> purple_data = NULL;
    (data->error_callback)(data->conn, data->user_data, "cancelled");

    g_free(data);
}


gchar *_build_login_body(const gchar *username, const gchar *password, const gchar *device_id)
{
    JsonObject *body, *ident;
    JsonNode *node;
    JsonGenerator *generator;
    gchar *result;

    body = json_object_new();
    json_object_set_string_member(body, "type", "m.login.password");

    ident = json_object_new();
    /* TODO: Support 3pid rather than username */
    json_object_set_string_member(ident, "type", "m.id.user");
    json_object_set_string_member(ident, "user", username);
    json_object_set_object_member(body, "identifier", ident);

    json_object_set_string_member(body, "password", password);
    json_object_set_string_member(body, "initial_device_display_name", "purple-matrix");
    if (device_id != NULL)
        json_object_set_string_member(body, "device_id", device_id);
    
    node = json_node_new(JSON_NODE_OBJECT);
    json_node_set_object(node, body);
    json_object_unref(body);

    generator = json_generator_new();
    json_generator_set_root(generator, node);
    result = json_generator_to_data(generator, NULL);
    g_object_unref(G_OBJECT(generator));
    json_node_free(node);
    return result;
}

MatrixApiRequestData *matrix_api_password_login(MatrixConnectionData *conn,
        const gchar *username,
        const gchar *password,
        const gchar *device_id,
        MatrixApiCallback callback,
        gpointer user_data)
{
    gchar *url, *json;
    MatrixApiRequestData *fetch_data;

    purple_debug_info("matrixprpl", "logging in %s\n", username);

    url = g_strconcat(conn->homeserver, "_matrix/client/r0/login",
            NULL);

    json = _build_login_body(username, password, device_id);

    fetch_data = matrix_api_start(url, "POST", json, conn, callback,
            NULL, NULL, user_data, 0);
    g_free(json);
    g_free(url);

    return fetch_data;
}


MatrixApiRequestData *matrix_api_sync(MatrixConnectionData *conn,
        const gchar *since, int timeout, gboolean full_state,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append_printf(url,
            "_matrix/client/r0/sync?access_token=%s&timeout=%i",
            purple_url_encode(conn->access_token), timeout);

    if(since != NULL)
        g_string_append_printf(url, "&since=%s", purple_url_encode(since));

    if(full_state)
        g_string_append(url, "&full_state=true");

    purple_debug_info("matrixprpl", "syncing %s since %s (full_state=%i)\n",
                conn->pc->account->username, since, full_state);

    /* XXX: stream the response, so that we don't need to allocate so much
     * memory? But it's JSON
     */
    fetch_data = matrix_api_start(url->str, "GET", NULL, conn, callback,
            error_callback, bad_response_callback, user_data, 40*1024*1024);
    g_string_free(url, TRUE);
    
    return fetch_data;
}


MatrixApiRequestData *matrix_api_send(MatrixConnectionData *conn,
        const gchar *room_id, const gchar *event_type, const gchar *txn_id,
        JsonObject *content, MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;
    JsonNode *body_node;
    JsonGenerator *generator;
    gchar *json;

    /* purple_url_encode uses a single static buffer, so we have to build up
     * the url gradually
     */
    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/rooms/");
    g_string_append(url, purple_url_encode(room_id));
    g_string_append(url, "/send/");
    g_string_append(url, purple_url_encode(event_type));
    g_string_append(url, "/");
    g_string_append(url, purple_url_encode(txn_id));
    g_string_append(url, "?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    body_node = json_node_new(JSON_NODE_OBJECT);
    json_node_set_object(body_node, content);

    generator = json_generator_new();
    json_generator_set_root(generator, body_node);
    json = json_generator_to_data(generator, NULL);
    g_object_unref(G_OBJECT(generator));
    json_node_free(body_node);

    purple_debug_info("matrixprpl", "sending %s on %s\n", event_type, room_id);

    fetch_data = matrix_api_start(url->str, "PUT", json, conn, callback,
            error_callback, bad_response_callback,
            user_data, 0);
    g_free(json);
    g_string_free(url, TRUE);

    return fetch_data;
}

void matrix_api_invite_user(MatrixConnectionData *conn,
        const gchar *room_id,
        const gchar *who,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    JsonNode *body_node;
    JsonGenerator *generator;
    gchar *json;

    JsonObject *invitee;
    invitee = json_object_new();
    json_object_set_string_member(invitee, "user_id", who);

    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/rooms/");
    g_string_append(url, purple_url_encode(room_id));
    g_string_append(url, "/invite?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    body_node = json_node_new(JSON_NODE_OBJECT);
    json_node_set_object(body_node, invitee);

    generator = json_generator_new();
    json_generator_set_root(generator, body_node);
    json = json_generator_to_data(generator, NULL);
    g_object_unref(G_OBJECT(generator));
    json_node_free(body_node);

    purple_debug_info("matrixprpl", "sending an invite on %s\n", room_id);

    matrix_api_start(url->str, "POST", json, conn, callback,
            error_callback, bad_response_callback,
            user_data, 0);
    g_free(json);
    g_string_free(url, TRUE);
    json_object_unref(invitee);
}

MatrixApiRequestData *matrix_api_join_room(MatrixConnectionData *conn,
        const gchar *room,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/join/");
    g_string_append(url, purple_url_encode(room));
    g_string_append(url, "?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    purple_debug_info("matrixprpl", "joining %s\n", room);

    fetch_data = matrix_api_start(url->str, "POST", "{}", conn, callback,
            error_callback, bad_response_callback,
            user_data, 0);
    g_string_free(url, TRUE);

    return fetch_data;
}

MatrixApiRequestData *matrix_api_typing(MatrixConnectionData *conn,
        const gchar *room_id, gboolean typing,
        gint typing_timeout, MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;
    JsonNode *body_node;
    JsonGenerator *generator;
    gchar *json;
    JsonObject *content;

    /* purple_url_encode uses a single static buffer, so we have to build up
     * the url gradually
     */
    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/rooms/");
    g_string_append(url, purple_url_encode(room_id));
    g_string_append(url, "/typing/");
    g_string_append(url, purple_url_encode(conn->user_id));
    g_string_append(url, "?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    body_node = json_node_new(JSON_NODE_OBJECT);
    content = json_object_new();
    json_object_set_boolean_member(content, "typing", typing);
    if (typing == TRUE) {
        json_object_set_int_member(content, "timeout", typing_timeout);
    }
    json_node_set_object(body_node, content);

    generator = json_generator_new();
    json_generator_set_root(generator, body_node);
    json = json_generator_to_data(generator, NULL);
    g_object_unref(G_OBJECT(generator));
    json_node_free(body_node);

    purple_debug_info("matrixprpl", "typing in %s\n", room_id);

    fetch_data = matrix_api_start(url->str, "PUT", json, conn, callback,
            error_callback, bad_response_callback,
            user_data, 0);
    g_free(json);
    g_string_free(url, TRUE);
    json_object_unref(content);

    return fetch_data;
}


MatrixApiRequestData *matrix_api_leave_room(MatrixConnectionData *conn,
        const gchar *room_id,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/rooms/");
    g_string_append(url, purple_url_encode(room_id));
    g_string_append(url, "/leave?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    purple_debug_info("matrixprpl", "leaving %s\n", room_id);

    fetch_data = matrix_api_start(url->str, "POST", "{}", conn, callback,
            error_callback, bad_response_callback,
            user_data, 0);
    g_string_free(url, TRUE);

    return fetch_data;
}

/**
 * Upload a file
 *
 * @param conn             The connection with which to make the request
 * @param ctype            Content type of file
 * @param data             Raw data content of file
 * @param data_len         Length of the data
 * @param callback         Function to be called when the request completes
 * @param user_data        Opaque data to be passed to the callback
 */
MatrixApiRequestData *matrix_api_upload_file(MatrixConnectionData *conn,
        const gchar *ctype, const gchar *data, gsize data_len,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url, *extra_header;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/media/r0/upload");
    g_string_append(url, "?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    extra_header = g_string_new("Content-Type: ");
    g_string_append(extra_header, ctype);
    g_string_append(extra_header, "\r\n");

    fetch_data = matrix_api_start_full(url->str, "POST", extra_header->str, "",
            data, data_len, conn,
            callback, error_callback, bad_response_callback, user_data, 0);
    g_string_free(url, TRUE);
    g_string_free(extra_header, TRUE);

    return fetch_data;
}

GString *get_download_url(const gchar *homeserver, const gchar *uri)
{
    GString *url;

    /* Sanity check the uri - TODO: Add more sanity */
    if (strncmp(uri, "mxc://", 6)) {
        return NULL;
    }
    url = g_string_new(homeserver);
    g_string_append(url, "_matrix/media/r0/download/");
    g_string_append(url, uri + 6); /* i.e. after the mxc:// */
    return url;
}

/**
 * Download a file
 * @param uri       URI string in the form mxc://example.com/unique
 */
MatrixApiRequestData *matrix_api_download_file(MatrixConnectionData *conn,
        const gchar *uri,
        gsize max_size,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = get_download_url(conn->homeserver, uri);
    if (!url) {
        error_callback(conn, user_data, "bad media uri");
        return NULL;
    }

    /* I'd like to validate the headers etc a bit before downloading the
     * data (maybe using _handle_header_completed), also I'm not convinced
     * purple always does sane things on over-size.
     */
    fetch_data = matrix_api_start(url->str, "GET", NULL, conn, callback,
            error_callback, bad_response_callback, user_data, max_size);
    g_string_free(url, TRUE);

    return fetch_data;
}

/**
 * Download a thumbnail for a file
 * @param uri       URI string in the form mxc://example.com/unique
 */
MatrixApiRequestData *matrix_api_download_thumb(MatrixConnectionData *conn,
        const gchar *uri,
        gsize max_size,
        unsigned int width, unsigned int height, gboolean scale,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;
    char tmp[64];

    /* Sanity check the uri - TODO: Add more sanity */
    if (strncmp(uri, "mxc://", 6)) {
        error_callback(conn, user_data, "bad media uri");
        return NULL;
    }
    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/media/r0/thumbnail/");
    g_string_append(url, uri + 6); /* i.e. after the mxc:// */
    sprintf(tmp, "?width=%u", width);
    g_string_append(url, tmp);
    sprintf(tmp, "&height=%u", height);
    g_string_append(url, tmp);
    g_string_append(url, scale ? "&method=scale": "&method=crop");

    /* I'd like to validate the headers etc a bit before downloading the
     * data (maybe using _handle_header_completed), also I'm not convinced
     * purple always does sane things on over-size.
     */
    fetch_data = matrix_api_start(url->str, "GET", NULL, conn, callback,
            error_callback, bad_response_callback, user_data, max_size);
    g_string_free(url, TRUE);

    return fetch_data;
}

/**
 * Returns the userid for our access token, mostly as a check our token
 * is valid.
 */
MatrixApiRequestData *matrix_api_whoami(MatrixConnectionData *conn,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append_printf(url,
            "_matrix/client/r0/account/whoami?access_token=%s",
            purple_url_encode(conn->access_token));

    fetch_data = matrix_api_start(url->str, "GET", NULL, conn, callback,
            error_callback, bad_response_callback, user_data, 10*1024);
    g_string_free(url, TRUE);

    return fetch_data;
}

MatrixApiRequestData *matrix_api_upload_keys(MatrixConnectionData *conn,
        JsonObject *device_keys, JsonObject *one_time_keys,
        MatrixApiCallback callback,
        MatrixApiErrorCallback error_callback,
        MatrixApiBadResponseCallback bad_response_callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;
    JsonNode *body_node;
    JsonObject *top_obj;
    JsonGenerator *generator;
    gchar *json;

    url = g_string_new(conn->homeserver);
    g_string_append(url, "_matrix/client/r0/keys/upload?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    top_obj = json_object_new();
    if (device_keys) {
        json_object_set_object_member(top_obj, "device_keys", device_keys);
    }
    if (one_time_keys) {
        json_object_set_object_member(top_obj, "one_time_keys", one_time_keys);
    }
    body_node = json_node_new(JSON_NODE_OBJECT);
    json_node_set_object(body_node, top_obj);
    json_object_unref(top_obj);

    generator = json_generator_new();
    json_generator_set_root(generator, body_node);
    json = json_generator_to_data(generator, NULL);
    g_object_unref(G_OBJECT(generator));
    json_node_free(body_node);

    fetch_data = matrix_api_start_full(url->str, "POST",
            "Content-Type: application/json", json, NULL, 0,
            conn, callback, error_callback, bad_response_callback,
            user_data, 10*1024);
    g_free(json);
    g_string_free(url, TRUE);

    return fetch_data;
}


#if 0
MatrixApiRequestData *matrix_api_get_room_state(MatrixConnectionData *conn,
        const gchar *room_id,
        MatrixApiCallback callback,
        gpointer user_data)
{
    GString *url;
    MatrixApiRequestData *fetch_data;

    url = g_string_new(conn->homeserver);
    g_string_append(url, "/_matrix/client/r0/rooms/");
    g_string_append(url, purple_url_encode(room_id));
    g_string_append(url, "/state?access_token=");
    g_string_append(url, purple_url_encode(conn->access_token));

    purple_debug_info("matrixprpl", "getting state for %s\n", room_id);

    fetch_data = matrix_api_start(url->str, NULL, conn, callback,
            NULL, NULL, user_data, 10*1024*1024);
    g_string_free(url, TRUE);

    return fetch_data;
}
#endif
