From 1f91a7927056f0c37da0157c1ac88c24f13cfb2f Mon Sep 17 00:00:00 2001
From: Nicolas Werner <nicolas.werner@hotmail.de>
Date: Sun, 13 Jun 2021 20:25:26 +0200
Subject: [PATCH] Add space types

---
 CMakeLists.txt                     |   1 +
 include/mtx/events/collections.hpp |  15 ++
 include/mtx/events/create.hpp      |   9 +
 include/mtx/events/event_type.hpp  |   6 +
 include/mtx/events/spaces.hpp      |  81 +++++++
 include/mtxclient/http/client.hpp  |   2 +
 lib/http/client.cpp                |   2 +
 lib/structs/events.cpp             |  12 +
 lib/structs/events/collections.cpp |  12 +
 lib/structs/events/create.cpp      |   5 +
 lib/structs/events/spaces.cpp      |  73 ++++++
 lib/structs/responses/common.cpp   |  56 +++++
 tests/events.cpp                   | 363 +++++++++++++++++++++++++++++
 13 files changed, 637 insertions(+)
 create mode 100644 include/mtx/events/spaces.hpp
 create mode 100644 lib/structs/events/spaces.cpp

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 55ebac0d9..87579e451 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -166,6 +166,7 @@ target_sources(matrix_client
 	lib/structs/events/presence.cpp
 	lib/structs/events/reaction.cpp
 	lib/structs/events/redaction.cpp
+	lib/structs/events/spaces.cpp
 	lib/structs/events/tag.cpp
 	lib/structs/events/tombstone.cpp
 	lib/structs/events/topic.cpp
diff --git a/include/mtx/events/collections.hpp b/include/mtx/events/collections.hpp
index df4a17ee5..efa112f22 100644
--- a/include/mtx/events/collections.hpp
+++ b/include/mtx/events/collections.hpp
@@ -27,6 +27,7 @@
 #include "mtx/events/presence.hpp"
 #include "mtx/events/reaction.hpp"
 #include "mtx/events/redaction.hpp"
+#include "mtx/events/spaces.hpp"
 #include "mtx/events/tag.hpp"
 #include "mtx/events/tombstone.hpp"
 #include "mtx/events/topic.hpp"
@@ -94,6 +95,8 @@ using StateEvents = std::variant<events::StateEvent<states::Aliases>,
                                  events::StateEvent<states::Name>,
                                  events::StateEvent<states::PinnedEvents>,
                                  events::StateEvent<states::PowerLevels>,
+                                 events::StateEvent<states::space::Child>,
+                                 events::StateEvent<states::space::Parent>,
                                  events::StateEvent<states::Tombstone>,
                                  events::StateEvent<states::Topic>,
                                  events::StateEvent<msgs::Redacted>,
@@ -112,6 +115,8 @@ using StrippedEvents = std::variant<events::StrippedEvent<states::Aliases>,
                                     events::StrippedEvent<states::Name>,
                                     events::StrippedEvent<states::PinnedEvents>,
                                     events::StrippedEvent<states::PowerLevels>,
+                                    events::StrippedEvent<states::space::Child>,
+                                    events::StrippedEvent<states::space::Parent>,
                                     events::StrippedEvent<states::Tombstone>,
                                     events::StrippedEvent<states::Topic>,
                                     events::StrippedEvent<Unknown>>;
@@ -130,6 +135,8 @@ using TimelineEvents = std::variant<events::StateEvent<states::Aliases>,
                                     events::StateEvent<states::Name>,
                                     events::StateEvent<states::PinnedEvents>,
                                     events::StateEvent<states::PowerLevels>,
+                                    events::StateEvent<states::space::Child>,
+                                    events::StateEvent<states::space::Parent>,
                                     events::StateEvent<states::Tombstone>,
                                     events::StateEvent<states::Topic>,
                                     events::StateEvent<msc2545::ImagePack>,
@@ -262,6 +269,14 @@ constexpr inline EventType state_content_to_type<mtx::events::state::PowerLevels
 template<>
 constexpr inline EventType state_content_to_type<mtx::events::state::Tombstone> =
   EventType::RoomTombstone;
+
+template<>
+constexpr inline EventType state_content_to_type<mtx::events::state::space::Child> =
+  EventType::SpaceChild;
+template<>
+constexpr inline EventType state_content_to_type<mtx::events::state::space::Parent> =
+  EventType::SpaceParent;
+
 template<>
 constexpr inline EventType state_content_to_type<mtx::events::state::Topic> = EventType::RoomTopic;
 template<>
diff --git a/include/mtx/events/create.hpp b/include/mtx/events/create.hpp
index ff0422bfc..f69ad8106 100644
--- a/include/mtx/events/create.hpp
+++ b/include/mtx/events/create.hpp
@@ -23,6 +23,12 @@ struct PreviousRoom
         std::string event_id;
 };
 
