diff options
-rw-r--r-- | README.md | 6 | ||||
-rw-r--r-- | TODO | 8 | ||||
-rw-r--r-- | include/QuickMedia.hpp | 4 | ||||
-rw-r--r-- | launcher/QuickMedia-4chan.desktop | 2 | ||||
-rw-r--r-- | launcher/QuickMedia-youtube-audio.desktop | 2 | ||||
-rw-r--r-- | launcher/QuickMedia-youtube.desktop | 2 | ||||
-rw-r--r-- | plugins/Matrix.hpp | 32 | ||||
-rw-r--r-- | src/QuickMedia.cpp | 167 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 261 |
9 files changed, 361 insertions, 123 deletions
@@ -76,14 +76,14 @@ See project.conf \[dependencies]. `curl` is required for network requests.\ `noto-fonts` and `noto-fonts-cjk` is required for latin and japanese characters. ### Optional -`mpv` is required for playing videos. This is not required if you dont plan on playing videos.\ -`youtube-dl` needs to be installed to play videos from youtube.\ +`mpv` needs to be installed to play videos.\ +`youtube-dl` needs to be installed to play youtube videos.\ `notify-send` needs to be installed to show notifications (on Linux and other systems that uses d-bus notification system).\ `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` or `--upscale-images-always` option.\ `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. +`ffmpeg (and ffprobe which is included in ffmpeg)` needs to be installed to upload videos with thumbnails on matrix. # Screenshots ## Youtube search ![](https://www.dec05eba.com/images/youtube.jpg) @@ -111,7 +111,7 @@ 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! -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. +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 (similar to chromium multithreading). 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. @@ -132,4 +132,8 @@ Add a notifications tab to show messages that mention us in all rooms (and then Disable message input in matrix when muted. Preview rooms? Instantly show messages posted by me until we receive our message from the server (use post response which includes event id). -Get user displayname, avatar, room name, etc updates from /sync and update them in gui.
\ No newline at end of file +Get user displayname, avatar, room name, etc updates from /sync and update them in gui. +Test if glScissor doesn't break loading of embedded body item. +Handle matrix token being invalidated while running. +Update upload limit if its updated on the server (can it be updated while the server is running?). +Apply search filter when updating rooms (including when switching from cache to server response sync data).
\ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index c043962..0641228 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -72,7 +72,7 @@ namespace QuickMedia { void image_continuous_page(MangaImagesPage *images_page); void image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body); void chat_login_page(); - void chat_page(MatrixChatPage *chat_page, RoomData *current_room); + void chat_page(MatrixChatPage *matrix_chat_page, RoomData *current_room); void after_matrix_login_page(); enum class LoadImageResult { @@ -109,6 +109,8 @@ namespace QuickMedia { const char *plugin_name = nullptr; sf::Texture plugin_logo; sf::Texture loading_icon; + sf::Sprite load_sprite; + sf::Clock load_sprite_timer; PageType current_page; std::stack<PageType> page_stack; int image_index; diff --git a/launcher/QuickMedia-4chan.desktop b/launcher/QuickMedia-4chan.desktop index 7ebe55f..5cd074a 100644 --- a/launcher/QuickMedia-4chan.desktop +++ b/launcher/QuickMedia-4chan.desktop @@ -6,4 +6,4 @@ Comment=4chan client with autoplay support and quick comment navigation Icon=/usr/share/quickmedia/icons/4chan_launcher.png Exec=QuickMedia 4chan Terminal=false -Keywords=4chan;quickmedia;mpv; +Keywords=4chan;quickmedia; diff --git a/launcher/QuickMedia-youtube-audio.desktop b/launcher/QuickMedia-youtube-audio.desktop index 0e2ea7c..21221de 100644 --- a/launcher/QuickMedia-youtube-audio.desktop +++ b/launcher/QuickMedia-youtube-audio.desktop @@ -6,4 +6,4 @@ Comment=YouTube search and audio playing Icon=/usr/share/quickmedia/icons/yt_launcher.png Exec=QuickMedia youtube --no-video Terminal=false -Keywords=youtube;player;quickmedia;mpv;audio;music; +Keywords=youtube;player;quickmedia;audio;music; diff --git a/launcher/QuickMedia-youtube.desktop b/launcher/QuickMedia-youtube.desktop index ba97727..fc781e6 100644 --- a/launcher/QuickMedia-youtube.desktop +++ b/launcher/QuickMedia-youtube.desktop @@ -6,4 +6,4 @@ Comment=YouTube search and video playing Icon=/usr/share/quickmedia/icons/yt_launcher.png Exec=QuickMedia youtube Terminal=false -Keywords=youtube;player;quickmedia;mpv;video;multimedia; +Keywords=youtube;player;quickmedia;video;multimedia; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 6a876d0..6af1bb8 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -189,6 +189,8 @@ namespace QuickMedia { virtual void remove_invite(const std::string &room_id) = 0; virtual void update(MatrixPageType page_type) { (void)page_type; } + + virtual void clear_data() = 0; }; class Matrix; @@ -212,6 +214,8 @@ namespace QuickMedia { void update(MatrixPageType page_type) override; + void clear_data() override; + Program *program; Matrix *matrix; MatrixRoomsPage *rooms_page; @@ -226,6 +230,7 @@ namespace QuickMedia { }; std::map<RoomData*, std::shared_ptr<BodyItem>> room_body_item_by_room; + std::mutex room_body_items_mutex; std::map<RoomData*, RoomMessagesData> pending_room_messages; std::mutex pending_room_messages_mutex; }; @@ -246,15 +251,18 @@ namespace QuickMedia { void set_current_chat_page(MatrixChatPage *chat_page); + void clear_data(); + MatrixQuickMedia *matrix_delegate = nullptr; private: std::mutex mutex; std::vector<std::shared_ptr<BodyItem>> room_body_items; std::vector<std::string> pending_remove_body_items; - Body *body; + Body *body = nullptr; std::string title; - MatrixRoomTagsPage *room_tags_page; - MatrixChatPage *current_chat_page; + MatrixRoomTagsPage *room_tags_page = nullptr; + MatrixChatPage *current_chat_page = nullptr; + bool clear_data_on_update = false; }; class MatrixRoomTagsPage : public Page { @@ -272,6 +280,8 @@ namespace QuickMedia { void set_current_rooms_page(MatrixRoomsPage *rooms_page); + void clear_data(); + MatrixQuickMedia *matrix_delegate = nullptr; private: struct TagData { @@ -285,6 +295,7 @@ namespace QuickMedia { std::map<std::string, std::vector<std::shared_ptr<BodyItem>>> add_room_body_items_by_tags; std::map<std::string, std::vector<std::shared_ptr<BodyItem>>> remove_room_body_items_by_tags; MatrixRoomsPage *current_rooms_page = nullptr; + bool clear_data_on_update = false; }; class MatrixInvitesPage : public Page { @@ -297,6 +308,8 @@ namespace QuickMedia { void update() override; void add_body_item(std::shared_ptr<BodyItem> body_item); void remove_body_item_by_room_id(const std::string &room_id); + + void clear_data(); private: Matrix *matrix; std::mutex mutex; @@ -305,6 +318,7 @@ namespace QuickMedia { Body *body; std::string title = "Invites (0)"; size_t prev_invite_count = 0; + bool clear_data_on_update = false; }; class MatrixInviteDetailsPage : public Page { @@ -352,6 +366,7 @@ namespace QuickMedia { const std::string room_id; MatrixQuickMedia *matrix_delegate = nullptr; MatrixRoomsPage *rooms_page = nullptr; + bool should_clear_data = false; }; class Matrix { @@ -359,6 +374,8 @@ namespace QuickMedia { void start_sync(MatrixDelegate *delegate); void stop_sync(); bool is_initial_sync_finished() const; + // Returns true if initial sync failed, and |err_msg| is set to the error reason in that case + bool did_initial_sync_fail(std::string &err_msg); void get_room_sync_data(RoomData *room, SyncData &sync_data); void get_all_synced_room_messages(RoomData *room, Messages &messages); @@ -380,7 +397,7 @@ namespace QuickMedia { // |message| is from |BodyItem.userdata| and is of type |Message*| PluginResult delete_message(RoomData *room, void *message, std::string &err_msg); - PluginResult load_and_verify_cached_session(); + PluginResult load_cached_session(); PluginResult on_start_typing(RoomData *room); PluginResult on_stop_typing(RoomData *room); @@ -426,23 +443,28 @@ namespace QuickMedia { void set_invite(const std::string &room_id, Invite invite); // Returns true if an invite for |room_id| exists bool remove_invite(const std::string &room_id); + void set_next_batch(std::string new_next_batch); + std::string get_next_batch(); + void clear_sync_cache_for_new_sync(); DownloadResult download_json(rapidjson::Document &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent = false, std::string *err_msg = nullptr) const; private: std::vector<std::unique_ptr<RoomData>> rooms; std::unordered_map<std::string, size_t> room_data_by_id; // value is an index into |rooms| std::recursive_mutex room_data_mutex; std::string user_id; - std::string username; std::string access_token; std::string homeserver; std::optional<int> upload_limit; std::string next_batch; + std::mutex next_batch_mutex; std::unordered_map<std::string, Invite> invites; std::mutex invite_mutex; std::thread sync_thread; bool sync_running = false; + bool sync_failed = false; + std::string sync_fail_reason; MatrixDelegate *delegate = nullptr; }; }
\ No newline at end of file diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index e6d7b9a..ad9cc7f 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -330,6 +330,9 @@ namespace QuickMedia { abort(); } loading_icon.setSmooth(true); + load_sprite.setTexture(loading_icon, true); + sf::Vector2u loading_icon_size = loading_icon.getSize(); + load_sprite.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); struct sigaction action; action.sa_handler = sigpipe_handler; @@ -589,23 +592,15 @@ namespace QuickMedia { if(matrix) { matrix->use_tor = use_tor; - TaskResult task_result = run_task_with_loading_screen([this]() { - return matrix->load_and_verify_cached_session() == PluginResult::OK; - }); - - if(task_result == TaskResult::TRUE) { + if(matrix->load_cached_session() == PluginResult::OK) { current_page = PageType::CHAT; - } else if(task_result == TaskResult::FALSE) { + } else { fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); current_page = PageType::CHAT_LOGIN; chat_login_page(); - } else { - exit(exit_code); } after_matrix_login_page(); - exit(exit_code); // Exit immediately without waiting for anything to finish - //matrix->stop_sync(); } return exit_code; @@ -1329,6 +1324,12 @@ namespace QuickMedia { window.draw(tab_associated_data[selected_tab].search_result_text); } + if(matrix && !matrix->is_initial_sync_finished()) { + load_sprite.setPosition(body_pos.x + body_size.x * 0.5f, body_pos.y + body_size.y * 0.5f); + load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); + window.draw(load_sprite); + } + window.display(); if(go_to_previous_page) { @@ -1516,13 +1517,8 @@ namespace QuickMedia { promise.set_value(callback()); }, std::move(result_promise), std::move(callback)); - sf::Sprite load_sprite(loading_icon); - sf::Vector2u loading_icon_size = loading_icon.getSize(); - load_sprite.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); - window_size.x = window.getSize().x; window_size.y = window.getSize().y; - sf::Clock timer; sf::Event event; while(window.isOpen()) { while(window.pollEvent(event)) { @@ -1555,7 +1551,7 @@ namespace QuickMedia { window.clear(back_color); load_sprite.setPosition(window_size.x * 0.5f, window_size.y * 0.5f); - load_sprite.setRotation(timer.getElapsedTime().asSeconds() * 400.0); + load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); window.draw(load_sprite); window.display(); } @@ -3073,7 +3069,7 @@ namespace QuickMedia { Message *message = nullptr; }; - void Program::chat_page(MatrixChatPage *chat_page, RoomData *current_room) { + void Program::chat_page(MatrixChatPage *matrix_chat_page, RoomData *current_room) { assert(strcmp(plugin_name, "matrix") == 0); auto video_page = std::make_unique<MatrixVideoPage>(this); @@ -3341,20 +3337,18 @@ namespace QuickMedia { }; AsyncTask<Messages> previous_messages_future; - bool fetching_previous_messages_running = false; RoomData *previous_messages_future_room = nullptr; //const int num_fetch_message_threads = 4; AsyncTask<std::shared_ptr<Message>> fetch_message_future; - bool fetching_message_running = false; RoomData *fetch_future_room = nullptr; BodyItem *fetch_body_item = nullptr; int fetch_message_tab = -1; // TODO: How about instead fetching all messages we have, not only the visible ones? also fetch with multiple threads. // TODO: Cancel when going to another room? - tabs[PINNED_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &fetch_message_future, &tabs, &find_body_item_by_event_id, &fetching_message_running, &fetch_future_room, &fetch_body_item, &fetch_message_tab](BodyItem *body_item) { - if(fetching_message_running || !current_room) + tabs[PINNED_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &fetch_message_future, &tabs, &find_body_item_by_event_id, &fetch_future_room, &fetch_body_item, &fetch_message_tab](BodyItem *body_item) { + if(fetch_message_future.valid() || !current_room) return; PinnedEventData *event_data = static_cast<PinnedEventData*>(body_item->userdata); @@ -3371,7 +3365,6 @@ namespace QuickMedia { return; } - fetching_message_running = true; std::string message_event_id = event_data->event_id; fetch_future_room = current_room; fetch_body_item = body_item; @@ -3385,8 +3378,8 @@ namespace QuickMedia { // TODO: How about instead fetching all messages we have, not only the visible ones? also fetch with multiple threads. // TODO: Cancel when going to another room? - tabs[MESSAGES_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &fetch_message_future, &tabs, &find_body_item_by_event_id, &fetching_message_running, &fetch_future_room, &fetch_body_item, &fetch_message_tab](BodyItem *body_item) { - if(fetching_message_running || !current_room) + tabs[MESSAGES_TAB_INDEX].body->body_item_render_callback = [this, ¤t_room, &fetch_message_future, &tabs, &find_body_item_by_event_id, &fetch_future_room, &fetch_body_item, &fetch_message_tab](BodyItem *body_item) { + if(fetch_message_future.valid() || !current_room) return; Message *message = static_cast<Message*>(body_item->userdata); @@ -3402,7 +3395,6 @@ namespace QuickMedia { return; } - fetching_message_running = true; std::string message_event_id = message->related_event_id; fetch_future_room = current_room; fetch_body_item = body_item; @@ -3565,6 +3557,28 @@ namespace QuickMedia { return false; }; + auto cleanup_tasks = [&set_read_marker_future, &previous_messages_future, &fetch_message_future, &typing_state_queue, &typing_state_thread, &post_task_queue, &post_thread, &unreferenced_event_by_room, &tabs]() { + set_read_marker_future.cancel(); + previous_messages_future.cancel(); + fetch_message_future.cancel(); + typing_state_queue.close(); + program_kill_in_thread(typing_state_thread.get_id()); + if(typing_state_thread.joinable()) + typing_state_thread.join(); + post_task_queue.close(); + program_kill_in_thread(post_thread.get_id()); + if(post_thread.joinable()) + post_thread.join(); + + unreferenced_event_by_room.clear(); + + for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) { + delete (PinnedEventData*)body_item->userdata; + } + + tabs.clear(); + }; + float tab_shade_height = 0.0f; bool frame_skip_text_entry = false; @@ -3600,9 +3614,8 @@ namespace QuickMedia { hit_top = false; break; } - if(hit_top && !fetching_previous_messages_running && selected_tab == MESSAGES_TAB_INDEX && current_room) { + if(hit_top && !previous_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX && current_room) { gradient_inc = 0; - fetching_previous_messages_running = true; previous_messages_future_room = current_room; previous_messages_future = [this, &previous_messages_future_room]() { Messages messages; @@ -3809,7 +3822,7 @@ namespace QuickMedia { } frame_skip_text_entry = false; - chat_page->update(); + matrix_chat_page->update(); switch(new_page) { case PageType::FILE_MANAGER: { @@ -3853,26 +3866,11 @@ namespace QuickMedia { break; } case PageType::CHAT_LOGIN: { - set_read_marker_future.cancel(); - previous_messages_future.cancel(); - fetch_message_future.cancel(); - typing_state_queue.close(); - program_kill_in_thread(typing_state_thread.get_id()); - if(typing_state_thread.joinable()) - typing_state_thread.join(); - post_task_queue.close(); - program_kill_in_thread(post_thread.get_id()); - if(post_thread.joinable()) - post_thread.join(); + cleanup_tasks(); new_page = PageType::CHAT; matrix->stop_sync(); matrix->logout(); - for(ChatTab &tab : tabs) { - tab.body->clear_cache(); - } // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. - // This doesn't currently work because at the end of this function there are futures that need to wait - // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow. //delete current_plugin; //current_plugin = new Matrix(); current_page = PageType::CHAT_LOGIN; @@ -3974,7 +3972,7 @@ namespace QuickMedia { setting_read_marker = false; } - if(fetching_previous_messages_running && previous_messages_future.ready()) { + if(previous_messages_future.ready()) { Messages new_messages = previous_messages_future.get(); fprintf(stderr, "Finished fetching older messages, num new messages: %zu\n", new_messages.size()); // Ignore finished fetch of messages if it happened in another room. When we navigate back to the room we will get the messages again @@ -3992,10 +3990,9 @@ namespace QuickMedia { modify_related_messages_in_current_room(new_messages); resolve_unreferenced_events_with_body_items(tabs[MESSAGES_TAB_INDEX].body->items.data(), num_new_body_items); } - fetching_previous_messages_running = false; } - if(fetching_message_running && fetch_message_future.ready()) { + if(fetch_message_future.ready()) { std::shared_ptr<Message> message = fetch_message_future.get(); fprintf(stderr, "Finished fetching message: %s\n", message ? message->event_id.c_str() : "(null)"); // Ignore finished fetch of messages if it happened in another room. When we navigate back to the room we will get the messages again @@ -4020,7 +4017,6 @@ namespace QuickMedia { } } } - fetching_message_running = false; fetch_message_tab = -1; } @@ -4079,7 +4075,7 @@ namespace QuickMedia { } // TODO: Have one for each room. Also add bottom one? for fetching new messages (currently not implemented, is it needed?) - if(fetching_previous_messages_running && selected_tab == MESSAGES_TAB_INDEX) { + if(previous_messages_future.valid() && selected_tab == MESSAGES_TAB_INDEX) { double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; gradient_inc += (frame_time_ms * 0.5); sf::Color top_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); @@ -4135,7 +4131,7 @@ namespace QuickMedia { } } - if(selected_tab == MESSAGES_TAB_INDEX && current_room) { + if(selected_tab == MESSAGES_TAB_INDEX && current_room && matrix->is_initial_sync_finished()) { BodyItem *last_visible_item = tabs[selected_tab].body->get_last_fully_visible_item(); if(is_window_focused && chat_state != ChatState::URL_SELECTION && current_room && last_visible_item && !setting_read_marker && read_marker_timer.getElapsedTime().asMilliseconds() >= read_marker_timeout_ms) { Message *message = (Message*)last_visible_item->userdata; @@ -4159,8 +4155,31 @@ namespace QuickMedia { window.draw(logo_sprite); } + if(matrix && !matrix->is_initial_sync_finished()) { + load_sprite.setPosition(body_pos.x + body_size.x * 0.5f, body_pos.y + body_size.y * 0.5f); + load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); + window.draw(load_sprite); + } + window.display(); + if(matrix_chat_page->should_clear_data) { + matrix_chat_page->should_clear_data = false; + cleanup_tasks(); + + while(!matrix->is_initial_sync_finished()) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + current_room = matrix->get_room_by_id(current_room->id); + if(current_room) { + chat_page(matrix_chat_page, current_room); + return; + } else { + go_to_previous_page = true; + } + } + if(go_to_previous_page) { go_to_previous_page = false; goto chat_page_end; @@ -4168,21 +4187,7 @@ namespace QuickMedia { } chat_page_end: - set_read_marker_future.cancel(); - previous_messages_future.cancel(); - fetch_message_future.cancel(); - typing_state_queue.close(); - program_kill_in_thread(typing_state_thread.get_id()); - if(typing_state_thread.joinable()) - typing_state_thread.join(); - post_task_queue.close(); - program_kill_in_thread(post_thread.get_id()); - if(post_thread.joinable()) - post_thread.join(); - - for(auto &body_item : tabs[PINNED_TAB_INDEX].body->items) { - delete (PinnedEventData*)body_item->userdata; - } + cleanup_tasks(); } void Program::after_matrix_login_page() { @@ -4209,13 +4214,27 @@ namespace QuickMedia { tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); tabs.push_back(Tab{std::move(invites_body), std::move(matrix_invites_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - sf::Sprite load_sprite(loading_icon); - sf::Vector2u loading_icon_size = loading_icon.getSize(); - load_sprite.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); - - sf::Clock timer; sf::Event event; - while(window.isOpen() && !matrix->is_initial_sync_finished()) { + std::string sync_err_msg; +#if 0 + while(window.isOpen()) { + if(matrix->is_initial_sync_finished()) + break; + else if(matrix->did_initial_sync_fail(sync_err_msg)) { + show_notification("QuickMedia", "Initial sync failed, reason: " + sync_err_msg, Urgency::CRITICAL); + matrix->stop_sync(); + matrix->logout(); + // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. + //delete current_plugin; + //current_plugin = new Matrix(); + current_page = PageType::CHAT_LOGIN; + chat_login_page(); + if(current_page == PageType::CHAT) + after_matrix_login_page(); + exit(0); + break; + } + while(window.pollEvent(event)) { if(event.type == sf::Event::Closed) window.close(); @@ -4228,15 +4247,17 @@ namespace QuickMedia { window.close(); } } + window.clear(back_color); load_sprite.setPosition(window_size.x * 0.5f, window_size.y * 0.5f); - load_sprite.setRotation(timer.getElapsedTime().asSeconds() * 400.0); + load_sprite.setRotation(load_sprite_timer.getElapsedTime().asSeconds() * 400.0); window.draw(load_sprite); window.display(); } - +#endif while(window.isOpen()) { page_loop(tabs); } + matrix->stop_sync(); } } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 22c2447..de33c3c 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -7,6 +7,8 @@ #include <rapidjson/document.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> +#include <rapidjson/filereadstream.h> +#include <rapidjson/filewritestream.h> #include <fcntl.h> #include <unistd.h> #include "../../include/QuickMedia.hpp" @@ -230,6 +232,7 @@ namespace QuickMedia { } void MatrixQuickMedia::join_room(RoomData *room) { + std::lock_guard<std::mutex> lock(room_body_items_mutex); std::string room_name = room->get_name(); if(room_name.empty()) room_name = room->id; @@ -246,6 +249,7 @@ namespace QuickMedia { } void MatrixQuickMedia::leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) { + std::lock_guard<std::mutex> lock(room_body_items_mutex); room_body_item_by_room.erase(room); rooms_page->remove_body_item_by_room_id(room->id); room_tags_page->remove_body_item_by_room_id(room->id); @@ -254,10 +258,12 @@ namespace QuickMedia { } void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) { + std::lock_guard<std::mutex> lock(room_body_items_mutex); room_tags_page->add_room_body_item_to_tag(room_body_item_by_room[room], tag); } void MatrixQuickMedia::room_remove_tag(RoomData *room, const std::string &tag) { + std::lock_guard<std::mutex> lock(room_body_items_mutex); room_tags_page->remove_room_body_item_from_tag(room_body_item_by_room[room], tag); } @@ -317,6 +323,16 @@ namespace QuickMedia { update_pending_room_messages(page_type); } + void MatrixQuickMedia::clear_data() { + std::lock_guard<std::mutex> lock(pending_room_messages_mutex); + std::lock_guard<std::mutex> room_body_lock(room_body_items_mutex); + room_body_item_by_room.clear(); + pending_room_messages.clear(); + rooms_page->clear_data(); + room_tags_page->clear_data(); + invites_page->clear_data(); + } + void MatrixQuickMedia::update_pending_room_messages(MatrixPageType page_type) { std::lock_guard<std::mutex> lock(pending_room_messages_mutex); bool is_window_focused = program->is_window_focused(); @@ -405,6 +421,11 @@ namespace QuickMedia { void MatrixRoomsPage::update() { { std::lock_guard<std::mutex> lock(mutex); + if(clear_data_on_update) { + clear_data_on_update = false; + body->clear_items(); + } + for(const std::string &room_id : pending_remove_body_items) { remove_body_item_by_url(body->items, room_id); // TODO: There can be a race condition where current_chat_page is set after entering a room and then we will enter a room we left @@ -414,6 +435,7 @@ namespace QuickMedia { current_chat_page = nullptr; } } + pending_remove_body_items.clear(); body->clamp_selection(); body->append_items(std::move(room_body_items)); @@ -459,6 +481,15 @@ namespace QuickMedia { current_chat_page = chat_page; } + void MatrixRoomsPage::clear_data() { + std::lock_guard<std::mutex> lock(mutex); + room_body_items.clear(); + pending_remove_body_items.clear(); + clear_data_on_update = true; + if(current_chat_page) + current_chat_page->should_clear_data = true; + } + PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { (void)title; std::lock_guard<std::recursive_mutex> lock(mutex); @@ -477,6 +508,11 @@ namespace QuickMedia { void MatrixRoomTagsPage::update() { { std::lock_guard<std::recursive_mutex> lock(mutex); + if(clear_data_on_update) { + clear_data_on_update = false; + body->clear_items(); + } + for(auto &it : remove_room_body_items_by_tags) { auto tag_body_it = tag_body_items_by_name.find(it.first); if(tag_body_it == tag_body_items_by_name.end()) @@ -558,6 +594,16 @@ namespace QuickMedia { current_rooms_page = rooms_page; } + void MatrixRoomTagsPage::clear_data() { + std::lock_guard<std::recursive_mutex> lock(mutex); + tag_body_items_by_name.clear(); + add_room_body_items_by_tags.clear(); + remove_room_body_items_by_tags.clear(); + clear_data_on_update = true; + if(current_rooms_page) + current_rooms_page->clear_data(); + } + MatrixInvitesPage::MatrixInvitesPage(Program *program, Matrix *matrix, Body *body) : Page(program), matrix(matrix), body(body) { } @@ -597,6 +643,11 @@ namespace QuickMedia { void MatrixInvitesPage::update() { std::lock_guard<std::mutex> lock(mutex); + if(clear_data_on_update) { + clear_data_on_update = false; + body->clear_items(); + } + for(const std::string &room_id : pending_remove_body_items) { remove_body_item_by_url(body->items, room_id); } @@ -621,6 +672,15 @@ namespace QuickMedia { pending_remove_body_items.push_back(room_id); } + void MatrixInvitesPage::clear_data() { + std::lock_guard<std::mutex> lock(mutex); + body_items.clear(); + pending_remove_body_items.clear(); + title = "Invites (0)"; + prev_invite_count = 0; + clear_data_on_update = true; + } + MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page) : Page(program), room_id(std::move(room_id)), rooms_page(rooms_page) { if(rooms_page) rooms_page->set_current_chat_page(this); @@ -637,6 +697,66 @@ namespace QuickMedia { rooms_page->update(); } + static std::array<const char*, 7> sync_fail_error_codes = { + "M_FORBIDDEN", + "M_UNKNOWN_TOKEN", + "M_MISSING_TOKEN", + "M_UNAUTHORIZED", + "M_USER_DEACTIVATED", + "M_CAPTCHA_NEEDED", + "M_MISSING_PARAM" + }; + + static void remove_empty_fields_in_sync_rooms_response(rapidjson::Value &rooms_json) { + for(const char *member_name : {"join", "invite", "leave"}) { + auto join_it = rooms_json.FindMember(member_name); + if(join_it != rooms_json.MemberEnd() && join_it->value.IsObject() && join_it->value.MemberCount() == 0) + rooms_json.RemoveMember(join_it); + } + } + + static void remove_ephemeral_field_in_sync_rooms_response(rapidjson::Value &rooms_json) { + auto join_it = rooms_json.FindMember("join"); + if(join_it == rooms_json.MemberEnd() || !join_it->value.IsObject()) + return; + + for(auto &it : join_it->value.GetObject()) { + if(!it.value.IsObject()) + continue; + it.value.RemoveMember("ephemeral"); + } + } + + static void remove_empty_fields_in_sync_account_data_response(rapidjson::Value &account_data_json) { + for(const char *member_name : {"events"}) { + auto join_it = account_data_json.FindMember(member_name); + if(join_it != account_data_json.MemberEnd() && join_it->value.IsObject() && join_it->value.MemberCount() == 0) + account_data_json.RemoveMember(join_it); + } + } + + static void remove_unused_sync_data_fields(rapidjson::Value &json_root) { + for(auto it = json_root.MemberBegin(); it != json_root.MemberEnd();) { + if(strcmp(it->name.GetString(), "account_data") == 0 && it->value.IsObject()) { + remove_empty_fields_in_sync_account_data_response(it->value); + if(it->value.MemberCount() == 0) + it = json_root.RemoveMember(it); + else + ++it; + } else if(strcmp(it->name.GetString(), "rooms") == 0 && it->value.IsObject()) { + // TODO: Call this, but dont remove our read marker (needed for notifications on mentions for example). Or maybe we can get it from "account_data"? + //remove_ephemeral_field_in_sync_rooms_response(rooms_it->value); + remove_empty_fields_in_sync_rooms_response(it->value); + if(it->value.MemberCount() == 0) + it = json_root.RemoveMember(it); + else + ++it; + } else { + it = json_root.EraseMember(it); + } + } + } + void Matrix::start_sync(MatrixDelegate *delegate) { if(sync_running) return; @@ -644,10 +764,31 @@ namespace QuickMedia { assert(!this->delegate); this->delegate = delegate; + Path matrix_cache_dir = get_cache_dir().join("matrix"); + if(create_directory_recursive(matrix_cache_dir) != 0) + fprintf(stderr, "Failed to create matrix cache directory\n"); + matrix_cache_dir.join("sync_data.json"); + sync_running = true; - sync_thread = std::thread([this, delegate]() { + sync_thread = std::thread([this, delegate, matrix_cache_dir]() { + FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), "rb"); + if(sync_cache_file) { + rapidjson::Document doc; + char read_buffer[8192]; + rapidjson::FileReadStream is(sync_cache_file, read_buffer, sizeof(read_buffer)); + while(true) { + rapidjson::ParseResult parse_result = doc.ParseStream<rapidjson::kParseStopWhenDoneFlag>(is); + if(parse_result.IsError()) + break; + if(parse_sync_response(doc, delegate) != PluginResult::OK) + fprintf(stderr, "Failed to parse cached sync response\n"); + } + fclose(sync_cache_file); + } + const rapidjson::Value *next_batch_json; PluginResult result; + bool initial_sync = true; while(sync_running) { std::vector<CommandArg> additional_args = { { "-H", "Authorization: Bearer " + access_token }, @@ -661,12 +802,32 @@ namespace QuickMedia { snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); rapidjson::Document json_root; - DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + std::string err_msg; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) { - fprintf(stderr, "Fetch response failed\n"); + fprintf(stderr, "/sync failed\n"); + if(initial_sync && json_root.IsObject()) { + const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); + if(errcode_json.IsString()) { + for(const char *sync_fail_error_code : sync_fail_error_codes) { + if(strcmp(errcode_json.GetString(), sync_fail_error_code) == 0) { + sync_fail_reason = sync_fail_error_code; + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) + sync_fail_reason = error_json.GetString(); + sync_failed = true; + sync_running = false; + break; + } + } + } + } goto sync_end; } + if(next_batch.empty()) + clear_sync_cache_for_new_sync(); + result = parse_sync_response(json_root, delegate); if(result != PluginResult::OK) { fprintf(stderr, "Failed to parse sync response\n"); @@ -675,15 +836,31 @@ namespace QuickMedia { next_batch_json = &GetMember(json_root, "next_batch"); if(next_batch_json->IsString()) { - next_batch = next_batch_json->GetString(); + set_next_batch(next_batch_json->GetString()); fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); } else { + set_next_batch("Invalid"); fprintf(stderr, "Matrix: missing next batch\n"); } sync_end: if(sync_running) std::this_thread::sleep_for(std::chrono::seconds(1)); + + if(!json_root.IsObject()) + continue; + + // TODO: Circulate file + FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), initial_sync ? "wb" : "ab"); + initial_sync = false; + if(sync_cache_file) { + char buffer[4096]; + rapidjson::FileWriteStream file_write_stream(sync_cache_file, buffer, sizeof(buffer)); + rapidjson::Writer<rapidjson::FileWriteStream> writer(file_write_stream); + remove_unused_sync_data_fields(json_root); + json_root.Accept(writer); + fclose(sync_cache_file); + } } }); } @@ -694,12 +871,23 @@ namespace QuickMedia { if(sync_thread.joinable()) sync_thread.join(); delegate = nullptr; + sync_failed = false; + sync_fail_reason.clear(); } bool Matrix::is_initial_sync_finished() const { return !next_batch.empty(); } + bool Matrix::did_initial_sync_fail(std::string &err_msg) { + if(sync_failed) { + err_msg = sync_fail_reason; + return true; + } else { + return false; + } + } + void Matrix::get_room_sync_data(RoomData *room, SyncData &sync_data) { room->acquire_room_lock(); auto &room_messages = room->get_messages_thread_unsafe(); @@ -1692,7 +1880,7 @@ namespace QuickMedia { if(from.empty()) { fprintf(stderr, "Info: missing previous batch for room: %s, using /sync next batch\n", room_data->id.c_str()); // TODO: When caching /sync, remember to add lock around getting next_batch! - from = next_batch; + from = get_next_batch(); if(from.empty()) { fprintf(stderr, "Error: missing next batch!\n"); return PluginResult::OK; @@ -2105,22 +2293,13 @@ namespace QuickMedia { auto fetched_message_it = room->fetched_messages_by_event_id.find(event_id); if(fetched_message_it != room->fetched_messages_by_event_id.end()) return fetched_message_it->second; - - rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); - - rapidjson::StringBuffer buffer; - rapidjson::Writer<rapidjson::StringBuffer> writer(buffer); - request_data.Accept(writer); std::vector<CommandArg> additional_args = { { "-H", "Authorization: Bearer " + access_token } }; - std::string filter = url_param_encode(buffer.GetString()); - char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/event/%s", homeserver.c_str(), room->id.c_str(), event_id.c_str()); std::string err_msg; rapidjson::Document json_root; @@ -2136,8 +2315,7 @@ namespace QuickMedia { } } - const rapidjson::Value &event_json = GetMember(json_root, "event"); - std::shared_ptr<Message> new_message = parse_message_event(event_json, room); + std::shared_ptr<Message> new_message = parse_message_event(json_root, room); room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); return new_message; } @@ -2282,6 +2460,9 @@ namespace QuickMedia { DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/login", std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + Path matrix_sync_data_path = get_cache_dir().join("matrix").join("sync_data.json"); + remove(matrix_sync_data_path.data.c_str()); + if(!json_root.IsObject()) { err_msg = "Failed to parse matrix login response"; return PluginResult::ERR; @@ -2310,7 +2491,6 @@ namespace QuickMedia { json_root.AddMember("homeserver", rapidjson::StringRef(homeserver.c_str()), request_data.GetAllocator()); this->user_id = user_id_json.GetString(); - this->username = extract_user_name_from_user_id(this->user_id); this->access_token = access_token_json.GetString(); this->homeserver = homeserver; @@ -2330,6 +2510,7 @@ namespace QuickMedia { } PluginResult Matrix::logout() { + assert(!sync_running); Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); remove(session_path.data.c_str()); @@ -2345,11 +2526,10 @@ namespace QuickMedia { rooms.clear(); room_data_by_id.clear(); user_id.clear(); - username.clear(); access_token.clear(); homeserver.clear(); upload_limit.reset(); - next_batch.clear(); + set_next_batch(""); invites.clear(); return PluginResult::OK; @@ -2403,7 +2583,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::load_and_verify_cached_session() { + PluginResult Matrix::load_cached_session() { Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); std::string session_json_content; if(file_get_content(session_path, session_json_content) != 0) { @@ -2443,20 +2623,7 @@ namespace QuickMedia { std::string access_token = access_token_json.GetString(); std::string homeserver = homeserver_json.GetString(); - std::vector<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token } - }; - - std::string server_response; - // We want to make any request to the server that can verify that our token is still valid, doesn't matter which call - DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/account/whoami", server_response, std::move(additional_args), use_tor, true); - if(download_result != DownloadResult::OK) { - fprintf(stderr, "Matrix whoami response: %s\n", server_response.c_str()); - return download_result_to_plugin_result(download_result); - } - this->user_id = std::move(user_id); - this->username = extract_user_name_from_user_id(this->user_id); this->access_token = std::move(access_token); this->homeserver = std::move(homeserver); return PluginResult::OK; @@ -2582,7 +2749,7 @@ namespace QuickMedia { DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/leave", server_response, std::move(additional_args), use_tor, true); if(download_result == DownloadResult::OK) { RoomData *room = get_room_by_id(room_id); - if(!room) { + if(room) { delegate->leave_room(room, LeaveType::LEAVE, ""); remove_room(room_id); } @@ -2678,9 +2845,31 @@ namespace QuickMedia { return false; } + void Matrix::set_next_batch(std::string new_next_batch) { + std::lock_guard<std::mutex> lock(next_batch_mutex); + next_batch = std::move(new_next_batch); + } + + std::string Matrix::get_next_batch() { + std::lock_guard<std::mutex> lock(next_batch_mutex); + return next_batch; + } + + void Matrix::clear_sync_cache_for_new_sync() { + std::lock_guard<std::recursive_mutex> room_data_lock(room_data_mutex); + std::lock_guard<std::mutex> invites_lock(invite_mutex); + for(auto &room : rooms) { + room->clear_data(); + } + // We intentionally dont clear |rooms| here because we want the objects inside it to still be valid. TODO: Clear |rooms| here + room_data_by_id.clear(); + invites.clear(); + delegate->clear_data(); + } + DownloadResult Matrix::download_json(rapidjson::Document &result, const std::string &url, std::vector<CommandArg> additional_args, bool use_browser_useragent, std::string *err_msg) const { if(download_to_json(url, result, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { - // Cant get error since we parse directory to json. TODO: Make this work somehow? + // Cant get error since we parse directly to json. TODO: Make this work somehow? if(err_msg) *err_msg = "Failed to download/parse json"; return DownloadResult::NET_ERR; |