diff --git a/include/mtx/events/common.hpp b/include/mtx/events/common.hpp index 094be0e8f5b00624723fc55345832d3951326749..e9cb5e27c7db22fc05065a236562200a281f5015 100644 --- a/include/mtx/events/common.hpp +++ b/include/mtx/events/common.hpp @@ -163,6 +163,8 @@ enum class RelationType Replace, //! im.nheko.relations.v1.in_reply_to rel_type InReplyTo, + //! m.thread + Thread, //! not one of the supported types Unsupported }; @@ -183,6 +185,9 @@ struct Relation //! key is the reaction itself std::optional<std::string> key = std::nullopt; + //! proprietary field to track if this is a fallback for something else + bool is_fallback = false; + friend void from_json(const nlohmann::json &obj, Relation &relation); friend void to_json(nlohmann::json &obj, const Relation &relation); }; @@ -196,10 +201,11 @@ struct Relations //! im.nheko.relactions.v1.relations bool synthesized = false; - std::optional<std::string> reply_to() const; - std::optional<std::string> replaces() const; - std::optional<std::string> references() const; - std::optional<Relation> annotates() const; + std::optional<std::string> reply_to(bool include_fallback = true) const; + std::optional<std::string> replaces(bool include_fallback = true) const; + std::optional<std::string> references(bool include_fallback = true) const; + std::optional<std::string> thread(bool include_fallback = true) const; + std::optional<Relation> annotates(bool include_fallback = true) const; }; /// @brief Parses relations from a content object diff --git a/lib/structs/events/common.cpp b/lib/structs/events/common.cpp index 9fb6f07215dc1b1dea94b92dd8183e6d8e7dd450..6983b239e8e9b325f8a38cce37694a1ec33ece34 100644 --- a/lib/structs/events/common.cpp +++ b/lib/structs/events/common.cpp @@ -185,6 +185,9 @@ to_json(json &obj, const RelationType &type) case RelationType::InReplyTo: obj = "im.nheko.relations.v1.in_reply_to"; break; + case RelationType::Thread: + obj = "m.thread"; + break; case RelationType::Unsupported: default: obj = "unsupported"; @@ -203,6 +206,8 @@ from_json(const json &obj, RelationType &type) type = RelationType::Replace; else if (obj.get<std::string>() == "im.nheko.relations.v1.in_reply_to") type = RelationType::InReplyTo; + else if (obj.get<std::string>() == "m.thread") + type = RelationType::Thread; else type = RelationType::Unsupported; } @@ -218,18 +223,27 @@ parse_relations(const nlohmann::json &content) rels.synthesized = false; return rels; } else if (content.contains("m.relates_to")) { - if (content.at("m.relates_to").contains("m.in_reply_to")) { + const auto &relates_to = content.at("m.relates_to"); + if (relates_to.contains("m.in_reply_to")) { Relation r; - r.event_id = - content.at("m.relates_to").at("m.in_reply_to").at("event_id").get<std::string>(); + r.event_id = relates_to.at("m.in_reply_to").at("event_id").get<std::string>(); r.rel_type = RelationType::InReplyTo; Relations rels; + if (auto thread_type = relates_to.find("rel_type"); + thread_type != relates_to.end() && *thread_type == "m.thread") { + if (auto thread_id = relates_to.find("event_id"); + thread_id != relates_to.end()) { + r.is_fallback = relates_to.value("is_falling_back", false); + rels.relations.push_back(relates_to.get<mtx::common::Relation>()); + } + } + rels.relations.push_back(r); rels.synthesized = true; return rels; } else { - Relation r = content.at("m.relates_to").get<mtx::common::Relation>(); + Relation r = relates_to.get<mtx::common::Relation>(); Relations rels; rels.relations.push_back(r); rels.synthesized = true; @@ -262,20 +276,24 @@ add_relations(nlohmann::json &content, const Relations &relations) if (relations.relations.empty()) return; - std::optional<Relation> edit, not_edit; + std::optional<Relation> edit, not_edit, reply; for (const auto &r : relations.relations) { if (r.rel_type == RelationType::Replace) edit = r; + else if (r.rel_type == RelationType::InReplyTo) + reply = r; else not_edit = r; } if (not_edit) { - if (not_edit->rel_type == RelationType::InReplyTo) { - content["m.relates_to"]["m.in_reply_to"]["event_id"] = not_edit->event_id; - } else { - content["m.relates_to"] = *not_edit; - } + content["m.relates_to"] = *not_edit; + } + + if (reply) { + content["m.relates_to"]["m.in_reply_to"]["event_id"] = reply->event_id; + if (reply->is_fallback && not_edit && not_edit->rel_type == RelationType::Thread) + content["m.relates_to"]["is_falling_back"] = true; } if (edit) { @@ -316,12 +334,14 @@ apply_relations(nlohmann::json &content, const Relations &relations) void from_json(const json &obj, Relation &relates_to) { - if (obj.find("rel_type") != obj.end()) - relates_to.rel_type = obj.at("rel_type").get<RelationType>(); - if (obj.find("event_id") != obj.end()) - relates_to.event_id = obj.at("event_id").get<std::string>(); - if (obj.find("key") != obj.end()) - relates_to.key = obj.at("key").get<std::string>(); + if (auto it = obj.find("rel_type"); it != obj.end()) + relates_to.rel_type = it->get<RelationType>(); + if (auto it = obj.find("event_id"); it != obj.end()) + relates_to.event_id = it->get<std::string>(); + if (auto it = obj.find("key"); it != obj.end()) + relates_to.key = it->get<std::string>(); + if (auto it = obj.find("im.nheko.relations.v1.is_fallback"); it != obj.end()) + relates_to.is_fallback = it->get<bool>(); } void @@ -331,36 +351,43 @@ to_json(json &obj, const Relation &relates_to) obj["event_id"] = relates_to.event_id; if (relates_to.key.has_value()) obj["key"] = relates_to.key.value(); + if (relates_to.is_fallback) + obj["im.nheko.relations.v1.is_fallback"] = true; } static inline std::optional<std::string> -return_first_relation_matching(RelationType t, const Relations &rels) +return_first_relation_matching(RelationType t, const Relations &rels, bool include_fallback) { for (const auto &r : rels.relations) - if (r.rel_type == t) + if (r.rel_type == t && (include_fallback || r.is_fallback == false)) return r.event_id; return std::nullopt; } std::optional<std::string> -Relations::reply_to() const +Relations::reply_to(bool include_fallback) const +{ + return return_first_relation_matching(RelationType::InReplyTo, *this, include_fallback); +} +std::optional<std::string> +Relations::replaces(bool include_fallback) const { - return return_first_relation_matching(RelationType::InReplyTo, *this); + return return_first_relation_matching(RelationType::Replace, *this, include_fallback); } std::optional<std::string> -Relations::replaces() const +Relations::references(bool include_fallback) const { - return return_first_relation_matching(RelationType::Replace, *this); + return return_first_relation_matching(RelationType::Reference, *this, include_fallback); } std::optional<std::string> -Relations::references() const +Relations::thread(bool include_fallback) const { - return return_first_relation_matching(RelationType::Reference, *this); + return return_first_relation_matching(RelationType::Thread, *this, include_fallback); } std::optional<Relation> -Relations::annotates() const +Relations::annotates(bool include_fallback) const { for (const auto &r : relations) - if (r.rel_type == RelationType::Annotation) + if (r.rel_type == RelationType::Annotation && (include_fallback || r.is_fallback == false)) return r; return std::nullopt; } diff --git a/tests/messages.cpp b/tests/messages.cpp index e9cc598ae986e38f6aca22d98fb325b385be9afc..fc534aa720f2d546e1557fed74a6ffcb4d46e340 100644 --- a/tests/messages.cpp +++ b/tests/messages.cpp @@ -768,3 +768,48 @@ TEST(RoomEvents, Encrypted) EXPECT_EQ(data, json(event)); } + +TEST(RoomEvents, ThreadedMessage) +{ + json data = R"({ + "origin_server_ts": 1510489356530, + "sender": "@nheko_test:matrix.org", + "event_id": "$15104893562785758wEgEU:matrix.org", + "unsigned": { + "age": 2225, + "transaction_id": "m1510489356267.2" + }, + "content": { + "body": "hey there", + "msgtype": "m.text", +"m.relates_to": { + "rel_type": "m.thread", + "event_id": "$root", + "m.in_reply_to": { + "event_id": "$target" + }, + "is_falling_back": true +} + }, + "type": "m.room.message", + "room_id": "!lfoDRlNFWlvOnvkBwQ:matrix.org" + })"_json; + + RoomEvent<msg::Text> event = data.get<RoomEvent<msg::Text>>(); + + EXPECT_EQ(event.type, EventType::RoomMessage); + EXPECT_EQ(event.event_id, "$15104893562785758wEgEU:matrix.org"); + EXPECT_EQ(event.room_id, "!lfoDRlNFWlvOnvkBwQ:matrix.org"); + EXPECT_EQ(event.sender, "@nheko_test:matrix.org"); + EXPECT_EQ(event.origin_server_ts, 1510489356530L); + EXPECT_EQ(event.unsigned_data.age, 2225); + EXPECT_EQ(event.unsigned_data.transaction_id, "m1510489356267.2"); + + EXPECT_EQ(event.content.body, "hey there"); + EXPECT_EQ(event.content.msgtype, "m.text"); + EXPECT_EQ(event.content.relations.reply_to(), "$target"); + EXPECT_EQ(event.content.relations.reply_to(false), std::nullopt); + EXPECT_EQ(event.content.relations.thread(), "$root"); + + EXPECT_EQ(data.dump(), json(event).dump()); +}