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 --- TODO | 3 +- plugins/Matrix.hpp | 11 +++- src/QuickMedia.cpp | 1 - src/plugins/Matrix.cpp | 154 ++++++++++++++++++++++++++++++++++--------------- 4 files changed, 118 insertions(+), 51 deletions(-) diff --git a/TODO b/TODO index 0a0f880..53a3d93 100644 --- a/TODO +++ b/TODO @@ -251,4 +251,5 @@ allow navigating to cross-post/dead thread with ctrl+i, fallback to 4chan archiv Add option to open youtube/4chan links in a new instance of quickmedia (maybe with ctrl+enter?). AAAAA Resizing 4chan post with no images but with replies (reactions) gives incorrect body item height compared to the rendered height. Some mangadex chapter images are .webp files, such as the images for chapter 0 for "the lolicon killer". Quickmedia doesn't work for that because quickmedia doesn't support manga webp files. Fix that. -Fully support all emoji, including the minimally-qualified ones. Sometimes twemoji doesn't have the correct sequence images. \ No newline at end of file +Fully support all emoji, including the minimally-qualified ones. Sometimes twemoji doesn't have the correct sequence images. +@room in rooms do not currently work (for show as notifications in quickmedia matrix). This is because quickmedia matrix doesn't fetch power level states and doesn't parse them. Quickmedia has code to parse room events power levels but it doesn't actually fetch the data so that part is never used. \ No newline at end of file diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index 7c2103c..4e3309f 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -67,6 +67,7 @@ namespace QuickMedia { std::string text_to_decrypt; }; + // TODO: Remove. Not needed anymore. struct TimestampedDisplayData { std::string data; time_t timestamp = 0; // In milliseconds @@ -83,6 +84,7 @@ namespace QuickMedia { RoomData *room; const mgl::Color display_name_color; const std::string user_id; + int power_level = 0; private: TimestampedDisplayData display_name; TimestampedDisplayData avatar_url; @@ -226,9 +228,9 @@ namespace QuickMedia { bool avatar_is_fallback = false; std::atomic_int64_t last_message_timestamp = 0; - std::atomic_int unread_notification_count = 0; std::atomic_int64_t read_marker_event_timestamp = 0; + int notification_power_level = 50; size_t index = 0; private: std::mutex user_mutex; @@ -324,6 +326,8 @@ namespace QuickMedia { virtual void remove_user(MatrixEventUserInfo user_info) = 0; virtual void set_user_info(MatrixEventUserInfo user_info) = 0; virtual void set_room_info(MatrixEventRoomInfo room_info) = 0; + + virtual void set_room_as_read(RoomData *room) = 0; }; class Matrix; @@ -356,7 +360,7 @@ namespace QuickMedia { void set_room_info(MatrixEventRoomInfo room_info) override; void for_each_user_in_room(RoomData *room, std::function callback); - void set_room_as_read(RoomData *room); + void set_room_as_read(RoomData *room) override; Program *program; Matrix *matrix; @@ -370,6 +374,7 @@ namespace QuickMedia { private: std::map> room_body_item_by_room; std::map> last_message_by_room; + std::map unread_mention_count_by_room; std::unordered_set notifications_shown; UsersByRoom users_by_room; }; @@ -736,6 +741,8 @@ namespace QuickMedia { void async_decrypt_message(std::shared_ptr decrypt_job); + MatrixDelegate* get_delegate(); + // Calls the |MatrixDelegate| pending events. // Should be called from the main (ui) thread void update(); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 9e360b7..c2b2b95 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -7788,7 +7788,6 @@ namespace QuickMedia { current_room->body_item->set_title_color(get_theme().text_color); current_room->last_message_read = true; // TODO: Maybe set this instead when the mention is visible on the screen? - current_room->unread_notification_count = 0; Message *read_message = get_latest_message_in_edit_chain(static_cast(body_items[last_timeline_message]->userdata)); // TODO: What if two messages have the same timestamp? 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