aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md3
-rw-r--r--TODO4
-rw-r--r--include/FileAnalyzer.hpp57
-rw-r--r--include/Path.hpp14
-rw-r--r--plugins/Matrix.hpp26
-rw-r--r--src/AsyncImageLoader.cpp7
-rw-r--r--src/Body.cpp4
-rw-r--r--src/FileAnalyzer.cpp227
-rw-r--r--src/ImageViewer.cpp2
-rw-r--r--src/QuickMedia.cpp42
-rw-r--r--src/main.cpp1
-rw-r--r--src/plugins/Matrix.cpp240
12 files changed, 534 insertions, 93 deletions
diff --git a/README.md b/README.md
index 7d65bd0..fd0ece8 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,8 @@ See project.conf \[dependencies].
`torsocks` needs to be installed when using the `--tor` option.\
[automedia](https://git.dec05eba.com/AutoMedia/) needs to be installed when tracking manga with `Ctrl + T`.\
`waifu2x-ncnn-vulkan` needs to be installed when using the `--upscale-images` option.\
-`xdg-utils` which provides `xdg-open` needs to be installed when downloading torrents with `nyaa.si` plugin.
+`xdg-utils` which provides `xdg-open` needs to be installed when downloading torrents with `nyaa.si` plugin.\
+`ffmpeg (and ffprobe)` to upload videos with thumbnails on matrix.
# Screenshots
## Youtube search
![](https://www.dec05eba.com/images/youtube.jpg)
diff --git a/TODO b/TODO
index f1f8f0b..7d2fb94 100644
--- a/TODO
+++ b/TODO
@@ -69,4 +69,6 @@ Scroll to bottom when receiving a new message even if the selected message is no
Add ".." directory to file-manager, to go up one directory. Also add a tab for common directories and recently accessed files/directories (the directories would be the directory of used files).
Provide a way to go to the first unread message in matrix and also show a marker in the body (maybe a red line?) where the first unread message is.
Sort matrix messages by timestamp. This may be needed to make notification messages show properly in the timeline?
-Load already downloaded images/thumbnails in a separate thread, to instantly load them instead of waiting for new downloads... \ No newline at end of file
+Load already downloaded images/thumbnails in a separate thread, to instantly load them instead of waiting for new downloads...
+Make text that mentions us red in matrix.
+Allow scrolling body item. A body item can be long and we wont be able to read all of it otherwise (such as a message on matrix). Pressing up/down should scroll such a large body item rather than moving to another one. \ No newline at end of file
diff --git a/include/FileAnalyzer.hpp b/include/FileAnalyzer.hpp
new file mode 100644
index 0000000..be0cc25
--- /dev/null
+++ b/include/FileAnalyzer.hpp
@@ -0,0 +1,57 @@
+#pragma once
+
+#include <stddef.h>
+#include <optional>
+#include <string>
+
+namespace QuickMedia {
+ struct Dimensions {
+ int width;
+ int height;
+ };
+
+ enum class ContentType {
+ UNKNOWN,
+ VIDEO_AVI,
+ VIDEO_MP4,
+ VIDEO_WEBM,
+ AUDIO_BASIC,
+ AUDIO_AIFF,
+ AUDIO_MPEG,
+ AUDIO_MIDI,
+ AUDIO_WAVE,
+ AUDIO_FLAC,
+ AUDIO_OPUS,
+ IMAGE_JPEG,
+ IMAGE_PNG,
+ IMAGE_GIF,
+ IMAGE_BMP,
+ IMAGE_WEBP
+ };
+
+ bool is_content_type_video(ContentType content_type);
+ bool is_content_type_audio(ContentType content_type);
+ bool is_content_type_image(ContentType content_type);
+ const char* content_type_to_string(ContentType content_type);
+
+ bool video_get_first_frame(const char *filepath, const char *destination_path);
+
+ class FileAnalyzer {
+ public:
+ FileAnalyzer();
+ bool load_file(const char *filepath);
+ ContentType get_content_type() const;
+ size_t get_file_size() const;
+ std::optional<Dimensions> get_dimensions() const;
+ std::optional<double> get_duration_seconds() const;
+ private:
+ FileAnalyzer(FileAnalyzer&) = delete;
+ FileAnalyzer& operator=(FileAnalyzer&) = delete;
+ private:
+ ContentType content_type;
+ size_t file_size;
+ std::optional<Dimensions> dimensions;
+ std::optional<double> duration_seconds;
+ bool loaded;
+ };
+} \ No newline at end of file
diff --git a/include/Path.hpp b/include/Path.hpp
index bdc31c1..95a5d23 100644
--- a/include/Path.hpp
+++ b/include/Path.hpp
@@ -26,12 +26,20 @@ namespace QuickMedia {
return *this;
}
+ const char* filename() const {
+ size_t index = data.rfind('/');
+ if(index == std::string::npos)
+ return "/";
+ return data.c_str() + index + 1;
+ }
+
// Returns empty string if no extension
const char* ext() const {
+ size_t slash_index = data.rfind('/');
size_t index = data.rfind('.');
- if(index == std::string::npos)
- return "";
- return data.c_str() + index;
+ if(index != std::string::npos && (slash_index == std::string::npos || index > slash_index))
+ return data.c_str() + index;
+ return "";
}
std::string data;
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index 36b8072..bbf35db 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -1,5 +1,6 @@
#pragma once
+#include "../include/FileAnalyzer.hpp"
#include "Plugin.hpp"
#include <unordered_map>
#include <json/value.h>
@@ -31,13 +32,6 @@ namespace QuickMedia {
MessageType type;
};
- struct MessageInfo {
- int size = 0;
- int w = 0;
- int h = 0;
- const char* mimetype = nullptr;
- };
-
struct RoomData {
std::string id;
// Each room has its own list of user data, even if multiple rooms has the same user
@@ -59,6 +53,14 @@ namespace QuickMedia {
AFTER
};
+ struct UploadInfo {
+ ContentType content_type;
+ size_t file_size;
+ std::optional<Dimensions> dimensions;
+ std::optional<double> duration_seconds;
+ std::string content_uri;
+ };
+
using RoomSyncMessages = std::unordered_map<RoomData*, std::vector<std::shared_ptr<Message>>>;
class Matrix : public Plugin {
@@ -81,14 +83,13 @@ namespace QuickMedia {
// |url| should only be set when uploading media.
// TODO: Make api better.
- PluginResult post_message(const std::string &room_id, const std::string &body, const std::string &url = "", MessageType msgtype = MessageType::TEXT, MessageInfo *info = nullptr);
+ PluginResult post_message(const std::string &room_id, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info);
// |relates_to| is from |BodyItem.userdata| and is of type |Message*|
PluginResult post_reply(const std::string &room_id, const std::string &body, void *relates_to);
// |relates_to| is from |BodyItem.userdata| and is of type |Message*|
PluginResult post_edit(const std::string &room_id, const std::string &body, void *relates_to);
- // TODO: Make this work for all image types and videos and regular files
- PluginResult post_file(const std::string &room_id, const std::string &filepath);
+ PluginResult post_file(const std::string &room_id, const std::string &filepath, std::string &err_msg);
PluginResult login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg);
PluginResult logout();
@@ -106,12 +107,16 @@ namespace QuickMedia {
bool was_message_posted_by_me(const std::string &room_id, void *message) const;
std::string message_get_author_displayname(RoomData *room_data, Message *message) const;
+
+ // Cached
+ PluginResult get_config(int *upload_size);
private:
PluginResult sync_response_to_body_items(const Json::Value &root, RoomSyncMessages &room_messages);
PluginResult get_previous_room_messages(const std::string &room_id, RoomData *room_data);
void events_add_user_info(const Json::Value &events_json, RoomData *room_data);
void events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications);
void events_set_room_name(const Json::Value &events_json, RoomData *room_data);
+ PluginResult upload_file(const std::string &room_id, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg);
std::shared_ptr<Message> get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message);
private:
@@ -120,6 +125,7 @@ namespace QuickMedia {
std::string username;
std::string access_token;
std::string homeserver;
+ std::optional<int> upload_limit;
std::string next_batch;
};
} \ No newline at end of file
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index 020baf1..2fc33e8 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -63,10 +63,11 @@ namespace QuickMedia {
// Returns empty string if no extension
static const char* get_ext(const std::string &path) {
+ size_t slash_index = path.rfind('/');
size_t index = path.rfind('.');
- if(index == std::string::npos)
- return "";
- return path.c_str() + index;
+ if(index != std::string::npos && (slash_index == std::string::npos || index > slash_index))
+ return path.c_str() + index;
+ return "";
}
bool AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, bool use_tor, std::shared_ptr<ThumbnailData> thumbnail_data) {
diff --git a/src/Body.cpp b/src/Body.cpp
index 6b352b3..4aa194b 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -473,6 +473,8 @@ namespace QuickMedia {
if(item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->getSize().x > 0 && item_thumbnail->image->getSize().y > 0) {
if(!item_thumbnail->texture.loadFromImage(*item_thumbnail->image))
fprintf(stderr, "Warning: failed to load texture from image: %s\n", item->thumbnail_url.c_str());
+ //item_thumbnail->texture.setSmooth(true);
+ //item_thumbnail->texture.generateMipmap();
item_thumbnail->image.reset();
item_thumbnail->loading_state = LoadingState::APPLIED_TO_TEXTURE;
}
@@ -631,6 +633,8 @@ namespace QuickMedia {
if(!item->visible && !item->get_description().empty())
item->visible = string_find_case_insensitive(item->get_description(), text);
}
+
+ select_first_item();
}
bool Body::no_items_visible() const {
diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp
new file mode 100644
index 0000000..690e40e
--- /dev/null
+++ b/src/FileAnalyzer.cpp
@@ -0,0 +1,227 @@
+#include "../include/FileAnalyzer.hpp"
+#include "../include/Program.h"
+#include <sys/stat.h>
+#include <stdio.h>
+#include <array>
+#include <json/reader.h> // TODO: Remove this dependency
+
+static const int MAGIC_NUMBER_BUFFER_SIZE = 36;
+
+namespace QuickMedia {
+ struct MagicNumber {
+ std::array<int, MAGIC_NUMBER_BUFFER_SIZE> data;
+ size_t size;
+ ContentType content_type;
+ };
+
+ // Sources:
+ // https://en.wikipedia.org/wiki/List_of_file_signatures
+ // https://mimesniff.spec.whatwg.org/
+
+ // What about audio ogg files that are not opus?
+ // TODO: Test all of these
+ static const std::array<MagicNumber, 19> magic_numbers = {
+ MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'A', 'V', 'I', ' '}, 12, ContentType::VIDEO_AVI },
+ MagicNumber{ {0x00, 0x00, 0x00, -1, 'f', 't', 'y', 'p', 'i', 's', 'o', 'm'}, 12, ContentType::VIDEO_MP4 },
+ MagicNumber{ {0x1A, 0x45, 0xDF, 0xA3}, 4, ContentType::VIDEO_WEBM },
+ MagicNumber{ {'.', 's', 'n', 'd'}, 4, ContentType::AUDIO_BASIC },
+ MagicNumber{ {'F', 'O', 'R', 'M', -1, -1, -1, -1, 'A', 'I', 'F', 'F'}, 12, ContentType::AUDIO_AIFF },
+ MagicNumber{ { 'I', 'D', '3' }, 3, ContentType::AUDIO_MPEG },
+ MagicNumber{ { 0xFF, 0xFB }, 2, ContentType::AUDIO_MPEG },
+ MagicNumber{ { 0xFF, 0xF3 }, 2, ContentType::AUDIO_MPEG },
+ MagicNumber{ { 0xFF, 0xF2 }, 2, ContentType::AUDIO_MPEG },
+ //MagicNumber{ {'O', 'g', 'g', 'S', 0x00}, 5 },
+ MagicNumber{ {'M', 'T', 'h', 'd', -1, -1, -1, -1}, 8, ContentType::AUDIO_MIDI },
+ MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'W', 'A', 'V', 'E'}, 12, ContentType::AUDIO_WAVE },
+ MagicNumber{ {'f', 'L', 'a', 'C'}, 4, ContentType::AUDIO_FLAC },
+ MagicNumber{ {'O', 'g', 'g', 'S', 0x00, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 ,-1, -1, -1, -1, -1, -1, -1, 'O', 'p', 'u', 's', 'H', 'e', 'a', 'd'}, 36, ContentType::AUDIO_OPUS },
+ MagicNumber{ {0xFF, 0xD8, 0xFF}, 3, ContentType::IMAGE_JPEG },
+ MagicNumber{ {0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A}, 8, ContentType::IMAGE_PNG },
+ MagicNumber{ {'G', 'I', 'F', '8', '7', 'a'}, 6, ContentType::IMAGE_GIF },
+ MagicNumber{ {'G', 'I', 'F', '8', '9', 'a'}, 6, ContentType::IMAGE_GIF },
+ MagicNumber{ {'B', 'M'}, 2, ContentType::IMAGE_BMP },
+ MagicNumber{ {'R', 'I', 'F', 'F', -1, -1, -1, -1, 'W', 'E', 'B', 'V', 'P'}, 6, ContentType::IMAGE_WEBP }
+ };
+
+ bool is_content_type_video(ContentType content_type) {
+ return content_type >= ContentType::VIDEO_AVI && content_type <= ContentType::VIDEO_WEBM;
+ }
+
+ bool is_content_type_audio(ContentType content_type) {
+ return content_type >= ContentType::AUDIO_BASIC && content_type <= ContentType::AUDIO_OPUS;
+ }
+
+ bool is_content_type_image(ContentType content_type) {
+ return content_type >= ContentType::IMAGE_JPEG && content_type <= ContentType::IMAGE_WEBP;
+ }
+
+ const char* content_type_to_string(ContentType content_type) {
+ switch(content_type) {
+ case ContentType::UNKNOWN: return "application/octet-stream";
+ case ContentType::VIDEO_AVI: return "video/avi";
+ case ContentType::VIDEO_MP4: return "video/mp4";
+ case ContentType::VIDEO_WEBM: return "video/webm";
+ case ContentType::AUDIO_BASIC: return "audio/basic";
+ case ContentType::AUDIO_AIFF: return "audio/aiff";
+ case ContentType::AUDIO_MPEG: return "audio/mpeg";
+ case ContentType::AUDIO_MIDI: return "audio/midi";
+ case ContentType::AUDIO_WAVE: return "audio/wave";
+ case ContentType::AUDIO_FLAC: return "audio/wave";
+ case ContentType::AUDIO_OPUS: return "audio/ogg";
+ case ContentType::IMAGE_JPEG: return "image/jpeg";
+ case ContentType::IMAGE_PNG: return "image/png";
+ case ContentType::IMAGE_GIF: return "image/gif";
+ case ContentType::IMAGE_BMP: return "image/bmp";
+ case ContentType::IMAGE_WEBP: return "image/webp";
+ }
+ return "application/octet-stream";
+ }
+
+ static int accumulate_string(char *data, int size, void *userdata) {
+ std::string *str = (std::string*)userdata;
+ str->append(data, size);
+ return 0;
+ }
+
+ bool video_get_first_frame(const char *filepath, const char *destination_path) {
+ const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", destination_path, nullptr };
+ std::string ffmpeg_result;
+ if(exec_program(program_args, nullptr, nullptr) != 0) {
+ fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n");
+ return false;
+ }
+ return true;
+ }
+
+ // TODO: Remove dependency on ffprobe
+ static bool ffprobe_extract_metadata(const char *filepath, std::optional<Dimensions> &dimensions, std::optional<double> &duration_seconds) {
+ const char *program_args[] = { "ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "--", filepath, nullptr };
+ std::string ffprobe_result;
+ if(exec_program(program_args, accumulate_string, &ffprobe_result) != 0) {
+ fprintf(stderr, "Failed to execute ffprobe, maybe its not installed?\n");
+ return false;
+ }
+
+ 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(&ffprobe_result[0], &ffprobe_result[ffprobe_result.size()], &json_root, &json_errors)) {
+ fprintf(stderr, "ffprobe response parsing failed: %s\n", json_errors.c_str());
+ return false;
+ }
+
+ if(!json_root.isObject())
+ return false;
+
+ const Json::Value &streams_json = json_root["streams"];
+ if(!streams_json.isArray())
+ return false;
+
+ for(const Json::Value &stream_json : streams_json) {
+ if(!stream_json.isObject())
+ continue;
+
+ const Json::Value &codec_type = stream_json["codec_type"];
+ if(!codec_type.isString())
+ continue;
+
+ if(strcmp(codec_type.asCString(), "video") == 0) {
+ const Json::Value &width_json = stream_json["width"];
+ const Json::Value &height_json = stream_json["height"];
+ const Json::Value &duration_json = stream_json["duration"];
+ if(width_json.isNumeric() && height_json.isNumeric())
+ dimensions = { width_json.asInt(), height_json.asInt() };
+ if(duration_json.isString())
+ duration_seconds = atof(duration_json.asCString());
+ break;
+ } else if(strcmp(codec_type.asCString(), "audio") == 0) {
+ const Json::Value &duration_json = stream_json["duration"];
+ if(duration_json.isString())
+ duration_seconds = atof(duration_json.asCString());
+ // No break here because if there is video after this, we want it to overwrite this
+ }
+ }
+
+ return true;
+ }
+
+ FileAnalyzer::FileAnalyzer() : content_type(ContentType::UNKNOWN), file_size(0), loaded(false) {
+
+ }
+
+ bool FileAnalyzer::load_file(const char *filepath) {
+ if(loaded) {
+ fprintf(stderr, "File already loaded\n");
+ return false;
+ }
+
+ FILE *file = fopen(filepath, "rb");
+ if(!file) {
+ perror(filepath);
+ return false;
+ }
+
+ content_type = ContentType::UNKNOWN;
+ file_size = 0;
+ dimensions = std::nullopt;
+ duration_seconds = std::nullopt;
+
+ struct stat stat;
+ if(fstat(fileno(file), &stat) == -1) {
+ perror(filepath);
+ fclose(file);
+ return false;
+ }
+
+ file_size = stat.st_size;
+
+ unsigned char magic_number_buffer[MAGIC_NUMBER_BUFFER_SIZE];
+ size_t num_bytes_read = fread(magic_number_buffer, 1, sizeof(magic_number_buffer), file);
+ fclose(file);
+
+ for(const MagicNumber &magic_number : magic_numbers) {
+ if(num_bytes_read >= magic_number.size) {
+ bool matching_magic_numbers = true;
+ for(size_t i = 0; i < magic_number.size; ++i) {
+ if(magic_number.data[i] != (int)magic_number_buffer[i] && (int)magic_number.data[i] != -1) {
+ matching_magic_numbers = false;
+ break;
+ }
+ }
+ if(matching_magic_numbers) {
+ content_type = magic_number.content_type;
+ break;
+ }
+ }
+ }
+
+ if(content_type != ContentType::UNKNOWN) {
+ if(!ffprobe_extract_metadata(filepath, dimensions, duration_seconds)) {
+ // This is not an error, matrix allows files to be uploaded without metadata
+ fprintf(stderr, "Failed to extract metadata from file: %s, is ffprobe not installed?\n", filepath);
+ }
+ if(is_content_type_image(content_type))
+ duration_seconds = std::nullopt;
+ }
+
+ loaded = true;
+ return true;
+ }
+
+ ContentType FileAnalyzer::get_content_type() const {
+ return content_type;
+ }
+
+ size_t FileAnalyzer::get_file_size() const {
+ return file_size;
+ }
+
+ std::optional<Dimensions> FileAnalyzer::get_dimensions() const {
+ return dimensions;
+ }
+
+ std::optional<double> FileAnalyzer::get_duration_seconds() const {
+ return duration_seconds;
+ }
+} \ No newline at end of file
diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp
index 24894fd..77c53b4 100644
--- a/src/ImageViewer.cpp
+++ b/src/ImageViewer.cpp
@@ -261,6 +261,8 @@ namespace QuickMedia {
if(page_data) {
if(page_data->image_status == ImageStatus::LOADED && page_data->image->getSize().x > 0 && page_data->image->getSize().y > 0) {
if(page_data->texture.loadFromImage(*page_data->image)) {
+ //page_data->texture.setSmooth(true);
+ //page_data->texture.generateMipmap();
double height_before = get_page_size(page_i).y;
page_data->image_status = ImageStatus::APPLIED_TO_TEXTURE;
page_data->sprite.setTexture(page_data->texture, true);
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 8835b1d..fd49556 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -3117,6 +3117,7 @@ namespace QuickMedia {
if(attached_image_texture->loadFromMemory(image_data.data(), image_data.size())) {
attached_image_texture->setSmooth(true);
+ //attached_image_texture->generateMipmap();
attached_image_sprite.setTexture(*attached_image_texture, true);
} else {
BodyItem *selected_item = body->get_selected();
@@ -3280,14 +3281,17 @@ namespace QuickMedia {
source.a + diff_a * progress);
}
- static std::string extract_first_line(const std::string &str) {
+ static std::string extract_first_line(const std::string &str, size_t max_length) {
size_t index = str.find('\n');
- if(index == std::string::npos)
+ if(index == std::string::npos) {
+ if(str.size() > max_length)
+ return str.substr(0, max_length) + "...";
return str;
- else if(index == 0)
+ } else if(index == 0) {
return "";
- else
- return str.substr(0, index);
+ } else {
+ return str.substr(0, std::min(index, max_length)) + "...";
+ }
}
void Program::chat_page() {
@@ -3361,7 +3365,7 @@ namespace QuickMedia {
if(only_show_mentions) {
std::string room_desc;
if(!messages.empty())
- room_desc = matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body);
+ room_desc = matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150);
if(was_mentioned) {
room_desc += "\n** You were mentioned **"; // TODO: Better notification?
room_body_item_it->second.body_item->title_color = sf::Color(255, 100, 100);
@@ -3369,7 +3373,7 @@ namespace QuickMedia {
}
room_body_item_it->second.body_item->set_description(std::move(room_desc));
} else if(!messages.empty()) {
- std::string room_desc = "Unread: " + matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body);
+ std::string room_desc = "Unread: " + matrix->message_get_author_displayname(room, messages.back().get()) + ": " + extract_first_line(messages.back()->body, 150);
if(was_mentioned)
room_desc += "\n** You were mentioned **"; // TODO: Better notification?
room_body_item_it->second.body_item->set_description(std::move(room_desc));
@@ -3409,7 +3413,7 @@ namespace QuickMedia {
if(text.isEmpty())
return false;
- if(text[0] == '/') {
+ if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') {
std::string command = text;
strip(command);
if(command == "/upload") {
@@ -3432,7 +3436,7 @@ namespace QuickMedia {
if(chat_state == ChatState::TYPING_MESSAGE) {
// TODO: Make asynchronous
- if(matrix->post_message(current_room_id, text) == PluginResult::OK) {
+ if(matrix->post_message(current_room_id, text, std::nullopt, std::nullopt) == PluginResult::OK) {
chat_input.set_editable(false);
chat_state = ChatState::NAVIGATING;
return true;
@@ -3575,11 +3579,18 @@ namespace QuickMedia {
}
};
+ bool is_window_focused = true;
+
while (current_page == Page::CHAT) {
sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds();
while (window.pollEvent(event)) {
base_event_handler(event, Page::EXIT, false, false, false);
- if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) {
+ if(event.type == sf::Event::GainedFocus) {
+ is_window_focused = true;
+ redraw = true;
+ } else if(event.type == sf::Event::LostFocus) {
+ is_window_focused = false;
+ } else if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) {
redraw = true;
} else if(event.type == sf::Event::KeyPressed && chat_state == ChatState::NAVIGATING) {
if(event.key.code == sf::Keyboard::Up || event.key.code == sf::Keyboard::PageUp || event.key.code == sf::Keyboard::Home) {
@@ -3833,8 +3844,11 @@ namespace QuickMedia {
} 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);
+ std::string err_msg;
+ if(matrix->post_file(current_room_id, selected_files[0], err_msg) != PluginResult::OK) {
+ std::string desc = "Failed to upload image to room, error: " + err_msg;
+ show_notification("QuickMedia", desc.c_str(), Urgency::CRITICAL);
+ }
}
redraw = true;
break;
@@ -3887,6 +3901,8 @@ namespace QuickMedia {
if(room_avatar_thumbnail_data->loading_state == LoadingState::FINISHED_LOADING && room_avatar_thumbnail_data->image->getSize().x > 0 && room_avatar_thumbnail_data->image->getSize().y > 0) {
if(!room_avatar_thumbnail_data->texture.loadFromImage(*room_avatar_thumbnail_data->image))
fprintf(stderr, "Warning: failed to load texture for room avatar\n");
+ room_avatar_thumbnail_data->texture.setSmooth(true);
+ //room_avatar_thumbnail_data->texture.generateMipmap();
room_avatar_thumbnail_data->image.reset();
room_avatar_thumbnail_data->loading_state = LoadingState::APPLIED_TO_TEXTURE;
room_avatar_sprite.setTexture(room_avatar_thumbnail_data->texture, true);
@@ -4152,7 +4168,7 @@ namespace QuickMedia {
if(tabs[selected_tab].type == ChatTabType::MESSAGES) {
BodyItem *last_visible_item = tabs[selected_tab].body->get_last_fully_visible_item();
- if(chat_state != ChatState::URL_SELECTION && current_room_body_data && last_visible_item && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) {
+ if(is_window_focused && chat_state != ChatState::URL_SELECTION && current_room_body_data && last_visible_item && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) {
Message *message = (Message*)last_visible_item->userdata;
if(message->timestamp > current_room_body_data->last_read_message_timestamp) {
current_room_body_data->last_read_message_timestamp = message->timestamp;
diff --git a/src/main.cpp b/src/main.cpp
index 2899565..3383363 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -5,6 +5,7 @@
int main(int argc, char **argv) {
chdir(dirname(argv[0]));
+ setlocale(LC_ALL, "C"); // Sigh... stupid C
XInitThreads();
QuickMedia::Program program;
return program.run(argc, argv);
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