diff --git a/CMakeLists.txt b/CMakeLists.txt
index 759f9d9d63bdd50c85c4f2f204e5bfd2052dbcf8..32c21ea22d5c30d94b2dfd33261aceebdfa4b7b5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -110,6 +110,7 @@ add_library(matrix_client
             lib/structs/events/avatar.cpp
             lib/structs/events/canonical_alias.cpp
             lib/structs/events/common.cpp
+            lib/structs/events/collections.cpp
             lib/structs/events/create.cpp
             lib/structs/events/encrypted.cpp
             lib/structs/events/encryption.cpp
diff --git a/include/mtx/events/collections.hpp b/include/mtx/events/collections.hpp
index a5bf3eae4db2e96f86d8df2be97b1ba26afadd1c..61b9d3e47d43f1bbc1609b189cca4b7db1dc05b2 100644
--- a/include/mtx/events/collections.hpp
+++ b/include/mtx/events/collections.hpp
@@ -108,154 +108,9 @@ struct TimelineEvent
         TimelineEvents data;
 };
 
-inline void
-from_json(const json &obj, TimelineEvent &e)
-{
-        const auto type = mtx::events::getEventType(obj);
-        using namespace mtx::events::state;
-        using namespace mtx::events::msg;
-
-        switch (type) {
-        case events::EventType::RoomAliases: {
-                e.data = events::StateEvent<Aliases>(obj);
-                break;
-        }
-        case events::EventType::RoomAvatar: {
-                e.data = events::StateEvent<Avatar>(obj);
-                break;
-        }
-        case events::EventType::RoomCanonicalAlias: {
-                e.data = events::StateEvent<CanonicalAlias>(obj);
-                break;
-        }
-        case events::EventType::RoomCreate: {
-                e.data = events::StateEvent<Create>(obj);
-                break;
-        }
-        case events::EventType::RoomEncrypted: {
-                e.data = events::EncryptedEvent<mtx::events::msg::Encrypted>(obj);
-                break;
-        }
-        case events::EventType::RoomEncryption: {
-                e.data = events::StateEvent<Encryption>(obj);
-                break;
-        }
-        case events::EventType::RoomGuestAccess: {
-                e.data = events::StateEvent<GuestAccess>(obj);
-                break;
-        }
-        case events::EventType::RoomHistoryVisibility: {
-                e.data = events::StateEvent<HistoryVisibility>(obj);
-                break;
-        }
-        case events::EventType::RoomJoinRules: {
-                e.data = events::StateEvent<JoinRules>(obj);
-                break;
-        }
-        case events::EventType::RoomMember: {
-                e.data = events::StateEvent<Member>(obj);
-                break;
-        }
-        case events::EventType::RoomName: {
-                e.data = events::StateEvent<Name>(obj);
-                break;
-        }
-        case events::EventType::RoomPowerLevels: {
-                e.data = events::StateEvent<PowerLevels>(obj);
-                break;
-        }
-        case events::EventType::RoomRedaction: {
-                e.data = events::RedactionEvent<mtx::events::msg::Redaction>(obj);
-                break;
-        }
-        case events::EventType::RoomTombstone: {
-                e.data = events::StateEvent<Tombstone>(obj);
-                break;
-        }
-        case events::EventType::RoomTopic: {
-                e.data = events::StateEvent<Topic>(obj);
-                break;
-        }
-        case events::EventType::RoomMessage: {
-                using MsgType       = mtx::events::MessageType;
-                const auto msg_type = mtx::events::getMessageType(obj.at("content"));
-
-                if (msg_type == events::MessageType::Unknown) {
-                        try {
-                                auto unsigned_data =
-                                  obj.at("unsigned").at("redacted_by").get<std::string>();
-
-                                if (unsigned_data.empty())
-                                        return;
-
-                                e.data = events::RoomEvent<events::msg::Redacted>(obj);
-                                return;
-                        } catch (json::exception &err) {
-                                std::cout << "Invalid event type: " << err.what() << " "
-                                          << obj.dump(2) << '\n';
-                                return;
-                        }
-
-                        std::cout << "Invalid event type: " << obj.dump(2) << '\n';
-                        break;
-                }
+void
+from_json(const json &obj, TimelineEvent &e);
 
-                switch (msg_type) {
-                case MsgType::Audio: {
-                        e.data = events::RoomEvent<events::msg::Audio>(obj);
-                        break;
-                }
-                case MsgType::Emote: {
-                        e.data = events::RoomEvent<events::msg::Emote>(obj);
-                        break;
-                }
-                case MsgType::File: {
-                        e.data = events::RoomEvent<events::msg::File>(obj);
-                        break;
-                }
-                case MsgType::Image: {
-                        e.data = events::RoomEvent<events::msg::Image>(obj);
-                        break;
-                }
-                case MsgType::Location: {
-                        /* events::RoomEvent<events::msg::Location> location = e; */
-                        /* container.emplace_back(location); */
-                        break;
-                }
-                case MsgType::Notice: {
-                        e.data = events::RoomEvent<events::msg::Notice>(obj);
-                        break;
-                }
-                case MsgType::Text: {
-                        e.data = events::RoomEvent<events::msg::Text>(obj);
-                        break;
-                }
-                case MsgType::Video: {
-                        e.data = events::RoomEvent<events::msg::Video>(obj);
-                        break;
-                }
-                case MsgType::Unknown:
-                        return;
-                }
-                break;
-        }
-        case events::EventType::Sticker: {
-                e.data = events::Sticker(obj);
-                break;
-        }
-        case events::EventType::RoomPinnedEvents:
-        case events::EventType::RoomKeyRequest: // Not part of the timeline
-        case events::EventType::Tag:            // Not part of the timeline
-        case events::EventType::KeyVerificationCancel:
-        case events::EventType::KeyVerificationRequest:
-        case events::EventType::KeyVerificationStart:
-        case events::EventType::KeyVerificationAccept:
-        case events::EventType::KeyVerificationKey:
-        case events::EventType::KeyVerificationMac:
-        case events::EventType::Unsupported:
-                return;
-        }
-}
 } // namespace collections
 } // namespace events
 } // namespace mtx
