aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--matrix-api.c54
-rw-r--r--matrix-api.h32
-rw-r--r--matrix-connection.c9
-rw-r--r--matrix-room.c166
5 files changed, 252 insertions, 12 deletions
diff --git a/README.md b/README.md
index 70dbc00..d414dda 100644
--- a/README.md
+++ b/README.md
@@ -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;