From 9602603135f456d906192112288dcd84429c8fee Mon Sep 17 00:00:00 2001 From: dec05eba Date: Wed, 30 Sep 2020 22:05:41 +0200 Subject: Matrix: implement message editing --- src/plugins/Matrix.cpp | 271 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 242 insertions(+), 29 deletions(-) (limited to 'src/plugins/Matrix.cpp') diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 50be2de..b93164e 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -140,6 +140,7 @@ namespace QuickMedia { auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); + room_data->id = room_id_json.asString(); room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_name = room_id_str; fprintf(stderr, "Missing room %s from /sync, adding in joined_rooms\n", room_id_str.c_str()); @@ -268,6 +269,7 @@ namespace QuickMedia { auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); + room_data->id = room_id_str; room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_it = room_data_by_id.find(room_id_str); // TODO: Get iterator from above insert } @@ -294,6 +296,7 @@ namespace QuickMedia { auto room_it = room_data_by_id.find(room_id_str); if(room_it == room_data_by_id.end()) { auto room_data = std::make_unique(); + room_data->id = room_id_str; room_data_by_id.insert(std::make_pair(room_id_str, std::move(room_data))); room_it = room_data_by_id.find(room_id_str); // TODO: Get iterator from above insert } @@ -442,53 +445,45 @@ namespace QuickMedia { if(!body_json.isString()) continue; - bool is_reply = false; + std::string replaces_event_id; const Json::Value &relates_to_json = content_json["m.relates_to"]; if(relates_to_json.isObject()) { - const Json::Value &in_reply_to_json = relates_to_json["m.in_reply_to"]; - is_reply = in_reply_to_json.isObject(); + const Json::Value &replaces_event_id_json = relates_to_json["event_id"]; + const Json::Value &rel_type_json = relates_to_json["rel_type"]; + if(replaces_event_id_json.isString() && rel_type_json.isString() && strcmp(rel_type_json.asCString(), "m.replace") == 0) + replaces_event_id = replaces_event_id_json.asString(); } + auto message = std::make_shared(); + if(strcmp(content_type.asCString(), "m.text") == 0) { - auto message = std::make_shared(); - message->user_id = user_it->second; - message->event_id = event_id_str; - message->body = body_json.asString(); message->type = MessageType::TEXT; - message->is_reply = is_reply; - new_messages.push_back(message); - room_data->message_by_event_id[event_id_str] = message; } else if(strcmp(content_type.asCString(), "m.image") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; - auto message = std::make_shared(); - message->user_id = user_it->second; - message->event_id = event_id_str; - message->body = body_json.asString(); message->url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message->thumbnail_url = message_content_extract_thumbnail_url(content_json, homeserver); message->type = MessageType::IMAGE; - message->is_reply = is_reply; - new_messages.push_back(message); - room_data->message_by_event_id[event_id_str] = message; } else if(strcmp(content_type.asCString(), "m.video") == 0) { const Json::Value &url_json = content_json["url"]; if(!url_json.isString() || strncmp(url_json.asCString(), "mxc://", 6) != 0) continue; - auto message = std::make_shared(); - message->event_id = event_id_str; - message->user_id = user_it->second; - message->body = body_json.asString(); message->url = homeserver + "/_matrix/media/r0/download/" + url_json.asString().substr(6); message->thumbnail_url = message_content_extract_thumbnail_url(content_json, homeserver); message->type = MessageType::VIDEO; - message->is_reply = is_reply; - new_messages.push_back(message); - room_data->message_by_event_id[event_id_str] = message; + } else { + continue; } + + message->user_id = user_it->second; + message->event_id = event_id_str; + message->body = body_json.asString(); + message->replaces_event_id = std::move(replaces_event_id); + new_messages.push_back(message); + room_data->message_by_event_id[event_id_str] = message; } // TODO: Loop and std::move instead? doesn't insert create copies? @@ -774,9 +769,13 @@ namespace QuickMedia { static std::string create_body_for_message_reply(const RoomData *room_data, const Message *message, const std::string &body) { std::string related_to_body; switch(message->type) { - case MessageType::TEXT: - related_to_body = message->body; + case MessageType::TEXT: { + if(!message->replaces_event_id.empty() && strncmp(message->body.c_str(), " * ", 3) == 0) + related_to_body = message->body.substr(3); + else + related_to_body = message->body; break; + } case MessageType::IMAGE: related_to_body = "sent an image"; break; @@ -795,7 +794,13 @@ namespace QuickMedia { return PluginResult::ERR; } - Message *relates_to_message = (Message*)relates_to; + Message *relates_to_message_raw = (Message*)relates_to; + std::shared_ptr relates_to_message_shared = room_it->second->message_by_event_id[relates_to_message_raw->event_id]; + std::shared_ptr relates_to_message_original = get_edited_message_original_message(room_it->second.get(), 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))) @@ -804,14 +809,14 @@ namespace QuickMedia { std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); Json::Value in_reply_to_json(Json::objectValue); - in_reply_to_json["event_id"] = relates_to_message->event_id; + in_reply_to_json["event_id"] = relates_to_message_original->event_id; Json::Value relates_to_json(Json::objectValue); relates_to_json["m.in_reply_to"] = std::move(in_reply_to_json); Json::Value request_data(Json::objectValue); request_data["msgtype"] = message_type_to_request_msg_type_str(MessageType::TEXT); - request_data["body"] = create_body_for_message_reply(room_it->second.get(), relates_to_message, body); + request_data["body"] = create_body_for_message_reply(room_it->second.get(), relates_to_message_raw, body); // Yes, the reply is to the edited message but the event_id reference is to the original message... request_data["m.relates_to"] = std::move(relates_to_json); Json::StreamWriterBuilder builder; @@ -856,6 +861,204 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult Matrix::post_edit(const std::string &room_id, const std::string &body, void *relates_to) { + auto room_it = room_data_by_id.find(room_id); + if(room_it == room_data_by_id.end()) { + fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); + return PluginResult::ERR; + } + + Message *relates_to_message_raw = (Message*)relates_to; + std::shared_ptr relates_to_message_shared = room_it->second->message_by_event_id[relates_to_message_raw->event_id]; + std::shared_ptr relates_to_message_original = get_edited_message_original_message(room_it->second.get(), 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))) + return PluginResult::ERR; + + std::string random_readable_chars = random_characters_to_readable_string(random_characters, sizeof(random_characters)); + + std::string formatted_body; + bool contains_formatted_text = false; + int line = 0; + string_split(body, '\n', [&formatted_body, &contains_formatted_text, &line](const char *str, size_t size){ + if(line > 0) + formatted_body += "
"; + if(size > 0 && str[0] == '>') { + std::string line(str, size); + html_escape_sequences(line); + formatted_body += ""; + formatted_body += line; + formatted_body += ""; + contains_formatted_text = true; + } else { + formatted_body.append(str, size); + } + ++line; + return true; + }); + + Json::Value new_content_json(Json::objectValue); + new_content_json["msgtype"] = "m.text"; + new_content_json["body"] = body; + if(contains_formatted_text) { + new_content_json["format"] = "org.matrix.custom.html"; + new_content_json["formatted_body"] = formatted_body; + } + + Json::Value relates_to_json(Json::objectValue); + relates_to_json["event_id"] = relates_to_message_original->event_id; + relates_to_json["rel_type"] = "m.replace"; + + Json::Value request_data(Json::objectValue); + request_data["msgtype"] = message_type_to_request_msg_type_str(MessageType::TEXT); + request_data["body"] = " * " + body; + if(contains_formatted_text) { + request_data["format"] = "org.matrix.custom.html"; + request_data["formatted_body"] = " * " + formatted_body; + } + request_data["m.new_content"] = std::move(new_content_json); + request_data["m.relates_to"] = std::move(relates_to_json); + + Json::StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = ""; + + std::vector additional_args = { + { "-X", "PUT" }, + { "-H", "content-type: application/json" }, + { "-H", "Authorization: Bearer " + access_token }, + { "--data-binary", Json::writeString(builder, std::move(request_data)) } + }; + + char request_url[512]; + snprintf(request_url, sizeof(request_url), "%s/_matrix/client/r0/rooms/%s/send/m.room.message/m%ld.%.*s", homeserver.c_str(), room_id.c_str(), time(NULL), (int)random_readable_chars.size(), random_readable_chars.c_str()); + fprintf(stderr, "Post message to |%s|\n", request_url); + + std::string server_response; + if(download_to_string(request_url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + return PluginResult::NET_ERR; + + if(server_response.empty()) + return PluginResult::ERR; + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Matrix post message response parse error: %s\n", json_errors.c_str()); + return PluginResult::ERR; + } + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &event_id_json = json_root["event_id"]; + if(!event_id_json.isString()) + return PluginResult::ERR; + + fprintf(stderr, "Matrix post edit, response event id: %s\n", event_id_json.asCString()); + return PluginResult::OK; + } + + // TODO: Right now this recursively calls /rooms//context/ 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 Matrix::get_edited_message_original_message(RoomData *room_data, std::shared_ptr message) { + if(message->replaces_event_id.empty()) + return message; + + auto message_it = room_data->message_by_event_id.find(message->replaces_event_id); + if(message_it == room_data->message_by_event_id.end()) { + Json::Value request_data(Json::objectValue); + request_data["lazy_load_members"] = true; + + Json::StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = ""; + + std::vector additional_args = { + { "-H", "Authorization: Bearer " + access_token } + }; + + std::string filter = url_param_encode(Json::writeString(builder, std::move(request_data))); + + char url[512]; + snprintf(url, sizeof(url), "%s/_matrix/client/r0/rooms/%s/context/%s?limit=0&filter=%s", homeserver.c_str(), room_data->id.c_str(), message->event_id.c_str(), filter.c_str()); + fprintf(stderr, "get message context, url: |%s|\n", url); + + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), use_tor, true) != DownloadResult::OK) + return nullptr; + + if(server_response.empty()) + return nullptr; + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Matrix /rooms//context/ response parse error: %s\n", json_errors.c_str()); + return nullptr; + } + + if(!json_root.isObject()) + return nullptr; + + const Json::Value &event_json = json_root["event"]; + if(!event_json.isObject()) + return nullptr; + + const Json::Value &event_id_json = event_json["event_id"]; + if(!event_id_json.isString()) + return nullptr; + + const Json::Value &content_json = event_json["content"]; + if(!content_json.isObject()) + return nullptr; + + const Json::Value &body_json = content_json["body"]; + if(!body_json.isString()) + return nullptr; + + std::string replaces_event_id; + const Json::Value &relates_to_json = content_json["m.relates_to"]; + if(relates_to_json.isObject()) { + const Json::Value &event_id_json = relates_to_json["event_id"]; + const Json::Value &rel_type_json = relates_to_json["rel_type"]; + if(event_id_json.isString() && rel_type_json.isString() && strcmp(rel_type_json.asCString(), "m.replace") == 0) + replaces_event_id = event_id_json.asString(); + } + + const Json::Value &content_type = content_json["msgtype"]; + if(!content_type.isString()) + return nullptr; + + auto new_message = std::make_shared(); + new_message->user_id = -1; + new_message->event_id = event_id_json.asString(); + new_message->replaces_event_id = std::move(replaces_event_id); + if(strcmp(content_type.asCString(), "m.text") == 0) { + new_message->type = MessageType::TEXT; + } else if(strcmp(content_type.asCString(), "m.image") == 0) { + new_message->type = MessageType::IMAGE; + } else if(strcmp(content_type.asCString(), "m.video") == 0) { + new_message->type = MessageType::VIDEO; + } else { + return nullptr; + } + + return get_edited_message_original_message(room_data, std::move(new_message)); + } else { + return get_edited_message_original_message(room_data, message_it->second); + } + } + // Returns empty string on error static const char* file_get_filename(const std::string &filepath) { size_t index = filepath.rfind('/'); @@ -1155,4 +1358,14 @@ namespace QuickMedia { 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()) { + fprintf(stderr, "Error: no such room: %s\n", room_id.c_str()); + return false; + } + Message *message_typed = (Message*)message; + return user_id == room_it->second->user_info[message_typed->user_id].user_id; + } } \ No newline at end of file -- cgit v1.2.3