aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-02 16:27:59 +0200
committerdec05eba <dec05eba@protonmail.com>2020-10-02 16:27:59 +0200
commitd9cb6885ab741ba69a966109cb05e26692143ce0 (patch)
treee4e356c182011bc389bc895665dab1507ecdb767 /src/plugins
parent9e68dfad4449d5c0180e252fada6de56b4f405d1 (diff)
Matrix: add video/regular file upload
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/Matrix.cpp240
1 files changed, 178 insertions, 62 deletions
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 <json/reader.h>
#include <json/writer.h>
#include <fcntl.h>
@@ -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<UploadInfo> &file_info, const std::optional<UploadInfo> &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<UploadInfo> file_info_opt = std::move(file_info);
+ std::optional<UploadInfo> 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<CommandArg> 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::CharReader> 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<CommandArg> 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::CharReader> 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