aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-09-23 00:56:54 +0200
committerdec05eba <dec05eba@protonmail.com>2020-09-23 01:00:37 +0200
commit6b347e7310c501b826785e9639d962ba1d448b4b (patch)
tree6d84b547078d009565a75ac2df42423c06c578b7 /src
parenta8e0846a7c111a8d5b5cf8592ecb9b9bbd15ce26 (diff)
Add matrix image upload
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp7
-rw-r--r--src/ImageUtils.cpp15
-rw-r--r--src/QuickMedia.cpp90
-rw-r--r--src/Storage.cpp40
-rw-r--r--src/plugins/FileManager.cpp35
-rw-r--r--src/plugins/Matrix.cpp152
6 files changed, 276 insertions, 63 deletions
diff --git a/src/Body.cpp b/src/Body.cpp
index ae60da2..d650d20 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -285,9 +285,12 @@ namespace QuickMedia {
if(new_image_size.x < image->getSize().x || new_image_size.y < image->getSize().y) {
sf::Image destination_image;
copy_resize(*image, destination_image, new_image_size);
- image.reset();
- if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url)))
+ if(save_image_as_thumbnail_atomic(destination_image, thumbnail_path, get_ext(url))) {
+ image.reset();
result->loadFromImage(destination_image);
+ } else {
+ result->loadFromImage(*image);
+ }
loading_thumbnail = false;
return;
} else {
diff --git a/src/ImageUtils.cpp b/src/ImageUtils.cpp
index 5f493d1..a0b2c1e 100644
--- a/src/ImageUtils.cpp
+++ b/src/ImageUtils.cpp
@@ -98,7 +98,7 @@ namespace QuickMedia {
// TODO: Also support exif files. Exif files are containers for jpeg, tiff etc so once tiff has been implemented so can exif.
// Exif header is similar to jpeg.
- bool image_get_resolution(const Path &path, int *width, int *height) {
+ bool image_get_resolution(const Path &path, int *width, int *height, ImageType *image_type) {
FILE *file = fopen(path.data.c_str(), "rb");
if(!file)
return false;
@@ -107,12 +107,19 @@ namespace QuickMedia {
size_t bytes_read = fread(data, 1, sizeof(data), file);
fclose(file);
- if(png_get_size(data, bytes_read, width, height))
+ if(png_get_size(data, bytes_read, width, height)) {
+ if(image_type)
+ *image_type = ImageType::PNG;
return true;
- else if(gif_get_size(data, bytes_read, width, height))
+ } else if(gif_get_size(data, bytes_read, width, height)) {
+ if(image_type)
+ *image_type = ImageType::GIF;
return true;
- else if(jpeg_get_size(data, bytes_read, width, height))
+ } else if(jpeg_get_size(data, bytes_read, width, height)) {
+ if(image_type)
+ *image_type = ImageType::JPG;
return true;
+ }
return false;
}
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index e7a310d..a2fd42b 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -216,9 +216,14 @@ namespace QuickMedia {
} else {
running = false;
}
- delete related_media_body;
- delete body;
- delete current_plugin;
+ if(related_media_body)
+ delete related_media_body;
+ if(body)
+ delete body;
+ if(file_manager)
+ delete file_manager;
+ if(current_plugin)
+ delete current_plugin;
if(disp)
XCloseDisplay(disp);
}
@@ -310,7 +315,7 @@ namespace QuickMedia {
plugin_logo_path = resources_root + "images/nyaa_si_logo.png";
} else if(strcmp(argv[i], "matrix") == 0) {
current_plugin = new Matrix();
- plugin_logo_path = resources_root + "images/matrix_logo.png";
+ //plugin_logo_path = resources_root + "images/matrix_logo.png";
} else if(strcmp(argv[i], "file-manager") == 0) {
current_plugin = new FileManager();
} else if(strcmp(argv[i], "dmenu") == 0) {
@@ -359,6 +364,7 @@ namespace QuickMedia {
if(current_plugin->name == "file-manager") {
current_page = Page::FILE_MANAGER;
+ file_manager = static_cast<FileManager*>(current_plugin);
} else {
if(start_dir) {
fprintf(stderr, "Option --dir is only valid with file-manager\n");
@@ -444,7 +450,6 @@ namespace QuickMedia {
fprintf(stderr, "Failed to load session cache, redirecting to login page\n");
current_page = Page::CHAT_LOGIN;
}
- search_placeholder = "Send a message...";
}
if(search_placeholder.empty())
@@ -2489,12 +2494,9 @@ namespace QuickMedia {
void Program::file_manager_page() {
selected_files.clear();
int prev_autosearch_delay = search_bar->text_autosearch_delay;
- search_bar->text_autosearch_delay = current_plugin->get_search_delay();
+ search_bar->text_autosearch_delay = file_manager->get_search_delay();
Page previous_page = pop_page_stack();
- assert(current_plugin->name == "file-manager");
- FileManager *file_manager = static_cast<FileManager*>(current_plugin);
-
sf::Text current_dir_text(file_manager->get_current_dir().string(), bold_font, 18);
// TODO: Make asynchronous.
@@ -2514,7 +2516,6 @@ namespace QuickMedia {
if(!selected_item)
return false;
- FileManager *file_manager = static_cast<FileManager*>(current_plugin);
if(file_manager->set_child_directory(selected_item->get_title())) {
std::string current_dir_str = file_manager->get_current_dir().string();
current_dir_text.setString(current_dir_str);
@@ -3200,14 +3201,56 @@ namespace QuickMedia {
// TODO: Allow empty initial room (if the user hasn't joined any room yet)
assert(!current_room_id.empty());
+ {
+ std::string plugin_logo_path = resources_root + "images/matrix_logo.png";
+ if(!plugin_logo.loadFromFile(plugin_logo_path)) {
+ show_notification("QuickMedia", "Failed to load plugin logo, path: " + plugin_logo_path, Urgency::CRITICAL);
+ exit(1);
+ }
+ plugin_logo.generateMipmap();
+ plugin_logo.setSmooth(true);
+ }
+
+ SearchBar chat_input(font, &plugin_logo, "Send a message...");
+
// TODO: Filer for rooms and settings
- search_bar->onTextUpdateCallback = nullptr;
+ chat_input.onTextUpdateCallback = nullptr;
- search_bar->onTextSubmitCallback = [matrix, &tabs, &selected_tab, &room_message_index, &current_room_id](const std::string &text) -> bool {
+ // TODO: Show post message immediately, instead of waiting for sync. Otherwise it can take a while until we receive the message,
+ // which happens when uploading an image.
+ chat_input.onTextSubmitCallback = [this, matrix, &tabs, &selected_tab, &room_message_index, &current_room_id](const std::string &text) -> bool {
if(tabs[selected_tab].type == ChatTabType::MESSAGES) {
if(text.empty())
return false;
+ if(text[0] == '/') {
+ std::string command = text;
+ strip(command);
+ if(command == "/upload") {
+ if(!file_manager)
+ file_manager = new FileManager();
+ page_stack.push(Page::CHAT);
+ current_page = Page::FILE_MANAGER;
+ file_manager_page();
+ if(selected_files.empty()) {
+ fprintf(stderr, "No files selected!\n");
+ return true;
+ } else {
+ // TODO: Make asynchronous.
+ // TODO: Upload multiple files.
+ if(matrix->post_file(current_room_id, selected_files[0]) != PluginResult::OK) {
+ show_notification("QuickMedia", "Failed to upload image to room", Urgency::CRITICAL);
+ return false;
+ } else {
+ return true;
+ }
+ }
+ } else {
+ fprintf(stderr, "Error: invalid command: %s, expected /upload\n", command.c_str());
+ return false;
+ }
+ }
+
// TODO: Make asynchronous
if(matrix->post_message(current_room_id, text) != PluginResult::OK) {
show_notification("QuickMedia", "Failed to post matrix message", Urgency::CRITICAL);
@@ -3262,7 +3305,7 @@ namespace QuickMedia {
while (current_page == Page::CHAT) {
while (window.pollEvent(event)) {
- base_event_handler(event, Page::EXIT, false, false);
+ base_event_handler(event, Page::EXIT, false, false, false);
if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) {
redraw = true;
} else if(event.type == sf::Event::KeyPressed) {
@@ -3274,17 +3317,16 @@ namespace QuickMedia {
current_page = Page::EXIT;
body->clear_items();
body->reset_selected();
- search_bar->clear();
} else if(event.key.code == sf::Keyboard::Left) {
tabs[selected_tab].body->filter_search_fuzzy("");
tabs[selected_tab].body->clamp_selection();
selected_tab = std::max(0, selected_tab - 1);
- search_bar->clear();
+ chat_input.clear();
} else if(event.key.code == sf::Keyboard::Right) {
tabs[selected_tab].body->filter_search_fuzzy("");
tabs[selected_tab].body->clamp_selection();
selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1);
- search_bar->clear();
+ chat_input.clear();
}
if(tabs[selected_tab].type == ChatTabType::MESSAGES && event.key.control && event.key.code == sf::Keyboard::I) {
@@ -3302,12 +3344,16 @@ namespace QuickMedia {
video_content_page();
}
}
+
+ if(event.type == sf::Event::TextEntered)
+ chat_input.onTextEntered(event.text.unicode);
+ chat_input.on_event(event);
}
if(redraw) {
redraw = false;
- search_bar->onWindowResize(window_size);
- search_bar->set_vertical_position(window_size.y - search_bar->getBottomWithoutShadow());
+ chat_input.onWindowResize(window_size);
+ chat_input.set_vertical_position(window_size.y - chat_input.getBottomWithoutShadow());
float body_padding_horizontal = 25.0f;
float body_padding_vertical = 25.0f;
@@ -3318,10 +3364,10 @@ namespace QuickMedia {
body_padding_vertical = 10.0f;
}
- float search_bottom = search_bar->getBottomWithoutShadow();
+ float search_bottom = chat_input.getBottomWithoutShadow();
body_pos = sf::Vector2f(body_padding_horizontal, body_padding_vertical + tab_height);
body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_height);
- //get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true);
+ //get_body_dimensions(window_size, &chat_input, body_pos, body_size, true);
}
if(!sync_running && sync_timer.getElapsedTime().asMilliseconds() >= sync_min_time_ms) {
@@ -3361,7 +3407,7 @@ namespace QuickMedia {
sync_running = false;
}
- search_bar->update();
+ chat_input.update();
window.clear(back_color);
@@ -3391,7 +3437,7 @@ namespace QuickMedia {
tab_drop_shadow.setPosition(0.0f, std::floor(tab_vertical_offset + tab_height));
window.draw(tab_drop_shadow);
- search_bar->draw(window, false);
+ chat_input.draw(window, false);
window.display();
}
diff --git a/src/Storage.cpp b/src/Storage.cpp
index 0c3479a..f82aba3 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -121,6 +121,16 @@ namespace QuickMedia {
return 0;
}
+ int file_get_size(const Path &path, size_t *size) {
+ struct stat file_stat;
+ if(stat(path.data.c_str(), &file_stat) == 0 && S_ISREG(file_stat.st_mode)) {
+ *size = file_stat.st_size;
+ return 0;
+ }
+ *size = 0;
+ return -1;
+ }
+
int file_overwrite(const Path &path, const std::string &data) {
FILE *file = fopen(path.data.c_str(), "wb");
if(!file)
@@ -135,20 +145,38 @@ namespace QuickMedia {
}
void for_files_in_dir(const Path &path, FileIteratorCallback callback) {
- for(auto &p : std::filesystem::directory_iterator(path.data)) {
- if(!callback(p.path()))
- break;
+ try {
+ for(auto &p : std::filesystem::directory_iterator(path.data)) {
+ if(!callback(p.path()))
+ break;
+ }
+ } catch(const std::filesystem::filesystem_error &err) {
+ fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what());
+ return;
+ }
+ }
+
+ static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) {
+ try {
+ return path.last_write_time();
+ } catch(const std::filesystem::filesystem_error &err) {
+ return default_value;
}
}
void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback) {
std::vector<std::filesystem::directory_entry> paths;
- for(auto &p : std::filesystem::directory_iterator(path.data)) {
- paths.push_back(p);
+ try {
+ for(auto &p : std::filesystem::directory_iterator(path.data)) {
+ paths.push_back(p);
+ }
+ } catch(const std::filesystem::filesystem_error &err) {
+ fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what());
+ return;
}
std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
- return path1.last_write_time() > path2.last_write_time();
+ return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min());
});
for(auto &p : paths) {
diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp
index fc6205c..c68bb94 100644
--- a/src/plugins/FileManager.cpp
+++ b/src/plugins/FileManager.cpp
@@ -18,23 +18,40 @@ namespace QuickMedia {
return "";
}
+ static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) {
+ try {
+ return path.last_write_time();
+ } catch(const std::filesystem::filesystem_error &err) {
+ return default_value;
+ }
+ }
+
PluginResult FileManager::get_files_in_directory(BodyItems &result_items) {
+ std::vector<std::filesystem::directory_entry> paths;
try {
for(auto &p : std::filesystem::directory_iterator(current_dir)) {
- auto body_item = std::make_unique<BodyItem>(p.path().filename().string());
- if(p.is_regular_file()) {
- if(is_image_ext(get_ext(p.path()))) {
- body_item->thumbnail_is_local = true;
- body_item->thumbnail_url = p.path().string();
- }
- }
- result_items.push_back(std::move(body_item));
+ paths.push_back(p);
}
- return PluginResult::OK;
} catch(const std::filesystem::filesystem_error &err) {
fprintf(stderr, "Failed to list files in directory %s, error: %s\n", current_dir.c_str(), err.what());
return PluginResult::ERR;
}
+
+ std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
+ return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min());
+ });
+
+ for(auto &p : paths) {
+ auto body_item = std::make_unique<BodyItem>(p.path().filename().string());
+ // TODO: Check file magic number instead of extension?
+ if(p.is_regular_file() && is_image_ext(get_ext(p.path()))) {
+ body_item->thumbnail_is_local = true;
+ body_item->thumbnail_url = p.path().string();
+ }
+ result_items.push_back(std::move(body_item));
+ }
+
+ return PluginResult::OK;
}
bool FileManager::set_current_directory(const std::string &path) {
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index ff854c5..e806f39 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -1,5 +1,6 @@
#include "../../plugins/Matrix.hpp"
#include "../../include/Storage.hpp"
+#include "../../include/ImageUtils.hpp"
#include <json/reader.h>
#include <json/writer.h>
#include <fcntl.h>
@@ -557,7 +558,16 @@ namespace QuickMedia {
return result.str();
}
- PluginResult Matrix::post_message(const std::string &room_id, const std::string &text) {
+ 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";
+ }
+
+ PluginResult Matrix::post_message(const std::string &room_id, const std::string &body, const std::string &url, MessageType msgtype, MessageInfo *info) {
char random_characters[18];
if(!generate_random_characters(random_characters, sizeof(random_characters)))
return PluginResult::ERR;
@@ -566,27 +576,40 @@ namespace QuickMedia {
std::string formatted_body;
bool contains_formatted_text = false;
- string_split(text, '\n', [&formatted_body, &contains_formatted_text](const char *str, size_t size){
- if(size > 0 && str[0] == '>') {
- std::string line(str, size);
- html_escape_sequences(line);
- formatted_body += "<font color=\"#789922\">";
- formatted_body += line;
- formatted_body += "</font>";
- contains_formatted_text = true;
- } else {
- formatted_body.append(str, size);
- }
- return true;
- });
+ if(msgtype == MessageType::TEXT) {
+ string_split(body, '\n', [&formatted_body, &contains_formatted_text](const char *str, size_t size){
+ if(size > 0 && str[0] == '>') {
+ std::string line(str, size);
+ html_escape_sequences(line);
+ formatted_body += "<font color=\"#789922\">";
+ formatted_body += line;
+ formatted_body += "</font>";
+ contains_formatted_text = true;
+ } else {
+ formatted_body.append(str, size);
+ }
+ return true;
+ });
+ }
Json::Value request_data(Json::objectValue);
- request_data["msgtype"] = "m.text";
- request_data["body"] = text;
+ request_data["msgtype"] = message_type_to_request_msg_type_str(msgtype);
+ 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);
+ }
+ request_data["url"] = url;
+ }
Json::StreamWriterBuilder builder;
builder["commentStyle"] = "None";
@@ -599,12 +622,12 @@ namespace QuickMedia {
{ "--data-binary", Json::writeString(builder, request_data) }
};
- char url[512];
- snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/send/m.room.message/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str());
- fprintf(stderr, "Post message to |%s|\n", url);
+ char request_url[512];
+ snprintf(request_url, sizeof(request_url), "%s/_matrix/client/r0/rooms/%s/send/m.room.message/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str());
+ fprintf(stderr, "Post message to |%s|\n", request_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(request_url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK)
return PluginResult::NET_ERR;
if(server_response.empty())
@@ -630,6 +653,95 @@ namespace QuickMedia {
return PluginResult::OK;
}
+ // Returns empty string on error
+ static const char* file_get_filename(const std::string &filepath) {
+ size_t index = filepath.rfind('/');
+ if(index == std::string::npos)
+ return "";
+ const char *filename = filepath.c_str() + index + 1;
+ if(filename[0] == '\0')
+ return "";
+ 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) {
+ 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());
+ return PluginResult::ERR;
+ }
+
+ const char *mimetype = image_type_to_mimetype(image_type);
+
+ // 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());
+ 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");
+ return PluginResult::ERR;
+ }
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "POST" },
+ { "-H", std::string("content-type: ") + mimetype },
+ { "-H", "Authorization: Bearer " + access_token },
+ { "--data-binary", "@" + filepath }
+ };
+
+ const char *filename = file_get_filename(filepath);
+
+ char url[512];
+ snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", homeserver.c_str(), filename);
+ 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)
+ 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 upload response parse error: %s\n", json_errors.c_str());
+ return PluginResult::ERR;
+ }
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &content_uri_json = json_root["content_uri"];
+ if(!content_uri_json.isString())
+ 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);
+ }
+
static std::string parse_login_error_response(std::string json_str) {
if(json_str.empty())
return "Unknown error";