From e19a29c7e51860144f02d7e7b08ac5e430e1f78f Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 7 Nov 2022 22:21:52 +0100 Subject: Support images in text, add custom emoji to matrix --- src/plugins/Matrix.cpp | 595 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 502 insertions(+), 93 deletions(-) (limited to 'src/plugins/Matrix.cpp') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index f79b10c..28b4823 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -9,6 +9,7 @@ #include "../../include/AsyncImageLoader.hpp" #include "../../include/Config.hpp" #include "../../include/Theme.hpp" +#include "../../include/Scale.hpp" #include #include #include @@ -27,14 +28,15 @@ namespace QuickMedia { static const mgl::vec2i thumbnail_max_size(600, 337); + static const mgl::vec2i custom_emoji_max_size(64, 64); static const char* SERVICE_NAME = "matrix"; static const char* OTHERS_ROOM_TAG = "tld.name.others"; // Filter without account data. TODO: We include pinned events but limit events to 1. That means if the last event is a pin, // then we cant see room message preview. TODO: Fix this somehow. // TODO: What about state events in initial sync in timeline? such as user display name change. - static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; - static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}"; - static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; + static const char* INITIAL_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"types\":[\"m.room.message\"],\"limit\":1,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; + static const char* ADDITIONAL_MESSAGES_FILTER = "{\"presence\":{\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":20,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true}}}"; + static const char* CONTINUE_FILTER = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"types\":[\"qm.emoji\",\"m.direct\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"types\":[\"m.fully_read\",\"m.tag\",\"qm.last_read_message_timestamp\"],\"lazy_load_members\":true}}}"; static std::string capitalize(const std::string &str) { if(str.size() >= 1) @@ -49,6 +51,8 @@ namespace QuickMedia { if(tag.size() >= 2 && memcmp(tag.data(), "m.", 2) == 0) { if(strcmp(tag.c_str() + 2, "favourite") == 0) return "Favorites"; + else if(strcmp(tag.c_str() + 2, "direct") == 0) + return "Direct messages"; else if(strcmp(tag.c_str() + 2, "lowpriority") == 0) return "Low priority"; else if(strcmp(tag.c_str() + 2, "server_notice") == 0) @@ -117,7 +121,7 @@ namespace QuickMedia { return colors[color_hash_code(user_id) % num_colors]; } - static std::string remove_reply_formatting(const std::string &str) { + static std::string remove_reply_formatting(Matrix *matrix, const std::string &str) { if(strncmp(str.c_str(), "> <@", 4) == 0) { size_t index = str.find("> ", 4); if(index != std::string::npos) { @@ -126,12 +130,12 @@ namespace QuickMedia { return str.substr(msg_begin + 2); } } else { - return formatted_text_to_qm_text(str.c_str(), str.size(), false); + return formatted_text_to_qm_text(matrix, str.c_str(), str.size(), false); } return str; } - static std::string remove_reply_formatting(const Message *message, bool keep_formatted = false) { + static std::string remove_reply_formatting(Matrix *matrix, const Message *message, bool keep_formatted = false) { if(!message->body_is_formatted && strncmp(message->body.c_str(), "> <@", 4) == 0) { size_t index = message->body.find("> ", 4); if(index != std::string::npos) { @@ -144,7 +148,7 @@ namespace QuickMedia { if(keep_formatted) return message->body; else - return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), false); + return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), false); } } @@ -464,7 +468,7 @@ namespace QuickMedia { if(!sync_is_cache && message_dir == MessageDirection::AFTER) { for(auto &message : messages) { if(message->notification_mentions_me) { - std::string body = remove_reply_formatting(message.get()); + std::string body = remove_reply_formatting(matrix, message->body); bool read = true; // TODO: What if the message or username begins with "-"? also make the notification image be the avatar of the user if((!is_window_focused || room != current_room) && message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION) { @@ -629,15 +633,15 @@ namespace QuickMedia { return nullptr; } - static std::string message_to_qm_text(const Message *message, bool allow_formatted_text = true) { + std::string message_to_qm_text(Matrix *matrix, const Message *message, bool allow_formatted_text) { if(message->body_is_formatted) - return formatted_text_to_qm_text(message->body.c_str(), message->body.size(), allow_formatted_text); + return formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), allow_formatted_text); else return message->body; } - static std::string message_to_room_description_text(Message *message) { - std::string body = strip(message_to_qm_text(message)); + static std::string message_to_room_description_text(Matrix *matrix, Message *message) { + std::string body = strip(formatted_text_to_qm_text(matrix, message->body.c_str(), message->body.size(), true)); if(message->type == MessageType::REACTION) return "Reacted with: " + body; else if(message->related_event_type == RelatedEventType::REPLY) @@ -704,7 +708,7 @@ namespace QuickMedia { room_desc += "Unread: "; if(last_unread_message) - room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(last_unread_message); + room_desc += extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_unread_message), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_unread_message); int unread_notification_count = room->unread_notification_count; if(unread_notification_count > 0 && set_room_as_unread) { @@ -724,7 +728,7 @@ namespace QuickMedia { rooms_page->move_room_to_top(room); room_tags_page->move_room_to_top(room); } else if(last_new_message) { - room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(last_new_message.get())); + room->body_item->set_description(extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(last_new_message.get()), AUTHOR_MAX_LENGTH) + ": " + message_to_room_description_text(matrix, last_new_message.get())); room->body_item->set_description_color(get_theme().faded_text_color); room->body_item->set_description_max_lines(3); @@ -974,6 +978,9 @@ namespace QuickMedia { matrix->logout(); program->set_go_to_previous_page(); return PluginResult::OK; + } else if(args.url == "emoji") { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; } else { return PluginResult::ERR; } @@ -991,9 +998,170 @@ namespace QuickMedia { } else { show_notification("QuickMedia", "Failed to join " + args.title, Urgency::CRITICAL); } + + return PluginResult::OK; + } + + static const char* file_get_filename(const std::string &filepath) { + size_t index = filepath.rfind('/'); + if(index == std::string::npos) + return filepath.c_str(); + return filepath.c_str() + index + 1; + } + + static bool generate_random_characters(char *buffer, int buffer_size) { + int fd = open("/dev/urandom", O_RDONLY); + if(fd == -1) { + perror("/dev/urandom"); + return false; + } + + if(read(fd, buffer, buffer_size) < buffer_size) { + fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size); + close(fd); + return false; + } + + close(fd); + return true; + } + + static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) { + std::ostringstream result; + result << std::hex; + for(int i = 0; i < buffer_size; ++i) + result << (int)(unsigned char)buffer[i]; + return result.str(); + } + + PluginResult MatrixCustomEmojiPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + if(args.url == "add") { + auto submit_handler = [this](FileManagerPage*, const std::filesystem::path &filepath) { + program->run_task_with_loading_screen([this, filepath] { + std::string key = filepath.filename().string(); + if(!key.empty()) { + size_t ext_index = key.rfind('.'); + if(ext_index != std::string::npos) + key = key.substr(0, ext_index); + } + + if(key.empty()) { + char random_characters[10]; + if(!generate_random_characters(random_characters, sizeof(random_characters))) { + show_notification("QuickMedia", "Failed to generate random string", Urgency::CRITICAL); + return false; + } + key = random_characters_to_readable_string(random_characters, sizeof(random_characters)); + } + + if(matrix->does_custom_emoji_with_name_exist(key)) { + show_notification("QuickMedia", "Failed to upload custom emoji. You already have a custom emoji with the name " + key, Urgency::CRITICAL); + return false; + } + + std::string mxc_url; + std::string err_msg; + if(matrix->upload_custom_emoji(filepath, key, mxc_url, err_msg) != PluginResult::OK) { + show_notification("QuickMedia", "Failed to upload custom emoji, error: " + err_msg, Urgency::CRITICAL); + return false; + } + + return true; + }); + return std::vector{}; + }; + + auto file_manager_body = create_body(); + auto file_manager_page = std::make_unique(program, FILE_MANAGER_MIME_TYPE_IMAGE, std::move(submit_handler)); + file_manager_page->set_current_directory(get_home_dir().data); + BodyItems body_items; + file_manager_page->get_files_in_directory(body_items); + file_manager_body->set_items(std::move(body_items)); + + result_tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else if(args.url == "rename") { + result_tabs.push_back(Tab{create_body(false, true), std::make_unique(program, matrix), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else if(args.url == "delete") { + auto body = create_body(false, true); + BodyItems body_items; + for(auto &emoji : matrix->get_custom_emojis()) { + auto emoji_item = BodyItem::create(":" + emoji.first + ":"); + emoji_item->url = emoji.first; + emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url); + emoji_item->thumbnail_size = emoji.second.size; + body_items.push_back(std::move(emoji_item)); + } + body->set_items(std::move(body_items)); + + Body *body_p = body.get(); + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, matrix, body_p), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } else { + return PluginResult::ERR; + } + } + + PluginResult MatrixCustomEmojiPage::lazy_fetch(BodyItems &result_items) { + auto add_emoji_item = BodyItem::create("Add emoji"); + add_emoji_item->url = "add"; + result_items.push_back(std::move(add_emoji_item)); + + auto rename_emoji_item = BodyItem::create("Rename emoji"); + rename_emoji_item->url = "rename"; + result_items.push_back(std::move(rename_emoji_item)); + + auto delete_emoji_item = BodyItem::create("Delete emoji"); + delete_emoji_item->set_title_color(mgl::Color(255, 45, 47)); + delete_emoji_item->url = "delete"; + result_items.push_back(std::move(delete_emoji_item)); + + return PluginResult::OK; + } + + bool MatrixCustomEmojiPage::is_ready() { + return matrix->is_initial_sync_finished(); + } + + PluginResult MatrixCustomEmojiRenameSelectPage::submit(const SubmitArgs &args, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, matrix, args.url), create_search_bar("Enter a new name for the emoji...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } + + PluginResult MatrixCustomEmojiRenameSelectPage::lazy_fetch(BodyItems &result_items) { + for(auto &emoji : matrix->get_custom_emojis()) { + auto emoji_item = BodyItem::create(":" + emoji.first + ":"); + emoji_item->url = emoji.first; + emoji_item->thumbnail_url = matrix->get_media_url(emoji.second.url); + emoji_item->thumbnail_size = emoji.second.size; + result_items.push_back(std::move(emoji_item)); + } return PluginResult::OK; } + PluginResult MatrixCustomEmojiRenamePage::submit(const SubmitArgs &args, std::vector&) { + if(matrix->rename_custom_emoji(emoji_key, args.title)) { + program->set_go_to_previous_page(); + return PluginResult::OK; + } else { + show_notification("QuickMedia", "Failed to rename emoji " + emoji_key + " to " + args.title, Urgency::CRITICAL); + return PluginResult::OK; + } + } + + PluginResult MatrixCustomEmojiDeletePage::submit(const SubmitArgs &args, std::vector&) { + if(matrix->delete_custom_emoji(args.url)) { + body->erase_item([&args](std::shared_ptr &item) { + return item->url == args.url; + }); + return PluginResult::OK; + } else { + show_notification("QuickMedia", "Failed to delete emoji: " + args.url, Urgency::CRITICAL); + return PluginResult::OK; + } + } + MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) : Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id)) { @@ -1533,19 +1701,21 @@ namespace QuickMedia { notification_thread.join(); } + std::lock_guard lock(room_data_mutex); delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); - next_batch.clear(); + set_next_batch(""); next_notifications_token.clear(); invites.clear(); filter_cached.reset(); my_events_transaction_ids.clear(); finished_fetching_notifications = false; + custom_emoji_by_key.clear(); } - bool Matrix::is_initial_sync_finished() const { - return !next_batch.empty(); + bool Matrix::is_initial_sync_finished() { + return initial_sync_finished; } bool Matrix::did_initial_sync_fail(std::string &err_msg) { @@ -1705,14 +1875,12 @@ namespace QuickMedia { if(!root.IsObject()) return PluginResult::ERR; - //const rapidjson::Value &account_data_json = GetMember(root, "account_data"); - //std::optional> dm_rooms; - //parse_sync_account_data(account_data_json, dm_rooms); - // TODO: Include "Direct messages" as a tag using |dm_rooms| above - const rapidjson::Value &rooms_json = GetMember(root, "rooms"); parse_sync_room_data(rooms_json, is_additional_messages_sync, initial_sync); + const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + parse_sync_account_data(account_data_json); + return PluginResult::OK; } @@ -1771,7 +1939,7 @@ namespace QuickMedia { notification.room = room; notification.event_id = std::move(event_id); notification.sender_user_id.assign(sender_json.GetString(), sender_json.GetStringLength()); - notification.body = remove_reply_formatting(body_json.GetString()); + notification.body = remove_reply_formatting(this, body_json.GetString()); notification.timestamp = timestamp; notification.read = read_json.GetBool(); callback_func(notification); @@ -1781,7 +1949,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional> &dm_rooms) { + PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) { if(!account_data_json.IsObject()) return PluginResult::OK; @@ -1789,36 +1957,74 @@ namespace QuickMedia { if(!events_json.IsArray()) return PluginResult::OK; - bool has_direct_rooms = false; - std::set dm_rooms_tmp; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; const rapidjson::Value &type_json = GetMember(event_item_json, "type"); - if(!type_json.IsString() || strcmp(type_json.GetString(), "m.direct") != 0) + if(!type_json.IsString()) continue; const rapidjson::Value &content_json = GetMember(event_item_json, "content"); if(!content_json.IsObject()) continue; - has_direct_rooms = true; - for(auto const &it : content_json.GetObject()) { - if(!it.value.IsArray()) - continue; + if(strcmp(type_json.GetString(), "m.direct") == 0) { + for(auto const &it : content_json.GetObject()) { + if(!it.name.IsString()) + continue; - for(const rapidjson::Value &room_id_json : it.value.GetArray()) { - if(!room_id_json.IsString()) + if(!it.value.IsArray()) continue; - - dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + + for(const rapidjson::Value &room_id_json : it.value.GetArray()) { + if(!room_id_json.IsString()) + continue; + + RoomData *room = get_room_by_id(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + if(!room) { + fprintf(stderr, "Warning: got m.direct for room %s that we haven't created yet\n", room_id_json.GetString()); + continue; + } + + auto user = get_user_by_id(room, std::string(it.name.GetString(), it.name.GetStringLength()), nullptr, false); + if(!user) { + fprintf(stderr, "Warning: got m.direct for user %s that doesn't exist in the room %s yet\n", it.name.GetString(), room_id_json.GetString()); + continue; + } + + room->acquire_room_lock(); + std::set &room_tags = room->get_tags_thread_unsafe(); + auto room_tag_it = room_tags.find("m.direct"); + if(room_tag_it == room_tags.end()) { + room_tags.insert("m.direct"); + ui_thread_tasks.push([this, room]{ delegate->room_add_tag(room, "m.direct"); }); + } + room->release_room_lock(); + } } - } - } + } else if(strcmp(type_json.GetString(), "qm.emoji") == 0) { + std::lock_guard lock(room_data_mutex); + for(auto const &emoji_json : content_json.GetObject()) { + if(!emoji_json.name.IsString() || !emoji_json.value.IsObject()) + continue; - if(has_direct_rooms) - dm_rooms = std::move(dm_rooms_tmp); + const rapidjson::Value &url_json = GetMember(emoji_json.value, "url"); + const rapidjson::Value &width_json = GetMember(emoji_json.value, "width"); + const rapidjson::Value &height_json = GetMember(emoji_json.value, "height"); + if(!url_json.IsString()) + continue; + + CustomEmoji custom_emoji; + custom_emoji.url = url_json.GetString(); + if(width_json.IsInt() && height_json.IsInt()) { + custom_emoji.size.x = width_json.GetInt(); + custom_emoji.size.y = height_json.GetInt(); + } + custom_emoji_by_key[emoji_json.name.GetString()] = std::move(custom_emoji); + } + } + } return PluginResult::OK; } @@ -1986,6 +2192,20 @@ namespace QuickMedia { item_timestamp = origin_server_ts.GetInt64(); } + const rapidjson::Value &is_direct_json = GetMember(content_json, "is_direct"); + if(is_direct_json.IsBool() && is_direct_json.GetBool()) { + room_data->acquire_room_lock(); + std::set &room_tags = room_data->get_tags_thread_unsafe(); + + auto room_tag_it = room_tags.find("m.direct"); + if(room_tag_it == room_tags.end()) { + room_tags.insert("m.direct"); + ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, "m.direct"); }); + } + + room_data->release_room_lock(); + } + parse_user_info(content_json, sender_json->GetString(), room_data, item_timestamp); } } @@ -2003,7 +2223,7 @@ namespace QuickMedia { return media_url.substr(start, end - start); } - static std::string get_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) { + static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) { if(mxc_id.empty()) return ""; @@ -2011,6 +2231,10 @@ namespace QuickMedia { return homeserver + "/_matrix/media/r0/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop"; } + std::string Matrix::get_media_url(const std::string &mxc_id) { + return homeserver + "/_matrix/media/r0/download/" + thumbnail_url_extract_media_id(mxc_id); + } + std::shared_ptr Matrix::parse_user_info(const rapidjson::Value &json, const std::string &user_id, RoomData *room_data, int64_t timestamp) { assert(json.IsObject()); std::string avatar_url_str; @@ -2023,7 +2247,7 @@ namespace QuickMedia { std::string display_name = display_name_json.IsString() ? display_name_json.GetString() : user_id; std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_str); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) //auto user_info = std::make_shared(room_data, user_id, std::move(display_name), std::move(avatar_url)); // Overwrites user data //room_data->add_user(user_info); @@ -2195,8 +2419,8 @@ namespace QuickMedia { return false; } - bool message_contains_user_mention(const Message *message, const std::string &username, const std::string &user_id) { - const std::string formatted_text = message_to_qm_text(message, false); + bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id) { + const std::string formatted_text = message_to_qm_text(matrix, message, false); return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id); } @@ -2263,7 +2487,7 @@ namespace QuickMedia { // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) // TODO: Is comparing against read marker timestamp ok enough? if(me && message->timestamp > read_marker_message_timestamp) { - std::string message_str = message_to_qm_text(message.get(), false); + std::string message_str = message_to_qm_text(this, message.get(), false); message->notification_mentions_me = message_contains_user_mention(message_str, my_display_name) || message_contains_user_mention(message_str, me->user_id) || message_contains_user_mention(message_str, "@room"); } } @@ -2359,7 +2583,11 @@ namespace QuickMedia { bool allow_formatted_text = false; bool inside_source_highlight = false; bool supports_syntax_highlight = false; + bool inside_img_tag = false; + std::string_view img_src; + mgl::vec2i img_size; mgl::Color font_color = mgl::Color(255, 255, 255, 255); + Matrix *matrix = nullptr; }; static int accumulate_string(char *data, int size, void *userdata) { @@ -2384,6 +2612,10 @@ namespace QuickMedia { else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { parse_userdata.inside_code_tag = true; parse_userdata.code_tag_language = std::string_view(); + } else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) { + parse_userdata.inside_img_tag = true; + parse_userdata.img_src = std::string_view(); + parse_userdata.img_size = { 0, 0 }; } break; } @@ -2397,6 +2629,17 @@ namespace QuickMedia { parse_userdata.mx_reply_depth = std::max(0, parse_userdata.mx_reply_depth - 1); } else if(html_parser->tag_name.size == 4 && memcmp(html_parser->tag_name.data, "code", 4) == 0) { parse_userdata.inside_code_tag = false; + } else if(html_parser->tag_name.size == 3 && memcmp(html_parser->tag_name.data, "img", 3) == 0) { + if(parse_userdata.matrix && parse_userdata.inside_img_tag && parse_userdata.img_src.size() > 0) { + std::string image_url(parse_userdata.img_src); + html_unescape_sequences(image_url); + mgl::vec2i img_size = parse_userdata.img_size; + // TODO: Better solution when size not given? + if(img_size.x == 0 || img_size.y == 0) + img_size = custom_emoji_max_size; + parse_userdata.result += Text::formatted_image(parse_userdata.matrix->get_media_url(image_url), false, img_size); + } + parse_userdata.inside_img_tag = false; } break; } @@ -2407,6 +2650,20 @@ namespace QuickMedia { } else if(parse_userdata.inside_code_tag && html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "class", 5) == 0) { if(html_parser->attribute_value.size > 9 && memcmp(html_parser->attribute_value.data, "language-", 9) == 0) parse_userdata.code_tag_language = std::string_view(html_parser->attribute_value.data + 9, html_parser->attribute_value.size - 9); + } else if(parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag) { + if(html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "src", 3) == 0) { + parse_userdata.img_src = std::string_view(html_parser->attribute_value.data, html_parser->attribute_value.size); + } else if(html_parser->attribute_key.size == 5 && memcmp(html_parser->attribute_key.data, "width", 5) == 0) { + const std::string width(html_parser->attribute_value.data, html_parser->attribute_value.size); + parse_userdata.img_size.x = atoi(width.c_str()); + } else if(html_parser->attribute_key.size == 6 && memcmp(html_parser->attribute_key.data, "height", 6) == 0) { + const std::string height(html_parser->attribute_value.data, html_parser->attribute_value.size); + parse_userdata.img_size.y = atoi(height.c_str()); + } + } else if(!parse_userdata.allow_formatted_text && parse_userdata.inside_img_tag && html_parser->attribute_key.size == 3 && memcmp(html_parser->attribute_key.data, "alt", 3) == 0) { + std::string text_to_add(html_parser->attribute_value.data, html_parser->attribute_value.size); + html_unescape_sequences(text_to_add); + parse_userdata.result += std::move(text_to_add); } break; } @@ -2455,10 +2712,11 @@ namespace QuickMedia { return 0; } - std::string formatted_text_to_qm_text(const char *str, size_t size, bool allow_formatted_text) { + std::string formatted_text_to_qm_text(Matrix *matrix, const char *str, size_t size, bool allow_formatted_text) { FormattedTextParseUserdata parse_userdata; parse_userdata.allow_formatted_text = allow_formatted_text; parse_userdata.supports_syntax_highlight = is_program_executable_by_name("source-highlight"); + parse_userdata.matrix = matrix; html_parser_parse(str, size, formattext_text_parser_callback, &parse_userdata); return std::move(parse_userdata.result); } @@ -2648,7 +2906,7 @@ namespace QuickMedia { body = user_display_name + " changed his profile picture"; std::string new_avatar_url_str = thumbnail_url_extract_media_id(new_avatar_url_json.GetString()); if(!new_avatar_url_str.empty()) - new_avatar_url_str = get_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + new_avatar_url_str = get_avatar_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) new_avatar_url = new_avatar_url_str; update_user_display_info = room_data->set_user_avatar_url(user, std::move(new_avatar_url_str), timestamp); } else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) { @@ -2989,7 +3247,7 @@ namespace QuickMedia { if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0) continue; - update_room_avatar_url |= room_data->set_avatar_url(get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); + update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp); room_data->avatar_is_fallback = false; } else if(strcmp(type_json.GetString(), "m.room.topic") == 0) { const rapidjson::Value &content_json = GetMember(event_item_json, "content"); @@ -3144,7 +3402,10 @@ namespace QuickMedia { ui_thread_tasks.push([this, room_data]{ delegate->room_add_tag(room_data, OTHERS_ROOM_TAG); }); } + const bool contains_direct_messaging = room_tags.find("m.direct") != room_tags.end(); room_tags = std::move(new_tags); + if(contains_direct_messaging) + room_tags.insert("m.direct"); room_data->release_room_lock(); } } @@ -3362,31 +3623,6 @@ namespace QuickMedia { return PluginResult::OK; } - static bool generate_random_characters(char *buffer, int buffer_size) { - int fd = open("/dev/urandom", O_RDONLY); - if(fd == -1) { - perror("/dev/urandom"); - return false; - } - - if(read(fd, buffer, buffer_size) < buffer_size) { - fprintf(stderr, "Failed to read %d bytes from /dev/urandom\n", buffer_size); - close(fd); - return false; - } - - close(fd); - return true; - } - - static std::string random_characters_to_readable_string(const char *buffer, int buffer_size) { - std::ostringstream result; - result << std::hex; - for(int i = 0; i < buffer_size; ++i) - result << (int)(unsigned char)buffer[i]; - return result.str(); - } - std::string create_transaction_id() { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -3505,11 +3741,29 @@ namespace QuickMedia { } } + static void replace_emoji_references_with_formatted_images(std::string &str, const std::unordered_map &custom_emojis) { + for(const auto &it : custom_emojis) { + std::string keybind = ":" + it.first + ":"; + std::string url = it.second.url; + html_escape_sequences(url); + std::string width = std::to_string(it.second.size.x); + std::string height = std::to_string(it.second.size.y); + std::string tag = "\"""; + string_replace_all(str, keybind, tag); + } + } + std::string Matrix::body_to_formatted_body(RoomData *room, const std::string &body) { + std::unordered_map custom_emojis_copy; + { + std::lock_guard lock(room_data_mutex); + custom_emojis_copy = custom_emoji_by_key; + } + std::string formatted_body; bool is_inside_code_block = false; bool is_first_line = true; - string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line](const char *str, size_t size){ + string_split(body, '\n', [this, room, &formatted_body, &is_inside_code_block, &is_first_line, &custom_emojis_copy](const char *str, size_t size){ if(!is_first_line) { if(is_inside_code_block) formatted_body += '\n'; @@ -3533,6 +3787,9 @@ namespace QuickMedia { } is_first_line = true; } else { + if(!is_inside_code_block) + replace_emoji_references_with_formatted_images(line_str, custom_emojis_copy); + if(!is_inside_code_block && size > 0 && str[0] == '>') { formatted_body += ""; formatted_body_add_line(room, formatted_body, line_str); @@ -3653,17 +3910,17 @@ namespace QuickMedia { return result; } - static std::string get_reply_message(const Message *message, bool keep_formatted = false) { + static std::string get_reply_message(Matrix *matrix, const Message *message, bool keep_formatted = false) { std::string related_to_body; switch(message->type) { case MessageType::TEXT: { if(message->related_event_type != RelatedEventType::NONE) { - related_to_body = remove_reply_formatting(message, keep_formatted); + related_to_body = remove_reply_formatting(matrix, message, keep_formatted); } else { if(keep_formatted && message->body_is_formatted) related_to_body = message->body; else - related_to_body = message_to_qm_text(message, false); + related_to_body = message_to_qm_text(matrix, message, false); } break; } @@ -3683,15 +3940,15 @@ namespace QuickMedia { if(keep_formatted && message->body_is_formatted) related_to_body = message->body; else - related_to_body = message_to_qm_text(message, false); + related_to_body = message_to_qm_text(matrix, message, false); break; } } return related_to_body; } - static std::string create_body_for_message_reply(const Message *message, const std::string &body) { - return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(message)) + "\n\n" + body; + static std::string create_body_for_message_reply(Matrix *matrix, const Message *message, const std::string &body) { + return "> <" + message->user->user_id + "> " + block_quote(get_reply_message(matrix, message)) + "\n\n" + body; } static std::string extract_homeserver_from_room_id(const std::string &room_id) { @@ -3703,7 +3960,7 @@ namespace QuickMedia { std::string Matrix::create_formatted_body_for_message_reply(RoomData *room, const Message *message, const std::string &body) { std::string formatted_body = body_to_formatted_body(room, body); - std::string related_to_body = get_reply_message(message, true); + std::string related_to_body = get_reply_message(this, message, true); if(!message->body_is_formatted) html_escape_sequences(related_to_body); // TODO: Add keybind to navigate to the reply message, which would also depend on this formatting. @@ -3746,7 +4003,7 @@ namespace QuickMedia { rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); - std::string message_reply_body = create_body_for_message_reply(related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... + std::string message_reply_body = create_body_for_message_reply(this, related_to_text_message, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... std::string formatted_message_reply_body = create_formatted_body_for_message_reply(room, related_to_text_message, body); rapidjson::Document request_data(rapidjson::kObjectType); @@ -4079,11 +4336,162 @@ namespace QuickMedia { room->set_prev_batch(""); } - static const char* file_get_filename(const std::string &filepath) { - size_t index = filepath.rfind('/'); - if(index == std::string::npos) - return filepath.c_str(); - return filepath.c_str() + index + 1; + PluginResult Matrix::upload_custom_emoji(const std::string &filepath, const std::string &key, std::string &mxc_url, std::string &err_msg) { + UploadInfo file_info; + UploadInfo thumbnail_info; + // TODO: Do not create and upload thumbnail + PluginResult upload_file_result = upload_file(filepath, "", file_info, thumbnail_info, err_msg); + if(upload_file_result != PluginResult::OK) + return upload_file_result; + + mxc_url = std::move(file_info.content_uri); + + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + for(const auto &it : custom_emoji_by_key) { + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + CustomEmoji custom_emoji; + custom_emoji.url = mxc_url; + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(mxc_url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + if(file_info.dimensions) { + custom_emoji.size = clamp_to_size(mgl::vec2i(file_info.dimensions->width, file_info.dimensions->height), custom_emoji_max_size); + emoji_obj.AddMember("width", custom_emoji.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", custom_emoji.size.y, request_data.GetAllocator()); + } + request_data.AddMember(rapidjson::Value(key.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return download_result_to_plugin_result(download_result); + + std::lock_guard lock(room_data_mutex); + custom_emoji_by_key[key] = std::move(custom_emoji); + return PluginResult::OK; + } + + bool Matrix::delete_custom_emoji(const std::string &key) { + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it == custom_emoji_by_key.end()) + return false; + + for(const auto &it : custom_emoji_by_key) { + if(it.first == key) + continue; + + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return false; + + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it != custom_emoji_by_key.end()) + custom_emoji_by_key.erase(it); + + return true; + } + + bool Matrix::rename_custom_emoji(const std::string &key, const std::string &new_key) { + rapidjson::Document request_data(rapidjson::kObjectType); + { + std::lock_guard lock(room_data_mutex); + auto custom_emoji_list_copy = custom_emoji_by_key; + auto it = custom_emoji_list_copy.find(key); + if(it == custom_emoji_list_copy.end()) + return false; + + auto custom_emoji_copy = it->second; + custom_emoji_list_copy.erase(it); + custom_emoji_list_copy[new_key] = std::move(custom_emoji_copy); + for(const auto &it : custom_emoji_list_copy) { + rapidjson::Document emoji_obj(rapidjson::kObjectType); + emoji_obj.AddMember("url", rapidjson::Value(it.second.url.c_str(), request_data.GetAllocator()).Move(), request_data.GetAllocator()); + emoji_obj.AddMember("width", it.second.size.x, request_data.GetAllocator()); + emoji_obj.AddMember("height", it.second.size.y, request_data.GetAllocator()); + request_data.AddMember(rapidjson::Value(it.first.c_str(), request_data.GetAllocator()).Move(), std::move(emoji_obj), request_data.GetAllocator()); + } + } + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string server_response; + DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/user/" + my_user_id + "/account_data/qm.emoji", server_response, std::move(additional_args), true); + if(download_result != DownloadResult::OK) + return false; + + std::lock_guard lock(room_data_mutex); + auto it = custom_emoji_by_key.find(key); + if(it != custom_emoji_by_key.end()) { + auto custom_emoji_copy = it->second; + custom_emoji_by_key.erase(it); + custom_emoji_by_key[new_key] = std::move(custom_emoji_copy); + } + + return true; + } + + bool Matrix::does_custom_emoji_with_name_exist(const std::string &name) { + assert(is_initial_sync_finished()); + std::lock_guard lock(room_data_mutex); + return custom_emoji_by_key.find(name) != custom_emoji_by_key.end(); + } + + std::unordered_map Matrix::get_custom_emojis() { + assert(is_initial_sync_finished()); + std::lock_guard lock(room_data_mutex); + return custom_emoji_by_key; } PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string filename, std::string &event_id_response, std::string &err_msg, void *relates_to) { @@ -4092,7 +4500,7 @@ namespace QuickMedia { UploadInfo file_info; UploadInfo thumbnail_info; - PluginResult upload_file_result = upload_file(room, filepath, filename, file_info, thumbnail_info, err_msg); + PluginResult upload_file_result = upload_file(filepath, filename, file_info, thumbnail_info, err_msg); if(upload_file_result != PluginResult::OK) return upload_file_result; @@ -4107,7 +4515,7 @@ namespace QuickMedia { return post_message(room, filename, event_id_response, file_info_opt, thumbnail_info_opt); } - PluginResult Matrix::upload_file(RoomData *room, const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) { + PluginResult Matrix::upload_file(const std::string &filepath, std::string filename, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg, bool upload_thumbnail) { FileAnalyzer file_analyzer; if(!file_analyzer.load_file(filepath.c_str(), true)) { err_msg = "Failed to load " + filepath; @@ -4149,7 +4557,7 @@ namespace QuickMedia { if(video_get_middle_frame(file_analyzer, tmp_filename, thumbnail_max_size.x, thumbnail_max_size.y)) { UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(tmp_filename, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -4177,7 +4585,7 @@ namespace QuickMedia { thumbnail_filename = thumbnail_filename.filename_no_ext() + ".thumb" + thumbnail_filename.ext(); UploadInfo upload_info_ignored; // Ignore because it wont be set anyways. Thumbnails dont have thumbnails. - PluginResult upload_thumbnail_result = upload_file(room, thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); + PluginResult upload_thumbnail_result = upload_file(thumbnail_path, thumbnail_filename.data, thumbnail_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { close(tmp_file); remove(tmp_filename); @@ -4811,7 +5219,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString()); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); if(!avatar_url.empty()) room_body_item->thumbnail_url = std::move(avatar_url); @@ -4883,7 +5291,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) { std::string avatar_url = thumbnail_url_extract_media_id(std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength())); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, avatar_url); + avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); body_item->thumbnail_url = std::move(avatar_url); } body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; @@ -5037,6 +5445,7 @@ namespace QuickMedia { void Matrix::set_next_batch(std::string new_next_batch) { std::lock_guard lock(next_batch_mutex); next_batch = std::move(new_next_batch); + initial_sync_finished = !next_batch.empty(); } std::string Matrix::get_next_batch() { @@ -5129,7 +5538,7 @@ namespace QuickMedia { if(avatar_url_json.IsString()) avatar_url = std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength()); if(!avatar_url.empty()) - avatar_url = get_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) + avatar_url = get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb) room->set_user_avatar_url(user, avatar_url, 0); room->set_user_display_name(user, display_name, 0); -- cgit v1.2.3