From 3bfa7ea4beac7710ac5484c46ce181027131ebf8 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 12 Jan 2023 22:54:07 +0100 Subject: Matrix: do not trust synapse when it comes to unread messages --- src/plugins/Matrix.cpp | 154 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 47 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 0da73eb..1734fb0 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -648,13 +648,21 @@ namespace QuickMedia { if(message->notification_mentions_me) { std::string body = remove_reply_formatting(matrix, message->body); bool read = true; + bool count_unread = 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) && message->related_event_type != RelatedEventType::EDIT && message->related_event_type != RelatedEventType::REDACTION) { - if(notifications_shown.insert(message->event_id).second) + if(notifications_shown.insert(message->event_id).second) { show_notification("QuickMedia matrix - " + extract_first_line_remove_newline_elipses(matrix->message_get_author_displayname(message.get()), AUTHOR_MAX_LENGTH) + " (" + room->get_name() + ")", body); + } else { + count_unread = false; + } read = false; } + if(count_unread) + ++unread_mention_count_by_room[room]; + MatrixNotification notification; notification.room = room; notification.event_id = message->event_id; @@ -694,8 +702,11 @@ namespace QuickMedia { } void MatrixQuickMedia::add_unread_notification(MatrixNotification notification) { - if(notifications_shown.insert(notification.event_id).second) + if(notifications_shown.insert(notification.event_id).second) { show_notification("QuickMedia matrix - " + notification.sender_user_id + " (" + notification.room->get_name() + ")", notification.body); + ++unread_mention_count_by_room[notification.room]; + update_room_description(notification.room, {}, false); + } } void MatrixQuickMedia::add_user(MatrixEventUserInfo user_info) { @@ -756,6 +767,11 @@ namespace QuickMedia { void MatrixQuickMedia::set_room_as_read(RoomData *room) { notifications_page->set_room_as_read(room); + int &unread_mention_count = unread_mention_count_by_room[room]; + if(unread_mention_count > 0) { + unread_mention_count = 0; + update_room_description(room, {}, false); + } } static void sort_room_body_items(std::vector> &room_body_items) { @@ -903,7 +919,7 @@ namespace QuickMedia { bool unread_mentions = false; std::string room_desc; - const int unread_notification_count = room->unread_notification_count; + const int unread_notification_count = unread_mention_count_by_room[room]; if(unread_notification_count > 0 && set_room_as_unread) { unread_mentions = true; room_desc += "** " + std::to_string(unread_notification_count) + " unread mention(s) **"; // TODO: Better notification? @@ -1794,6 +1810,9 @@ namespace QuickMedia { filter_encoded = url_param_encode(CONTINUE_FILTER); // TODO: limit messages in this continue filter? // TODO: This ignores new rooms that are not part of the previous sync message. Fix this. + // This is needed for system notifications for new messages because on initial sync + // there might be messages that were sent 200 messages ago that mentioned us and such + // messages wont be part of the intial /sync, so we want to see such messages anyways. notification_thread = std::thread([this, initial_sync_show_notifications]() { get_previous_notifications([this, initial_sync_show_notifications](const MatrixNotification ¬ification) { if(!initial_sync_show_notifications || notification.read) @@ -2109,18 +2128,35 @@ namespace QuickMedia { RoomData *room = get_room_by_id(room_id); if(!room) { //fprintf(stderr, "Warning: got notification in unknown room %s\n", room_id.c_str()); + // TODO: continue; } std::string event_id(event_id_json.GetString(), event_id_json.GetStringLength()); if(notifications_by_event_id.insert(event_id).second) { + auto me = 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; + } + + bool actually_read = read_json.GetBool(); + // TODO: Make sure |events_set_user_read_marker| is called before |events_add_messages| so this is set + const int64_t qm_read_marker = room->read_marker_event_timestamp; + if(read_marker_message_timestamp == 0 || read_marker_message_timestamp < qm_read_marker) { + read_marker_message_timestamp = qm_read_marker; + actually_read = read_marker_message_timestamp >= timestamp; + } + MatrixNotification notification; notification.room = room; notification.event_id = std::move(event_id); notification.sender_user_id.assign(sender_json.GetString(), sender_json.GetStringLength()); notification.body = remove_reply_formatting(this, body_json.GetString()); notification.timestamp = timestamp; - notification.read = read_json.GetBool(); + notification.read = actually_read; //read_json.GetBool(); // Intentionally ignore servers read_json value because it's invalid because of synapse cache bug callback_func(notification); notifications.push_back(std::move(notification)); } @@ -2279,11 +2315,8 @@ namespace QuickMedia { const rapidjson::Value &unread_notification_json = GetMember(it.value, "unread_notifications"); if(unread_notification_json.IsObject()) { const rapidjson::Value &highlight_count_json = GetMember(unread_notification_json, "highlight_count"); - if(highlight_count_json.IsInt64() && (highlight_count_json.GetInt64() > 0 || initial_sync)) { - room->unread_notification_count = highlight_count_json.GetInt64(); - if(highlight_count_json.GetInt64() > 0) - has_unread_notifications = true; - } + if(highlight_count_json.IsInt64() && highlight_count_json.GetInt64() > 0) + has_unread_notifications = true; } const rapidjson::Value &events_json = GetMember(timeline_json, "events"); @@ -2545,6 +2578,24 @@ namespace QuickMedia { return ""; } + // TODO: Custom names for power levels + static std::string power_level_to_name(int power_level) { + switch(power_level) { + case 0: + return "Default"; + case 50: + return "Moderator"; + case 100: + return "Administrator"; + default: + return "Custom (" + std::to_string(power_level) + ")"; + } + } + + static bool power_level_is_at_least_admin(int power_level, RoomData *room) { + return power_level >= room->notification_power_level; + } + // TODO: Is this really the proper way to check for username mentions? static bool is_username_seperating_character(char c) { switch(c) { @@ -2606,12 +2657,12 @@ namespace QuickMedia { bool message_contains_user_mention(Matrix *matrix, const Message *message, const std::string &username, const std::string &user_id) { const std::string formatted_text = message_to_qm_text(matrix, message, false); - return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id); + return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id) || (power_level_is_at_least_admin(message->user->power_level, message->user->room) && message_contains_user_mention(formatted_text, "@room")); } bool message_contains_user_mention(const BodyItem *body_item, const std::string &username, const std::string &user_id) { const std::string formatted_text = Text::to_printable_string(body_item->get_description()); - return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id); + return message_contains_user_mention(formatted_text, username) || message_contains_user_mention(formatted_text, user_id) || (body_item->userdata && power_level_is_at_least_admin(static_cast(body_item->userdata)->user->power_level, static_cast(body_item->userdata)->user->room) && message_contains_user_mention(formatted_text, "®room")); } bool message_is_timeline(Message *message) { @@ -2627,6 +2678,8 @@ namespace QuickMedia { }); } + // Note: has_unread_notifications cant really be trusted because it's from synapse and synapse can return old cached data... + // But its better than nothing when its the first time we launch quickmedia (or the first time we see a room) and have no read markers in a room. size_t Matrix::events_add_messages(const rapidjson::Value &events_json, RoomData *room_data, MessageDirection message_dir, bool has_unread_notifications) { if(!events_json.IsArray()) return 0; @@ -2652,27 +2705,30 @@ namespace QuickMedia { num_new_messages = room_data->append_messages(new_messages); } - if(has_unread_notifications) { - 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; - } + 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; + } - // TODO: Make sure |events_set_user_read_marker| is called before |events_add_messages| so this is set - const int64_t qm_read_marker = room_data->read_marker_event_timestamp; - if(read_marker_message_timestamp == 0 || read_marker_message_timestamp < qm_read_marker) - read_marker_message_timestamp = qm_read_marker; - - std::lock_guard lock(notifications_mutex); - 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(message_is_timeline(message.get()) && me && message->timestamp > read_marker_message_timestamp) { - std::string message_str = message_to_qm_text(this, message.get(), false); - message->notification_mentions_me = message_contains_user_mention(message_str, my_display_name) || message_contains_user_mention(message_str, me->user_id) || message_contains_user_mention(message_str, "@room"); - } + // TODO: Make sure |events_set_user_read_marker| is called before |events_add_messages| so this is set + const int64_t qm_read_marker = room_data->read_marker_event_timestamp; + if(read_marker_message_timestamp == 0 || read_marker_message_timestamp < qm_read_marker) + read_marker_message_timestamp = qm_read_marker; + + const bool may_have_unread_notifications = read_marker_message_timestamp > 0 || has_unread_notifications; + + std::lock_guard lock(notifications_mutex); + 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(message_is_timeline(message.get()) && me && message->timestamp > read_marker_message_timestamp && may_have_unread_notifications) { + std::string message_str = message_to_qm_text(this, message.get(), false); + message->notification_mentions_me = message_contains_user_mention(message_str, my_display_name) + || message_contains_user_mention(message_str, me->user_id) + || (has_unread_notifications && message_contains_user_mention(message_str, "@room")); // TODO: ... + //|| (power_level_is_at_least_admin(message->user->power_level, room_data) && message_contains_user_mention(message_str, "@room")); } } @@ -2684,20 +2740,6 @@ namespace QuickMedia { return num_new_messages; } - // TODO: Custom names for power levels - static std::string power_level_to_name(int power_level) { - switch(power_level) { - case 0: - return "Default"; - case 50: - return "Moderator"; - case 100: - return "Administrator"; - default: - return "Custom (" + std::to_string(power_level) + ")"; - } - } - struct UserPowerLevelChange { int new_power_level = 0; int old_power_level = 0; @@ -3188,6 +3230,15 @@ namespace QuickMedia { message->transaction_id = std::move(transaction_id); return message; } else if(strcmp(type_json.GetString(), "m.room.power_levels") == 0) { + const rapidjson::Value ¬ifications_json = GetMember(*content_json, "notifications"); + if(notifications_json.IsObject()) { + const rapidjson::Value &room_json = GetMember(notifications_json, "room"); + if(room_json.IsInt()) + room_data->notification_power_level = room_json.GetInt(); + } + + // TODO: Implement users_default + const rapidjson::Value &users_json = GetMember(*content_json, "users"); if(!users_json.IsObject()) return nullptr; @@ -3196,7 +3247,10 @@ namespace QuickMedia { for(auto const &user_json : users_json.GetObject()) { if(!user_json.name.IsString() || !user_json.value.IsInt()) continue; - power_level_changes[std::string(user_json.name.GetString(), user_json.name.GetStringLength())].new_power_level = user_json.value.GetInt(); + + std::string user_id(user_json.name.GetString(), user_json.name.GetStringLength()); + get_user_by_id(room_data, user_id)->power_level = user_json.value.GetInt(); + power_level_changes[std::move(user_id)].new_power_level = user_json.value.GetInt(); } if(unsigned_json.IsObject()) { @@ -3208,7 +3262,9 @@ namespace QuickMedia { for(auto const &user_json : prev_content_users_json.GetObject()) { if(!user_json.name.IsString() || !user_json.value.IsInt()) continue; - power_level_changes[std::string(user_json.name.GetString(), user_json.name.GetStringLength())].old_power_level = user_json.value.GetInt(); + + std::string user_id(user_json.name.GetString(), user_json.name.GetStringLength()); + power_level_changes[std::move(user_id)].old_power_level = user_json.value.GetInt(); } } } @@ -4049,6 +4105,10 @@ namespace QuickMedia { decrypt_task.push(std::move(decrypt_job)); } + MatrixDelegate* Matrix::get_delegate() { + return delegate; + } + PluginResult Matrix::post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional &file_info, const std::optional &thumbnail_info, const std::string &msgtype, const std::string &custom_transaction_id) { std::string transaction_id = custom_transaction_id; if(transaction_id.empty()) -- cgit v1.2.3