From ef1dd33682ae26b4af1343aaecf443e7cd883674 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 1 Oct 2020 22:21:14 +0200 Subject: Matrix: implement mention/reply notifications --- src/plugins/Matrix.cpp | 105 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 101 insertions(+), 4 deletions(-) (limited to 'src/plugins') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 6d89ff4..e182c11 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -54,7 +54,7 @@ namespace QuickMedia { char url[512]; if(next_batch.empty()) - snprintf(url, sizeof(url), "%s/_matrix/client/r0/sync?timeout=0&full_state=true", homeserver.c_str()); + 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()); @@ -312,9 +312,18 @@ namespace QuickMedia { room_it->second->prev_batch = prev_batch_json.asString(); } + // TODO: Is there no better way to check for notifications? this is not robust... + bool has_unread_notifications = false; + const Json::Value &unread_notification_json = (*it)["unread_notifications"]; + if(unread_notification_json.isObject()) { + const Json::Value &highlight_count_json = unread_notification_json["highlight_count"]; + if(highlight_count_json.isNumeric() && highlight_count_json.asInt64() > 0) + has_unread_notifications = true; + } + const Json::Value &events_json = timeline_json["events"]; events_add_user_info(events_json, room_it->second.get()); - events_add_messages(events_json, room_it->second.get(), MessageDirection::AFTER, &room_messages); + events_add_messages(events_json, room_it->second.get(), MessageDirection::AFTER, &room_messages, has_unread_notifications); events_set_room_name(events_json, room_it->second.get()); } @@ -400,13 +409,65 @@ namespace QuickMedia { return ""; } - void Matrix::events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages) { + // TODO: Is this really the proper way to check for username mentions? + static bool is_username_seperating_character(char c) { + switch(c) { + case ' ': + case '\n': + case '\t': + case '\v': + case '.': + case ',': + case '@': + case ':': + case '?': + case '!': + case '<': + case '>': + case '\0': + return true; + default: + return false; + } + return false; + } + + // 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 + static bool message_contains_user_mention(const std::string &msg, const std::string &username) { + if(msg.empty()) + return false; + + size_t index = 0; + while(index < msg.size()) { + size_t found_index = msg.find(username, index); + if(found_index == std::string::npos) + return false; + + char prev_char = ' '; + if(found_index > 0) + prev_char = msg[found_index - 1]; + + char next_char = '\0'; + if(found_index + username.size() < msg.size() - 1) + next_char = msg[found_index + username.size()]; + + if(is_username_seperating_character(prev_char) && is_username_seperating_character(next_char)) + return true; + + index += username.size(); + } + + return false; + } + + void Matrix::events_add_messages(const Json::Value &events_json, RoomData *room_data, MessageDirection message_dir, RoomSyncMessages *room_messages, bool has_unread_notifications) { if(!events_json.isArray()) return; std::vector> *room_sync_messages = nullptr; if(room_messages) room_sync_messages = &(*room_messages)[room_data]; + std::vector> new_messages; for(const Json::Value &event_item_json : events_json) { @@ -448,6 +509,11 @@ namespace QuickMedia { if(!body_json.isString()) continue; + time_t timestamp = 0; + const Json::Value &origin_server_ts = event_item_json["origin_server_ts"]; + if(origin_server_ts.isNumeric()) + timestamp = origin_server_ts.asInt64(); + std::string replaces_event_id; const Json::Value &relates_to_json = content_json["m.relates_to"]; if(relates_to_json.isObject()) { @@ -485,6 +551,10 @@ namespace QuickMedia { message->event_id = event_id_str; message->body = body_json.asString(); message->replaces_event_id = std::move(replaces_event_id); + // TODO: Is @room ok? shouldn't we also check if the user has permission to do @room? (only when notifications are limited to @mentions) + if(has_unread_notifications && !username.empty()) + message->mentions_me = message_contains_user_mention(message->body, username) || message_contains_user_mention(message->body, "@room"); + message->timestamp = timestamp; new_messages.push_back(message); room_data->message_by_event_id[event_id_str] = message; if(room_sync_messages) @@ -631,7 +701,7 @@ namespace QuickMedia { events_set_room_name(state_json, room_data); const Json::Value &chunk_json = json_root["chunk"]; - events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr); + events_add_messages(chunk_json, room_data, MessageDirection::BEFORE, nullptr, false); const Json::Value &end_json = json_root["end"]; if(!end_json.isString()) { @@ -1232,6 +1302,7 @@ namespace QuickMedia { json_root["homeserver"] = homeserver; this->user_id = user_id_json.asString(); + this->username = extract_user_name_from_user_id(this->user_id); this->access_token = access_token_json.asString(); this->homeserver = homeserver; @@ -1266,6 +1337,7 @@ namespace QuickMedia { // Make sure all fields are reset here! room_data_by_id.clear(); user_id.clear(); + username.clear(); access_token.clear(); homeserver.clear(); next_batch.clear(); @@ -1390,6 +1462,7 @@ namespace QuickMedia { } this->user_id = std::move(user_id); + this->username = extract_user_name_from_user_id(this->user_id); this->access_token = std::move(access_token); this->homeserver = std::move(homeserver); return PluginResult::OK; @@ -1440,6 +1513,30 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult Matrix::set_read_marker(const std::string &room_id, const Message *message) { + Json::Value request_data(Json::objectValue); + request_data["m.fully_read"] = message->event_id; + request_data["m.read"] = message->event_id; + request_data["m.hidden"] = false; // What is this for? element sends it but its not part of the documentation. Is it for hiding read receipt from other users? in that case, TODO: make it configurable + + Json::StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = ""; + + std::vector additional_args = { + { "-X", "POST" }, + { "-H", "content-type: application/json" }, + { "--data-binary", Json::writeString(builder, std::move(request_data)) }, + { "-H", "Authorization: Bearer " + access_token } + }; + + std::string server_response; + if(download_to_string(homeserver + "/_matrix/client/r0/rooms/" + room_id + "/read_markers", server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + return PluginResult::NET_ERR; + + return PluginResult::OK; + } + bool Matrix::was_message_posted_by_me(const std::string &room_id, void *message) const { auto room_it = room_data_by_id.find(room_id); if(room_it == room_data_by_id.end()) { -- cgit v1.2.3