diff --git a/lib/structs/events/collections.cpp b/lib/structs/events/collections.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..55b88556bedadc0bae36edd5fbcc07e1621ad423
--- /dev/null
+++ b/lib/structs/events/collections.cpp
@@ -0,0 +1,152 @@
+#include "mtx/events/collections.hpp"
+
+namespace mtx::events::collections {
+void
+from_json(const json &obj, TimelineEvent &e)
+{
+        const auto type = mtx::events::getEventType(obj);
+        using namespace mtx::events::state;
+        using namespace mtx::events::msg;
+
+        switch (type) {
+        case events::EventType::RoomAliases: {
+                e.data = events::StateEvent<Aliases>(obj);
+                break;
+        }
+        case events::EventType::RoomAvatar: {
+                e.data = events::StateEvent<Avatar>(obj);
+                break;
+        }
+        case events::EventType::RoomCanonicalAlias: {
+                e.data = events::StateEvent<CanonicalAlias>(obj);
+                break;
+        }
+        case events::EventType::RoomCreate: {
+                e.data = events::StateEvent<Create>(obj);
+                break;
+        }
+        case events::EventType::RoomEncrypted: {
+                e.data = events::EncryptedEvent<mtx::events::msg::Encrypted>(obj);
+                break;
+        }
+        case events::EventType::RoomEncryption: {
+                e.data = events::StateEvent<Encryption>(obj);
+                break;
+        }
+        case events::EventType::RoomGuestAccess: {
+                e.data = events::StateEvent<GuestAccess>(obj);
+                break;
+        }
+        case events::EventType::RoomHistoryVisibility: {
+                e.data = events::StateEvent<HistoryVisibility>(obj);
+                break;
+        }
+        case events::EventType::RoomJoinRules: {
+                e.data = events::StateEvent<JoinRules>(obj);
+                break;
+        }
+        case events::EventType::RoomMember: {
+                e.data = events::StateEvent<Member>(obj);
+                break;
+        }
+        case events::EventType::RoomName: {
+                e.data = events::StateEvent<Name>(obj);
+                break;
+        }
+        case events::EventType::RoomPowerLevels: {
+                e.data = events::StateEvent<PowerLevels>(obj);
+                break;
+        }
+        case events::EventType::RoomRedaction: {
+                e.data = events::RedactionEvent<mtx::events::msg::Redaction>(obj);
+                break;
+        }
+        case events::EventType::RoomTombstone: {
+                e.data = events::StateEvent<Tombstone>(obj);
+                break;
+        }
+        case events::EventType::RoomTopic: {
+                e.data = events::StateEvent<Topic>(obj);
+                break;
+        }
+        case events::EventType::RoomMessage: {
+                using MsgType       = mtx::events::MessageType;
+                const auto msg_type = mtx::events::getMessageType(obj.at("content"));
+
+                if (msg_type == events::MessageType::Unknown) {
+                        try {
+                                auto unsigned_data =
+                                  obj.at("unsigned").at("redacted_by").get<std::string>();
+
+                                if (unsigned_data.empty())
+                                        return;
+
+                                e.data = events::RoomEvent<events::msg::Redacted>(obj);
+                                return;
+                        } catch (json::exception &err) {
+                                std::cout << "Invalid event type: " << err.what() << " "
+                                          << obj.dump(2) << '\n';
+                                return;
+                        }
+
+                        std::cout << "Invalid event type: " << obj.dump(2) << '\n';
+                        break;
+                }
+
+                switch (msg_type) {
+                case MsgType::Audio: {
+                        e.data = events::RoomEvent<events::msg::Audio>(obj);
+                        break;
+                }
+                case MsgType::Emote: {
+                        e.data = events::RoomEvent<events::msg::Emote>(obj);
+                        break;
+                }
+                case MsgType::File: {
+                        e.data = events::RoomEvent<events::msg::File>(obj);
+                        break;
+                }
+                case MsgType::Image: {
+                        e.data = events::RoomEvent<events::msg::Image>(obj);
+                        break;
+                }
+                case MsgType::Location: {
+                        /* events::RoomEvent<events::msg::Location> location = e; */
+                        /* container.emplace_back(location); */
+                        break;
+                }
+                case MsgType::Notice: {
+                        e.data = events::RoomEvent<events::msg::Notice>(obj);
+                        break;
+                }
+                case MsgType::Text: {
+                        e.data = events::RoomEvent<events::msg::Text>(obj);
+                        break;
+                }
+                case MsgType::Video: {
+                        e.data = events::RoomEvent<events::msg::Video>(obj);
+                        break;
+                }
+                case MsgType::Unknown:
+                        return;
+                }
+                break;
+        }
+        case events::EventType::Sticker: {
+                e.data = events::Sticker(obj);
+                break;
+        }
+        case events::EventType::RoomPinnedEvents:
+        case events::EventType::RoomKeyRequest: // Not part of the timeline
+        case events::EventType::Tag:            // Not part of the timeline
+        case events::EventType::KeyVerificationCancel:
+        case events::EventType::KeyVerificationRequest:
+        case events::EventType::KeyVerificationStart:
+        case events::EventType::KeyVerificationAccept:
+        case events::EventType::KeyVerificationKey:
+        case events::EventType::KeyVerificationMac:
+        case events::EventType::Unsupported:
+                return;
+        }
+}
+}