From 276395f468c8ee05951401a1d352e0dec3c3a3a8 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 1 Apr 2021 17:59:36 +0200 Subject: Matrix: add room directory for joining rooms, resize video thumbnail --- TODO | 3 +- include/FileAnalyzer.hpp | 2 +- plugins/Matrix.hpp | 35 ++++++++- plugins/Page.hpp | 2 + src/FileAnalyzer.cpp | 5 +- src/QuickMedia.cpp | 35 +++++---- src/plugins/Matrix.cpp | 184 +++++++++++++++++++++++++++++++++++++++++------ 7 files changed, 225 insertions(+), 41 deletions(-) diff --git a/TODO b/TODO index 0d19b85..cb64535 100644 --- a/TODO +++ b/TODO @@ -159,4 +159,5 @@ Check what happens with xsrf_token if comments are not fetched for a long time. Add support for comments in live youtube videos, api is at: https://www.youtube.com/youtubei/v1/live_chat/get_live_chat?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8. Make video visible when reading comments (youtube). Convert nyaa.si/spotify/soundcloud date from ISO date string to local time. -When ui is scaled then the predicated thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested. \ No newline at end of file +When ui is scaled then the predicated thumbnail size will be wrong since its scaled in Body but not in the plugins where they are requested. +Check if get_page handlers in pages need to check if next batch is valid. If the server returns empty next batch we shouldn't fetch the first page... \ No newline at end of file diff --git a/include/FileAnalyzer.hpp b/include/FileAnalyzer.hpp index be0cc25..92bd042 100644 --- a/include/FileAnalyzer.hpp +++ b/include/FileAnalyzer.hpp @@ -34,7 +34,7 @@ namespace QuickMedia { bool is_content_type_image(ContentType content_type); const char* content_type_to_string(ContentType content_type); - bool video_get_first_frame(const char *filepath, const char *destination_path); + bool video_get_first_frame(const char *filepath, const char *destination_path, int width, int height); class FileAnalyzer { public: diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index b240cb8..eea7f9b 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -427,7 +427,7 @@ namespace QuickMedia { class MatrixChatPage : public Page { public: MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page); - ~MatrixChatPage() override; + ~MatrixChatPage(); const char* get_title() const override { return ""; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override { @@ -445,6 +445,33 @@ namespace QuickMedia { bool should_clear_data = false; }; + class MatrixRoomDirectoryPage : public Page { + public: + MatrixRoomDirectoryPage(Program *program, Matrix *matrix) : Page(program), matrix(matrix) {} + const char* get_title() const override { return "Room directory"; } + bool allow_submit_no_selection() const override { return true; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + private: + Matrix *matrix; + }; + + class MatrixServerRoomListPage : public LazyFetchPage { + public: + MatrixServerRoomListPage(Program *program, Matrix *matrix, const std::string &server_name) : LazyFetchPage(program), matrix(matrix), server_name(server_name), current_page(0) {} + const char* get_title() const override { return "Select a room to join"; } + bool search_is_filter() override { return false; } + PluginResult lazy_fetch(BodyItems &result_items) override; + PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + private: + Matrix *matrix; + const std::string server_name; + std::string next_batch; + std::string search_term; + int current_page; + }; + class Matrix { public: void start_sync(MatrixDelegate *delegate, bool &cached); @@ -486,6 +513,9 @@ namespace QuickMedia { PluginResult join_room(const std::string &room_id); PluginResult leave_room(const std::string &room_id); + // If |since| is empty, then the first page is fetched + PluginResult get_public_rooms(const std::string &server, const std::string &search_term, const std::string &since, BodyItems &rooms, std::string &next_batch); + // |message| is from |BodyItem.userdata| and is of type |Message*| bool was_message_posted_by_me(void *message); @@ -496,6 +526,8 @@ namespace QuickMedia { std::shared_ptr get_me(RoomData *room); + const std::string& get_homeserver_domain() const; + // Returns nullptr if message cant be found. Note: cached std::shared_ptr get_message_by_id(RoomData *room, const std::string &event_id); @@ -541,6 +573,7 @@ namespace QuickMedia { std::string my_user_id; std::string access_token; std::string homeserver; + std::string homeserver_domain; std::optional upload_limit; std::string next_batch; std::mutex next_batch_mutex; diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 8ef2b59..b5af7a9 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -47,6 +47,8 @@ namespace QuickMedia { virtual bool is_single_page() const { return false; } virtual bool is_trackable() const { return false; } virtual bool is_lazy_fetch_page() const { return false; } + // Note: If submit is done without any selection, then the search term is sent as the |title|, not |url| + virtual bool allow_submit_no_selection() const { return false; } // This is called both when first navigating to page and when going back to page virtual void on_navigate_to_page(Body *body) { (void)body; } diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp index b397def..adfb7cc 100644 --- a/src/FileAnalyzer.cpp +++ b/src/FileAnalyzer.cpp @@ -87,8 +87,9 @@ namespace QuickMedia { return 0; } - bool video_get_first_frame(const char *filepath, const char *destination_path) { - const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", destination_path, nullptr }; + bool video_get_first_frame(const char *filepath, const char *destination_path, int width, int height) { + std::string thumbnail_size = std::to_string(width) + "x" + std::to_string(height); + const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", filepath, "-vframes", "1", "-f", "singlejpeg", "-s", thumbnail_size.c_str(), destination_path, nullptr }; std::string ffmpeg_result; if(exec_program(program_args, nullptr, nullptr) != 0) { fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n"); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 95eb47a..92f9309 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1096,16 +1096,16 @@ namespace QuickMedia { window_size.x = window_size_u.x; window_size.y = window_size_u.y; - std::function submit_handler; + std::function submit_handler; - submit_handler = [this, &submit_handler, &after_submit_handler, &json_chapters, &tabs, &tab_associated_data, &selected_tab, &loop_running, &redraw]() { + submit_handler = [this, &submit_handler, &after_submit_handler, &json_chapters, &tabs, &tab_associated_data, &selected_tab, &loop_running, &redraw](const std::string &search_text) { auto selected_item = tabs[selected_tab].body->get_selected_shared(); - if(!selected_item) + if(!selected_item && !tabs[selected_tab].page->allow_submit_no_selection()) return; std::vector new_tabs; tabs[selected_tab].page->submit_body_item = selected_item; - PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs); + PluginResult submit_result = tabs[selected_tab].page->submit(selected_item ? selected_item->get_title() : search_text, selected_item ? selected_item->url : "", new_tabs); if(submit_result != PluginResult::OK) { // TODO: Show the exact cause of error (get error message from curl). show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL); @@ -1147,7 +1147,9 @@ namespace QuickMedia { hide_virtual_keyboard(); - if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) { + if(tabs[selected_tab].page->allow_submit_no_selection()) { + page_loop(new_tabs, 0, after_submit_handler); + } else if(new_tabs.size() == 1 && new_tabs[0].page->get_type() == PageTypez::MANGA_IMAGES) { select_episode(selected_item.get(), 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 @@ -1205,7 +1207,7 @@ namespace QuickMedia { } tabs[selected_tab].body->body_item_select_callback = [&submit_handler](BodyItem *body_item) { - submit_handler(); + submit_handler(body_item->get_title()); }; //select_body_item_by_room(tabs[selected_tab].body.get(), current_chat_room); current_chat_room = nullptr; @@ -1231,7 +1233,7 @@ namespace QuickMedia { for(size_t i = 0; i < tabs.size(); ++i) { Tab &tab = tabs[i]; tab.body->body_item_select_callback = [&submit_handler](BodyItem *body_item) { - submit_handler(); + submit_handler(body_item->get_title()); }; TabAssociatedData &associated_data = tab_associated_data[i]; @@ -1255,10 +1257,10 @@ namespace QuickMedia { associated_data.typing = false; }; - tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string&) { + tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string &search_text) { if(associated_data.typing) return; - submit_handler(); + submit_handler(search_text); }; } @@ -1355,7 +1357,10 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::Tab) { if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete(); } else if(event.key.code == sf::Keyboard::Enter) { - if(!tabs[selected_tab].search_bar) submit_handler(); + if(!tabs[selected_tab].search_bar) { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + submit_handler(selected_item ? selected_item->get_title() : ""); + } } else if(event.key.code == sf::Keyboard::T && event.key.control) { BodyItem *selected_item = tabs[selected_tab].body->get_selected(); if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { @@ -5132,11 +5137,10 @@ namespace QuickMedia { auto matrix_invites_page_search_bar = create_search_bar("Search...", SEARCH_DELAY_FILTER); auto matrix_invites_page = std::make_unique(this, matrix, invites_body.get(), matrix_invites_page_search_bar.get()); - //Tab options_tab = create_menu_selection_tab("Options", - //{ - // { "Notifications", nullptr, 0, [](Program *program) { return std::make_unique(program); } }, - // { "Room directory", "Search for search on...", SEARCH_DELAY_FILTER, [](Program *program) { return std::make_unique(program); } } - //}); + auto room_directory_body = create_body(); + room_directory_body->items.push_back(BodyItem::create(matrix->get_homeserver_domain())); + room_directory_body->items.push_back(BodyItem::create("matrix.org")); + auto matrix_room_directory_page = std::make_unique(this, matrix); MatrixQuickMedia matrix_handler(this, matrix, matrix_rooms_page.get(), matrix_rooms_tag_page.get(), matrix_invites_page.get()); bool sync_cached = false; @@ -5147,6 +5151,7 @@ namespace QuickMedia { tabs.push_back(Tab{std::move(rooms_body), std::move(matrix_rooms_page), std::move(matrix_rooms_page_search_bar)}); tabs.push_back(Tab{std::move(rooms_tags_body), std::move(matrix_rooms_tag_page), std::move(matrix_rooms_tage_page_search_bar)}); tabs.push_back(Tab{std::move(invites_body), std::move(matrix_invites_page), std::move(matrix_invites_page_search_bar)}); + tabs.push_back(Tab{std::move(room_directory_body), std::move(matrix_room_directory_page), create_search_bar("Server to search on...", SEARCH_DELAY_FILTER)}); while(window.isOpen()) { page_loop(tabs); diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 128e610..5d942ed 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -18,7 +18,7 @@ #include #include "../../include/QuickMedia.hpp" -// TODO: Update avatar/display name when its changed in the room/globally. +// TODO: Use string assign with string length instead of assigning to c string (which calls strlen) // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. @@ -72,6 +72,8 @@ static std::string extract_first_line_elipses(const std::string &str, size_t max } namespace QuickMedia { + static const sf::Vector2i thumbnail_max_size(600, 337); + static void remove_body_item_by_url(BodyItems &body_items, const std::string &url) { for(auto it = body_items.begin(); it != body_items.end();) { if((*it)->url == url) @@ -980,6 +982,46 @@ namespace QuickMedia { rooms_page->update(); } + PluginResult MatrixRoomDirectoryPage::submit(const std::string &title, const std::string&, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, matrix, title), create_search_bar("Search...", 350)}); + return PluginResult::OK; + } + + PluginResult MatrixServerRoomListPage::lazy_fetch(BodyItems &result_items) { + return matrix->get_public_rooms(server_name, search_term, next_batch, result_items, next_batch); + } + + PluginResult MatrixServerRoomListPage::get_page(const std::string&, int page, BodyItems &result_items) { + while(current_page < page && !next_batch.empty()) { + PluginResult plugin_result = lazy_fetch(result_items); + if(plugin_result != PluginResult::OK) return plugin_result; + ++current_page; + } + return PluginResult::OK; + } + + SearchResult MatrixServerRoomListPage::search(const std::string &str, BodyItems &result_items) { + next_batch.clear(); + current_page = 0; + search_term = str; + return plugin_result_to_search_result(lazy_fetch(result_items)); + } + + PluginResult MatrixServerRoomListPage::submit(const std::string &title, const std::string &url, std::vector&) { + TaskResult task_result = program->run_task_with_loading_screen([this, url]() { + return matrix->join_room(url) == PluginResult::OK; + }); + + if(task_result == TaskResult::TRUE) { + show_notification("QuickMedia", "You joined " + title, Urgency::NORMAL); + program->set_go_to_previous_page(); + } else if(task_result == TaskResult::FALSE) { + show_notification("QuickMedia", "Failed to join " + title, Urgency::CRITICAL); + } + + return PluginResult::OK; + } + static std::array sync_fail_error_codes = { "M_FORBIDDEN", "M_UNKNOWN_TOKEN", @@ -998,18 +1040,6 @@ namespace QuickMedia { } } - 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); @@ -2416,6 +2446,7 @@ namespace QuickMedia { std::string room_id_str(room_id.GetString(), room_id.GetStringLength()); if(set_invite(room_id_str, invite)) delegate->add_invite(room_id_str, std::move(invite)); + break; } } @@ -3168,7 +3199,7 @@ namespace QuickMedia { char tmp_filename[] = "/tmp/quickmedia_video_frame_XXXXXX"; int tmp_file = mkstemp(tmp_filename); if(tmp_file != -1) { - if(video_get_first_frame(filepath.c_str(), tmp_filename)) { + if(video_get_first_frame(filepath.c_str(), 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_info, upload_info_ignored, err_msg, false); if(upload_thumbnail_result != PluginResult::OK) { @@ -3189,7 +3220,7 @@ namespace QuickMedia { int tmp_file = mkstemp(tmp_filename); if(tmp_file != -1) { std::string thumbnail_path; - if(create_thumbnail(filepath, tmp_filename, sf::Vector2i(600, 337))) + if(create_thumbnail(filepath, tmp_filename, thumbnail_max_size)) thumbnail_path = tmp_filename; else thumbnail_path = filepath; @@ -3306,6 +3337,10 @@ namespace QuickMedia { return PluginResult::ERR; } + const rapidjson::Value &home_server_json = GetMember(json_root, "home_server"); + if(home_server_json.IsString()) + this->homeserver_domain = home_server_json.GetString(); + // Use the user-provided homeserver instead of the one the server tells us about, otherwise this wont work with a proxy // such as pantalaimon json_root.AddMember("homeserver", rapidjson::StringRef(homeserver.c_str()), request_data.GetAllocator()); @@ -3348,6 +3383,7 @@ namespace QuickMedia { my_user_id.clear(); access_token.clear(); homeserver.clear(); + homeserver_domain.clear(); upload_limit.reset(); set_next_batch(""); invites.clear(); @@ -3440,13 +3476,13 @@ namespace QuickMedia { return PluginResult::ERR; } - std::string user_id = user_id_json.GetString(); - std::string access_token = access_token_json.GetString(); - std::string homeserver = homeserver_json.GetString(); + const rapidjson::Value &home_server_json = GetMember(json_root, "home_server"); + if(home_server_json.IsString()) + this->homeserver_domain = home_server_json.GetString(); - this->my_user_id = std::move(user_id); - this->access_token = std::move(access_token); - this->homeserver = std::move(homeserver); + this->my_user_id = user_id_json.GetString(); + this->access_token = access_token_json.GetString(); + this->homeserver = homeserver_json.GetString(); return PluginResult::OK; } @@ -3600,6 +3636,108 @@ namespace QuickMedia { return download_result_to_plugin_result(download_result); } + PluginResult Matrix::get_public_rooms(const std::string &server, const std::string &search_term, const std::string &since, BodyItems &rooms, std::string &next_batch) { + rapidjson::Document filter_data(rapidjson::kObjectType); + if(!search_term.empty()) + filter_data.AddMember("generic_search_term", rapidjson::StringRef(search_term.c_str()), filter_data.GetAllocator()); + + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("limit", 20, request_data.GetAllocator()); + + if(!search_term.empty()) + request_data.AddMember("filter", std::move(filter_data), request_data.GetAllocator()); + + if(!since.empty()) + request_data.AddMember("since", rapidjson::StringRef(since.c_str()), request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); + + std::vector additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", buffer.GetString() } + }; + + std::string url = homeserver + "/_matrix/client/r0/publicRooms?server="; + url += url_param_encode(server); + + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.IsObject()) + return PluginResult::ERR; + + const rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch"); + if(next_batch_json.IsString()) + next_batch = next_batch_json.GetString(); + else + next_batch.clear(); + + const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); + if(chunk_json.IsArray()) { + for(const rapidjson::Value &chunk_item_json : chunk_json.GetArray()) { + if(!chunk_item_json.IsObject()) + continue; + + const rapidjson::Value &room_id_json = GetMember(chunk_item_json, "room_id"); + if(!room_id_json.IsString()) + continue; + + std::string room_name; + const rapidjson::Value &name_json = GetMember(chunk_item_json, "name"); + if(name_json.IsString()) + room_name = name_json.GetString(); + else + room_name = room_id_json.GetString(); + + auto room_body_item = BodyItem::create(std::move(room_name)); + room_body_item->url = room_id_json.GetString(); + std::string description; + + const rapidjson::Value &topic_json = GetMember(chunk_item_json, "topic"); + if(topic_json.IsString()) + description = strip(topic_json.GetString()); + + const rapidjson::Value &canonical_alias_json = GetMember(chunk_item_json, "canonical_alias"); + if(canonical_alias_json.IsString()) { + if(!description.empty()) + description += '\n'; + description += canonical_alias_json.GetString(); + } + + const rapidjson::Value &num_joined_members_json = GetMember(chunk_item_json, "num_joined_members"); + if(num_joined_members_json.IsInt()) { + if(!description.empty()) + description += '\n'; + description += "👤" + std::to_string(num_joined_members_json.GetInt()); + } + + room_body_item->set_description(std::move(description)); + + const rapidjson::Value &avatar_url_json = GetMember(chunk_item_json, "avatar_url"); + 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); + + if(!avatar_url.empty()) { + room_body_item->thumbnail_url = std::move(avatar_url); + room_body_item->thumbnail_size = sf::Vector2i(32 * get_ui_scale(), 32 * get_ui_scale()); + room_body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + } + } + + rooms.push_back(std::move(room_body_item)); + } + } + + return PluginResult::OK; + } + bool Matrix::was_message_posted_by_me(void *message) { Message *message_typed = (Message*)message; return my_user_id == message_typed->user->user_id; @@ -3645,6 +3783,10 @@ namespace QuickMedia { return get_user_by_id(room, my_user_id); } + const std::string& Matrix::get_homeserver_domain() const { + return homeserver_domain; + } + RoomData* Matrix::get_room_by_id(const std::string &id) { std::lock_guard lock(room_data_mutex); auto room_it = room_data_by_id.find(id); -- cgit v1.2.3