+//! Definitions of different room types.
+namespace room_type {
+//! The room type for a space.
+constexpr std::string_view space = "m.space";
+}
+
 //! Content of the `m.room.create` event.
 //
 //! This is the first event in a room and cannot be changed.
@@ -32,6 +38,9 @@ struct Create
         //! The `user_id` of the room creator. This is set by the homeserver.
         std::string creator;
 
+        //! The room type, for example `m.space` for spaces.
+        std::optional<std::string> type;
+
         //! Whether users on other servers can join this room.
         //! Defaults to **true** if key does not exist.
         bool federate = true;
diff --git a/include/mtx/events/event_type.hpp b/include/mtx/events/event_type.hpp
index e602a18cb..0dd5b614d 100644
--- a/include/mtx/events/event_type.hpp
+++ b/include/mtx/events/event_type.hpp
@@ -82,6 +82,12 @@ enum class EventType
         Presence,
         // m.push_rules
         PushRules,
+
+        // m.space.child
+        SpaceChild,
+        // m.space.parent
+        SpaceParent,
+
         // m.call.invite
         CallInvite,
         // m.call.candidates
diff --git a/include/mtx/events/spaces.hpp b/include/mtx/events/spaces.hpp
new file mode 100644
index 000000000..e80f00b5b
--- /dev/null
+++ b/include/mtx/events/spaces.hpp
@@ -0,0 +1,81 @@
+#pragma once
+
+/// @file
+/// @brief Space related events to make child and parent relations
+
+#include <optional>
+
+#if __has_include(<nlohmann/json_fwd.hpp>)
+#include <nlohmann/json_fwd.hpp>
+#else
+#include <nlohmann/json.hpp>
+#endif
+
+namespace mtx {
+namespace events {
+namespace state {
+//! Namespace for space related state events.
+namespace space {
+/// @brief Event to point at a parent space from a room or space
+///
+/// To avoid abuse where a room admin falsely claims that a room is part of a space that it
+/// should not be, clients could ignore such m.space.parent events unless either (a) there
+/// is a corresponding m.space.child event in the claimed parent, or (b) the sender of the
+/// m.space.child event has a sufficient power-level to send such an m.space.child event in
+/// the parent. (It is not necessarily required that that user currently be a member of the
+/// parent room - only the m.room.power_levels event is inspected.)
+struct Parent
+{
+        /// @brief Servers to join the parent space via.
+        ///
+        /// Needs to contain at least one server.
+        std::optional<std::vector<std::string>> via;
+        /// @brief Determines whether this is the main parent for the space.
+        ///
+        /// When a user joins a room with a canonical parent, clients may switch to view the room in
+        /// the context of that space, peeking into it in order to find other rooms and group them
+        /// together. In practice, well behaved rooms should only have one canonical parent, but
+        /// given this is not enforced: if multiple are present the client should select the one
+        /// with the lowest room ID, as determined via a lexicographic ordering of the Unicode
+        /// code-points.
+        bool canonical = false;
+};
+
+void
+from_json(const nlohmann::json &obj, Parent &child);
+
+void
+to_json(nlohmann::json &obj, const Parent &child);
+
+/// @brief Event to point at a child room or space from a parent space
+///
+/// The admins of a space can advertise rooms and subspaces for their space by setting m.space.child
+/// state events. The state_key is the ID of a child room or space, and the content must contain a
+/// via key which gives a list of candidate servers that can be used to join the room.
+struct Child
+{
+        /// @brief Servers to join the child room/space via.
+        ///
+        /// Needs to contain at least one server.
+        std::optional<std::vector<std::string>> via;
+        /// @brief A string which is used to provide a default ordering of siblings in the room
+        /// list.
+        ///
+        /// Rooms are sorted based on a lexicographic ordering of the Unicode codepoints of the
+        /// characters in order values. Rooms with no order come last, in ascending numeric order of
+        /// the origin_server_ts of their m.room.create events, or ascending lexicographic order of
+        /// their room_ids in case of equal origin_server_ts. orders which are not strings, or do
+        /// not consist solely of ascii characters in the range \x20 (space) to \x7E (~), or consist
+        /// of more than 50 characters, are forbidden and the field should be ignored if received.
+        std::optional<std::string> order;
+};
+
+void
+from_json(const nlohmann::json &obj, Child &child);
+
+void
+to_json(nlohmann::json &obj, const Child &child);
+}
+}
+}
+}
diff --git a/include/mtxclient/http/client.hpp b/include/mtxclient/http/client.hpp
index 5bb888ea4..0b1e8a4d2 100644
--- a/include/mtxclient/http/client.hpp
+++ b/include/mtxclient/http/client.hpp
@@ -698,6 +698,8 @@ MTXCLIENT_SEND_STATE_EVENT_FWD(state::PinnedEvents)
 MTXCLIENT_SEND_STATE_EVENT_FWD(state::PowerLevels)
 MTXCLIENT_SEND_STATE_EVENT_FWD(state::Tombstone)
 MTXCLIENT_SEND_STATE_EVENT_FWD(state::Topic)
