From 2a973ff9402dab9d6c751a146a9f83617d0e5211 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 23 Oct 2020 10:25:08 +0200 Subject: Matrix: start on room tags, fix thread race condition on accessing room variables (name, avatar url, prev batch) --- TODO | 8 ++- plugins/ImageBoard.hpp | 2 +- plugins/Manga.hpp | 2 +- plugins/Matrix.hpp | 26 ++++++++-- plugins/Page.hpp | 14 +++-- plugins/Pornhub.hpp | 2 +- plugins/Youtube.hpp | 2 +- project.conf | 2 +- src/Body.cpp | 1 + src/QuickMedia.cpp | 12 ++--- src/plugins/Matrix.cpp | 137 +++++++++++++++++++++++++++++++++++++++++++------ 11 files changed, 171 insertions(+), 37 deletions(-) diff --git a/TODO b/TODO index 5109ec2..076900a 100644 --- a/TODO +++ b/TODO @@ -70,7 +70,7 @@ Update 4chan thread in real time, just like 4chan-x. Save the original event message, so when replying for example we can use the original message as the replying to message, rather than our converted "body" text. Remove tidy dependency and use my own html-parser. Add option to sort by other than timestamp for nyaa.si. -Add url preview for matrix (using matrix api, fallback to client url preview (using our own url preview project)). +Add url preview for matrix (using matrix api, fallback to client url preview (using our own url preview project) if disabled by the homeserver). IMPORTANT: Cleanup old messages in matrix (from matrix plugin), and instead either save them to disk or refetch them from server when going up to read old messages. (High memory usage, high disk space) Use memberName() instead of key() when iterating json object. key() creates a copy, memberName() doesn't. Do not try to reload/redownload thumbnail that fails to download after its cleared when its no longer visible on screen and then becomes visible. @@ -117,4 +117,8 @@ Cache pinned messages on disk (messages by event id), but take into consider edi Display file list for nyaa. Remove reply formatting for NOTICE in matrix as well. Scroll body when adding new items and the selected item fits after the scroll (needed for matrix where we want to see new messages when the last item is not selected). Or show the last item when its not visible in matrix (at the bottom, just like when replying/editing). -Implement our own encryption for matrix. This is also needed to make forwarded message work. Pantalaimon ignores them! \ No newline at end of file +Implement our own encryption for matrix. This is also needed to make forwarded message work. Pantalaimon ignores them! +Modify matrix sync to download and parse json but not handle it, and then add a function to handle the json. This would allow us to remove all the mutex code if we would call that new method from the main thread. +Room list in matrix ignores edited messages, which it should for unread messages but it should show the edited message if the edited message is the last message in the room. +For messages that mention us we only want a notification for the last edited version to show as a notification. +Fetch replies/pinned message using multiple threads. \ No newline at end of file diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp index 6f2a276..2d235ec 100644 --- a/plugins/ImageBoard.hpp +++ b/plugins/ImageBoard.hpp @@ -21,7 +21,7 @@ namespace QuickMedia { return PluginResult::ERR; } - bool is_image_board_thread_page() const override { return true; } + PageTypez get_type() const override { return PageTypez::IMAGE_BOARD_THREAD; } virtual BodyItems get_related_media(const std::string &url) override; virtual PluginResult login(const std::string &token, const std::string &pin, std::string &response_msg); diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp index 96a5d53..d3725da 100644 --- a/plugins/Manga.hpp +++ b/plugins/Manga.hpp @@ -24,7 +24,7 @@ namespace QuickMedia { return PluginResult::OK; } - bool is_manga_images_page() const override { return true; } + PageTypez get_type() const override { return PageTypez::MANGA_IMAGES; } virtual ImageResult get_number_of_images(int &num_images) = 0; virtual ImageResult for_each_page_in_chapter(PageCallback callback) = 0; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index c3c5539..f0ca4f5 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -20,7 +20,7 @@ namespace QuickMedia { (void)result_tabs; return PluginResult::ERR; } - bool is_video_page() const override { return true; } + PageTypez get_type() const override { return PageTypez::VIDEO; } }; struct RoomData; @@ -90,10 +90,19 @@ namespace QuickMedia { const std::vector>& get_messages_thread_unsafe() const; const std::vector& get_pinned_events_unsafe() const; + bool has_prev_batch(); + void set_prev_batch(const std::string &new_prev_batch); + std::string get_prev_batch(); + + bool has_name(); + void set_name(const std::string &new_name); + std::string get_name(); + + bool has_avatar_url(); + void set_avatar_url(const std::string &new_avatar_url); + std::string get_avatar_url(); + std::string id; - std::string name; - std::string avatar_url; - std::string prev_batch; bool initial_fetch_finished = false; // These 4 variables are set by QuickMedia, not the matrix plugin @@ -106,9 +115,16 @@ namespace QuickMedia { // The value is nullptr if the message is fetched and cached but the event if referenced an invalid message. // TODO: Verify if replied to messages are also part of /sync; then this is not needed. std::unordered_map> fetched_messages_by_event_id; + + size_t index; private: std::mutex user_mutex; std::mutex room_mutex; + + std::string name; + std::string avatar_url; + std::string prev_batch; + // Each room has its own list of user data, even if multiple rooms has the same user // because users can have different display names and avatars in different rooms. std::unordered_map> user_info_by_user_id; @@ -194,6 +210,7 @@ namespace QuickMedia { void events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncData *room_sync_data, bool has_unread_notifications); void events_set_room_name(const rapidjson::Value &events_json, RoomData *room_data); void events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data, RoomSyncData &room_sync_data); + void events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data); std::shared_ptr parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data); PluginResult upload_file(RoomData *room, const std::string &filepath, UploadInfo &file_info, UploadInfo &thumbnail_info, std::string &err_msg); @@ -205,6 +222,7 @@ namespace QuickMedia { private: std::vector> rooms; std::unordered_map room_data_by_id; // value is an index into |rooms| + std::map> rooms_by_tag_name; // value is an index into |rooms| size_t room_list_read_index = 0; std::mutex room_data_mutex; std::string user_id; diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 2e85cad..de80b4f 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -9,6 +9,14 @@ namespace QuickMedia { constexpr int SEARCH_DELAY_FILTER = 50; + // TODO: Remove to PageType when the other PageType is removed + enum class PageTypez { + REGULAR, + MANGA_IMAGES, + IMAGE_BOARD_THREAD, + VIDEO + }; + class Page { public: Page(Program *program) : program(program) {} @@ -29,10 +37,8 @@ namespace QuickMedia { DownloadResult download_json(Json::Value &result, const std::string &url, std::vector additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr); - virtual bool is_manga_images_page() const { return false; } - virtual bool is_image_board_thread_page() const { return false; } - virtual bool is_video_page() const { return false; } - // Mutually exclusive with |is_manga_images_page|, |is_image_board_thread_page| and |is_video_page| + virtual PageTypez get_type() const { return PageTypez::REGULAR; } + // Mutually exclusive with |get_type| when |get_type| is not PageTypez::REGULAR virtual bool is_single_page() const { return false; } virtual bool is_trackable() const { return false; } virtual bool is_lazy_fetch_page() const { return false; } diff --git a/plugins/Pornhub.hpp b/plugins/Pornhub.hpp index b058bbe..74fb00e 100644 --- a/plugins/Pornhub.hpp +++ b/plugins/Pornhub.hpp @@ -24,6 +24,6 @@ namespace QuickMedia { return PluginResult::ERR; } BodyItems get_related_media(const std::string &url) override; - bool is_video_page() const override { return true; } + PageTypez get_type() const override { return PageTypez::VIDEO; } }; } \ No newline at end of file diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index 007f398..bdb9c8b 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -30,6 +30,6 @@ namespace QuickMedia { return PluginResult::ERR; } BodyItems get_related_media(const std::string &url) override; - bool is_video_page() const override { return true; } + PageTypez get_type() const override { return PageTypez::VIDEO; } }; } \ No newline at end of file diff --git a/project.conf b/project.conf index 3c200db..a5d1157 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "QuickMedia" type = "executable" -version = "0.1.0" +version = "1.0.0" platforms = ["posix"] # This needs to be commented out for now because rapidjson depends on undefined behavior according to gcc... diff --git a/src/Body.cpp b/src/Body.cpp index 3fd2324..0af6407 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -835,6 +835,7 @@ namespace QuickMedia { return spacing_y; } + // TODO: Support utf-8 case insensitive find //static bool Body::string_find_case_insensitive(const std::string &str, const std::string &substr) { auto it = std::search(str.begin(), str.end(), substr.begin(), substr.end(), diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 48d3689..33ebb06 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1012,7 +1012,7 @@ namespace QuickMedia { tab.body->clear_cache(); } - if(new_tabs.size() == 1 && new_tabs[0].page->is_manga_images_page()) { + if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) { select_episode(selected_item, false); Body *chapters_body = tabs[selected_tab].body.get(); chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter @@ -1048,11 +1048,11 @@ namespace QuickMedia { } window.setKeyRepeatEnabled(true); redraw = true; - } else if(new_tabs.size() == 1 && new_tabs[0].page->is_image_board_thread_page()) { + } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::IMAGE_BOARD_THREAD) { current_page = PageType::IMAGE_BOARD_THREAD; image_board_thread_page(static_cast(new_tabs[0].page.get()), new_tabs[0].body.get()); redraw = true; - } else if(new_tabs.size() == 1 && new_tabs[0].page->is_video_page()) { + } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::VIDEO) { current_page = PageType::VIDEO_CONTENT; video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title()); redraw = true; @@ -3049,7 +3049,7 @@ namespace QuickMedia { room->has_unread_mention = 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 || is_first_sync || selected_tab == ROOMS_TAB_INDEX) - show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->name + ")", message->body); + show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body); } } } @@ -3537,12 +3537,12 @@ namespace QuickMedia { for(size_t i = 0; i < rooms.size(); ++i) { auto &room = rooms[i]; - std::string room_name = room->name; + std::string room_name = room->get_name(); if(room_name.empty()) room_name = room->id; auto body_item = BodyItem::create(std::move(room_name)); - body_item->thumbnail_url = room->avatar_url; + body_item->thumbnail_url = room->get_avatar_url(); body_item->userdata = room; // Note: this has to be valid as long as the room list is valid! body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; body_item->thumbnail_size = sf::Vector2i(32, 32); diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 9a2f70b..99d6bed 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -115,6 +115,51 @@ namespace QuickMedia { return pinned_events; } + bool RoomData::has_prev_batch() { + std::lock_guard lock(room_mutex); + return !prev_batch.empty(); + } + + void RoomData::set_prev_batch(const std::string &new_prev_batch) { + std::lock_guard lock(room_mutex); + prev_batch = new_prev_batch; + } + + std::string RoomData::get_prev_batch() { + std::lock_guard lock(room_mutex); + return prev_batch; + } + + bool RoomData::has_name() { + std::lock_guard lock(room_mutex); + return !name.empty(); + } + + void RoomData::set_name(const std::string &new_name) { + std::lock_guard lock(room_mutex); + name = new_name; + } + + std::string RoomData::get_name() { + std::lock_guard lock(room_mutex); + return name; + } + + bool RoomData::has_avatar_url() { + std::lock_guard lock(room_mutex); + return !avatar_url.empty(); + } + + void RoomData::set_avatar_url(const std::string &new_avatar_url) { + std::lock_guard lock(room_mutex); + avatar_url = new_avatar_url; + } + + std::string RoomData::get_avatar_url() { + std::lock_guard lock(room_mutex); + return avatar_url; + } + PluginResult Matrix::sync(RoomSyncData &room_sync_data) { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token }, @@ -227,11 +272,11 @@ namespace QuickMedia { const rapidjson::Value &timeline_json = GetMember(it.value, "timeline"); if(timeline_json.IsObject()) { - if(room->prev_batch.empty()) { + if(!room->has_prev_batch()) { // This may be non-existent if this is the first event in the room const rapidjson::Value &prev_batch_json = GetMember(timeline_json, "prev_batch"); if(prev_batch_json.IsString()) - room->prev_batch = prev_batch_json.GetString(); + room->set_prev_batch(prev_batch_json.GetString()); } // TODO: Use /_matrix/client/r0/notifications ? or remove this and always look for displayname/user_id in messages @@ -258,6 +303,12 @@ namespace QuickMedia { events_add_user_read_markers(events_json, room); } } + + const rapidjson::Value &account_data_json = GetMember(it.value, "account_data"); + if(account_data_json.IsObject()) { + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + events_add_room_to_tags(events_json, room); + } } return PluginResult::OK; @@ -709,11 +760,14 @@ namespace QuickMedia { if(!name_json.IsString()) continue; - room_data->name = name_json.GetString(); + room_data->set_name(name_json.GetString()); } + bool has_room_name = room_data->has_name(); + bool has_room_avatar_url = room_data->has_avatar_url(); + std::vector> users_excluding_me; - if(room_data->name.empty() || room_data->avatar_url.empty()) + if(!has_room_name || !has_room_avatar_url) users_excluding_me = room_data->get_users_excluding_me(user_id); // TODO: What about thread safety with user_id? its reset in /logout for(const rapidjson::Value &event_item_json : events_json.GetArray()) { @@ -732,18 +786,21 @@ namespace QuickMedia { if(!creator_json.IsString()) continue; - if(room_data->name.empty()) - room_data->name = combine_user_display_names_for_room_name(users_excluding_me, creator_json.GetString()); + if(!has_room_name) { + room_data->set_name(combine_user_display_names_for_room_name(users_excluding_me, creator_json.GetString())); + has_room_name = true; + } - if(room_data->avatar_url.empty()) { + if(!has_room_avatar_url) { if(users_excluding_me.empty()) { auto user = room_data->get_user_by_id(creator_json.GetString()); if(user) - room_data->avatar_url = user->avatar_url; + room_data->set_avatar_url(user->avatar_url); } else { // TODO: If there are multiple users, then we want to use some other type of avatar, not the first users avatar - room_data->avatar_url = users_excluding_me.front()->avatar_url; + room_data->set_avatar_url(users_excluding_me.front()->avatar_url); } + has_room_avatar_url = true; } } @@ -764,7 +821,7 @@ namespace QuickMedia { continue; std::string url_json_str = url_json.GetString() + 6; - room_data->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + std::move(url_json_str) + "?width=32&height=32&method=crop"; + room_data->set_avatar_url(homeserver + "/_matrix/media/r0/thumbnail/" + std::move(url_json_str) + "?width=32&height=32&method=crop"); } } @@ -801,8 +858,54 @@ namespace QuickMedia { room_data->append_pinned_events(std::move(pinned_events)); } + // TODO: According to spec: "Any tag in the tld.name.* form but not matching the namespace of the current client should be ignored", + // should we follow this? + static const char* tag_get_name(const char *name, size_t size) { + if(size >= 2 && (memcmp(name, "m.", 2) == 0 || memcmp(name, "u.", 2) == 0)) + return name + 2; + else if(size >= 9 && memcmp(name, "tld.name.", 9) == 0) + return name + 9; + else + return name; + } + + void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data) { + if(!events_json.IsArray()) + return; + + std::vector pinned_events; + 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.tag") != 0) + continue; + + const rapidjson::Value &content_json = GetMember(event_item_json, "content"); + if(!content_json.IsObject()) + continue; + + const rapidjson::Value &tags_json = GetMember(content_json, "tags"); + if(!tags_json.IsObject()) + continue; + + for(auto const &tag_json : tags_json.GetObject()) { + if(!tag_json.name.IsString() || !tag_json.value.IsObject()) + continue; + + const char *tag_name = tag_get_name(tag_json.name.GetString(), tag_json.name.GetStringLength()); + if(!tag_name) + continue; + + // TODO: Support tag order + rooms_by_tag_name[tag_name].push_back(room_data->index); + } + } + } + PluginResult Matrix::get_previous_room_messages(RoomData *room_data) { - std::string from = room_data->prev_batch; + std::string from = room_data->get_prev_batch(); if(from.empty()) { fprintf(stderr, "Info: missing previous batch for room: %s, using /sync next batch\n", room_data->id.c_str()); from = next_batch; @@ -835,9 +938,9 @@ namespace QuickMedia { if(!json_root.IsObject()) return PluginResult::ERR; - const rapidjson::Value &state_json = GetMember(json_root, "state"); - events_add_user_info(state_json, room_data); - events_set_room_name(state_json, room_data); + //const rapidjson::Value &state_json = GetMember(json_root, "state"); + //events_add_user_info(state_json, room_data); + //events_set_room_name(state_json, room_data); const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); @@ -848,7 +951,7 @@ namespace QuickMedia { return PluginResult::OK; } - room_data->prev_batch = end_json.GetString(); + room_data->set_prev_batch(end_json.GetString()); return PluginResult::OK; } @@ -1478,6 +1581,7 @@ namespace QuickMedia { rooms.clear(); room_list_read_index = 0; room_data_by_id.clear(); + rooms_by_tag_name.clear(); user_id.clear(); username.clear(); access_token.clear(); @@ -1722,7 +1826,8 @@ namespace QuickMedia { void Matrix::add_room(std::unique_ptr room) { std::lock_guard lock(room_data_mutex); - room_data_by_id.insert(std::make_pair(room->id, rooms.size())); + room->index = rooms.size(); + room_data_by_id.insert(std::make_pair(room->id, room->index)); rooms.push_back(std::move(room)); } -- cgit v1.2.3