From 16ca6e63f4fd1b407c826a5574dc20b3f9e71675 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 3 Nov 2020 01:07:57 +0100 Subject: Matrix: sync with filter, lazy member fetch (reducing sync time from 35 sec with huge server to 3 seconds) and cached fetch to 150ms). Properly show notifications for older messages. Reduce memory usage from 120mb to 13mb --- src/plugins/Matrix.cpp | 518 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 360 insertions(+), 158 deletions(-) (limited to 'src/plugins/Matrix.cpp') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index b7f911f..fd05399 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -17,7 +17,6 @@ // Show images/videos inline. // TODO: Verify if buffer of size 512 is enough for endpoints // Remove older messages (outside screen) to save memory. Reload them when the selected body item is the top/bottom one. -// 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"; @@ -82,7 +81,7 @@ namespace QuickMedia { } std::shared_ptr RoomData::get_user_by_id(const std::string &user_id) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); auto user_it = user_info_by_user_id.find(user_id); if(user_it == user_info_by_user_id.end()) return nullptr; @@ -90,22 +89,22 @@ namespace QuickMedia { } void RoomData::add_user(std::shared_ptr user) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); user_info_by_user_id.insert(std::make_pair(user->user_id, user)); } void RoomData::set_user_read_marker(std::shared_ptr &user, const std::string &event_id) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); user->read_marker_event_id = event_id; } std::string RoomData::get_user_read_marker(std::shared_ptr &user) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); return user->read_marker_event_id; } void RoomData::prepend_messages_reverse(const std::vector> &new_messages) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); @@ -115,7 +114,7 @@ namespace QuickMedia { } void RoomData::append_messages(const std::vector> &new_messages) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); for(auto it = new_messages.begin(); it != new_messages.end(); ++it) { if(message_by_event_id.find((*it)->event_id) == message_by_event_id.end()) { message_by_event_id.insert(std::make_pair((*it)->event_id, *it)); @@ -125,7 +124,7 @@ namespace QuickMedia { } std::shared_ptr RoomData::get_message_by_id(const std::string &id) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); auto message_it = message_by_event_id.find(id); if(message_it == message_by_event_id.end()) return nullptr; @@ -133,7 +132,7 @@ namespace QuickMedia { } std::vector> RoomData::get_users_excluding_me(const std::string &my_user_id) { - std::lock_guard lock(user_mutex); + std::lock_guard lock(user_mutex); std::vector> users_excluding_me; for(auto &[user_id, user] : user_info_by_user_id) { if(user->user_id != my_user_id) { @@ -160,52 +159,52 @@ namespace QuickMedia { } bool RoomData::has_prev_batch() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !prev_batch.empty(); } void RoomData::set_prev_batch(const std::string &new_prev_batch) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); prev_batch = new_prev_batch; } std::string RoomData::get_prev_batch() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return prev_batch; } bool RoomData::has_name() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !name.empty(); } void RoomData::set_name(const std::string &new_name) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); name = new_name; } std::string RoomData::get_name() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return name; } bool RoomData::has_avatar_url() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return !avatar_url.empty(); } void RoomData::set_avatar_url(const std::string &new_avatar_url) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); avatar_url = new_avatar_url; } std::string RoomData::get_avatar_url() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); return avatar_url; } void RoomData::set_pinned_events(std::vector new_pinned_events) { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); pinned_events = std::move(new_pinned_events); pinned_events_updated = true; } @@ -215,10 +214,11 @@ namespace QuickMedia { } void RoomData::clear_data() { - std::lock_guard lock(room_mutex); + std::lock_guard lock(room_mutex); fetched_messages_by_event_id.clear(); user_info_by_user_id.clear(); messages.clear(); + messages_read_index = 0; message_by_event_id.clear(); pinned_events.clear(); tags.clear(); @@ -292,6 +292,15 @@ namespace QuickMedia { invites_page->remove_body_item_by_room_id(room_id); } + void MatrixQuickMedia::add_unread_notification(RoomData *room, std::string event_id, std::string sender, std::string body) { + std::lock_guard lock(room_body_items_mutex); + Notification notification; + notification.event_id = std::move(event_id); + notification.sender = std::move(sender); + notification.body = std::move(body); + unread_notifications[room].push_back(std::move(notification)); + } + 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]; @@ -304,7 +313,7 @@ namespace QuickMedia { 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(body_item->userdata)->has_unread_mention || body_item.get() == item_to_swap) + if(static_cast(body_item->userdata)->unread_notification_count == 0 || body_item.get() == item_to_swap) return i; } return -1; @@ -314,14 +323,30 @@ namespace QuickMedia { std::sort(room_body_items.begin(), room_body_items.end(), [](const std::shared_ptr &body_item1, const std::shared_ptr &body_item2) { RoomData *room1 = static_cast(body_item1->userdata); RoomData *room2 = static_cast(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; + int room1_focus_sum = (int)(room1->unread_notification_count > 0) + (int)!room1->last_message_read; + int room2_focus_sum = (int)(room2->unread_notification_count > 0) + (int)!room2->last_message_read; return room1_focus_sum > room2_focus_sum; }); } void MatrixQuickMedia::update(MatrixPageType page_type) { update_pending_room_messages(page_type); + std::lock_guard room_body_lock(room_body_items_mutex); + bool is_window_focused = program->is_window_focused(); + RoomData *current_room = program->get_current_chat_room(); + for(auto &it : unread_notifications) { + if(!it.second.empty() && (!is_window_focused || it.first != current_room || page_type == MatrixPageType::ROOM_LIST)) { + for(auto &unread_notification : it.second) { + show_notification("QuickMedia matrix - " + unread_notification.sender + " (" + it.first->get_name() + ")", unread_notification.body); + } + } + update_room_description(it.first, false); + } + //if(!unread_notifications.empty()) { + // rooms_page->sort_rooms(); + // room_tags_page->sort_rooms(); + //} + unread_notifications.clear(); } void MatrixQuickMedia::clear_data() { @@ -332,6 +357,62 @@ namespace QuickMedia { rooms_page->clear_data(); room_tags_page->clear_data(); invites_page->clear_data(); + unread_notifications.clear(); + } + + void MatrixQuickMedia::update_room_description(RoomData *room, bool is_initial_sync) { + room->acquire_room_lock(); + const Messages &messages = room->get_messages_thread_unsafe(); + + time_t read_marker_message_timestamp = 0; + std::shared_ptr me = matrix->get_me(room); + 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; + } + } + if(!last_unread_message && !messages.empty() && messages.back()->timestamp > read_marker_message_timestamp) + last_unread_message = messages.back().get(); + + BodyItem *room_body_item = static_cast(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); + int unread_notification_count = room->unread_notification_count; + if(unread_notification_count > 0) + room_desc += "\n** " + std::to_string(unread_notification_count) + " unread mention(s) **"; // 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 && !messages.empty()) + last_message = messages.back().get(); + if(last_message) + room_body_item->set_description(matrix->message_get_author_displayname(last_message) + ": " + extract_first_line_elipses(last_message->body, 150)); + } + + room->release_room_lock(); } void MatrixQuickMedia::update_pending_room_messages(MatrixPageType page_type) { @@ -349,7 +430,6 @@ namespace QuickMedia { if(!it.second.sync_is_cache) { 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); @@ -357,48 +437,7 @@ namespace QuickMedia { } } - std::shared_ptr 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(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)); - } + update_room_description(room, is_initial_sync); } pending_room_messages.clear(); } @@ -443,6 +482,10 @@ namespace QuickMedia { body->clamp_selection(); body->append_items(std::move(room_body_items)); } + if(sort_on_update) { + sort_on_update = false; + sort_room_body_items(body->items); + } matrix_delegate->update(MatrixPageType::ROOM_LIST); } @@ -460,7 +503,7 @@ namespace QuickMedia { if(room_body_index != -1) { std::shared_ptr body_item = body->items[room_body_index]; int body_swap_index = -1; - if(room->has_unread_mention) + if(room->unread_notification_count > 0) 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()); @@ -493,6 +536,10 @@ namespace QuickMedia { current_chat_page->should_clear_data = true; } + void MatrixRoomsPage::sort_rooms() { + sort_on_update = true; + } + PluginResult MatrixRoomTagsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)title; std::lock_guard lock(mutex); @@ -607,6 +654,12 @@ namespace QuickMedia { current_rooms_page->clear_data(); } + void MatrixRoomTagsPage::sort_rooms() { + std::lock_guard lock(mutex); + if(current_rooms_page) + current_rooms_page->sort_rooms(); + } + MatrixInvitesPage::MatrixInvitesPage(Program *program, Matrix *matrix, Body *body) : Page(program), matrix(matrix), body(body) { } @@ -775,7 +828,7 @@ namespace QuickMedia { sync_is_cache = false; sync_running = true; - sync_thread = std::thread([this, delegate, matrix_cache_dir]() { + sync_thread = std::thread([this, matrix_cache_dir]() { sync_is_cache = true; FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), "rb"); if(sync_cache_file) { @@ -786,13 +839,24 @@ namespace QuickMedia { rapidjson::ParseResult parse_result = doc.ParseStream(is); if(parse_result.IsError()) break; - if(parse_sync_response(doc, delegate) != PluginResult::OK) + if(parse_sync_response(doc) != PluginResult::OK) fprintf(stderr, "Failed to parse cached sync response\n"); } fclose(sync_cache_file); } sync_is_cache = false; + // Filter with account data. TODO: Test if this is needed for encrypted chats + // {"presence":{"limit":0,"types":[""]},"account_data":{"not_types":["im.vector.setting.breadcrumbs","m.push_rules","im.vector.setting.allowed_widgets","io.element.recent_emoji"]},"room":{"state":{"limit":1,"not_types":["m.room.related_groups","m.room.power_levels","m.room.join_rules","m.room.history_visibility"],"lazy_load_members":true},"timeline":{"limit":3,"lazy_load_members":true},"ephemeral":{"limit":0,"types":[""],"lazy_load_members":true},"account_data":{"limit":1,"types":["m.fully_read"],"lazy_load_members":true}}} + // Filter without account data + const char *filter = "{\"presence\":{\"limit\":0,\"types\":[\"\"]},\"account_data\":{\"limit\":0,\"types\":[\"\"]},\"room\":{\"state\":{\"not_types\":[\"m.room.related_groups\",\"m.room.power_levels\",\"m.room.join_rules\",\"m.room.history_visibility\"],\"lazy_load_members\":true},\"timeline\":{\"limit\":3,\"lazy_load_members\":true},\"ephemeral\":{\"limit\":0,\"types\":[\"\"],\"lazy_load_members\":true},\"account_data\":{\"limit\":1,\"types\":[\"m.fully_read\",\"m.tag\"],\"lazy_load_members\":true}}}"; + const std::string filter_encoded = url_param_encode(filter); + + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token }, + { "-m", "35" } + }; + const rapidjson::Value *next_batch_json; PluginResult result; bool initial_sync = true; @@ -802,40 +866,43 @@ namespace QuickMedia { { "-m", "35" } }; - char url[512]; + char url[1024]; if(next_batch.empty()) - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0", homeserver.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?filter=%s&timeout=0", homeserver.c_str(), filter_encoded.c_str()); else - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=30000&since=%s", homeserver.c_str(), next_batch.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?filter=%s&timeout=30000&since=%s", homeserver.c_str(), filter_encoded.c_str(), next_batch.c_str()); rapidjson::Document json_root; std::string err_msg; - DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); + DownloadResult download_result = download_json(json_root, url, additional_args, true, &err_msg); if(download_result != DownloadResult::OK) { fprintf(stderr, "/sync failed\n"); - if(initial_sync && json_root.IsObject()) { - const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); - if(errcode_json.IsString()) { - for(const char *sync_fail_error_code : sync_fail_error_codes) { - if(strcmp(errcode_json.GetString(), sync_fail_error_code) == 0) { - sync_fail_reason = sync_fail_error_code; - const rapidjson::Value &error_json = GetMember(json_root, "error"); - if(error_json.IsString()) - sync_fail_reason = error_json.GetString(); - sync_failed = true; - sync_running = false; - break; - } + goto sync_end; + } + + if(initial_sync && json_root.IsObject()) { + const rapidjson::Value &errcode_json = GetMember(json_root, "errcode"); + if(errcode_json.IsString()) { + for(const char *sync_fail_error_code : sync_fail_error_codes) { + if(strcmp(errcode_json.GetString(), sync_fail_error_code) == 0) { + sync_fail_reason = sync_fail_error_code; + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) + sync_fail_reason = error_json.GetString(); + sync_failed = true; + sync_running = false; + break; } } + fprintf(stderr, "/sync failed\n"); + goto sync_end; } - goto sync_end; } if(next_batch.empty()) clear_sync_cache_for_new_sync(); - result = parse_sync_response(json_root, delegate); + result = parse_sync_response(json_root); if(result != PluginResult::OK) { fprintf(stderr, "Failed to parse sync response\n"); goto sync_end; @@ -850,22 +917,44 @@ namespace QuickMedia { fprintf(stderr, "Matrix: missing next batch\n"); } + if(initial_sync) { + notification_thread = std::thread([this]() { + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + // TODO: Instead of guessing notification limit with 100, accumulate rooms unread_notifications count and use that as the limit + // (and take into account that notification response may have notifications after call to sync above). + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/notifications?limit=100", homeserver.c_str()); + + rapidjson::Document json_root; + DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true); + if(download_result != DownloadResult::OK || !json_root.IsObject()) { + fprintf(stderr, "Fetching notifications failed!\n"); + return; + } + + const rapidjson::Value ¬ification_json = GetMember(json_root, "notifications"); + parse_notifications(notification_json); + }); + } + sync_end: if(sync_running) - std::this_thread::sleep_for(std::chrono::seconds(1)); - - if(!json_root.IsObject()) - continue; + std::this_thread::sleep_for(std::chrono::milliseconds(500)); // TODO: Circulate file FILE *sync_cache_file = fopen(matrix_cache_dir.data.c_str(), initial_sync ? "wb" : "ab"); initial_sync = false; if(sync_cache_file) { - char buffer[4096]; - rapidjson::FileWriteStream file_write_stream(sync_cache_file, buffer, sizeof(buffer)); - rapidjson::Writer writer(file_write_stream); - remove_unused_sync_data_fields(json_root); - json_root.Accept(writer); + if(json_root.IsObject()) { + char buffer[4096]; + rapidjson::FileWriteStream file_write_stream(sync_cache_file, buffer, sizeof(buffer)); + rapidjson::Writer writer(file_write_stream); + remove_unused_sync_data_fields(json_root); + json_root.Accept(writer); + } fclose(sync_cache_file); } } @@ -874,9 +963,17 @@ namespace QuickMedia { void Matrix::stop_sync() { sync_running = false; - program_kill_in_thread(sync_thread.get_id()); - if(sync_thread.joinable()) + + if(sync_thread.joinable()) { + program_kill_in_thread(sync_thread.get_id()); sync_thread.join(); + } + + if(notification_thread.joinable()) { + program_kill_in_thread(notification_thread.get_id()); + notification_thread.join(); + } + delegate = nullptr; sync_failed = false; sync_fail_reason.clear(); @@ -903,6 +1000,7 @@ namespace QuickMedia { room->messages_read_index = room_messages.size(); } else { fprintf(stderr, "Unexpected behavior!!!! get_room_sync_data said read index is %zu but we only have %zu messages\n", room->messages_read_index, room_messages.size()); + room->messages_read_index = room_messages.size(); } if(room->pinned_events_updated) { sync_data.pinned_events = room->get_pinned_events_unsafe(); @@ -938,22 +1036,75 @@ namespace QuickMedia { 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; + assert(room->messages_read_index <= room->get_messages_thread_unsafe().size()); room->release_room_lock(); return PluginResult::OK; } - PluginResult Matrix::parse_sync_response(const rapidjson::Document &root, MatrixDelegate *delegate) { + PluginResult Matrix::parse_sync_response(const rapidjson::Document &root) { if(!root.IsObject()) return PluginResult::ERR; - const rapidjson::Value &account_data_json = GetMember(root, "account_data"); - std::optional> dm_rooms; - parse_sync_account_data(account_data_json, dm_rooms); + //const rapidjson::Value &account_data_json = GetMember(root, "account_data"); + //std::optional> 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); + parse_sync_room_data(rooms_json); + + return PluginResult::OK; + } + + PluginResult Matrix::parse_notifications(const rapidjson::Value ¬ifications_json) { + if(!notifications_json.IsArray()) + return PluginResult::ERR; + for(const rapidjson::Value ¬ification_json : notifications_json.GetArray()) { + if(!notification_json.IsObject()) + continue; + + const rapidjson::Value &read_json = GetMember(notification_json, "read"); + if(!read_json.IsBool() || read_json.GetBool()) + continue; + + const rapidjson::Value &room_id_json = GetMember(notification_json, "room_id"); + if(!room_id_json.IsString()) + continue; + + const rapidjson::Value &event_json = GetMember(notification_json, "event"); + if(!event_json.IsObject()) + continue; + + const rapidjson::Value &event_id_json = GetMember(event_json, "event_id"); + if(!event_id_json.IsString()) + continue; + + const rapidjson::Value &sender_json = GetMember(event_json, "sender"); + if(!sender_json.IsString()) + continue; + + const rapidjson::Value &content_json = GetMember(event_json, "content"); + if(!content_json.IsObject()) + continue; + + const rapidjson::Value &body_json = GetMember(content_json, "body"); + if(!body_json.IsString()) + continue; + + std::string room_id(room_id_json.GetString(), room_id_json.GetStringLength()); + RoomData *room = get_room_by_id(room_id); + if(!room) { + fprintf(stderr, "Warning: got notification in unknown room %s\n", room_id.c_str()); + continue; + } + room->unread_notification_count++; + + std::string event_id(event_id_json.GetString(), event_id_json.GetStringLength()); + std::string sender(sender_json.GetString(), sender_json.GetStringLength()); + std::string body(body_json.GetString(), body_json.GetStringLength()); + delegate->add_unread_notification(room, std::move(event_id), std::move(sender), std::move(body)); + } return PluginResult::OK; } @@ -999,7 +1150,7 @@ namespace QuickMedia { return PluginResult::OK; } - PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json, MatrixDelegate *delegate) { + PluginResult Matrix::parse_sync_room_data(const rapidjson::Value &rooms_json) { if(!rooms_json.IsObject()) return PluginResult::OK; @@ -1033,7 +1184,7 @@ namespace QuickMedia { events_add_pinned_events(events_json, room); } - const rapidjson::Value &ephemeral_json = GetMember(it.value, "ephemeral"); + const rapidjson::Value &account_data_json = GetMember(it.value, "account_data"); const rapidjson::Value &timeline_json = GetMember(it.value, "timeline"); if(timeline_json.IsObject()) { @@ -1047,27 +1198,38 @@ namespace QuickMedia { // TODO: Use /_matrix/client/r0/notifications ? or remove this and always look for displayname/user_id in messages bool has_unread_notifications = false; const rapidjson::Value &unread_notification_json = GetMember(it.value, "unread_notifications"); - if(unread_notification_json.IsObject()) { + if(unread_notification_json.IsObject() && is_initial_sync_finished()) { const rapidjson::Value &highlight_count_json = GetMember(unread_notification_json, "highlight_count"); - if(highlight_count_json.IsNumber() && highlight_count_json.GetInt64() > 0) + if(highlight_count_json.IsNumber() && highlight_count_json.GetInt64() > 0) { + room->unread_notification_count = highlight_count_json.GetInt64(); has_unread_notifications = true; + } } const rapidjson::Value &events_json = GetMember(timeline_json, "events"); events_add_user_info(events_json, room); events_set_room_name(events_json, room); - // We want to do this before adding messages to know if a message that mentions us is a new mention - if(ephemeral_json.IsObject()) { - const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); - events_add_user_read_markers(events_json, room); + + if(account_data_json.IsObject()) { + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + auto me = get_me(room); + events_set_user_read_marker(events_json, room, me); } - events_add_messages(events_json, room, MessageDirection::AFTER, delegate, has_unread_notifications); + + if(is_new_room) + delegate->join_room(room); + + events_add_messages(events_json, room, MessageDirection::AFTER, has_unread_notifications); events_add_pinned_events(events_json, room); } else { - if(ephemeral_json.IsObject()) { - const rapidjson::Value &events_json = GetMember(ephemeral_json, "events"); - events_add_user_read_markers(events_json, room); + if(account_data_json.IsObject()) { + const rapidjson::Value &events_json = GetMember(account_data_json, "events"); + auto me = get_me(room); + events_set_user_read_marker(events_json, room, me); } + + if(is_new_room) + delegate->join_room(room); } if(remove_invite(room_id_str)) { @@ -1075,13 +1237,9 @@ namespace QuickMedia { delegate->remove_invite(room_id_str); } - if(is_new_room) - delegate->join_room(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, delegate); + events_add_room_to_tags(events_json, room); } if(is_new_room) { @@ -1097,10 +1255,10 @@ namespace QuickMedia { } const rapidjson::Value &leave_json = GetMember(rooms_json, "leave"); - remove_rooms(leave_json, delegate); + remove_rooms(leave_json); const rapidjson::Value &invite_json = GetMember(rooms_json, "invite"); - add_invites(invite_json, delegate); + add_invites(invite_json); return PluginResult::OK; } @@ -1158,7 +1316,7 @@ namespace QuickMedia { if(strncmp(user_info->avatar_url.c_str(), "mxc://", 6) == 0) user_info->avatar_url.erase(user_info->avatar_url.begin(), user_info->avatar_url.begin() + 6); if(!user_info->avatar_url.empty()) - user_info->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + user_info->avatar_url + "?width=32&height=32&method=crop"; + user_info->avatar_url = homeserver + "/_matrix/media/r0/thumbnail/" + user_info->avatar_url + "?width=32&height=32&method=crop"; // TODO: Remove the constant strings around to reduce memory usage (6.3mb) user_info->display_name = display_name_json.IsString() ? display_name_json.GetString() : sender_json_str; user_info->display_name_color = user_id_to_color(sender_json_str); @@ -1217,6 +1375,31 @@ namespace QuickMedia { } } + void Matrix::events_set_user_read_marker(const rapidjson::Value &events_json, RoomData *room_data, std::shared_ptr &me) { + assert(me); // TODO: Remove read marker from user and set it for the room instead. We need that in the matrix pages also + if(!events_json.IsArray() || !me) + return; + + for(const rapidjson::Value &event_json : events_json.GetArray()) { + if(!event_json.IsObject()) + continue; + + const rapidjson::Value &type_json = GetMember(event_json, "type"); + if(!type_json.IsString() || strcmp(type_json.GetString(), "m.fully_read") != 0) + continue; + + const rapidjson::Value &content_json = GetMember(event_json, "content"); + if(!content_json.IsObject()) + continue; + + const rapidjson::Value &event_id_json = GetMember(content_json, "event_id"); + if(!event_id_json.IsString()) + continue; + + room_data->set_user_read_marker(me, std::string(event_id_json.GetString(), event_id_json.GetStringLength())); + } + } + static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver) { const rapidjson::Value &info_json = GetMember(content_json, "info"); if(info_json.IsObject()) { @@ -1323,7 +1506,7 @@ namespace QuickMedia { return false; } - void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, MatrixDelegate *delegate, bool has_unread_notifications) { + void Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, bool has_unread_notifications) { if(!events_json.IsArray()) return; @@ -1346,18 +1529,20 @@ namespace QuickMedia { room_data->append_messages(new_messages); } - time_t read_marker_message_timestamp = 0; - if(me) { - auto read_marker_message = room_data->get_message_by_id(room_data->get_user_read_marker(me)); - if(read_marker_message) - read_marker_message_timestamp = read_marker_message->timestamp; - } + if(is_initial_sync_finished()) { + time_t read_marker_message_timestamp = 0; + if(me) { + auto read_marker_message = room_data->get_message_by_id(room_data->get_user_read_marker(me)); + if(read_marker_message) + read_marker_message_timestamp = read_marker_message->timestamp; + } - for(auto &message : new_messages) { - // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) - // TODO: Is comparing against read marker timestamp ok enough? - 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"); + for(auto &message : new_messages) { + // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) + // TODO: Is comparing against read marker timestamp ok enough? + 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) @@ -1426,7 +1611,7 @@ namespace QuickMedia { content_json = &new_content_json; const rapidjson::Value &content_type = GetMember(*content_json, "msgtype"); - if(!content_type.IsString() || strcmp(type_json.GetString(), "m.room.redaction") == 0) { + if(strcmp(type_json.GetString(), "m.room.redaction") == 0) { auto message = std::make_shared(); message->type = MessageType::REDACTION; message->user = user; @@ -1460,7 +1645,7 @@ namespace QuickMedia { // TODO: Also show joins, leave, invites, bans, kicks, mutes, etc - if(strcmp(content_type.GetString(), "m.text") == 0) { + if(!content_type.IsString() || strcmp(content_type.GetString(), "m.text") == 0) { message->type = MessageType::TEXT; } else if(strcmp(content_type.GetString(), "m.image") == 0) { const rapidjson::Value &url_json = GetMember(*content_json, "url"); @@ -1665,7 +1850,7 @@ namespace QuickMedia { room_data->set_pinned_events(std::move(pinned_events)); } - void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data, MatrixDelegate *delegate) { + void Matrix::events_add_room_to_tags(const rapidjson::Value &events_json, RoomData *room_data) { if(!events_json.IsArray()) return; @@ -1730,7 +1915,7 @@ namespace QuickMedia { } } - void Matrix::add_invites(const rapidjson::Value &invite_json, MatrixDelegate *delegate) { + void Matrix::add_invites(const rapidjson::Value &invite_json) { if(!invite_json.IsObject()) return; @@ -1801,7 +1986,7 @@ namespace QuickMedia { } } - void Matrix::remove_rooms(const rapidjson::Value &leave_json, MatrixDelegate *delegate) { + void Matrix::remove_rooms(const rapidjson::Value &leave_json) { if(!leave_json.IsObject()) return; @@ -1917,12 +2102,12 @@ namespace QuickMedia { if(!json_root.IsObject()) return PluginResult::ERR; - //const rapidjson::Value &state_json = GetMember(json_root, "state"); - //events_add_user_info(state_json, room_data); - //events_set_room_name(state_json, room_data); + const rapidjson::Value &state_json = GetMember(json_root, "state"); + events_add_user_info(state_json, room_data); + events_set_room_name(state_json, room_data); const rapidjson::Value &chunk_json = GetMember(json_root, "chunk"); - events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); + events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, false); const rapidjson::Value &end_json = GetMember(json_root, "end"); if(!end_json.IsString()) { @@ -2154,7 +2339,6 @@ namespace QuickMedia { PluginResult Matrix::post_reply(RoomData *room, const std::string &body, void *relates_to) { // TODO: Store shared_ptr instead of raw pointer... Message *relates_to_message_raw = (Message*)relates_to; - std::shared_ptr relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -2163,7 +2347,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_shared->event_id.c_str()), in_reply_to_json.GetAllocator()); + in_reply_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_raw->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()); @@ -2209,7 +2393,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 relates_to_message_shared = room->get_message_by_id(relates_to_message_raw->event_id); char random_characters[18]; if(!generate_random_characters(random_characters, sizeof(random_characters))) @@ -2246,7 +2429,7 @@ namespace QuickMedia { } rapidjson::Document relates_to_json(rapidjson::kObjectType); - relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_shared->event_id.c_str()), relates_to_json.GetAllocator()); + relates_to_json.AddMember("event_id", rapidjson::StringRef(relates_to_message_raw->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; @@ -2300,29 +2483,47 @@ namespace QuickMedia { auto fetched_message_it = room->fetched_messages_by_event_id.find(event_id); if(fetched_message_it != room->fetched_messages_by_event_id.end()) return fetched_message_it->second; + + rapidjson::Document request_data(rapidjson::kObjectType); + request_data.AddMember("lazy_load_members", true, request_data.GetAllocator()); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + request_data.Accept(writer); std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token } }; + std::string filter = url_param_encode(buffer.GetString()); + char url[512]; - snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/event/%s", homeserver.c_str(), room->id.c_str(), event_id.c_str()); + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room->id.c_str(), event_id.c_str(), filter.c_str()); std::string err_msg; rapidjson::Document json_root; DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg); if(download_result != DownloadResult::OK) return nullptr; - if(json_root.IsObject()) { - const rapidjson::Value &error_json = GetMember(json_root, "error"); - if(error_json.IsString()) { - fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); - room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); - return nullptr; - } + if(!json_root.IsObject()) { + fprintf(stderr, "Failed to get message by id %s, error: %s\n", event_id.c_str(), err_msg.c_str()); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); + return nullptr; } - std::shared_ptr new_message = parse_message_event(json_root, room); + const rapidjson::Value &error_json = GetMember(json_root, "error"); + if(error_json.IsString()) { + fprintf(stderr, "Matrix::get_message_by_id, error: %s\n", error_json.GetString()); + room->fetched_messages_by_event_id.insert(std::make_pair(event_id, nullptr)); + return nullptr; + } + + const rapidjson::Value &state_json = GetMember(json_root, "state"); + events_add_user_info(state_json, room); + events_set_room_name(state_json, room); + + const rapidjson::Value &event_json = GetMember(json_root, "event"); + std::shared_ptr new_message = parse_message_event(event_json, room); room->fetched_messages_by_event_id.insert(std::make_pair(event_id, new_message)); return new_message; } @@ -2443,6 +2644,7 @@ namespace QuickMedia { } PluginResult Matrix::login(const std::string &username, const std::string &password, const std::string &homeserver, std::string &err_msg) { + assert(!sync_running); rapidjson::Document identifier_json(rapidjson::kObjectType); identifier_json.AddMember("type", "m.id.user", identifier_json.GetAllocator()); // TODO: What if the server doesn't support this login type? redirect to sso web page etc identifier_json.AddMember("user", rapidjson::StringRef(username.c_str()), identifier_json.GetAllocator()); -- cgit v1.2.3