From d9cb6885ab741ba69a966109cb05e26692143ce0 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 2 Oct 2020 16:27:59 +0200 Subject: Matrix: add video/regular file upload --- src/plugins/Matrix.cpp | 240 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 178 insertions(+), 62 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index e182c11..60fc8a9 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1,6 +1,5 @@ #include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" -#include "../../include/ImageUtils.hpp" #include #include #include @@ -742,16 +741,18 @@ namespace QuickMedia { return result.str(); } - static const char* message_type_to_request_msg_type_str(MessageType msgtype) { - switch(msgtype) { - case MessageType::TEXT: return "m.text"; - case MessageType::IMAGE: return "m.image"; - case MessageType::VIDEO: return "m.video"; - } - return "m.text"; + static const char* content_type_to_message_type(ContentType content_type) { + if(is_content_type_video(content_type)) + return "m.video"; + else if(is_content_type_audio(content_type)) + return "m.audio"; + else if(is_content_type_image(content_type)) + return "m.image"; + else + return "m.file"; } - PluginResult Matrix::post_message(const std::string &room_id, const std::string &body, const std::string &url, MessageType msgtype, MessageInfo *info) { + PluginResult Matrix::post_message(const std::string &room_id, const std::string &body, const std::optional &file_info, const std::optional &thumbnail_info) { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) return PluginResult::ERR; @@ -760,7 +761,7 @@ namespace QuickMedia { std::string formatted_body; bool contains_formatted_text = false; - if(msgtype == MessageType::TEXT) { + if(!file_info) { int line = 0; string_split(body, '\n', [&formatted_body, &contains_formatted_text, &line](const char *str, size_t size){ if(line > 0) @@ -781,22 +782,42 @@ namespace QuickMedia { } Json::Value request_data(Json::objectValue); - request_data["msgtype"] = message_type_to_request_msg_type_str(msgtype); + request_data["msgtype"] = (file_info ? content_type_to_message_type(file_info->content_type) : "m.text"); request_data["body"] = body; if(contains_formatted_text) { request_data["format"] = "org.matrix.custom.html"; request_data["formatted_body"] = std::move(formatted_body); } - if(msgtype == MessageType::IMAGE) { - if(info) { - Json::Value info_json(Json::objectValue); - info_json["size"] = info->size; - info_json["w"] = info->w; - info_json["h"] = info->h; - info_json["mimetype"] = info->mimetype; - request_data["info"] = std::move(info_json); + + // TODO: Add hashblur? + if(file_info) { + Json::Value info_json(Json::objectValue); + info_json["size"] = file_info->file_size; + info_json["mimetype"] = content_type_to_string(file_info->content_type); + if(file_info->dimensions) { + info_json["w"] = file_info->dimensions->width; + info_json["h"] = file_info->dimensions->height; } - request_data["url"] = url; + if(file_info->duration_seconds) { + // TODO: Check for overflow? + info_json["duration"] = (int)file_info->duration_seconds.value() * 1000; + } + + if(thumbnail_info) { + Json::Value thumbnail_info_json(Json::objectValue); + thumbnail_info_json["size"] = thumbnail_info->file_size; + thumbnail_info_json["mimetype"] = content_type_to_string(thumbnail_info->content_type); + if(thumbnail_info->dimensions) { + thumbnail_info_json["w"] = thumbnail_info->dimensions->width; + thumbnail_info_json["h"] = thumbnail_info->dimensions->height; + } + + info_json["thumbnail_url"] = thumbnail_info->content_uri; + info_json["info"] = std::move(thumbnail_info_json); + } + + request_data["info"] = std::move(info_json); + request_data["url"] = file_info->content_uri; } Json::StreamWriterBuilder builder; @@ -902,7 +923,7 @@ namespace QuickMedia { relates_to_json["m.in_reply_to"] = std::move(in_reply_to_json); Json::Value request_data(Json::objectValue); - request_data["msgtype"] = message_type_to_request_msg_type_str(MessageType::TEXT); + request_data["msgtype"] = "m.text"; // TODO: Allow image reply? element doesn't do that but we could! request_data["body"] = create_body_for_message_reply(room_it->second.get(), relates_to_message_raw, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... request_data["m.relates_to"] = std::move(relates_to_json); @@ -1002,7 +1023,7 @@ namespace QuickMedia { relates_to_json["rel_type"] = "m.replace"; Json::Value request_data(Json::objectValue); - request_data["msgtype"] = message_type_to_request_msg_type_str(MessageType::TEXT); + request_data["msgtype"] = "m.text"; // TODO: Allow other types of edits request_data["body"] = " * " + body; if(contains_formatted_text) { request_data["format"] = "org.matrix.custom.html"; @@ -1157,82 +1178,129 @@ namespace QuickMedia { return filename; } - static const char* image_type_to_mimetype(ImageType image_type) { - switch(image_type) { - case ImageType::PNG: return "image/png"; - case ImageType::GIF: return "image/gif"; - case ImageType::JPG: return "image/jpeg"; - } - return "application/octet-stream"; + PluginResult Matrix::post_file(const std::string &room_id, const std::string &filepath, std::string &err_msg) { + UploadInfo file_info; + UploadInfo thumbnail_info; + PluginResult upload_file_result = upload_file(room_id, filepath, file_info, thumbnail_info, err_msg); + if(upload_file_result != PluginResult::OK) + return upload_file_result; + + std::optional file_info_opt = std::move(file_info); + std::optional thumbnail_info_opt; + if(!thumbnail_info.content_uri.empty()) + thumbnail_info_opt = std::move(thumbnail_info); + + const char *filename = file_get_filename(filepath); + return post_message(room_id, filename, file_info_opt, thumbnail_info_opt); } - PluginResult Matrix::post_file(const std::string &room_id, const std::string &filepath) { - int image_width, image_height; - ImageType image_type; - if(!image_get_resolution(filepath, &image_width, &image_height, &image_type)) { - fprintf(stderr, "Failed to get resolution of image: %s. Only image uploads are currently supported\n", filepath.c_str()); + PluginResult Matrix::upload_file(const std::string &room_id, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg) { + FileAnalyzer file_analyzer; + if(!file_analyzer.load_file(filepath.c_str())) { + err_msg = "Failed to load " + filepath; return PluginResult::ERR; } - const char *mimetype = image_type_to_mimetype(image_type); + file_info.content_type = file_analyzer.get_content_type(); + file_info.file_size = file_analyzer.get_file_size(); + file_info.dimensions = file_analyzer.get_dimensions(); + file_info.duration_seconds = file_analyzer.get_duration_seconds(); - // TODO: What if the file changes after this? is the file size really needed? - size_t file_size; - if(file_get_size(filepath, &file_size) != 0) { - fprintf(stderr, "Failed to get size of image: %s\n", filepath.c_str()); + int upload_limit; + PluginResult config_result = get_config(&upload_limit); + if(config_result != PluginResult::OK) { + err_msg = "Failed to get file size limit from server"; + return config_result; + } + + // Checking for sane file size limit client side, to prevent loading a huge file and crashing + if(file_analyzer.get_file_size() > 300 * 1024 * 1024) { // 300mb + err_msg = "File is too large! client-side limit is set to 300mb"; return PluginResult::ERR; } - // TODO: Check server file limit first: GET https://glowers.club/_matrix/media/r0/config - // and also have a sane limit client-side. - if(file_size > 100 * 1024 * 1024) { - fprintf(stderr, "Upload file size it too large!, max size is currently 100mb\n"); + if((int)file_analyzer.get_file_size() > upload_limit) { + err_msg = "File is too large! max upload size on your homeserver is " + std::to_string(upload_limit) + " bytes, the file you tried to upload is " + std::to_string(file_analyzer.get_file_size()) + " bytes"; return PluginResult::ERR; } + if(is_content_type_video(file_analyzer.get_content_type())) { + // TODO: Also upload thumbnail for images. Take into consideration below upload_file, we dont want to upload thumbnail of thumbnail + char tmp_filename[] = "/tmp/quickmedia_video_frame_XXXXXX"; + int tmp_file = mkstemp(tmp_filename); + if(tmp_file != -1) { + if(video_get_first_frame(filepath.c_str(), tmp_filename)) { + UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. + PluginResult upload_thumbnail_result = upload_file(room_id, tmp_filename, thumbnail_info, upload_info_ignored, err_msg); + if(upload_thumbnail_result != PluginResult::OK) { + close(tmp_file); + remove(tmp_filename); + return upload_thumbnail_result; + } + } else { + fprintf(stderr, "Failed to get first frame of video, ignoring thumbnail...\n"); + } + close(tmp_file); + remove(tmp_filename); + } else { + fprintf(stderr, "Failed to create temporary file for video thumbnail, ignoring thumbnail...\n"); + } + } + std::vector additional_args = { { "-X", "POST" }, - { "-H", std::string("content-type: ") + mimetype }, + { "-H", std::string("content-type: ") + content_type_to_string(file_analyzer.get_content_type()) }, { "-H", "Authorization: Bearer " + access_token }, { "--data-binary", "@" + filepath } }; const char *filename = file_get_filename(filepath); + std::string filename_escaped = url_param_encode(filename); char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", homeserver.c_str(), filename); + snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", homeserver.c_str(), filename_escaped.c_str()); fprintf(stderr, "Upload url: |%s|\n", url); std::string server_response; - if(download_to_string(url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + if(download_to_string(url, server_response, std::move(additional_args), use_tor, true, false) != DownloadResult::OK) { + err_msg = "Upload request failed, reason: " + server_response; return PluginResult::NET_ERR; + } - if(server_response.empty()) + if(server_response.empty()) { + err_msg = "Got corrupt response from server"; return PluginResult::ERR; + } Json::Value json_root; Json::CharReaderBuilder json_builder; std::unique_ptr json_reader(json_builder.newCharReader()); std::string json_errors; if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &json_root, &json_errors)) { - fprintf(stderr, "Matrix upload response parse error: %s\n", json_errors.c_str()); + err_msg = "Matrix upload response parse error: " + json_errors; return PluginResult::ERR; } - if(!json_root.isObject()) + if(!json_root.isObject()) { + err_msg = "Got corrupt response from server"; + return PluginResult::ERR; + } + + const Json::Value &error_json = json_root["error"]; + if(error_json.isString()) { + err_msg = error_json.asString(); return PluginResult::ERR; + } const Json::Value &content_uri_json = json_root["content_uri"]; - if(!content_uri_json.isString()) + if(!content_uri_json.isString()) { + err_msg = "Missing content_uri is server response"; return PluginResult::ERR; + } fprintf(stderr, "Matrix upload, response content uri: %s\n", content_uri_json.asCString()); - MessageInfo message_info; - message_info.size = file_size; - message_info.w = image_width; - message_info.h = image_height; - message_info.mimetype = mimetype; - return post_message(room_id, filename, content_uri_json.asString(), MessageType::IMAGE, &message_info); + file_info.content_uri = content_uri_json.asString(); + return PluginResult::OK; } PluginResult Matrix::login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg) { @@ -1279,9 +1347,9 @@ namespace QuickMedia { return PluginResult::ERR; } - const Json::Value &error = json_root["error"]; - if(error.isString()) { - err_msg = error.asString(); + const Json::Value &error_json = json_root["error"]; + if(error_json.isString()) { + err_msg = error_json.asString(); return PluginResult::ERR; } @@ -1340,6 +1408,7 @@ namespace QuickMedia { username.clear(); access_token.clear(); homeserver.clear(); + upload_limit.reset(); next_batch.clear(); return PluginResult::OK; @@ -1394,9 +1463,9 @@ namespace QuickMedia { return PluginResult::ERR; } - const Json::Value &error = json_root["error"]; - if(error.isString()) { - err_msg = error.asString(); + const Json::Value &error_json = json_root["error"]; + if(error_json.isString()) { + err_msg = error_json.asString(); return PluginResult::ERR; } @@ -1554,4 +1623,51 @@ namespace QuickMedia { } return room_data->user_info[message->user_id].display_name; } + + PluginResult Matrix::get_config(int *upload_size) { + // TODO: What if the upload limit changes? is it possible for the upload limit to change while the server is running? + if(upload_limit) { + *upload_size = upload_limit.value(); + return PluginResult::OK; + } + + *upload_size = 0; + + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/media/r0/config", homeserver.c_str()); + fprintf(stderr, "load initial room data, url: |%s|\n", url); + + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) { + fprintf(stderr, "Matrix /config failed\n"); + return PluginResult::NET_ERR; + } + + if(server_response.empty()) + return PluginResult::ERR; + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Matrix parsing /config response failed, error: %s\n", json_errors.c_str()); + return PluginResult::ERR; + } + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &upload_size_json = json_root["m.upload.size"]; + if(!upload_size_json.isNumeric()) + return PluginResult::ERR; + + upload_limit = upload_size_json.asInt(); + *upload_size = upload_limit.value(); + return PluginResult::OK; + } } \ No newline at end of file -- cgit v1.2.3