From a6bba48faa091932b5a51a3beb8c9d162c377adf Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 26 Jul 2021 17:35:52 +0200 Subject: Matrix: add /join and /invite commands --- README.md | 2 + TODO | 6 +- include/Page.hpp | 3 +- plugins/Matrix.hpp | 18 ++++- src/Body.cpp | 2 +- src/QuickMedia.cpp | 40 ++++++++++- src/plugins/Matrix.cpp | 178 ++++++++++++++++++++++++++++++++++++++++++------- 7 files changed, 219 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 6322cac..be3d3a1 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,8 @@ Type text and then wait and QuickMedia will automatically search.\ `Esc`/`Click on cancel`: Cancel download. ## Matrix text commands `/upload`: Bring up the file manager and select a file to upload to the room, `Esc` to cancel.\ +`/join [room]`: Join a room by name or id.\ +`/invite`: Invite a user to the room.\ `/logout`: Logout.\ `/leave`: Leave the current room.\ `/me [text]`: Send a message of type "m.emote".\ diff --git a/TODO b/TODO index 38b126e..95fb812 100644 --- a/TODO +++ b/TODO @@ -176,4 +176,8 @@ Allow resuming downloads. Support downloading live youtube videos. Youtube broke age restricted video again. Need to find a fix. It kinda works in yt-dlp, but not always. Use the new player innertube api. To make that work quickmedia will need to extract signatureTimestamp (sts) and set that in the form request. Youtube-dl does this. -Instead of resetting text items in body, add a clear function to text. That way we can easily cache the height of the text. \ No newline at end of file +Instead of resetting text items in body, add a clear function to text. That way we can easily cache the height of the text. +Check if message edits that are replies to me makes a now notification show up. This shouldn't happen, but also take into consideration initial sync. +Check if user has invite privileges and show error before bringing up invite gui when using /invite. +If only users in the same homeserver can join a room then filter out other users in room invite gui. +Exclude users that are already in the room from room invite gui. \ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index d0e40b3..4cf1821 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -9,6 +9,7 @@ namespace QuickMedia { IMAGE_BOARD_THREAD, CHAT_LOGIN, CHAT, - FILE_MANAGER + FILE_MANAGER, + CHAT_INVITE }; } \ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 2e25d69..293b00a 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -495,6 +495,19 @@ namespace QuickMedia { MatrixRoomsPage *all_rooms_page; }; + class MatrixInviteUserPage : public Page { + public: + MatrixInviteUserPage(Program *program, Matrix *matrix, std::string room_id) : Page(program), matrix(matrix), room_id(std::move(room_id)) {} + const char* get_title() const override { return "Invite user"; } + bool search_is_filter() override { return false; } + 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; + bool allow_submit_no_selection() const override { return true; } + private: + Matrix *matrix; + std::string room_id; + }; + class Matrix { public: // TODO: Make this return the Matrix object instead, to force users to call start_sync @@ -537,12 +550,15 @@ namespace QuickMedia { PluginResult set_read_marker(RoomData *room, const std::string &event_id, int64_t event_timestamp); - PluginResult join_room(const std::string &room_id); + PluginResult join_room(const std::string &room_id_or_name); 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); + PluginResult search_user(const std::string &search_term, unsigned int limit, BodyItems &result_items); + PluginResult invite_user(const std::string &room_id, const std::string &user_id); + // |message| is from |BodyItem.userdata| and is of type |Message*| bool was_message_posted_by_me(void *message); diff --git a/src/Body.cpp b/src/Body.cpp index 6c2cf9e..693086e 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -1551,7 +1551,7 @@ namespace QuickMedia { item_height += (get_item_height(item->embedded_item.get(), embedded_item_width, load_texture, false) + 6.0f + body_spacing[body_theme].embedded_item_padding_y * 2.0f); else item_height += ((body_spacing[body_theme].embedded_item_font_size + 5.0f) + 6.0f + body_spacing[body_theme].embedded_item_padding_y * 2.0f); - has_loaded_text = true; + has_loaded_text = true; // TODO: Remove this } if(item->description_text) { item_height += item->description_text->getHeight() - 2.0f; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 07d6640..f8566fa 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5045,6 +5045,29 @@ namespace QuickMedia { chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; + } else if(strncmp(text.c_str(), "/join ", 6) == 0) { + text.erase(text.begin(), text.begin() + 6); + text = strip(text); + if(text.empty()) { + return false; + } else { + TaskResult task_result = run_task_with_loading_screen([this, text{std::move(text)}] { + return matrix->join_room(text) == PluginResult::OK; + }); + + if(task_result == TaskResult::TRUE) { + chat_input.set_editable(false); + chat_state = ChatState::NAVIGATING; + return true; + } else { + return false; + } + } + } else if(text == "/invite") { + new_page = PageType::CHAT_INVITE; + chat_input.set_editable(false); + chat_state = ChatState::NAVIGATING; + return true; } else if(text == "/logout") { new_page = PageType::CHAT_LOGIN; chat_input.set_editable(false); @@ -5067,7 +5090,7 @@ namespace QuickMedia { msgtype = "m.reaction"; text.erase(text.begin(), text.begin() + 7); } else { - show_notification("QuickMedia", "Error: invalid command: " + text + ", expected /upload, /logout, /me or /react", Urgency::NORMAL); + show_notification("QuickMedia", "Error: invalid command: " + text + ", expected /upload, /join [room], /invite, /logout, /me [text] or /react [text]", Urgency::NORMAL); return false; } } else if(chat_state == ChatState::REPLYING && text[0] == '/') { @@ -5968,6 +5991,21 @@ namespace QuickMedia { exit(exit_code); break; } + case PageType::CHAT_INVITE: { + new_page = PageType::CHAT; + + for(ChatTab &tab : tabs) { + tab.body->clear_cache(); + } + + std::vector new_tabs; + new_tabs.push_back(Tab{create_body(), std::make_unique(this, matrix, current_room->id), create_search_bar("Search...", 350)}); + page_loop(new_tabs); + + redraw = true; + avatar_applied = false; + break; + } default: break; } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 4d00949..b0ff9b8 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1128,6 +1128,17 @@ namespace QuickMedia { } } + SearchResult MatrixInviteUserPage::search(const std::string &str, BodyItems &result_items) { + return plugin_result_to_search_result(matrix->search_user(str, 20, result_items)); + } + + PluginResult MatrixInviteUserPage::submit(const std::string&, const std::string &url, std::vector&) { + PluginResult result = matrix->invite_user(room_id, url); + if(result != PluginResult::OK) return result; + program->set_go_to_previous_page(); + return PluginResult::OK; + } + static std::array sync_fail_error_codes = { "M_FORBIDDEN", "M_UNKNOWN_TOKEN", @@ -3850,7 +3861,7 @@ namespace QuickMedia { return download_result_to_plugin_result(download_result); } - PluginResult Matrix::join_room(const std::string &room_id) { + PluginResult Matrix::join_room(const std::string &room_id_or_name) { assert(delegate); std::vector additional_args = { { "-X", "POST" }, @@ -3859,34 +3870,50 @@ namespace QuickMedia { { "-H", "Authorization: Bearer " + access_token } }; - std::string server_response; - DownloadResult download_result = download_to_string(homeserver + "/_matrix/client/r0/join/" + url_param_encode(room_id), server_response, std::move(additional_args), true); - if(download_result == DownloadResult::OK) { - std::lock_guard invite_lock(invite_mutex); - auto invite_it = invites.find(room_id); - if(invite_it != invites.end()) { - std::lock_guard lock(room_data_mutex); - RoomData *room = get_room_by_id(room_id); - if(!room) { - auto new_room = std::make_unique(); - new_room->id = room_id; - new_room->set_name(invite_it->second.room_name); - new_room->set_avatar_url(invite_it->second.room_avatar_url); - room = new_room.get(); - add_room(std::move(new_room)); + rapidjson::Document json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/join/" + url_param_encode(room_id_or_name), std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); - ui_thread_tasks.push([this, room]{ delegate->join_room(room); }); - room->acquire_room_lock(); - std::set &room_tags = room->get_tags_unsafe(); - if(room_tags.empty()) { - room_tags.insert(OTHERS_ROOM_TAG); - ui_thread_tasks.push([this, room]{ delegate->room_add_tag(room, OTHERS_ROOM_TAG); }); - } - room->release_room_lock(); + if(!json_root.IsObject()) + return PluginResult::ERR; + + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + show_notification("QuickMedia", "Failed to join " + room_id_or_name + ", error: " + std::string(error_json.GetString(), error_json.GetStringLength()), Urgency::CRITICAL); + return PluginResult::ERR; + } + + const rapidjson::Value &room_id_json = GetMember(json_root, "room_id"); + if(!room_id_json.IsString()) + return PluginResult::ERR; + + const std::string room_id(room_id_json.GetString(), room_id_json.GetStringLength()); + + std::lock_guard invite_lock(invite_mutex); + auto invite_it = invites.find(room_id); + if(invite_it != invites.end()) { + std::lock_guard lock(room_data_mutex); + RoomData *room = get_room_by_id(room_id); + if(!room) { + auto new_room = std::make_unique(); + new_room->id = room_id; + new_room->set_name(invite_it->second.room_name); + new_room->set_avatar_url(invite_it->second.room_avatar_url); + room = new_room.get(); + add_room(std::move(new_room)); + + ui_thread_tasks.push([this, room]{ delegate->join_room(room); }); + room->acquire_room_lock(); + std::set &room_tags = room->get_tags_unsafe(); + if(room_tags.empty()) { + room_tags.insert(OTHERS_ROOM_TAG); + ui_thread_tasks.push([this, room]{ delegate->room_add_tag(room, OTHERS_ROOM_TAG); }); } + room->release_room_lock(); } } - return download_result_to_plugin_result(download_result); + return PluginResult::OK; } PluginResult Matrix::leave_room(const std::string &room_id) { @@ -4012,6 +4039,107 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult Matrix::search_user(const std::string &search_term, unsigned int limit, BodyItems &result_items) { + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("search_term", rapidjson::StringRef(search_term.c_str()), request_data.GetAllocator()); + request_data.AddMember("limit", limit, 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" }, + { "--data-binary", buffer.GetString() }, + { "-H", "Authorization: Bearer " + access_token } + }; + + rapidjson::Document json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/user_directory/search", std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.IsObject()) + return PluginResult::ERR; + + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + show_notification("QuickMedia", "Failed to search for " + search_term + ", error: " + std::string(error_json.GetString(), error_json.GetStringLength()), Urgency::CRITICAL); + return PluginResult::ERR; + } + + const rapidjson::Value &results_json = GetMember(json_root, "results"); + if(!results_json.IsArray()) + return PluginResult::OK; + + for(const rapidjson::Value &result_item_json : results_json.GetArray()) { + if(!result_item_json.IsObject()) + continue; + + const rapidjson::Value &user_id_json = GetMember(result_item_json, "user_id"); + const rapidjson::Value &display_name_json = GetMember(result_item_json, "display_name"); + const rapidjson::Value &avatar_url_json = GetMember(result_item_json, "avatar_url"); + if(!user_id_json.IsString()) + continue; + + auto body_item = BodyItem::create(""); + body_item->url.assign(user_id_json.GetString(), user_id_json.GetStringLength()); + body_item->set_description(body_item->url); + body_item->set_description_color(get_current_theme().faded_text_color); + if(display_name_json.IsString()) + body_item->set_author(std::string(display_name_json.GetString(), display_name_json.GetStringLength())); + else + body_item->set_author(std::string(user_id_json.GetString(), user_id_json.GetStringLength())); + body_item->set_author_color(user_id_to_color(body_item->url)); + + 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); + body_item->thumbnail_url = std::move(avatar_url); + } + body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; + body_item->thumbnail_size = sf::Vector2i(32, 32); + + result_items.push_back(std::move(body_item)); + } + + return PluginResult::OK; + } + + PluginResult Matrix::invite_user(const std::string &room_id, const std::string &user_id) { + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("user_id", rapidjson::StringRef(user_id.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" }, + { "--data-binary", buffer.GetString() }, + { "-H", "Authorization: Bearer " + access_token } + }; + + rapidjson::Document json_root; + std::string err_msg; + DownloadResult download_result = download_json(json_root, homeserver + "/_matrix/client/r0/rooms/" + room_id + "/invite", std::move(additional_args), true, &err_msg); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + if(!json_root.IsObject()) + return PluginResult::ERR; + + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + show_notification("QuickMedia", "Failed to invite " + user_id + " to " + room_id + ", error: " + std::string(error_json.GetString(), error_json.GetStringLength()), Urgency::CRITICAL); + return PluginResult::ERR; + } + + 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; -- cgit v1.2.3