+MTXCLIENT_SEND_STATE_EVENT_FWD(state::space::Child)
+MTXCLIENT_SEND_STATE_EVENT_FWD(state::space::Parent)
 MTXCLIENT_SEND_STATE_EVENT_FWD(msc2545::ImagePack)
 
 #define MTXCLIENT_SEND_ROOM_MESSAGE_FWD(Content)                                                   \
diff --git a/lib/http/client.cpp b/lib/http/client.cpp
index 0d7d187e0..adc10519b 100644
--- a/lib/http/client.cpp
+++ b/lib/http/client.cpp
@@ -1449,6 +1449,8 @@ MTXCLIENT_SEND_STATE_EVENT(state::PinnedEvents)
 MTXCLIENT_SEND_STATE_EVENT(state::PowerLevels)
 MTXCLIENT_SEND_STATE_EVENT(state::Tombstone)
 MTXCLIENT_SEND_STATE_EVENT(state::Topic)
+MTXCLIENT_SEND_STATE_EVENT(state::space::Child)
+MTXCLIENT_SEND_STATE_EVENT(state::space::Parent)
 MTXCLIENT_SEND_STATE_EVENT(msc2545::ImagePack)
 
 #define MTXCLIENT_SEND_ROOM_MESSAGE(Content)                                                       \
diff --git a/lib/structs/events.cpp b/lib/structs/events.cpp
index 090f50d84..e1aa5240f 100644
--- a/lib/structs/events.cpp
+++ b/lib/structs/events.cpp
@@ -72,6 +72,12 @@ getEventType(const std::string &type)
                 return EventType::RoomTombstone;
         else if (type == "m.sticker")
                 return EventType::Sticker;
