diff options
Diffstat (limited to 'src/plugins')
-rw-r--r-- | src/plugins/Mangadex.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Manganelo.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Mangatown.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Matrix.cpp | 644 | ||||
-rw-r--r-- | src/plugins/NyaaSi.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Pornhub.cpp | 2 | ||||
-rw-r--r-- | src/plugins/Youtube.cpp | 2 |
7 files changed, 542 insertions, 114 deletions
diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index a52788d..a8318e8 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -210,7 +210,7 @@ namespace QuickMedia { } PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<MangadexImagesPage>(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index 7f0a2f9..f87081c 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -121,7 +121,7 @@ namespace QuickMedia { } PluginResult ManganeloChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<ManganeloImagesPage>(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Mangatown.cpp b/src/plugins/Mangatown.cpp index 89bf447..1d4d71a 100644 --- a/src/plugins/Mangatown.cpp +++ b/src/plugins/Mangatown.cpp @@ -110,7 +110,7 @@ namespace QuickMedia { } PluginResult MangatownChaptersPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { - result_tabs.push_back(Tab{create_body(), std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<MangatownImagesPage>(program, content_title, title, url), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 99d6bed..ede0821 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -2,26 +2,22 @@ #include "../../include/Storage.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" +#include "../../include/Notification.hpp" #include <rapidjson/document.h> #include <rapidjson/writer.h> #include <rapidjson/stringbuffer.h> #include <fcntl.h> #include <unistd.h> +#include "../../include/QuickMedia.hpp" // TODO: Update avatar/display name when its changed in the room/globally. -// Send read receipt to server and receive notifications in /sync and show the notifications. -// Delete messages. -// Edit messages. // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints -// TODO: POST /_matrix/client/r0/rooms/{roomId}/read_markers after 5 seconds of receiving a message when the client is focused -// to mark messages as read -// When reaching top/bottom message, show older/newer messages. // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. - -// TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync) +// TODO: Use lazy load filter for /sync (filter=0, required GET first to check if its available). If we use filter for sync then we also need to modify Matrix::get_message_by_id to parse state, etc. static const char* SERVICE_NAME = "matrix"; +static const char* OTHERS_ROOM_TAG = "tld.name.others"; static rapidjson::Value nullValue(rapidjson::kNullType); static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char *key) { @@ -31,6 +27,47 @@ static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char return nullValue; } +static std::string capitalize(const std::string &str) { + if(str.size() >= 1) + return (char)std::toupper(str[0]) + str.substr(1); + else + return ""; +} + +// 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 std::string tag_get_name(const std::string &tag) { + if(tag.size() >= 2 && memcmp(tag.data(), "m.", 2) == 0) { + if(strcmp(tag.c_str() + 2, "favourite") == 0) + return "Favorites"; + else if(strcmp(tag.c_str() + 2, "lowpriority") == 0) + return "Low priority"; + else if(strcmp(tag.c_str() + 2, "server_notice") == 0) + return "Server notice"; + else + return capitalize(tag.substr(2)); + } else if(tag.size() >= 2 && memcmp(tag.data(), "u.", 2) == 0) { + return capitalize(tag.substr(2)); + } else if(tag.size() >= 9 && memcmp(tag.data(), "tld.name.", 9) == 0) { + return capitalize(tag.substr(9)); + } else { + return ""; + } +} + +static std::string extract_first_line_elipses(const std::string &str, size_t max_length) { + size_t index = str.find('\n'); + if(index == std::string::npos) { + if(str.size() > max_length) + return str.substr(0, max_length) + " (...)"; + return str; + } else if(index == 0) { + return ""; + } else { + return str.substr(0, std::min(index, max_length)) + " (...)"; + } +} + namespace QuickMedia { std::shared_ptr<UserInfo> RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard<std::mutex> lock(room_mutex); @@ -75,11 +112,6 @@ namespace QuickMedia { } } - void RoomData::append_pinned_events(std::vector<std::string> new_pinned_events) { - std::lock_guard<std::mutex> lock(room_mutex); - pinned_events.insert(pinned_events.end(), new_pinned_events.begin(), new_pinned_events.end()); - } - std::shared_ptr<Message> RoomData::get_message_by_id(const std::string &id) { std::lock_guard<std::mutex> lock(room_mutex); auto message_it = message_by_event_id.find(id); @@ -160,57 +192,368 @@ namespace QuickMedia { return avatar_url; } - PluginResult Matrix::sync(RoomSyncData &room_sync_data) { - std::vector<CommandArg> additional_args = { - { "-H", "Authorization: Bearer " + access_token }, - { "-m", "35" } - }; + void RoomData::set_pinned_events(std::vector<std::string> new_pinned_events) { + std::lock_guard<std::mutex> lock(room_mutex); + pinned_events = std::move(new_pinned_events); + pinned_events_updated = true; + } - char url[512]; - if(next_batch.empty()) - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); - else - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); + std::set<std::string>& RoomData::get_tags_unsafe() { + return tags; + } - 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); + MatrixQuickMedia::MatrixQuickMedia(Program *program, Matrix *matrix, MatrixRoomsPage *rooms_page, MatrixRoomTagsPage *room_tags_page) : program(program), matrix(matrix), rooms_page(rooms_page), room_tags_page(room_tags_page) { + rooms_page->matrix_delegate = this; + room_tags_page->matrix_delegate = this; + } - PluginResult result = sync_response_to_body_items(json_root, room_sync_data); - if(result != PluginResult::OK) - return result; + void MatrixQuickMedia::room_create(RoomData *room) { + std::string room_name = room->get_name(); + if(room_name.empty()) + room_name = room->id; - const rapidjson::Value &next_batch_json = GetMember(json_root, "next_batch"); - if(next_batch_json.IsString()) { - next_batch = next_batch_json.GetString(); - fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); - } else { - fprintf(stderr, "Matrix: missing next batch\n"); + auto body_item = BodyItem::create(std::move(room_name)); + body_item->url = room->id; + 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); + room->userdata = body_item.get(); + room_body_items.push_back(body_item); + rooms_page->add_body_item(body_item); + room_body_item_by_room[room] = body_item; + } + + void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) { + 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) { + room_tags_page->remove_room_body_item_from_tag(room_body_item_by_room[room], tag); + } + + void MatrixQuickMedia::room_add_new_messages(RoomData *room, const Messages &messages, bool is_initial_sync) { + std::lock_guard<std::mutex> lock(pending_room_messages_mutex); + auto &room_messages_data = pending_room_messages[room]; + room_messages_data.messages.insert(room_messages_data.messages.end(), messages.begin(), messages.end()); + room_messages_data.is_initial_sync = is_initial_sync; + } + + static int find_top_body_position_for_unread_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { + for(int i = 0; i < (int)room_body_items.size(); ++i) { + const auto &body_item = room_body_items[i]; + if(static_cast<RoomData*>(body_item->userdata)->last_message_read || body_item.get() == item_to_swap) + return i; + } + return -1; + } + + static int find_top_body_position_for_mentioned_room(const BodyItems &room_body_items, BodyItem *item_to_swap) { + for(int i = 0; i < (int)room_body_items.size(); ++i) { + const auto &body_item = room_body_items[i]; + if(!static_cast<RoomData*>(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) + return i; + } + return -1; + } + + static void sort_room_body_items(std::vector<std::shared_ptr<BodyItem>> &room_body_items) { + std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr<BodyItem> &body_item1, const std::shared_ptr<BodyItem> &body_item2) { + RoomData *room1 = static_cast<RoomData*>(body_item1->userdata); + RoomData *room2 = static_cast<RoomData*>(body_item2->userdata); + int room1_focus_sum = (int)room1->has_unread_mention + (int)!room1->last_message_read; + int room2_focus_sum = (int)room2->has_unread_mention + (int)!room2->last_message_read; + return room1_focus_sum > room2_focus_sum; + }); + } + + void MatrixQuickMedia::update(MatrixPageType page_type) { + std::lock_guard<std::mutex> lock(pending_room_messages_mutex); + bool is_window_focused = program->is_window_focused(); + RoomData *current_room = program->get_current_chat_room(); + for(auto &it : pending_room_messages) { + RoomData *room = it.first; + auto &messages = it.second.messages; + bool is_initial_sync = it.second.is_initial_sync; + //auto &room_body_item = room_body_item_by_room[room]; + //std::string room_desc = matrix->message_get_author_displayname(it.second.back().get()) + ": " + extract_first_line_elipses(it.second.back()->body, 150); + //room_body_item->set_description(std::move(room_desc)); + + for(auto &message : messages) { + if(message->mentions_me) { + 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_initial_sync || page_type == MatrixPageType::ROOM_LIST) + show_notification("QuickMedia matrix - " + matrix->message_get_author_displayname(message.get()) + " (" + room->get_name() + ")", message->body); + } + } + + std::shared_ptr<UserInfo> me = matrix->get_me(room); + time_t read_marker_message_timestamp = 0; + if(me) { + auto read_marker_message = room->get_message_by_id(room->get_user_read_marker(me)); + if(read_marker_message) + read_marker_message_timestamp = read_marker_message->timestamp; + } + + // TODO: this wont always work because we dont display all types of messages from server, such as "joined", "left", "kicked", "banned", "changed avatar", "changed display name", etc. + // TODO: Binary search? + Message *last_unread_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION && (*it)->timestamp > read_marker_message_timestamp) { + last_unread_message = (*it).get(); + break; + } + } + + BodyItem *room_body_item = static_cast<BodyItem*>(room->userdata); + assert(room_body_item); + + if(last_unread_message) { + std::string room_desc = "Unread: " + matrix->message_get_author_displayname(last_unread_message) + ": " + extract_first_line_elipses(last_unread_message->body, 150); + if(room->has_unread_mention) + room_desc += "\n** You were mentioned **"; // TODO: Better notification? + room_body_item->set_description(std::move(room_desc)); + room_body_item->set_title_color(sf::Color(255, 100, 100)); + room->last_message_read = false; + + rooms_page->move_room_to_top(room); + room_tags_page->move_room_to_top(room); + } else if(is_initial_sync) { + Message *last_message = nullptr; + for(auto it = messages.rbegin(), end = messages.rend(); it != end; ++it) { + if((*it)->related_event_type != RelatedEventType::EDIT && (*it)->related_event_type != RelatedEventType::REDACTION) { + last_message = (*it).get(); + break; + } + } + if(last_message) + room_body_item->set_description(matrix->message_get_author_displayname(last_message) + ": " + extract_first_line_elipses(last_message->body, 150)); + } } + pending_room_messages.clear(); + } + + MatrixRoomsPage::MatrixRoomsPage(Program *program, Body *body, std::string title, MatrixRoomTagsPage *room_tags_page) : Page(program), body(body), title(std::move(title)), room_tags_page(room_tags_page) { + if(room_tags_page) + room_tags_page->current_rooms_page = this; + } + + MatrixRoomsPage::~MatrixRoomsPage() { + if(room_tags_page) + room_tags_page->current_rooms_page = nullptr; + } + PluginResult MatrixRoomsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + auto chat_page = std::make_unique<MatrixChatPage>(program, url); + chat_page->matrix_delegate = matrix_delegate; + result_tabs.push_back(Tab{nullptr, std::move(chat_page), nullptr}); return PluginResult::OK; } - void Matrix::get_room_join_updates(Rooms &new_rooms) { - std::lock_guard<std::mutex> lock(room_data_mutex); - size_t num_new_rooms = rooms.size() - room_list_read_index; - size_t new_rooms_prev_size = new_rooms.size(); - new_rooms.resize(new_rooms_prev_size + num_new_rooms); - for(size_t i = new_rooms_prev_size; i < new_rooms.size(); ++i) { - new_rooms[i] = rooms[room_list_read_index + i].get(); + void MatrixRoomsPage::update() { + { + std::lock_guard<std::mutex> lock(mutex); + body->append_items(std::move(room_body_items)); + } + matrix_delegate->update(MatrixPageType::ROOM_LIST); + } + + void MatrixRoomsPage::add_body_item(std::shared_ptr<BodyItem> body_item) { + std::lock_guard<std::mutex> lock(mutex); + room_body_items.push_back(body_item); + } + + void MatrixRoomsPage::move_room_to_top(RoomData *room) { + // Swap order of rooms in body list to put rooms with mentions at the top and then unread messages and then all the other rooms + // TODO: Optimize with hash map instead of linear search? or cache the index + std::lock_guard<std::mutex> lock(mutex); + BodyItem *room_body_item = static_cast<BodyItem*>(room->userdata); + int room_body_index = body->get_index_by_body_item(room_body_item); + if(room_body_index != -1) { + std::shared_ptr<BodyItem> body_item = body->items[room_body_index]; + int body_swap_index = -1; + if(room->has_unread_mention) + body_swap_index = find_top_body_position_for_mentioned_room(body->items, body_item.get()); + else if(!room->last_message_read) + body_swap_index = find_top_body_position_for_unread_room(body->items, body_item.get()); + if(body_swap_index != -1 && body_swap_index != room_body_index) { + body->items.erase(body->items.begin() + room_body_index); + if(body_swap_index < room_body_index) + body->items.insert(body->items.begin() + body_swap_index, std::move(body_item)); + else + body->items.insert(body->items.begin() + (body_swap_index - 1), std::move(body_item)); + } + } + } + + PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { + (void)title; + std::lock_guard<std::mutex> lock(mutex); + auto body = create_body(); + Body *body_ptr = body.get(); + TagData &tag_data = tag_body_items_by_name[url]; + body->items = tag_data.room_body_items; + sort_room_body_items(body->items); + auto rooms_page = std::make_unique<MatrixRoomsPage>(program, body_ptr, tag_data.tag_item->get_title(), this); + rooms_page->matrix_delegate = matrix_delegate; + result_tabs.push_back(Tab{std::move(body), std::move(rooms_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + return PluginResult::OK; + } + + // TODO: Also add/remove body items to above body (in submit) + void MatrixRoomTagsPage::update() { + { + std::lock_guard<std::mutex> lock(mutex); + 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()) + continue; + + for(auto &room_to_remove : it.second) { + auto room_body_item_it = std::find(tag_body_it->second.room_body_items.begin(), tag_body_it->second.room_body_items.end(), room_to_remove); + if(room_body_item_it != tag_body_it->second.room_body_items.end()) + tag_body_it->second.room_body_items.erase(room_body_item_it); + } + + if(tag_body_it->second.room_body_items.empty()) { + auto room_body_item_it = std::find(body->items.begin(), body->items.end(), tag_body_it->second.tag_item); + if(room_body_item_it != body->items.end()) + body->items.erase(room_body_item_it); + tag_body_items_by_name.erase(tag_body_it); + } + } + remove_room_body_items_by_tags.clear(); + + for(auto &it : add_room_body_items_by_tags) { + TagData *tag_data; + auto tag_body_it = tag_body_items_by_name.find(it.first); + if(tag_body_it == tag_body_items_by_name.end()) { + std::string tag_name = tag_get_name(it.first); + if(!tag_name.empty()) { + auto tag_body_item = BodyItem::create(std::move(tag_name)); + tag_body_item->url = it.first; + tag_body_items_by_name.insert(std::make_pair(it.first, TagData{tag_body_item, {}})); + // TODO: Sort by tag priority + body->items.push_back(tag_body_item); + tag_data = &tag_body_items_by_name[it.first]; + tag_data->tag_item = tag_body_item; + } + } else { + tag_data = &tag_body_it->second; + } + + for(auto &room_body_item : it.second) { + tag_data->room_body_items.push_back(room_body_item); + } + } + add_room_body_items_by_tags.clear(); + } + matrix_delegate->update(MatrixPageType::ROOM_LIST); + } + + void MatrixRoomTagsPage::add_room_body_item_to_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag) { + std::lock_guard<std::mutex> lock(mutex); + add_room_body_items_by_tags[tag].push_back(body_item); + } + + void MatrixRoomTagsPage::remove_room_body_item_from_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag) { + std::lock_guard<std::mutex> lock(mutex); + remove_room_body_items_by_tags[tag].push_back(body_item); + } + + void MatrixRoomTagsPage::move_room_to_top(RoomData *room) { + if(current_rooms_page) + current_rooms_page->move_room_to_top(room); + } + + void MatrixChatPage::update() { + matrix_delegate->update(MatrixPageType::CHAT); + } + + void Matrix::start_sync(MatrixDelegate *delegate) { + if(sync_running) + return; + + sync_running = true; + sync_thread = std::thread([this, delegate]() { + const rapidjson::Value *next_batch_json; + PluginResult result; + while(sync_running) { + std::vector<CommandArg> additional_args = { + { "-H", "Authorization: Bearer " + access_token }, + { "-m", "35" } + }; + + char url[512]; + if(next_batch.empty()) + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); + else + 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); + if(download_result != DownloadResult::OK) { + fprintf(stderr, "Fetch response failed\n"); + goto sync_end; + } + + result = parse_sync_response(json_root, delegate); + if(result != PluginResult::OK) { + fprintf(stderr, "Failed to parse sync response\n"); + goto sync_end; + } + + next_batch_json = &GetMember(json_root, "next_batch"); + if(next_batch_json->IsString()) { + next_batch = next_batch_json->GetString(); + fprintf(stderr, "Matrix: next batch: %s\n", next_batch.c_str()); + } else { + fprintf(stderr, "Matrix: missing next batch\n"); + } + + sync_end: + if(sync_running) + std::this_thread::sleep_for(std::chrono::seconds(1)); + } + }); + } + + void Matrix::stop_sync() { + // TODO: Kill the running download in |sync_thread| instead of waiting until sync returns (which can be up to 30 seconds) + sync_running = false; + if(sync_thread.joinable()) + sync_thread.join(); + } + + bool Matrix::is_initial_sync_finished() const { + return !next_batch.empty(); + } + + void Matrix::get_room_sync_data(RoomData *room, SyncData &sync_data) { + room->acquire_room_lock(); + auto &room_messages = room->get_messages_thread_unsafe(); + sync_data.messages.insert(sync_data.messages.end(), room_messages.begin() + room->messages_read_index, room_messages.end()); + room->messages_read_index = room_messages.size(); + if(room->pinned_events_updated) { + sync_data.pinned_events = room->get_pinned_events_unsafe(); + room->pinned_events_updated = false; } - room_list_read_index += num_new_rooms; + room->release_room_lock(); } void Matrix::get_all_synced_room_messages(RoomData *room, Messages &messages) { room->acquire_room_lock(); messages = room->get_messages_thread_unsafe(); + room->messages_read_index = messages.size(); room->release_room_lock(); } void Matrix::get_all_pinned_events(RoomData *room, std::vector<std::string> &events) { room->acquire_room_lock(); events = room->get_pinned_events_unsafe(); + room->pinned_events_updated = false; room->release_room_lock(); } @@ -226,15 +569,69 @@ namespace QuickMedia { size_t num_messages_after = room->get_messages_thread_unsafe().size(); size_t num_new_messages = num_messages_after - num_messages_before; messages.insert(messages.end(), room->get_messages_thread_unsafe().begin(), room->get_messages_thread_unsafe().begin() + num_new_messages); + room->messages_read_index += num_new_messages; room->release_room_lock(); return PluginResult::OK; } - PluginResult Matrix::sync_response_to_body_items(const rapidjson::Document &root, RoomSyncData &room_sync_data) { + PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate) { if(!root.IsObject()) return PluginResult::ERR; + const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + std::optional<std::set<std::string>> dm_rooms; + parse_sync_account_data(account_data_json, dm_rooms); + // TODO: Include "Direct messages" as a tag using |dm_rooms| above + const rapidjson::Value &rooms_json = GetMember(root, "rooms"); + parse_sync_room_data(rooms_json, delegate); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json, std::optional<std::set<std::string>> &dm_rooms) { + if(!account_data_json.IsObject()) + return PluginResult::OK; + + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + if(!events_json.IsArray()) + return PluginResult::OK; + + bool has_direct_rooms = false; + std::set<std::string> dm_rooms_tmp; + for(const rapidjson::Value &event_item_json : events_json.GetArray()) { + if(!event_item_json.IsObject()) + continue; + + const rapidjson::Value &type_json = GetMember(event_item_json, "type"); + if(!type_json.IsString() || strcmp(type_json.GetString(), "m.direct") != 0) + continue; + + const rapidjson::Value &content_json = GetMember(event_item_json, "content"); + if(!content_json.IsObject()) + continue; + + has_direct_rooms = true; + for(auto const &it : content_json.GetObject()) { + if(!it.value.IsArray()) + continue; + + for(const rapidjson::Value &room_id_json : it.value.GetArray()) { + if(!room_id_json.IsString()) + continue; + + dm_rooms_tmp.insert(std::string(room_id_json.GetString(), room_id_json.GetStringLength())); + } + } + } + + if(has_direct_rooms) + dm_rooms = std::move(dm_rooms_tmp); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate) { if(!rooms_json.IsObject()) return PluginResult::OK; @@ -252,12 +649,14 @@ namespace QuickMedia { std::string room_id_str = room_id.GetString(); + bool is_new_room = false; RoomData *room = get_room_by_id(room_id_str); if(!room) { auto new_room = std::make_unique<RoomData>(); new_room->id = room_id_str; room = new_room.get(); add_room(std::move(new_room)); + is_new_room = true; } const rapidjson::Value &state_json = GetMember(it.value, "state"); @@ -265,7 +664,7 @@ namespace QuickMedia { const rapidjson::Value &events_json = GetMember(state_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); - events_add_pinned_events(events_json, room, room_sync_data); + events_add_pinned_events(events_json, room); } const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral"); @@ -296,7 +695,8 @@ namespace QuickMedia { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); events_add_user_read_markers(events_json, room); } - events_add_messages(events_json, room, MessageDirection::AFTER, &room_sync_data, has_unread_notifications); + events_add_messages(events_json, room, MessageDirection::AFTER, delegate, has_unread_notifications); + events_add_pinned_events(events_json, room); } else { if(ephemeral_json.IsObject()) { const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); @@ -304,10 +704,23 @@ namespace QuickMedia { } } + if(is_new_room) + delegate->room_create(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); + events_add_room_to_tags(events_json, room, delegate); + } + + if(is_new_room) { + room->acquire_room_lock(); + std::set<std::string> &room_tags = room->get_tags_unsafe(); + if(room_tags.empty()) { + room_tags.insert(OTHERS_ROOM_TAG); + delegate->room_add_tag(room, OTHERS_ROOM_TAG); + } + room->release_room_lock(); } } @@ -416,7 +829,7 @@ namespace QuickMedia { auto user = room_data->get_user_by_id(user_id_json.GetString()); if(!user) { - fprintf(stderr, "Receipt read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString()); + fprintf(stderr, "Read receipt for unknown user: %s, ignoring...\n", user_id_json.GetString()); continue; } @@ -492,16 +905,28 @@ namespace QuickMedia { return false; } + static size_t string_find_case_insensitive(const char *haystack, size_t index, size_t length, const std::string &needle) { + const char *haystack_end = haystack + length; + auto it = std::search(haystack + index, haystack_end, needle.begin(), needle.end(), + [](char c1, char c2) { + return std::toupper(c1) == std::toupper(c2); + }); + if(it != haystack_end) + return it - haystack; + else + return std::string::npos; + } + // TODO: Do not show notification if mention is a reply to somebody else that replies to me? also dont show notification everytime a mention is edited bool message_contains_user_mention(const std::string &msg, const std::string &username) { - if(msg.empty()) + if(msg.empty() || username.empty()) return false; size_t index = 0; while(index < msg.size()) { - size_t found_index = msg.find(username, index); + size_t found_index = string_find_case_insensitive(&msg[0], index, msg.size(), username); if(found_index == std::string::npos) - return false; + break; char prev_char = ' '; if(found_index > 0) @@ -514,16 +939,17 @@ namespace QuickMedia { if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char)) return true; - index += username.size(); + index = found_index + username.size(); } return false; } - void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncData *room_sync_data, bool has_unread_notifications) { + void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications) { if(!events_json.IsArray()) return; + // TODO: Preallocate std::vector<std::shared_ptr<Message>> new_messages; auto me = get_me(room_data); @@ -536,11 +962,6 @@ namespace QuickMedia { if(new_messages.empty()) return; - // TODO: Add directly to this instead when set? otherwise add to new_messages - if(room_sync_data) - (*room_sync_data)[room_data].messages = new_messages; - - // TODO: Loop and std::move instead? doesn't insert create copies? if(message_dir == MessageDirection::BEFORE) { room_data->prepend_messages_reverse(new_messages); } else if(message_dir == MessageDirection::AFTER) { @@ -560,6 +981,9 @@ namespace QuickMedia { if(has_unread_notifications && me && message->timestamp > read_marker_message_timestamp) message->mentions_me = message_contains_user_mention(message->body, me->display_name) || message_contains_user_mention(message->body, me->user_id) || message_contains_user_mention(message->body, "@room"); } + + if(delegate) + delegate->room_add_new_messages(room_data, new_messages, next_batch.empty()); } std::shared_ptr<Message> Matrix::parse_message_event(const rapidjson::Value &event_item_json, RoomData *room_data) { @@ -706,6 +1130,9 @@ namespace QuickMedia { message->type = MessageType::TEXT; message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver); message_content_extract_thumbnail_size(*content_json, message->thumbnail_size); + } else if(strcmp(content_type.GetString(), "m.server_notice") == 0) { // TODO: show server notices differently + message->type = MessageType::TEXT; + prefix = "* Server notice * "; } else { return nullptr; } @@ -825,10 +1252,11 @@ namespace QuickMedia { } } - void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data, RoomSyncData &room_sync_data) { + void Matrix::events_add_pinned_events(const rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; + bool has_pinned_events = false; std::vector<std::string> pinned_events; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) @@ -846,34 +1274,25 @@ namespace QuickMedia { if(!pinned_json.IsArray()) continue; + has_pinned_events = true; + pinned_events.clear(); for(const rapidjson::Value &pinned_item_json : pinned_json.GetArray()) { if(!pinned_item_json.IsString()) continue; - pinned_events.push_back(std::string(pinned_item_json.GetString(), pinned_item_json.GetStringLength())); } } - room_sync_data[room_data].pinned_events = pinned_events; - room_data->append_pinned_events(std::move(pinned_events)); + if(has_pinned_events) + room_data->set_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) { + void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate) { if(!events_json.IsArray()) return; - std::vector<std::string> pinned_events; + bool has_tags = false; + std::set<std::string> new_tags; for(const rapidjson::Value &event_item_json : events_json.GetArray()) { if(!event_item_json.IsObject()) continue; @@ -890,17 +1309,46 @@ namespace QuickMedia { if(!tags_json.IsObject()) continue; + has_tags = true; + new_tags.clear(); 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; + //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); + new_tags.insert(std::string(tag_json.name.GetString(), tag_json.name.GetStringLength())); + } + } + + // Adding/removing tags is done with PUT and DELETE, but tags is part of account_data that contains all of the tags. + // When we receive a list of tags its always the full list of tags + if(has_tags) { + room_data->acquire_room_lock(); + std::set<std::string> &room_tags = room_data->get_tags_unsafe(); + + for(const std::string &room_tag : room_tags) { + auto it = new_tags.find(room_tag); + if(it == new_tags.end()) + delegate->room_remove_tag(room_data, room_tag); + } + + for(const std::string &new_tag : new_tags) { + auto it = room_tags.find(new_tag); + if(it == room_tags.end()) + delegate->room_add_tag(room_data, new_tag); } + + if(new_tags.empty()) { + new_tags.insert(OTHERS_ROOM_TAG); + delegate->room_add_tag(room_data, OTHERS_ROOM_TAG); + } + + room_tags = std::move(new_tags); + room_data->release_room_lock(); } } @@ -991,7 +1439,7 @@ namespace QuickMedia { return "m.file"; } - PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info) { + PluginResult Matrix::post_message(RoomData *room, const std::string &body, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype) { char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) return PluginResult::ERR; @@ -1021,7 +1469,10 @@ namespace QuickMedia { } rapidjson::Document request_data(rapidjson::kObjectType); - request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); + if(msgtype.empty()) + request_data.AddMember("msgtype", rapidjson::StringRef(file_info ? content_type_to_message_type(file_info->content_type) : "m.text"), request_data.GetAllocator()); + else + request_data.AddMember("msgtype", rapidjson::StringRef(msgtype.c_str()), request_data.GetAllocator()); request_data.AddMember("body", rapidjson::StringRef(body.c_str()), request_data.GetAllocator()); if(contains_formatted_text) { request_data.AddMember("format", "org.matrix.custom.html", request_data.GetAllocator()); @@ -1173,11 +1624,6 @@ namespace QuickMedia { // TODO: Store shared_ptr<Message> instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr<Message> relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared); - if(!relates_to_message_original) { - fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); - return PluginResult::ERR; - } char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -1186,7 +1632,7 @@ namespace QuickMedia { std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); rapidjson::Document in_reply_to_json(rapidjson::kObjectType); - in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), in_reply_to_json.GetAllocator()); + in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), in_reply_to_json.GetAllocator()); rapidjson::Document relates_to_json(rapidjson::kObjectType); relates_to_json.AddMember("m.in_reply_to", std::move(in_reply_to_json), relates_to_json.GetAllocator()); @@ -1233,11 +1679,6 @@ namespace QuickMedia { PluginResult Matrix::post_edit(RoomData *room, const std::string &body, void *relates_to) { Message *relates_to_message_raw = (Message*)relates_to; std::shared_ptr<Message> relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); - std::shared_ptr<Message> relates_to_message_original = get_edited_message_original_message(room, relates_to_message_shared); - if(!relates_to_message_original) { - fprintf(stderr, "Failed to get the original message for message with event id: %s\n", relates_to_message_raw->event_id.c_str()); - return PluginResult::ERR; - } char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -1274,7 +1715,7 @@ namespace QuickMedia { } rapidjson::Document relates_to_json(rapidjson::kObjectType); - relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_original->event_id.c_str()), relates_to_json.GetAllocator()); + relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), relates_to_json.GetAllocator()); relates_to_json.AddMember("rel_type", "m.replace", relates_to_json.GetAllocator()); std::string body_edit_str = " * " + body; @@ -1320,14 +1761,6 @@ namespace QuickMedia { return PluginResult::OK; } - // TODO: Right now this recursively calls /rooms/<room_id>/context/<event_id> and trusts server to not make it recursive. To make this robust, check iteration count and do not trust server. - // TODO: Optimize? - std::shared_ptr<Message> Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr<Message> message) { - if(!message || message->related_event_type != RelatedEventType::EDIT) - return message; - return get_edited_message_original_message(room_data, get_message_by_id(room_data, message->related_event_id)); - } - std::shared_ptr<Message> Matrix::get_message_by_id(RoomData *room, const std::string &event_id) { std::shared_ptr<Message> existing_room_message = room->get_message_by_id(event_id); if(existing_room_message) @@ -1373,15 +1806,11 @@ namespace QuickMedia { return new_message; } - // Returns empty string on error static const char* file_get_filename(const std::string &filepath) { size_t index = filepath.rfind('/'); if(index == std::string::npos) - return ""; - const char *filename = filepath.c_str() + index + 1; - if(filename[0] == '\0') - return ""; - return filename; + return filepath.c_str(); + return filepath.c_str() + index + 1; } PluginResult Matrix::post_file(RoomData *room, const std::string &filepath, std::string &err_msg) { @@ -1581,7 +2010,6 @@ 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(); diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp index 3fe6526..8b1efc7 100644 --- a/src/plugins/NyaaSi.cpp +++ b/src/plugins/NyaaSi.cpp @@ -51,7 +51,7 @@ namespace QuickMedia { size_t tbody_begin = website_data.find("<tbody>"); if(tbody_begin == std::string::npos) - return SearchResult::ERR; + return SearchResult::OK; size_t tbody_end = website_data.find("</tbody>", tbody_begin + 7); if(tbody_end == std::string::npos) diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp index c0e3fa1..f527e76 100644 --- a/src/plugins/Pornhub.cpp +++ b/src/plugins/Pornhub.cpp @@ -141,7 +141,7 @@ namespace QuickMedia { PluginResult PornhubSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique<PornhubVideoPage>(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<PornhubVideoPage>(program), nullptr}); return PluginResult::OK; } diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 12c156a..a157a8c 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -278,7 +278,7 @@ namespace QuickMedia { PluginResult YoutubeSearchPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) { (void)title; (void)url; - result_tabs.push_back(Tab{create_body(), std::make_unique<YoutubeVideoPage>(program), nullptr}); + result_tabs.push_back(Tab{nullptr, std::make_unique<YoutubeVideoPage>(program), nullptr}); return PluginResult::OK; } |