aboutsummaryrefslogtreecommitdiff
path: root/src/plugins
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2020-10-26 09:48:25 +0100
committerdec05eba <dec05eba@protonmail.com>2020-10-29 04:21:15 +0100
commit620123fbd6c18dc48a25cc735565f6d8d85f8639 (patch)
tree1563c8d2867f80f7c5cf00c15c8a1b6612de9f67 /src/plugins
parent0d432776c13f7b7bfd94d8ea2a7a41be33f21c8d (diff)
Matrix: add room tags
Fix pinned events that are added after starting QuickMedia (before this change it adds all elements again to the list). Add /me command. Other fixes...
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/Mangadex.cpp2
-rw-r--r--src/plugins/Manganelo.cpp2
-rw-r--r--src/plugins/Mangatown.cpp2
-rw-r--r--src/plugins/Matrix.cpp644
-rw-r--r--src/plugins/NyaaSi.cpp2
-rw-r--r--src/plugins/Pornhub.cpp2
-rw-r--r--src/plugins/Youtube.cpp2
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;
}