+
+        else if (type == "m.space.child")
+                return EventType::SpaceChild;
+        else if (type == "m.space.parent")
+                return EventType::SpaceParent;
+
         else if (type == "m.tag")
                 return EventType::Tag;
         else if (type == "m.presence")
@@ -174,6 +180,12 @@ to_string(EventType type)
                 return "m.room.tombstone";
         case EventType::Sticker:
                 return "m.sticker";
+
+        case EventType::SpaceChild:
+                return "m.space.child";
+        case EventType::SpaceParent:
+                return "m.space.parent";
+
         case EventType::Tag:
                 return "m.tag";
         case EventType::Presence:
diff --git a/lib/structs/events/collections.cpp b/lib/structs/events/collections.cpp
index bf22a3792..2f8c211f7 100644
--- a/lib/structs/events/collections.cpp
+++ b/lib/structs/events/collections.cpp
@@ -25,6 +25,8 @@ MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::PinnedEvents)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::PowerLevels)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Tombstone)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::Topic)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::space::Child)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, states::space::Parent)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, msgs::Redacted)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, msc2545::ImagePack)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StateEvent, Unknown)
@@ -68,6 +70,8 @@ MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::PinnedEvents
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::PowerLevels)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Tombstone)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::Topic)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::space::Child)
+MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, states::space::Parent)
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::StrippedEvent, Unknown)
 
 MTXCLIENT_INSTANTIATE_JSON_FUNCTIONS(events::DeviceEvent, msgs::Encrypted)
@@ -185,6 +189,14 @@ from_json(const json &obj, TimelineEvent &e)
                 e.data = events::StateEvent<Topic>(obj);
                 break;
         }
