diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | matrix-api.c | 54 | ||||
-rw-r--r-- | matrix-api.h | 32 | ||||
-rw-r--r-- | matrix-connection.c | 9 | ||||
-rw-r--r-- | matrix-room.c | 166 |
5 files changed, 252 insertions, 12 deletions
@@ -17,8 +17,7 @@ The following are not yet supported: * Joining existing rooms by alias instead of room_id * Presence indication * Typing indication - * Videos/images/rich text in messages - * File uploads + * Videos/rich text in messages * Account registration * Room topics * Voice/video calling diff --git a/matrix-api.c b/matrix-api.c index f5a7ebf..a8528ce 100644 --- a/matrix-api.c +++ b/matrix-api.c @@ -105,17 +105,18 @@ typedef struct { gchar *content_type; gboolean got_headers; JsonParser *json_parser; + const char *body; + size_t body_len; } MatrixApiResponseParserData; /** create a MatrixApiResponseParserData */ static MatrixApiResponseParserData *_response_parser_data_new() { - MatrixApiResponseParserData *res = g_new(MatrixApiResponseParserData, 1); + 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->content_type = NULL; res->json_parser = json_parser_new(); return res; } @@ -221,6 +222,12 @@ static int _handle_body(http_parser *http_parser, const char *at, g_error_free(err); return 1; } + } else { + /* Well if it's not JSON perhaps the callback is expecting to + * handle it itself, e.g. for an image. + */ + response_data->body = at; + response_data->body_len = length; } return 0; } @@ -306,7 +313,9 @@ static void matrix_api_complete(PurpleUtilFetchUrlData *url_data, (data->bad_response_callback)(data->conn, data->user_data, response_code, root); } else if (data->callback) { - (data->callback)(data->conn, data->user_data, root); + (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); @@ -784,7 +793,7 @@ MatrixApiRequestData *matrix_api_upload_file(MatrixConnectionData *conn, url = g_string_new(conn->homeserver); g_string_append(url, "/_matrix/media/r0/upload"); - g_string_append(url, "/join?access_token="); + g_string_append(url, "?access_token="); g_string_append(url, purple_url_encode(conn->access_token)); extra_header = g_string_new("Content-Type: "); @@ -800,6 +809,43 @@ MatrixApiRequestData *matrix_api_upload_file(MatrixConnectionData *conn, return fetch_data; } +/** + * 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; + + /* 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/download/"); + g_string_append(url, uri + 6); /* i.e. after the mxc:// */ + g_string_append(url, "?access_token="); + g_string_append(url, purple_url_encode(conn->access_token)); + + /* 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; +} + #if 0 MatrixApiRequestData *matrix_api_get_room_state(MatrixConnectionData *conn, const gchar *room_id, diff --git a/matrix-api.h b/matrix-api.h index cdeb134..4245cd7 100644 --- a/matrix-api.h +++ b/matrix-api.h @@ -55,10 +55,17 @@ typedef struct _MatrixApiRequestData MatrixApiRequestData; * @param json_root NULL if there was no body, or it could not be * parsed as JSON; otherwise the root of the JSON * tree in the response + * @param body NULL if the body was parsable as JSON, else the raw + * body. + * @param body_len The length of the body (valid when body is) + * + * @param content_type The content type of the body */ typedef void (*MatrixApiCallback)(MatrixConnectionData *conn, gpointer user_data, - struct _JsonNode *json_root); + struct _JsonNode *json_root, + const char *body, + size_t body_len, const char *content_type); /** * Signature for functions which are called when there is an error calling the @@ -246,6 +253,29 @@ MatrixApiRequestData *matrix_api_upload_file(MatrixConnectionData *conn, MatrixApiBadResponseCallback bad_response_callback, gpointer user_data); +/** + * Download a file + * + * @param conn The connection with which to make the request + * @param uri The Matrix uri to fetch starting mxc:// + * @param max_size A maximum size of file to receive. + * @param callback Function to be called when the request completes + * @param error_callback Function to be called if there is an error making + * the request. If NULL, matrix_api_error will be + * used. + * @param bad_response_callback Function to be called if the API gives a non-200 + * response. If NULL, matrix_api_bad_response will be + * used. + * @param user_data Opaque data to be passed to the callbacks + * + */ +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); #if 0 /** diff --git a/matrix-connection.c b/matrix-connection.c index f576c95..893e3db 100644 --- a/matrix-connection.c +++ b/matrix-connection.c @@ -104,7 +104,8 @@ void _sync_bad_response(MatrixConnectionData *ma, gpointer user_data, /* callback which is called when a /sync request completes */ static void _sync_complete(MatrixConnectionData *ma, gpointer user_data, - JsonNode *body) + JsonNode *body, + const char *raw_body, size_t raw_body_len, const char *content_type) { PurpleConnection *pc = ma->pc; const gchar *next_batch; @@ -159,7 +160,8 @@ static gboolean _account_has_active_conversations(PurpleAccount *account) static void _login_completed(MatrixConnectionData *conn, gpointer user_data, - JsonNode *json_root) + JsonNode *json_root, + const char *raw_body, size_t raw_body_len, const char *content_type) { PurpleConnection *pc = conn->pc; JsonObject *root_obj; @@ -237,7 +239,8 @@ void matrix_connection_start_login(PurpleConnection *pc) static void _join_completed(MatrixConnectionData *conn, gpointer user_data, - JsonNode *json_root) + JsonNode *json_root, + const char *raw_body, size_t raw_body_len, const char *content_type) { GHashTable *components = user_data; JsonObject *root_obj; diff --git a/matrix-room.c b/matrix-room.c index 6505b30..ab631af 100644 --- a/matrix-room.c +++ b/matrix-room.c @@ -19,6 +19,7 @@ #include "matrix-room.h" /* stdlib */ +#include <inttypes.h> #include <string.h> /* libpurple */ @@ -35,6 +36,7 @@ static gchar *_get_room_name(MatrixConnectionData *conn, PurpleConversation *conv); +static const gchar *_get_my_display_name(PurpleConversation *conv); static MatrixConnectionData *_get_connection_data_from_conversation( PurpleConversation *conv) @@ -67,6 +69,8 @@ static MatrixConnectionData *_get_connection_data_from_conversation( #define PURPLE_CONV_FLAGS "flags" #define PURPLE_CONV_FLAG_NEEDS_NAME_UPDATE 0x1 +/* Arbitrary limit on the size of an image to receive; should make configurable */ +static const size_t purple_max_image_size=250*1024; /** * Get the member table for a room @@ -298,7 +302,8 @@ static GList *_get_event_queue(PurpleConversation *conv) } static void _event_send_complete(MatrixConnectionData *account, gpointer user_data, - JsonNode *json_root) + JsonNode *json_root, + const char *raw_body, size_t raw_body_len, const char *content_type) { PurpleConversation *conv = user_data; JsonObject *response_object; @@ -364,7 +369,8 @@ struct SendImageEventData { * we put in the event. */ static void _image_upload_complete(MatrixConnectionData *ma, - gpointer user_data, JsonNode *json_root) + gpointer user_data, JsonNode *json_root, + const char *raw_body, size_t raw_body_len, const char *content_type) { MatrixApiRequestData *fetch_data = NULL; struct SendImageEventData *sied = user_data; @@ -444,6 +450,17 @@ static const char *type_guess(PurpleStoredImage *image) } } +/** + * Check if the declared content-type is an image type we recognise. + */ +static gboolean is_known_image_type(const char *content_type) +{ + return !strcmp(content_type, "image/png") || + !strcmp(content_type, "image/jpeg") || + !strcmp(content_type, "image/gif") || + !strcmp(content_type, "image/tiff"); +} + /* Structure hung off the event and used by _send_image_hook */ struct SendImageHookData { PurpleConversation *conv; @@ -503,6 +520,145 @@ static void _send_image_hook(MatrixRoomEvent *event, gboolean just_free) } } +/* Passed through matrix_api_download_file all the way + * downto _image_download_complete + */ +struct ReceiveImageData { + PurpleConversation *conv; + gint64 timestamp; + const gchar *room_id; + const gchar *sender_display_name; + gchar *original_body; +}; + +static void _image_download_complete(MatrixConnectionData *ma, + gpointer user_data, JsonNode *json_root, + const char *raw_body, size_t raw_body_len, const char *content_type) +{ + struct ReceiveImageData *rid = user_data; + if (is_known_image_type(content_type)) { + /* Excellent - something to work with */ + int img_id = purple_imgstore_add_with_id(g_memdup(raw_body, raw_body_len), + raw_body_len, NULL); + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), rid->sender_display_name, + PURPLE_MESSAGE_RECV | PURPLE_MESSAGE_IMAGES, + g_strdup_printf("<IMG ID=\"%d\">", img_id), rid->timestamp / 1000); + } else { + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), + rid->sender_display_name, PURPLE_MESSAGE_RECV, + g_strdup_printf("%s (unknown type %s)", + rid->original_body, content_type), rid->timestamp / 1000); + } + purple_conversation_set_data(rid->conv, PURPLE_CONV_DATA_ACTIVE_SEND, + NULL); + g_free(rid->original_body); + g_free(rid); +} + +static void _image_download_bad_response(MatrixConnectionData *ma, gpointer user_data, + int http_response_code, JsonNode *json_root) +{ + struct ReceiveImageData *rid = user_data; + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), + rid->sender_display_name, PURPLE_MESSAGE_RECV, + g_strdup_printf("%s (failed to download %d)", + rid->original_body, http_response_code), + rid->timestamp / 1000); + purple_conversation_set_data(rid->conv, PURPLE_CONV_DATA_ACTIVE_SEND, + NULL); + g_free(rid->original_body); + g_free(rid); +} + +static void _image_download_error(MatrixConnectionData *ma, gpointer user_data, + const gchar *error_message) +{ + struct ReceiveImageData *rid = user_data; + serv_got_chat_in(rid->conv->account->gc, g_str_hash(rid->room_id), + rid->sender_display_name, PURPLE_MESSAGE_RECV, + g_strdup_printf("%s (failed to download %s)", + rid->original_body, error_message), rid->timestamp / 1000); + purple_conversation_set_data(rid->conv, PURPLE_CONV_DATA_ACTIVE_SEND, + NULL); + g_free(rid->original_body); + g_free(rid); +} + + +/* + * Called from matrix_room_handle_timeline_event when it finds an m.image; + * msg_body has the fallback text, + * json_content_object has the json for the content sub object + * + * Return TRUE if we managed to download the image and everything needed + * FALSE if we failed; caller does fallback. + */ +static gboolean _handle_incoming_image(PurpleConversation *conv, + const gint64 timestamp, const gchar *room_id, + const gchar *sender_display_name, const gchar *msg_body, + JsonObject *json_content_object) { + MatrixConnectionData *conn = _get_connection_data_from_conversation(conv); + MatrixApiRequestData *fetch_data = NULL; + struct ReceiveImageData *rid; + + const gchar *url; + JsonObject *json_info_object; + + url = matrix_json_object_get_string_member(json_content_object, "url"); + if (!url) { + /* That seems odd, oh well, no point in getting upset */ + purple_debug_info("matrixprpl", "failed to get url for m.image"); + return FALSE; + } + + /* the 'info' member is optional but if we've got it we can check it to early + * reject the image if it's something that's huge or we don't know the title. + */ + json_info_object = matrix_json_object_get_object_member(json_content_object, + "info"); + purple_debug_info("matrixprpl", "%s: %s json_info_object=%p\n", __func__, + url, json_info_object); + if (json_info_object) { + guint64 size; + const gchar *mime_type; + + /* OK, we've got some (optional) info on the image */ + size = matrix_json_object_get_int_member(json_info_object, "size"); + if (size > purple_max_image_size) { + purple_debug_info("matrixprpl", "image too large %" PRId64 "\n", size); + /* TODO: Switch to a thumbnail */ + return FALSE; + } + mime_type = matrix_json_object_get_string_member(json_info_object, + "mimetype"); + if (mime_type) { + if (!is_known_image_type(mime_type)) { + purple_debug_info("matrixprpl", "%s: unknown mimetype %s", + __func__, mime_type); + return FALSE; + } + } + purple_debug_info("matrixprpl", "image info good: %s of %" PRId64, + mime_type, size); + } + + rid = g_new0(struct ReceiveImageData, 1); + rid->conv = conv; + rid->timestamp = timestamp; + rid->sender_display_name = sender_display_name; + rid->room_id = room_id; + rid->original_body = g_strdup(msg_body); + + fetch_data = matrix_api_download_file(conn, url, purple_max_image_size, + _image_download_complete, + _image_download_error, + _image_download_bad_response, rid); + + purple_conversation_set_data(conv, PURPLE_CONV_DATA_ACTIVE_SEND, + fetch_data); + + return fetch_data != NULL; +} /** * send the next queued event, provided the connection isn't shutting down. @@ -671,6 +827,12 @@ void matrix_room_handle_timeline_event(PurpleConversation *conv, if (!strcmp(msg_type, "m.emote")) { tmp_body = g_strdup_printf("/me %s", msg_body); + } else if (!strcmp(msg_type, "m.image")) { + if (_handle_incoming_image(conv, timestamp, room_id, sender_display_name, + msg_body, json_content_obj)) { + return; + } + /* Fall through - we couldn't get the image, treat as text */ } flags = PURPLE_MESSAGE_RECV; |