+        case events::EventType::SpaceChild: {
+                e.data = events::StateEvent<space::Child>(obj);
+                break;
+        }
+        case events::EventType::SpaceParent: {
+                e.data = events::StateEvent<space::Parent>(obj);
+                break;
+        }
         case events::EventType::ImagePackInRoom: {
                 e.data = events::StateEvent<msc2545::ImagePack>(obj);
                 break;
diff --git a/lib/structs/events/create.cpp b/lib/structs/events/create.cpp
index 6cf7861d3..32237acf4 100644
--- a/lib/structs/events/create.cpp
+++ b/lib/structs/events/create.cpp
@@ -28,6 +28,9 @@ from_json(const json &obj, Create &create)
 {
         create.creator = obj.at("creator");
 
+        if (obj.contains("type") && obj.at("type").is_string())
+                create.type = obj.at("type");
+
         if (obj.find("m.federate") != obj.end())
                 create.federate = obj.at("m.federate").get<bool>();
 
@@ -51,6 +54,8 @@ to_json(json &obj, const Create &create)
         else
                 obj["room_version"] = create.room_version;
 
+        if (create.type)
+                obj["type"] = create.type.value();
         if (create.predecessor)
                 obj["predecessor"] = *create.predecessor;
 }
diff --git a/lib/structs/events/spaces.cpp b/lib/structs/events/spaces.cpp
new file mode 100644
index 000000000..ad4331e14
--- /dev/null
+++ b/lib/structs/events/spaces.cpp
@@ -0,0 +1,73 @@
+#include "mtx/events/spaces.hpp"
+
+#include <string>
+
+#include <nlohmann/json.hpp>
+
+namespace mtx {
+namespace events {
+namespace state {
+namespace space {
+
+void
+from_json(const nlohmann::json &obj, Parent &parent)
+{
+        if (obj.contains("canonical") && obj.at("canonical").is_boolean())
+                parent.canonical = obj.at("canonical").get<bool>();
+        if (obj.contains("via") && obj.at("via").is_array() && !obj.at("via").empty())
+                parent.via = obj.at("via").get<std::vector<std::string>>();
+}
+
+void
+to_json(nlohmann::json &obj, const Parent &parent)
+{
+        // event without via is invalid.
+        if (!parent.via.has_value() || parent.via.value().empty())
+                return;
+
+        obj["via"] = parent.via.value();
+
+        if (parent.canonical)
+                obj["canonical"] = true;
+}
+
+static bool
+is_valid_order_str(std::string_view order)
+{
+        if (order.size() > 50)
+                return false;
+
+        for (auto c : order)
+                if (c < '\x20' || c > '\x7E')
+                        return false;
+
+        return true;
+}
+
+void
+from_json(const nlohmann::json &obj, Child &child)
+{
+        if (obj.contains("via") && obj.at("via").is_array() && !obj.at("via").empty())
+                child.via = obj.at("via").get<std::vector<std::string>>();
+
+        if (obj.contains("order") && obj.at("order").is_string() &&
+            is_valid_order_str(obj.at("order").get<std::string>()))
+                child.order = obj.at("order").get<std::string>();
+}
+
+void
+to_json(nlohmann::json &obj, const Child &child)
+{
+        // event without via is invalid.
+        if (!child.via.has_value() || child.via.value().empty())
+                return;
+
+        obj["via"] = child.via.value();
+
+        if (child.order && is_valid_order_str(child.order.value()))
+                obj["order"] = child.order.value();
+}
+}
+} // namespace state
+} // namespace events
+} // namespace mtx
diff --git a/lib/structs/responses/common.cpp b/lib/structs/responses/common.cpp
index 8a9804618..29b7e629d 100644
--- a/lib/structs/responses/common.cpp
+++ b/lib/structs/responses/common.cpp
@@ -176,6 +176,8 @@ parse_room_account_data_events(
                 case events::EventType::RoomRedaction:
                 case events::EventType::RoomTombstone:
                 case events::EventType::RoomTopic:
+                case events::EventType::SpaceChild:
+                case events::EventType::SpaceParent:
                 case events::EventType::Sticker:
                 case events::EventType::CallInvite:
                 case events::EventType::CallCandidates:
@@ -354,6 +356,24 @@ parse_timeline_events(const json &events,
 
                         break;
                 }
+                case events::EventType::SpaceChild: {
+                        try {
+                                container.emplace_back(events::StateEvent<space::Child>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
+                case events::EventType::SpaceParent: {
+                        try {
+                                container.emplace_back(events::StateEvent<space::Parent>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::ImagePackInRoom: {
                         try {
                                 container.emplace_back(
@@ -930,6 +950,24 @@ parse_state_events(const json &events,
 
                         break;
                 }
+                case events::EventType::SpaceChild: {
+                        try {
+                                container.emplace_back(events::StateEvent<space::Child>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
+                case events::EventType::SpaceParent: {
+                        try {
+                                container.emplace_back(events::StateEvent<space::Parent>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::ImagePackInRoom: {
                         try {
                                 container.emplace_back(
@@ -1106,6 +1144,24 @@ parse_stripped_events(const json &events,
 
                         break;
                 }
+                case events::EventType::SpaceChild: {
+                        try {
+                                container.emplace_back(events::StrippedEvent<space::Child>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
+                case events::EventType::SpaceParent: {
+                        try {
+                                container.emplace_back(events::StrippedEvent<space::Parent>(e));
+                        } catch (json::exception &err) {
+                                log_error(err, e);
+                        }
+
+                        break;
+                }
                 case events::EventType::Unsupported: {
                         try {
                                 container.emplace_back(events::StrippedEvent<events::Unknown>(e));
diff --git a/tests/events.cpp b/tests/events.cpp
index 09c25ea70..2db2e7a00 100644
--- a/tests/events.cpp
+++ b/tests/events.cpp
@@ -108,6 +108,8 @@ TEST(Events, Conversions)
         EXPECT_EQ("m.room.tombstone", ns::to_string(ns::EventType::RoomTombstone));
         EXPECT_EQ("m.room.redaction", ns::to_string(ns::EventType::RoomRedaction));
         EXPECT_EQ("m.room.pinned_events", ns::to_string(ns::EventType::RoomPinnedEvents));
+        EXPECT_EQ("m.space.child", ns::to_string(ns::EventType::SpaceChild));
+        EXPECT_EQ("m.space.parent", ns::to_string(ns::EventType::SpaceParent));
         EXPECT_EQ("m.tag", ns::to_string(ns::EventType::Tag));
 }
 
@@ -276,6 +278,71 @@ TEST(StateEvents, Create)
         EXPECT_EQ(event.content.predecessor->event_id, "$something:example.org");
 }
 
+TEST(StateEvents, CreateWithType)
+{
+        json data = R"({
+          "origin_server_ts": 1506761923948,
+          "sender": "@mujx:matrix.org",
+          "event_id": "$15067619231414398jhvQC:matrix.org",
+          "unsigned": {
+            "age": 3715756343
+          },
+          "state_key": "",
+          "content": {
+            "creator": "@mujx:matrix.org",
+            "type": "m.space"
+          },
+          "type": "m.room.create"
+        })"_json;
+
+        ns::StateEvent<ns::state::Create> event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::RoomCreate);
+        EXPECT_EQ(event.event_id, "$15067619231414398jhvQC:matrix.org");
+        EXPECT_EQ(event.sender, "@mujx:matrix.org");
+        EXPECT_EQ(event.unsigned_data.age, 3715756343L);
+        EXPECT_EQ(event.origin_server_ts, 1506761923948L);
+        EXPECT_EQ(event.state_key, "");
+        EXPECT_EQ(event.content.creator, "@mujx:matrix.org");
+        EXPECT_TRUE(event.content.type.has_value());
+        EXPECT_EQ(event.content.type.value(), ns::state::room_type::space);
+
+        json example_from_spec = R"({
+            "content": {
+                "creator": "@example:example.org",
+                "m.federate": true,
+                "predecessor": {
+                    "event_id": "$something:example.org",
+                    "room_id": "!oldroom:example.org"
+                },
+                "room_version": "1"
+            },
+            "event_id": "$143273582443PhrSn:example.org",
+            "origin_server_ts": 1432735824653,
+            "room_id": "!jEsUZKDJdhlrceRyVU:example.org",
+            "sender": "@example:example.org",
+            "state_key": "",
+            "type": "m.room.create",
+            "unsigned": {
+                "age": 1234
+            }
+        })"_json;
+
+        event = example_from_spec;
+
+        EXPECT_EQ(event.type, ns::EventType::RoomCreate);
+        EXPECT_EQ(event.event_id, "$143273582443PhrSn:example.org");
+        EXPECT_EQ(event.sender, "@example:example.org");
+        EXPECT_EQ(event.unsigned_data.age, 1234);
+        EXPECT_EQ(event.origin_server_ts, 1432735824653L);
+        EXPECT_EQ(event.state_key, "");
+        EXPECT_EQ(event.content.creator, "@example:example.org");
+        EXPECT_EQ(event.content.federate, true);
+        EXPECT_EQ(event.content.room_version, "1");
+        EXPECT_EQ(event.content.predecessor->room_id, "!oldroom:example.org");
+        EXPECT_EQ(event.content.predecessor->event_id, "$something:example.org");
+}
+
 TEST(StateEvents, GuestAccess)
 {
         json data = R"({
@@ -717,6 +784,302 @@ TEST(StateEvents, Topic)
         EXPECT_EQ(event.content.topic, "Test topic");
 }
 
+TEST(StateEvents, SpaceChild)
+{
+        json data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+      "type": "m.space.child",
+      "state_key": "!abcd:example.com",
+      "content": {
+          "via": ["example.com", "test.org"]
+      }
+}
+        )"_json;
+
+        ns::StateEvent<ns::state::space::Child> event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceChild);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!abcd:example.com");
+        ASSERT_TRUE(event.content.via.has_value());
+        std::vector<std::string> via{"example.com", "test.org"};
+        EXPECT_EQ(event.content.via, via);
+        EXPECT_FALSE(event.content.order.has_value());
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "via": ["example.com"],
+        "order": "abcd"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceChild);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!efgh:example.com");
+        ASSERT_TRUE(event.content.via.has_value());
+        std::vector<std::string> via2{"example.com"};
+        EXPECT_EQ(event.content.via, via2);
+        ASSERT_TRUE(event.content.order.has_value());
+        ASSERT_EQ(event.content.order, "abcd");
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!jklm:example.com",
+        "content": {}
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceChild);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!jklm:example.com");
+        ASSERT_FALSE(event.content.via.has_value());
+        ASSERT_FALSE(event.content.order.has_value());
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "via": ["example.com"],
+        "order": "01234567890123456789012345678901234567890123456789_"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceChild);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!efgh:example.com");
+        EXPECT_TRUE(event.content.via.has_value());
+        ASSERT_FALSE(event.content.order.has_value());
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "via": [],
+        "order": "01234567890123456789012345678901234567890123456789_"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_FALSE(event.content.via.has_value());
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "via": 5,
+        "order": "01234567890123456789012345678901234567890123456789_"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_FALSE(event.content.via.has_value());
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "via": null,
+        "order": "01234567890123456789012345678901234567890123456789_"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_FALSE(event.content.via.has_value());
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+        "type": "m.space.child",
+        "state_key": "!efgh:example.com",
+        "content": {
+        "order": "01234567890123456789012345678901234567890123456789_"
+    }
+}
+        )"_json;
+
+        event = data;
+
+        EXPECT_FALSE(event.content.via.has_value());
+}
+TEST(StateEvents, SpaceParent)
+{
+        json data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": ["example.com"],
+            "canonical": true
+          }
+        })"_json;
+
+        ns::StateEvent<ns::state::space::Parent> event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        ASSERT_TRUE(event.content.via.has_value());
+        std::vector<std::string> via{"example.com"};
+        EXPECT_EQ(event.content.via, via);
+        EXPECT_TRUE(event.content.canonical);
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": ["example.org"]
+          }
+        })"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        EXPECT_TRUE(event.content.via.has_value());
+        EXPECT_FALSE(event.content.canonical);
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": [],
+            "canonical": true
+          }
+        })"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        EXPECT_FALSE(event.content.via.has_value());
+        EXPECT_TRUE(event.content.canonical);
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": null,
+            "canonical": true
+          }
+        })"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        EXPECT_FALSE(event.content.via.has_value());
+        EXPECT_TRUE(event.content.canonical);
+
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": 5,
+            "canonical": true
+          }
+        })"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        EXPECT_FALSE(event.content.via.has_value());
+        EXPECT_TRUE(event.content.canonical);
+        data = R"({
+          "origin_server_ts": 1510476064445,
+          "sender": "@nheko_test:matrix.org",
+          "event_id": "$15104760642668662QICBu:matrix.org",
+          "type": "m.space.parent",
+          "state_key": "!space:example.com",
+          "content": {
+            "via": "adjsa",
+            "canonical": true
+          }
+        })"_json;
+
+        event = data;
+
+        EXPECT_EQ(event.type, ns::EventType::SpaceParent);
+        EXPECT_EQ(event.event_id, "$15104760642668662QICBu:matrix.org");
+        EXPECT_EQ(event.sender, "@nheko_test:matrix.org");
+        EXPECT_EQ(event.origin_server_ts, 1510476064445);
+        EXPECT_EQ(event.state_key, "!space:example.com");
+        EXPECT_FALSE(event.content.via.has_value());
+        EXPECT_TRUE(event.content.canonical);
+}
+
 TEST(StateEvents, ImagePack)
 {
         json data = R"({
-- 
GitLab