about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore10
-rw-r--r--.idea/.idea.LibMatrix/.idea/.gitignore13
-rw-r--r--.idea/.idea.LibMatrix/.idea/encodings.xml4
-rw-r--r--.idea/.idea.LibMatrix/.idea/indexLayout.xml8
-rw-r--r--.idea/.idea.LibMatrix/.idea/vcs.xml6
-rw-r--r--LibMatrix.sln22
-rw-r--r--LibMatrix/AuthenticatedHomeServer.cs174
-rw-r--r--LibMatrix/EventIdResponse.cs8
-rw-r--r--LibMatrix/Extensions/ClassCollector.cs22
-rw-r--r--LibMatrix/Extensions/DictionaryExtensions.cs33
-rw-r--r--LibMatrix/Extensions/HttpClientExtensions.cs76
-rw-r--r--LibMatrix/Extensions/IEnumerableExtensions.cs7
-rw-r--r--LibMatrix/Extensions/JsonElementExtensions.cs150
-rw-r--r--LibMatrix/Extensions/ObjectExtensions.cs14
-rw-r--r--LibMatrix/Extensions/StringExtensions.cs13
-rw-r--r--LibMatrix/Filters/LocalRoomQueryFilter.cs28
-rw-r--r--LibMatrix/Filters/SyncFilter.cs66
-rw-r--r--LibMatrix/Helpers/MediaResolver.cs6
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs233
-rw-r--r--LibMatrix/Interfaces/IHomeServer.cs29
-rw-r--r--LibMatrix/Interfaces/IStateEventType.cs5
-rw-r--r--LibMatrix/Interfaces/Services/IStorageProvider.cs58
-rw-r--r--LibMatrix/LibMatrix.csproj14
-rw-r--r--LibMatrix/MatrixException.cs59
-rw-r--r--LibMatrix/MessagesResponse.cs18
-rw-r--r--LibMatrix/RemoteHomeServer.cs13
-rw-r--r--LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs18
-rw-r--r--LibMatrix/Responses/Admin/AdminRoomListingResult.cs64
-rw-r--r--LibMatrix/Responses/CreateRoomRequest.cs74
-rw-r--r--LibMatrix/Responses/CreationContentBaseType.cs18
-rw-r--r--LibMatrix/Responses/LoginResponse.cs17
-rw-r--r--LibMatrix/Responses/StateEventResponse.cs47
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs186
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs25
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs96
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs86
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs29
-rw-r--r--LibMatrix/Services/TieredStorageService.cs13
-rw-r--r--LibMatrix/StateEvent.cs117
-rw-r--r--LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs26
-rw-r--r--LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs13
-rw-r--r--LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs16
-rw-r--r--LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs18
-rw-r--r--LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs56
-rw-r--r--LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs17
-rw-r--r--LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs12
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs28
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs27
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs15
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs29
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs19
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs56
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs12
-rw-r--r--LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs11
-rw-r--r--LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs17
-rw-r--r--LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs15
-rw-r--r--LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs14
-rw-r--r--LibMatrix/UserIdAndReason.cs10
63 files changed, 2345 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5350fd3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+**/bin/
+**/obj/
+MatrixRoomUtils/
+MatrixRoomUtils.Web/wwwroot/MRU.tar.xz
+/src/
+*.tar.xz
+matrix-sync.json
+/patches/
+MatrixRoomUtils.Bot/bot_data/
+appsettings.Local*.json
diff --git a/.idea/.idea.LibMatrix/.idea/.gitignore b/.idea/.idea.LibMatrix/.idea/.gitignore
new file mode 100644
index 0000000..7846dc8
--- /dev/null
+++ b/.idea/.idea.LibMatrix/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/projectSettingsUpdater.xml
+/contentModel.xml
+/modules.xml
+/.idea.LibMatrix.iml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/.idea.LibMatrix/.idea/encodings.xml b/.idea/.idea.LibMatrix/.idea/encodings.xml
new file mode 100644
index 0000000..df87cf9
--- /dev/null
+++ b/.idea/.idea.LibMatrix/.idea/encodings.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.LibMatrix/.idea/indexLayout.xml b/.idea/.idea.LibMatrix/.idea/indexLayout.xml
new file mode 100644
index 0000000..7b08163
--- /dev/null
+++ b/.idea/.idea.LibMatrix/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="UserContentModel">
+    <attachedFolders />
+    <explicitIncludes />
+    <explicitExcludes />
+  </component>
+</project>
\ No newline at end of file
diff --git a/.idea/.idea.LibMatrix/.idea/vcs.xml b/.idea/.idea.LibMatrix/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/.idea.LibMatrix/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>
\ No newline at end of file
diff --git a/LibMatrix.sln b/LibMatrix.sln
new file mode 100644
index 0000000..3030c2f
--- /dev/null
+++ b/LibMatrix.sln
@@ -0,0 +1,22 @@
+

+Microsoft Visual Studio Solution File, Format Version 12.00

+# Visual Studio Version 17

+VisualStudioVersion = 17.0.31903.59

+MinimumVisualStudioVersion = 10.0.40219.1

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix", "LibMatrix\LibMatrix.csproj", "{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}"

+EndProject

+Global

+	GlobalSection(SolutionConfigurationPlatforms) = preSolution

+		Debug|Any CPU = Debug|Any CPU

+		Release|Any CPU = Release|Any CPU

+	EndGlobalSection

+	GlobalSection(SolutionProperties) = preSolution

+		HideSolutionNode = FALSE

+	EndGlobalSection

+	GlobalSection(ProjectConfigurationPlatforms) = postSolution

+		{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{2A07D7DA-7B8F-432D-8AD3-9679B58A7C19}.Release|Any CPU.Build.0 = Release|Any CPU

+	EndGlobalSection

+EndGlobal

diff --git a/LibMatrix/AuthenticatedHomeServer.cs b/LibMatrix/AuthenticatedHomeServer.cs
new file mode 100644
index 0000000..102d448
--- /dev/null
+++ b/LibMatrix/AuthenticatedHomeServer.cs
@@ -0,0 +1,174 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Filters;
+using LibMatrix.Helpers;
+using LibMatrix.Interfaces;
+using LibMatrix.Responses;
+using LibMatrix.Responses.Admin;
+using LibMatrix.RoomTypes;
+using LibMatrix.Services;
+
+namespace LibMatrix;
+
+public class AuthenticatedHomeServer : IHomeServer {
+    private readonly TieredStorageService _storage;
+    public readonly HomeserverAdminApi Admin;
+    public readonly SyncHelper SyncHelper;
+
+    public AuthenticatedHomeServer(TieredStorageService storage, string canonicalHomeServerDomain, string accessToken) {
+        _storage = storage;
+        AccessToken = accessToken.Trim();
+        HomeServerDomain = canonicalHomeServerDomain.Trim();
+        Admin = new HomeserverAdminApi(this);
+        SyncHelper = new SyncHelper(this, storage);
+        _httpClient = new MatrixHttpClient();
+    }
+
+    public WhoAmIResponse WhoAmI { get; set; } = null!;
+    public string UserId => WhoAmI.UserId;
+    public string AccessToken { get; set; }
+
+
+    public async Task<GenericRoom> GetRoom(string roomId) => new(this, roomId);
+
+    public async Task<List<GenericRoom>> GetJoinedRooms() {
+        var rooms = new List<GenericRoom>();
+        var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms");
+
+        var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>();
+        foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new GenericRoom(this, room.GetString()));
+
+        Console.WriteLine($"Fetched {rooms.Count} rooms");
+
+        return rooms;
+    }
+
+    public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") {
+        var res = await _httpClient.PostAsync($"/_matrix/media/v3/upload?filename={fileName}", new StreamContent(fileStream));
+        if (!res.IsSuccessStatusCode) {
+            Console.WriteLine($"Failed to upload file: {await res.Content.ReadAsStringAsync()}");
+            throw new InvalidDataException($"Failed to upload file: {await res.Content.ReadAsStringAsync()}");
+        }
+
+        var resJson = await res.Content.ReadFromJsonAsync<JsonElement>();
+        return resJson.GetProperty("content_uri").GetString()!;
+    }
+
+    public async Task<GenericRoom> CreateRoom(CreateRoomRequest creationEvent) {
+        var res = await _httpClient.PostAsJsonAsync("/_matrix/client/v3/createRoom", creationEvent);
+        if (!res.IsSuccessStatusCode) {
+            Console.WriteLine($"Failed to create room: {await res.Content.ReadAsStringAsync()}");
+            throw new InvalidDataException($"Failed to create room: {await res.Content.ReadAsStringAsync()}");
+        }
+
+        return await GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString());
+    }
+
+    public class HomeserverAdminApi {
+        private readonly AuthenticatedHomeServer _authenticatedHomeServer;
+
+        public HomeserverAdminApi(AuthenticatedHomeServer authenticatedHomeServer) => _authenticatedHomeServer = authenticatedHomeServer;
+
+        public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) {
+            AdminRoomListingResult? res = null;
+            var i = 0;
+            int? totalRooms = null;
+            do {
+                var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 100)}&dir={dir}&order_by={orderBy}";
+                if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}";
+
+                if (res?.NextBatch is not null) url += $"&from={res.NextBatch}";
+
+                Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---");
+
+                res = await _authenticatedHomeServer._httpClient.GetFromJsonAsync<AdminRoomListingResult>(url);
+                totalRooms ??= res?.TotalRooms;
+                Console.WriteLine(res.ToJson(false));
+                foreach (var room in res.Rooms) {
+                    if (localFilter is not null) {
+                        if (!room.RoomId.Contains(localFilter.RoomIdContains)) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.Name?.Contains(localFilter.NameContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.Version.Contains(localFilter.VersionContains)) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.Creator.Contains(localFilter.CreatorContains)) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if(!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if(!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) {
+                            totalRooms--;
+                            continue;
+                        }
+
+                        if(localFilter.CheckFederation && room.Federatable != localFilter.Federatable) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if(localFilter.CheckPublic && room.Public != localFilter.Public) {
+                            totalRooms--;
+                            continue;
+                        }
+
+                        if(room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) {
+                            totalRooms--;
+                            continue;
+                        }
+                        if(room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) {
+                            totalRooms--;
+                            continue;
+                        }
+                    }
+                    // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) &&
+                    //     !(
+                    //         room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true ||
+                    //         room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true ||
+                    //         room.Creator?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true
+                    //     )
+                    //    ) {
+                    //     totalRooms--;
+                    //     continue;
+                    // }
+
+                    i++;
+                    yield return room;
+                }
+            } while (i < Math.Min(limit, totalRooms ?? limit));
+        }
+    }
+}
+
+public class WhoAmIResponse {
+    [JsonPropertyName("user_id")]
+    public string UserId { get; set; } = null!;
+
+    [JsonPropertyName("device_id")]
+    public string? DeviceId { get; set; }
+    [JsonPropertyName("is_guest")]
+    public bool? IsGuest { get; set; }
+}
diff --git a/LibMatrix/EventIdResponse.cs b/LibMatrix/EventIdResponse.cs
new file mode 100644
index 0000000..31a95b8
--- /dev/null
+++ b/LibMatrix/EventIdResponse.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix;
+
+public class EventIdResponse {
+    [JsonPropertyName("event_id")]
+    public string EventId { get; set; } = null!;
+}
diff --git a/LibMatrix/Extensions/ClassCollector.cs b/LibMatrix/Extensions/ClassCollector.cs
new file mode 100644
index 0000000..f53850a
--- /dev/null
+++ b/LibMatrix/Extensions/ClassCollector.cs
@@ -0,0 +1,22 @@
+using System.Reflection;
+
+namespace LibMatrix.Extensions;
+
+public class ClassCollector<T> where T : class {
+    static ClassCollector() {
+        if (!typeof(T).IsInterface)
+            throw new ArgumentException(
+                $"ClassCollector<T> must be used with an interface type. Passed type: {typeof(T).Name}");
+    }
+
+    public List<Type> ResolveFromAllAccessibleAssemblies() => AppDomain.CurrentDomain.GetAssemblies().SelectMany(ResolveFromAssembly).ToList();
+
+    public List<Type> ResolveFromObjectReference(object obj) => ResolveFromTypeReference(obj.GetType());
+
+    public List<Type> ResolveFromTypeReference(Type t) => Assembly.GetAssembly(t)?.GetReferencedAssemblies().SelectMany(ResolveFromAssemblyName).ToList() ?? new List<Type>();
+
+    public List<Type> ResolveFromAssemblyName(AssemblyName assemblyName) => ResolveFromAssembly(Assembly.Load(assemblyName));
+
+    public List<Type> ResolveFromAssembly(Assembly assembly) => assembly.GetTypes()
+        .Where(x => x is { IsClass: true, IsAbstract: false } && x.GetInterfaces().Contains(typeof(T))).ToList();
+}
diff --git a/LibMatrix/Extensions/DictionaryExtensions.cs b/LibMatrix/Extensions/DictionaryExtensions.cs
new file mode 100644
index 0000000..fbc5cf5
--- /dev/null
+++ b/LibMatrix/Extensions/DictionaryExtensions.cs
@@ -0,0 +1,33 @@
+namespace LibMatrix.Extensions;
+
+public static class DictionaryExtensions {
+    public static bool ChangeKey<TKey, TValue>(this IDictionary<TKey, TValue> dict,
+        TKey oldKey, TKey newKey) {
+        TValue value;
+        if (!dict.Remove(oldKey, out value))
+            return false;
+
+        dict[newKey] = value; // or dict.Add(newKey, value) depending on ur comfort
+        return true;
+    }
+
+    public static Y GetOrCreate<X, Y>(this IDictionary<X, Y> dict, X key) where Y : new() {
+        if (dict.TryGetValue(key, out var value)) {
+            return value;
+        }
+
+        value = new Y();
+        dict.Add(key, value);
+        return value;
+    }
+
+    public static Y GetOrCreate<X, Y>(this IDictionary<X, Y> dict, X key, Func<X, Y> valueFactory) {
+        if (dict.TryGetValue(key, out var value)) {
+            return value;
+        }
+
+        value = valueFactory(key);
+        dict.Add(key, value);
+        return value;
+    }
+}
diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs
new file mode 100644
index 0000000..797a077
--- /dev/null
+++ b/LibMatrix/Extensions/HttpClientExtensions.cs
@@ -0,0 +1,76 @@
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Text.Json;
+
+namespace LibMatrix.Extensions;
+
+public static class HttpClientExtensions {
+    public static async Task<bool> CheckSuccessStatus(this HttpClient hc, string url) {
+        //cors causes failure, try to catch
+        try {
+            var resp = await hc.GetAsync(url);
+            return resp.IsSuccessStatusCode;
+        }
+        catch (Exception e) {
+            Console.WriteLine($"Failed to check success status: {e.Message}");
+            return false;
+        }
+    }
+}
+
+public class MatrixHttpClient : HttpClient {
+    public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
+        CancellationToken cancellationToken) {
+        Console.WriteLine($"Sending request to {request.RequestUri}");
+        try {
+            HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingResponseKey =
+                new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
+            request.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
+        }
+        catch (Exception e) {
+            Console.WriteLine("Failed to set browser response streaming:");
+            Console.WriteLine(e);
+        }
+
+        var a = await base.SendAsync(request, cancellationToken);
+        if (!a.IsSuccessStatusCode) {
+            var content = await a.Content.ReadAsStringAsync(cancellationToken);
+            if (content.StartsWith('{')) {
+                var ex = JsonSerializer.Deserialize<MatrixException>(content);
+                ex.RawContent = content;
+                // Console.WriteLine($"Failed to send request: {ex}");
+                if (ex?.RetryAfterMs is not null) {
+                    await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
+                    typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance)
+                        ?.SetValue(request, 0);
+                    return await SendAsync(request, cancellationToken);
+                }
+
+                throw ex!;
+            }
+
+            throw new InvalidDataException("Encountered invalid data:\n" + content);
+        }
+
+        return a;
+    }
+
+    // GetFromJsonAsync
+    public async Task<T> GetFromJsonAsync<T>(string requestUri, CancellationToken cancellationToken = default) {
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
+        return await JsonSerializer.DeserializeAsync<T>(responseStream, cancellationToken: cancellationToken);
+    }
+
+    // GetStreamAsync
+    public async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) {
+        var request = new HttpRequestMessage(HttpMethod.Get, requestUri);
+        request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
+        var response = await SendAsync(request, cancellationToken);
+        response.EnsureSuccessStatusCode();
+        return await response.Content.ReadAsStreamAsync(cancellationToken);
+    }
+}
diff --git a/LibMatrix/Extensions/IEnumerableExtensions.cs b/LibMatrix/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 0000000..8124947
--- /dev/null
+++ b/LibMatrix/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,7 @@
+namespace LibMatrix.Extensions;
+
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
+public class MatrixEventAttribute : Attribute {
+    public string EventName { get; set; }
+    public bool Legacy { get; set; }
+}
diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs
new file mode 100644
index 0000000..caf96e1
--- /dev/null
+++ b/LibMatrix/Extensions/JsonElementExtensions.cs
@@ -0,0 +1,150 @@
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using LibMatrix.Responses;
+
+namespace LibMatrix.Extensions;
+
+public static class JsonElementExtensions {
+    public static bool FindExtraJsonElementFields(this JsonElement obj, Type objectType, string objectPropertyName) {
+        if (objectPropertyName == "content" && objectType == typeof(JsonObject))
+            objectType = typeof(StateEventResponse);
+        // if (t == typeof(JsonNode))
+        //     return false;
+
+        Console.WriteLine($"{objectType.Name} {objectPropertyName}");
+        bool unknownPropertyFound = false;
+        var mappedPropsDict = objectType.GetProperties()
+            .Where(x => x.GetCustomAttribute<JsonPropertyNameAttribute>() is not null)
+            .ToDictionary(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()!.Name, x => x);
+        objectType.GetProperties().Where(x => !mappedPropsDict.ContainsKey(x.Name))
+            .ToList().ForEach(x => mappedPropsDict.TryAdd(x.Name, x));
+
+        foreach (var field in obj.EnumerateObject()) {
+            if (mappedPropsDict.TryGetValue(field.Name, out var mappedProperty)) {
+                //dictionary
+                if (mappedProperty.PropertyType.IsGenericType &&
+                    mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) {
+                    unknownPropertyFound |= _checkDictionary(field, objectType, mappedProperty.PropertyType);
+                    continue;
+                }
+
+                if (mappedProperty.PropertyType.IsGenericType &&
+                    mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) {
+                    unknownPropertyFound |= _checkList(field, objectType, mappedProperty.PropertyType);
+                    continue;
+                }
+
+                if (field.Name == "content" && (objectType == typeof(StateEventResponse) || objectType == typeof(StateEvent))) {
+                    unknownPropertyFound |= field.FindExtraJsonPropertyFieldsByValueKind(
+                        StateEvent.GetStateEventType(obj.GetProperty("type").GetString()),
+                        mappedProperty.PropertyType);
+                    continue;
+                }
+
+                unknownPropertyFound |=
+                    field.FindExtraJsonPropertyFieldsByValueKind(objectType, mappedProperty.PropertyType);
+                continue;
+            }
+
+            Console.WriteLine($"[!!] Unknown property {field.Name} in {objectType.Name}!");
+            unknownPropertyFound = true;
+        }
+
+        return unknownPropertyFound;
+    }
+
+    private static bool FindExtraJsonPropertyFieldsByValueKind(this JsonProperty field, Type containerType,
+        Type propertyType) {
+        if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) {
+            propertyType = propertyType.GetGenericArguments()[0];
+        }
+
+        bool switchResult = false;
+        switch (field.Value.ValueKind) {
+            case JsonValueKind.Array:
+                switchResult = field.Value.EnumerateArray().Aggregate(switchResult,
+                    (current, element) => current | element.FindExtraJsonElementFields(propertyType, field.Name));
+                break;
+            case JsonValueKind.Object:
+                switchResult |= field.Value.FindExtraJsonElementFields(propertyType, field.Name);
+                break;
+            case JsonValueKind.True:
+            case JsonValueKind.False:
+                return _checkBool(field, containerType, propertyType);
+            case JsonValueKind.String:
+                return _checkString(field, containerType, propertyType);
+            case JsonValueKind.Number:
+                return _checkNumber(field, containerType, propertyType);
+            case JsonValueKind.Undefined:
+            case JsonValueKind.Null:
+                break;
+            default:
+                throw new ArgumentOutOfRangeException();
+        }
+
+        return switchResult;
+    }
+
+    private static bool _checkBool(this JsonProperty field, Type containerType, Type propertyType) {
+        if (propertyType == typeof(bool)) return true;
+        Console.WriteLine(
+            $"[!!] Encountered bool for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!");
+        return false;
+    }
+
+    private static bool _checkString(this JsonProperty field, Type containerType, Type propertyType) {
+        if (propertyType == typeof(string)) return true;
+        // ReSharper disable once BuiltInTypeReferenceStyle
+        if (propertyType == typeof(String)) return true;
+        Console.WriteLine(
+            $"[!!] Encountered string for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!");
+        return false;
+    }
+
+    private static bool _checkNumber(this JsonProperty field, Type containerType, Type propertyType) {
+        if (propertyType == typeof(int) ||
+            propertyType == typeof(double) ||
+            propertyType == typeof(float) ||
+            propertyType == typeof(decimal) ||
+            propertyType == typeof(long) ||
+            propertyType == typeof(short) ||
+            propertyType == typeof(uint) ||
+            propertyType == typeof(ulong) ||
+            propertyType == typeof(ushort) ||
+            propertyType == typeof(byte) ||
+            propertyType == typeof(sbyte))
+            return true;
+        Console.WriteLine(
+            $"[!!] Encountered number for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!");
+        return false;
+    }
+
+    private static bool _checkDictionary(this JsonProperty field, Type containerType, Type propertyType) {
+        var keyType = propertyType.GetGenericArguments()[0];
+        var valueType = propertyType.GetGenericArguments()[1];
+        valueType = Nullable.GetUnderlyingType(valueType) ?? valueType;
+        Console.WriteLine(
+            $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!");
+
+        return field.Value.EnumerateObject()
+            .Where(key => !valueType.IsPrimitive && valueType != typeof(string))
+            .Aggregate(false, (current, key) =>
+                current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType)
+            );
+    }
+
+    private static bool _checkList(this JsonProperty field, Type containerType, Type propertyType) {
+        var valueType = propertyType.GetGenericArguments()[0];
+        valueType = Nullable.GetUnderlyingType(valueType) ?? valueType;
+        Console.WriteLine(
+            $"Encountered list {field.Name} with value type {valueType.Name}!");
+
+        return field.Value.EnumerateArray()
+            .Where(key => !valueType.IsPrimitive && valueType != typeof(string))
+            .Aggregate(false, (current, key) =>
+                current | key.FindExtraJsonElementFields(valueType, field.Name)
+            );
+    }
+}
diff --git a/LibMatrix/Extensions/ObjectExtensions.cs b/LibMatrix/Extensions/ObjectExtensions.cs
new file mode 100644
index 0000000..085de7d
--- /dev/null
+++ b/LibMatrix/Extensions/ObjectExtensions.cs
@@ -0,0 +1,14 @@
+using System.Text.Encodings.Web;
+using System.Text.Json;
+
+namespace LibMatrix.Extensions;
+
+public static class ObjectExtensions {
+    public static string ToJson(this object obj, bool indent = true, bool ignoreNull = false, bool unsafeContent = false) {
+        var jso = new JsonSerializerOptions();
+        if (indent) jso.WriteIndented = true;
+        if (ignoreNull) jso.IgnoreNullValues = true;
+        if (unsafeContent) jso.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
+        return JsonSerializer.Serialize(obj, jso);
+    }
+}
diff --git a/LibMatrix/Extensions/StringExtensions.cs b/LibMatrix/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..491fa77
--- /dev/null
+++ b/LibMatrix/Extensions/StringExtensions.cs
@@ -0,0 +1,13 @@
+namespace LibMatrix.Extensions;
+
+public static class StringExtensions {
+    // public static async Task<string> GetMediaUrl(this string MxcUrl)
+    // {
+    //     //MxcUrl: mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg
+    //     //target: https://matrix.rory.gay/_matrix/media/v3/download/rory.gay/ocRVanZoUTCcifcVNwXgbtTg
+    //
+    //     var server = MxcUrl.Split('/')[2];
+    //     var mediaId = MxcUrl.Split('/')[3];
+    //     return $"{(await new RemoteHomeServer(server).Configure()).FullHomeServerDomain}/_matrix/media/v3/download/{server}/{mediaId}";
+    // }
+}
diff --git a/LibMatrix/Filters/LocalRoomQueryFilter.cs b/LibMatrix/Filters/LocalRoomQueryFilter.cs
new file mode 100644
index 0000000..668d408
--- /dev/null
+++ b/LibMatrix/Filters/LocalRoomQueryFilter.cs
@@ -0,0 +1,28 @@
+namespace LibMatrix.Filters;
+
+public class LocalRoomQueryFilter {
+    public string RoomIdContains { get; set; } = "";
+    public string NameContains { get; set; } = "";
+    public string CanonicalAliasContains { get; set; } = "";
+    public string VersionContains { get; set; } = "";
+    public string CreatorContains { get; set; } = "";
+    public string EncryptionContains { get; set; } = "";
+    public string JoinRulesContains { get; set; } = "";
+    public string GuestAccessContains { get; set; } = "";
+    public string HistoryVisibilityContains { get; set; } = "";
+
+    public bool Federatable { get; set; } = true;
+    public bool Public { get; set; } = true;
+
+    public int JoinedMembersGreaterThan { get; set; } = 0;
+    public int JoinedMembersLessThan { get; set; } = int.MaxValue;
+
+    public int JoinedLocalMembersGreaterThan { get; set; } = 0;
+    public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue;
+    public int StateEventsGreaterThan { get; set; } = 0;
+    public int StateEventsLessThan { get; set; } = int.MaxValue;
+
+
+    public bool CheckFederation { get; set; }
+    public bool CheckPublic { get; set; }
+}
diff --git a/LibMatrix/Filters/SyncFilter.cs b/LibMatrix/Filters/SyncFilter.cs
new file mode 100644
index 0000000..c907f6b
--- /dev/null
+++ b/LibMatrix/Filters/SyncFilter.cs
@@ -0,0 +1,66 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Filters;
+
+public class SyncFilter {
+    [JsonPropertyName("account_data")]
+    public EventFilter? AccountData { get; set; }
+
+    [JsonPropertyName("presence")]
+    public EventFilter? Presence { get; set; }
+
+    [JsonPropertyName("room")]
+    public RoomFilter? Room { get; set; }
+
+    public class RoomFilter {
+        [JsonPropertyName("account_data")]
+        public StateFilter? AccountData { get; set; }
+
+        [JsonPropertyName("ephemeral")]
+        public StateFilter? Ephemeral { get; set; }
+
+        [JsonPropertyName("state")]
+        public StateFilter? State { get; set; }
+
+        [JsonPropertyName("timeline")]
+        public StateFilter? Timeline { get; set; }
+
+
+        public class StateFilter : EventFilter {
+            [JsonPropertyName("contains_url")]
+            public bool? ContainsUrl { get; set; }
+
+            [JsonPropertyName("include_redundant_members")]
+            public bool? IncludeRedundantMembers { get; set; }
+
+            [JsonPropertyName("lazy_load_members")]
+            public bool? LazyLoadMembers { get; set; }
+
+            [JsonPropertyName("rooms")]
+            public List<string>? Rooms { get; set; }
+
+            [JsonPropertyName("not_rooms")]
+            public List<string>? NotRooms { get; set; }
+
+            [JsonPropertyName("unread_thread_notifications")]
+            public bool? UnreadThreadNotifications { get; set; }
+        }
+    }
+
+    public class EventFilter {
+        [JsonPropertyName("limit")]
+        public int? Limit { get; set; }
+
+        [JsonPropertyName("types")]
+        public List<string>? Types { get; set; }
+
+        [JsonPropertyName("not_types")]
+        public List<string>? NotTypes { get; set; }
+
+        [JsonPropertyName("senders")]
+        public List<string>? Senders { get; set; }
+
+        [JsonPropertyName("not_senders")]
+        public List<string>? NotSenders { get; set; }
+    }
+}
diff --git a/LibMatrix/Helpers/MediaResolver.cs b/LibMatrix/Helpers/MediaResolver.cs
new file mode 100644
index 0000000..6ddb221
--- /dev/null
+++ b/LibMatrix/Helpers/MediaResolver.cs
@@ -0,0 +1,6 @@
+namespace LibMatrix.Helpers;
+
+public class MediaResolver {
+    public static string ResolveMediaUri(string homeserver, string mxc) =>
+        mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/");
+}
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
new file mode 100644
index 0000000..2015eaa
--- /dev/null
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -0,0 +1,233 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Filters;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+
+namespace LibMatrix.Helpers;
+
+public class SyncHelper {
+    private readonly AuthenticatedHomeServer _homeServer;
+    private readonly TieredStorageService _storageService;
+
+    public SyncHelper(AuthenticatedHomeServer homeServer, TieredStorageService storageService) {
+        _homeServer = homeServer;
+        _storageService = storageService;
+    }
+
+    public async Task<SyncResult?> Sync(
+        string? since = null,
+        int? timeout = 30000,
+        string? setPresence = "online",
+        SyncFilter? filter = null,
+        CancellationToken? cancellationToken = null) {
+        var outFileName = "sync-" +
+                          (await _storageService.CacheStorageProvider.GetAllKeysAsync()).Count(
+                              x => x.StartsWith("sync")) +
+                          ".json";
+        var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}";
+        if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}";
+        if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}";
+        // else url += "&full_state=true";
+        Console.WriteLine("Calling: " + url);
+        try {
+            var req = await _homeServer._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None);
+
+            // var res = await JsonSerializer.DeserializeAsync<SyncResult>(await req.Content.ReadAsStreamAsync());
+
+#if DEBUG && false
+            var jsonObj = await req.Content.ReadFromJsonAsync<JsonElement>();
+            try {
+                await _homeServer._httpClient.PostAsJsonAsync(
+                    "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName, jsonObj);
+            }
+            catch (Exception e) {
+                Console.WriteLine("[!!] Checking sync response failed: " + e);
+            }
+
+            var res = jsonObj.Deserialize<SyncResult>();
+            return res;
+#else
+            return await req.Content.ReadFromJsonAsync<SyncResult>();
+#endif
+        }
+        catch (TaskCanceledException) {
+            Console.WriteLine("Sync cancelled!");
+        }
+        catch (Exception e) {
+            Console.WriteLine(e);
+        }
+
+        return null;
+    }
+
+    [SuppressMessage("ReSharper", "FunctionNeverReturns")]
+    public async Task RunSyncLoop(
+        bool skipInitialSyncEvents = true,
+        string? since = null,
+        int? timeout = 30000,
+        string? setPresence = "online",
+        SyncFilter? filter = null,
+        CancellationToken? cancellationToken = null
+    ) {
+        await Task.WhenAll((await _storageService.CacheStorageProvider.GetAllKeysAsync())
+            .Where(x => x.StartsWith("sync"))
+            .ToList()
+            .Select(x => _storageService.CacheStorageProvider.DeleteObjectAsync(x)));
+        SyncResult? sync = null;
+        string? nextBatch = since;
+        while (cancellationToken is null || !cancellationToken.Value.IsCancellationRequested) {
+            sync = await Sync(since: nextBatch, timeout: timeout, setPresence: setPresence, filter: filter,
+                cancellationToken: cancellationToken);
+            nextBatch = sync?.NextBatch ?? nextBatch;
+            if (sync is null) continue;
+            Console.WriteLine($"Got sync, next batch: {nextBatch}!");
+
+            if (sync.Rooms is { Invite.Count: > 0 }) {
+                foreach (var roomInvite in sync.Rooms.Invite) {
+                    var tasks = InviteReceivedHandlers.Select(x => x(roomInvite)).ToList();
+                    await Task.WhenAll(tasks);
+                }
+            }
+
+            if (sync.AccountData is { Events: { Count: > 0 } }) {
+                foreach (var accountDataEvent in sync.AccountData.Events) {
+                    var tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList();
+                    await Task.WhenAll(tasks);
+                }
+            }
+
+            // Things that are skipped on the first sync
+            if (skipInitialSyncEvents) {
+                skipInitialSyncEvents = false;
+                continue;
+            }
+
+            if (sync.Rooms is { Join.Count: > 0 }) {
+                foreach (var updatedRoom in sync.Rooms.Join) {
+                    foreach (var stateEventResponse in updatedRoom.Value.Timeline.Events) {
+                        stateEventResponse.RoomId = updatedRoom.Key;
+                        var tasks = TimelineEventHandlers.Select(x => x(stateEventResponse)).ToList();
+                        await Task.WhenAll(tasks);
+                    }
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// Event fired when a room invite is received
+    /// </summary>
+    public List<Func<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>, Task>>
+        InviteReceivedHandlers { get; } = new();
+
+    public List<Func<StateEventResponse, Task>> TimelineEventHandlers { get; } = new();
+    public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new();
+}
+
+public class SyncResult {
+    [JsonPropertyName("next_batch")]
+    public string NextBatch { get; set; }
+
+    [JsonPropertyName("account_data")]
+    public EventList? AccountData { get; set; }
+
+    [JsonPropertyName("presence")]
+    public PresenceDataStructure? Presence { get; set; }
+
+    [JsonPropertyName("device_one_time_keys_count")]
+    public Dictionary<string, int> DeviceOneTimeKeysCount { get; set; }
+
+    [JsonPropertyName("rooms")]
+    public RoomsDataStructure? Rooms { get; set; }
+
+    [JsonPropertyName("to_device")]
+    public EventList? ToDevice { get; set; }
+
+    [JsonPropertyName("device_lists")]
+    public DeviceListsDataStructure? DeviceLists { get; set; }
+
+    public class DeviceListsDataStructure {
+        [JsonPropertyName("changed")]
+        public List<string>? Changed { get; set; }
+
+        [JsonPropertyName("left")]
+        public List<string>? Left { get; set; }
+    }
+
+    // supporting classes
+    public class PresenceDataStructure {
+        [JsonPropertyName("events")]
+        public List<StateEventResponse> Events { get; set; }
+    }
+
+    public class RoomsDataStructure {
+        [JsonPropertyName("join")]
+        public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; }
+
+        [JsonPropertyName("invite")]
+        public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; }
+
+        public class JoinedRoomDataStructure {
+            [JsonPropertyName("timeline")]
+            public TimelineDataStructure Timeline { get; set; }
+
+            [JsonPropertyName("state")]
+            public EventList State { get; set; }
+
+            [JsonPropertyName("account_data")]
+            public EventList AccountData { get; set; }
+
+            [JsonPropertyName("ephemeral")]
+            public EventList Ephemeral { get; set; }
+
+            [JsonPropertyName("unread_notifications")]
+            public UnreadNotificationsDataStructure UnreadNotifications { get; set; }
+
+            [JsonPropertyName("summary")]
+            public SummaryDataStructure Summary { get; set; }
+
+            public class TimelineDataStructure {
+                [JsonPropertyName("events")]
+                public List<StateEventResponse> Events { get; set; }
+
+                [JsonPropertyName("prev_batch")]
+                public string PrevBatch { get; set; }
+
+                [JsonPropertyName("limited")]
+                public bool Limited { get; set; }
+            }
+
+            public class UnreadNotificationsDataStructure {
+                [JsonPropertyName("notification_count")]
+                public int NotificationCount { get; set; }
+
+                [JsonPropertyName("highlight_count")]
+                public int HighlightCount { get; set; }
+            }
+
+            public class SummaryDataStructure {
+                [JsonPropertyName("m.heroes")]
+                public List<string> Heroes { get; set; }
+
+                [JsonPropertyName("m.invited_member_count")]
+                public int InvitedMemberCount { get; set; }
+
+                [JsonPropertyName("m.joined_member_count")]
+                public int JoinedMemberCount { get; set; }
+            }
+        }
+
+        public class InvitedRoomDataStructure {
+            [JsonPropertyName("invite_state")]
+            public EventList InviteState { get; set; }
+        }
+    }
+}
+
+public class EventList {
+    [JsonPropertyName("events")]
+    public List<StateEventResponse> Events { get; set; }
+}
diff --git a/LibMatrix/Interfaces/IHomeServer.cs b/LibMatrix/Interfaces/IHomeServer.cs
new file mode 100644
index 0000000..5e7e374
--- /dev/null
+++ b/LibMatrix/Interfaces/IHomeServer.cs
@@ -0,0 +1,29 @@
+using System.Net.Http.Json;
+using LibMatrix.Extensions;
+using LibMatrix.StateEventTypes.Spec;
+
+namespace LibMatrix.Interfaces;
+
+public class IHomeServer {
+    private readonly Dictionary<string, object> _profileCache = new();
+    public string HomeServerDomain { get; set; }
+    public string FullHomeServerDomain { get; set; }
+
+    public MatrixHttpClient _httpClient { get; set; } = new();
+
+    public async Task<ProfileResponseEventData> GetProfile(string mxid) {
+        if(mxid is null) throw new ArgumentNullException(nameof(mxid));
+        if (_profileCache.ContainsKey(mxid)) {
+            if (_profileCache[mxid] is SemaphoreSlim s) await s.WaitAsync();
+            if (_profileCache[mxid] is ProfileResponseEventData p) return p;
+        }
+        _profileCache[mxid] = new SemaphoreSlim(1);
+
+        var resp = await _httpClient.GetAsync($"/_matrix/client/v3/profile/{mxid}");
+        var data = await resp.Content.ReadFromJsonAsync<ProfileResponseEventData>();
+        if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data);
+        _profileCache[mxid] = data;
+
+        return data;
+    }
+}
diff --git a/LibMatrix/Interfaces/IStateEventType.cs b/LibMatrix/Interfaces/IStateEventType.cs
new file mode 100644
index 0000000..d80f22d
--- /dev/null
+++ b/LibMatrix/Interfaces/IStateEventType.cs
@@ -0,0 +1,5 @@
+namespace LibMatrix.Interfaces;
+
+public interface IStateEventType {
+
+}
diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs
new file mode 100644
index 0000000..519d8ed
--- /dev/null
+++ b/LibMatrix/Interfaces/Services/IStorageProvider.cs
@@ -0,0 +1,58 @@
+namespace LibMatrix.Interfaces.Services;
+
+public interface IStorageProvider {
+    // save all children of a type with reflection
+    public Task SaveAllChildrenAsync<T>(string key, T value) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren<T>(key, value)!");
+        throw new NotImplementedException();
+    }
+
+    // load all children of a type with reflection
+    public Task<T?> LoadAllChildrenAsync<T>(string key) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren<T>(key)!");
+        throw new NotImplementedException();
+    }
+
+
+    public Task SaveObjectAsync<T>(string key, T value) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!");
+        throw new NotImplementedException();
+    }
+
+    // load
+    public Task<T?> LoadObjectAsync<T>(string key) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject<T>(key)!");
+        throw new NotImplementedException();
+    }
+
+    // check if exists
+    public Task<bool> ObjectExistsAsync(string key) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!");
+        throw new NotImplementedException();
+    }
+
+    // get all keys
+    public Task<List<string>> GetAllKeysAsync() {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!");
+        throw new NotImplementedException();
+    }
+
+
+    // delete
+    public Task DeleteObjectAsync(string key) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!");
+        throw new NotImplementedException();
+    }
+
+    // save stream
+    public Task SaveStreamAsync(string key, Stream stream) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveStream(key, stream)!");
+        throw new NotImplementedException();
+    }
+
+    // load stream
+    public Task<Stream?> LoadStreamAsync(string key) {
+        Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!");
+        throw new NotImplementedException();
+    }
+}
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
new file mode 100644
index 0000000..3571eab
--- /dev/null
+++ b/LibMatrix/LibMatrix.csproj
@@ -0,0 +1,14 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+    <PropertyGroup>
+        <TargetFramework>net7.0</TargetFramework>
+        <ImplicitUsings>enable</ImplicitUsings>
+        <Nullable>enable</Nullable>
+    </PropertyGroup>
+
+    <ItemGroup>
+        <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
+        <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
+    </ItemGroup>
+
+</Project>
diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs
new file mode 100644
index 0000000..99bacb5
--- /dev/null
+++ b/LibMatrix/MatrixException.cs
@@ -0,0 +1,59 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+
+namespace LibMatrix;
+
+public class MatrixException : Exception {
+    [JsonPropertyName("errcode")]
+    public string ErrorCode { get; set; }
+
+    [JsonPropertyName("error")]
+    public string Error { get; set; }
+
+    [JsonPropertyName("soft_logout")]
+    public bool? SoftLogout { get; set; }
+
+    [JsonPropertyName("retry_after_ms")]
+    public int? RetryAfterMs { get; set; }
+
+    public string RawContent { get; set; }
+
+    public override string Message =>
+        $"{ErrorCode}: {ErrorCode switch {
+            // common
+            "M_FORBIDDEN" => $"You do not have permission to perform this action: {Error}",
+            "M_UNKNOWN_TOKEN" => $"The access token specified was not recognised: {Error}{(SoftLogout == true ? " (soft logout)" : "")}",
+            "M_MISSING_TOKEN" => $"No access token was specified: {Error}",
+            "M_BAD_JSON" => $"Request contained valid JSON, but it was malformed in some way: {Error}",
+            "M_NOT_JSON" => $"Request did not contain valid JSON: {Error}",
+            "M_NOT_FOUND" => $"The requested resource was not found: {Error}",
+            "M_LIMIT_EXCEEDED" => $"Too many requests have been sent in a short period of time. Wait a while then try again: {Error}",
+            "M_UNRECOGNISED" => $"The server did not recognise the request: {Error}",
+            "M_UNKOWN" => $"The server encountered an unexpected error: {Error}",
+            // endpoint specific
+            "M_UNAUTHORIZED" => $"The request did not contain valid authentication information for the target of the request: {Error}",
+            "M_USER_DEACTIVATED" => $"The user ID associated with the request has been deactivated: {Error}",
+            "M_USER_IN_USE" => $"The user ID associated with the request is already in use: {Error}",
+            "M_INVALID_USERNAME" => $"The requested user ID is not valid: {Error}",
+            "M_ROOM_IN_USE" => $"The room alias requested is already taken: {Error}",
+            "M_INVALID_ROOM_STATE" => $"The room associated with the request is not in a valid state to perform the request: {Error}",
+            "M_THREEPID_IN_USE" => $"The threepid requested is already associated with a user ID on this server: {Error}",
+            "M_THREEPID_NOT_FOUND" => $"The threepid requested is not associated with any user ID: {Error}",
+            "M_THREEPID_AUTH_FAILED" => $"The provided threepid and/or token was invalid: {Error}",
+            "M_THREEPID_DENIED" => $"The homeserver does not permit the third party identifier in question: {Error}",
+            "M_SERVER_NOT_TRUSTED" => $"The homeserver does not trust the identity server: {Error}",
+            "M_UNSUPPORTED_ROOM_VERSION" => $"The room version is not supported: {Error}",
+            "M_INCOMPATIBLE_ROOM_VERSION" => $"The room version is incompatible: {Error}",
+            "M_BAD_STATE" => $"The request was invalid because the state was invalid: {Error}",
+            "M_GUEST_ACCESS_FORBIDDEN" => $"Guest access is forbidden: {Error}",
+            "M_CAPTCHA_NEEDED" => $"Captcha needed: {Error}",
+            "M_CAPTCHA_INVALID" => $"Captcha invalid: {Error}",
+            "M_MISSING_PARAM" => $"Missing parameter: {Error}",
+            "M_INVALID_PARAM" => $"Invalid parameter: {Error}",
+            "M_TOO_LARGE" => $"The request or entity was too large: {Error}",
+            "M_EXCLUSIVE" => $"The resource being requested is reserved by an application service, or the application service making the request has not created the resource: {Error}",
+            "M_RESOURCE_LIMIT_EXCEEDED" => $"Exceeded resource limit: {Error}",
+            "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM" => $"Cannot leave server notice room: {Error}",
+            _ => $"Unknown error: {new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true)}"
+        }}";
+}
diff --git a/LibMatrix/MessagesResponse.cs b/LibMatrix/MessagesResponse.cs
new file mode 100644
index 0000000..f09d136
--- /dev/null
+++ b/LibMatrix/MessagesResponse.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Responses;
+
+namespace LibMatrix;
+
+public class MessagesResponse {
+    [JsonPropertyName("start")]
+    public string Start { get; set; }
+
+    [JsonPropertyName("end")]
+    public string? End { get; set; }
+
+    [JsonPropertyName("chunk")]
+    public List<StateEventResponse> Chunk { get; set; } = new();
+
+    [JsonPropertyName("state")]
+    public List<StateEventResponse> State { get; set; } = new();
+}
diff --git a/LibMatrix/RemoteHomeServer.cs b/LibMatrix/RemoteHomeServer.cs
new file mode 100644
index 0000000..81ef9a7
--- /dev/null
+++ b/LibMatrix/RemoteHomeServer.cs
@@ -0,0 +1,13 @@
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix;
+
+public class RemoteHomeServer : IHomeServer {
+    public RemoteHomeServer(string canonicalHomeServerDomain) {
+        HomeServerDomain = canonicalHomeServerDomain;
+        _httpClient = new MatrixHttpClient();
+        _httpClient.Timeout = TimeSpan.FromSeconds(5);
+    }
+
+}
diff --git a/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs b/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs
new file mode 100644
index 0000000..f22c8d2
--- /dev/null
+++ b/LibMatrix/Responses/Admin/AdminRoomDeleteRequest.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses.Admin;
+
+public class AdminRoomDeleteRequest {
+    [JsonPropertyName("new_room_user_id")]
+    public string? NewRoomUserId { get; set; }
+    [JsonPropertyName("room_name")]
+    public string? RoomName { get; set; }
+    [JsonPropertyName("block")]
+    public bool Block { get; set; }
+    [JsonPropertyName("purge")]
+    public bool Purge { get; set; }
+    [JsonPropertyName("message")]
+    public string? Message { get; set; }
+    [JsonPropertyName("force_purge")]
+    public bool ForcePurge { get; set; }
+}
diff --git a/LibMatrix/Responses/Admin/AdminRoomListingResult.cs b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs
new file mode 100644
index 0000000..f035184
--- /dev/null
+++ b/LibMatrix/Responses/Admin/AdminRoomListingResult.cs
@@ -0,0 +1,64 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses.Admin;
+
+public class AdminRoomListingResult {
+    [JsonPropertyName("offset")]
+    public int Offset { get; set; }
+
+    [JsonPropertyName("total_rooms")]
+    public int TotalRooms { get; set; }
+
+    [JsonPropertyName("next_batch")]
+    public int? NextBatch { get; set; }
+
+    [JsonPropertyName("prev_batch")]
+    public int? PrevBatch { get; set; }
+
+    [JsonPropertyName("rooms")]
+    public List<AdminRoomListingResultRoom> Rooms { get; set; } = new();
+
+    public class AdminRoomListingResultRoom {
+        [JsonPropertyName("room_id")]
+        public string RoomId { get; set; }
+
+        [JsonPropertyName("name")]
+        public string? Name { get; set; }
+
+        [JsonPropertyName("canonical_alias")]
+        public string? CanonicalAlias { get; set; }
+
+        [JsonPropertyName("joined_members")]
+        public int JoinedMembers { get; set; }
+
+        [JsonPropertyName("joined_local_members")]
+        public int JoinedLocalMembers { get; set; }
+
+        [JsonPropertyName("version")]
+        public string Version { get; set; }
+
+        [JsonPropertyName("creator")]
+        public string Creator { get; set; }
+
+        [JsonPropertyName("encryption")]
+        public string? Encryption { get; set; }
+
+        [JsonPropertyName("federatable")]
+        public bool Federatable { get; set; }
+
+        [JsonPropertyName("public")]
+        public bool Public { get; set; }
+
+        [JsonPropertyName("join_rules")]
+        public string? JoinRules { get; set; }
+
+        [JsonPropertyName("guest_access")]
+        public string? GuestAccess { get; set; }
+
+        [JsonPropertyName("history_visibility")]
+        public string? HistoryVisibility { get; set; }
+
+        [JsonPropertyName("state_events")]
+        public int StateEvents { get; set; }
+    }
+}
diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
new file mode 100644
index 0000000..d59e6fd
--- /dev/null
+++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -0,0 +1,74 @@
+using System.Reflection;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+using LibMatrix.Extensions;
+using LibMatrix.StateEventTypes.Spec;
+
+namespace LibMatrix.Responses;
+
+public class CreateRoomRequest {
+    [JsonIgnore] public CreationContentBaseType _creationContentBaseType;
+
+    public CreateRoomRequest() => _creationContentBaseType = new CreationContentBaseType(this);
+
+    [JsonPropertyName("name")]
+    public string Name { get; set; } = null!;
+
+    [JsonPropertyName("room_alias_name")]
+    public string RoomAliasName { get; set; } = null!;
+
+    //we dont want to use this, we want more control
+    // [JsonPropertyName("preset")]
+    // public string Preset { get; set; } = null!;
+
+    [JsonPropertyName("initial_state")]
+    public List<StateEvent> InitialState { get; set; } = null!;
+
+    [JsonPropertyName("visibility")]
+    public string Visibility { get; set; } = null!;
+
+    [JsonPropertyName("power_level_content_override")]
+    public RoomPowerLevelEventData PowerLevelContentOverride { get; set; } = null!;
+
+    [JsonPropertyName("creation_content")]
+    public JsonObject CreationContent { get; set; } = new();
+
+    /// <summary>
+    ///     For use only when you can't use the CreationContent property
+    /// </summary>
+
+    public StateEvent this[string event_type, string event_key = ""] {
+        get {
+            var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key);
+            if (stateEvent == null) {
+                InitialState.Add(stateEvent = new StateEvent {
+                    Type = event_type,
+                    StateKey = event_key,
+                    TypedContent = Activator.CreateInstance(
+                        StateEvent.KnownStateEventTypes.FirstOrDefault(x =>
+                            x.GetCustomAttributes<MatrixEventAttribute>()?
+                                .Any(y => y.EventName == event_type) ?? false) ?? typeof(object)
+                        )
+                });
+            }
+            return stateEvent;
+        }
+        set {
+            var stateEvent = InitialState.FirstOrDefault(x => x.Type == event_type && x.StateKey == event_key);
+            if (stateEvent == null)
+                InitialState.Add(value);
+            else
+                InitialState[InitialState.IndexOf(stateEvent)] = value;
+        }
+    }
+
+    public Dictionary<string, string> Validate() {
+        Dictionary<string, string> errors = new();
+        if (!Regex.IsMatch(RoomAliasName, @"[a-zA-Z0-9_\-]+$"))
+            errors.Add("room_alias_name",
+                "Room alias name must only contain letters, numbers, underscores, and hyphens.");
+
+        return errors;
+    }
+}
diff --git a/LibMatrix/Responses/CreationContentBaseType.cs b/LibMatrix/Responses/CreationContentBaseType.cs
new file mode 100644
index 0000000..ba3ce5e
--- /dev/null
+++ b/LibMatrix/Responses/CreationContentBaseType.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses;
+
+public class CreationContentBaseType {
+    private readonly CreateRoomRequest createRoomRequest;
+
+    public CreationContentBaseType(CreateRoomRequest createRoomRequest) => this.createRoomRequest = createRoomRequest;
+
+    [JsonPropertyName("type")]
+    public string Type {
+        get => (string)createRoomRequest.CreationContent["type"];
+        set {
+            if (value is "null" or "") createRoomRequest.CreationContent.Remove("type");
+            else createRoomRequest.CreationContent["type"] = value;
+        }
+    }
+}
diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
new file mode 100644
index 0000000..2800a9c
--- /dev/null
+++ b/LibMatrix/Responses/LoginResponse.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses;
+
+public class LoginResponse {
+    [JsonPropertyName("access_token")]
+    public string AccessToken { get; set; }
+
+    [JsonPropertyName("device_id")]
+    public string DeviceId { get; set; }
+
+    [JsonPropertyName("home_server")]
+    public string Homeserver { get; set; }
+
+    [JsonPropertyName("user_id")]
+    public string UserId { get; set; }
+}
diff --git a/LibMatrix/Responses/StateEventResponse.cs b/LibMatrix/Responses/StateEventResponse.cs
new file mode 100644
index 0000000..b3d5b96
--- /dev/null
+++ b/LibMatrix/Responses/StateEventResponse.cs
@@ -0,0 +1,47 @@
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Responses;
+
+public class StateEventResponse : StateEvent {
+    [JsonPropertyName("origin_server_ts")]
+    public ulong OriginServerTs { get; set; }
+
+    [JsonPropertyName("room_id")]
+    public string RoomId { get; set; }
+
+    [JsonPropertyName("sender")]
+    public string Sender { get; set; }
+
+    [JsonPropertyName("unsigned")]
+    public UnsignedData? Unsigned { get; set; }
+
+    [JsonPropertyName("event_id")]
+    public string EventId { get; set; }
+
+    [JsonPropertyName("user_id")]
+    public string UserId { get; set; }
+
+    [JsonPropertyName("replaces_state")]
+    public string ReplacesState { get; set; }
+
+    public class UnsignedData {
+        [JsonPropertyName("age")]
+        public ulong? Age { get; set; }
+
+        [JsonPropertyName("redacted_because")]
+        public object? RedactedBecause { get; set; }
+
+        [JsonPropertyName("transaction_id")]
+        public string? TransactionId { get; set; }
+
+        [JsonPropertyName("replaces_state")]
+        public string? ReplacesState { get; set; }
+
+        [JsonPropertyName("prev_sender")]
+        public string? PrevSender { get; set; }
+
+        [JsonPropertyName("prev_content")]
+        public JsonObject? PrevContent { get; set; }
+    }
+}
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
new file mode 100644
index 0000000..b935b9d
--- /dev/null
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -0,0 +1,186 @@
+using System.Net.Http.Json;
+using System.Text.Json;
+using System.Web;
+using LibMatrix.Extensions;
+using LibMatrix.Responses;
+using LibMatrix.StateEventTypes.Spec;
+
+namespace LibMatrix.RoomTypes;
+
+public class GenericRoom {
+    internal readonly AuthenticatedHomeServer _homeServer;
+    internal readonly MatrixHttpClient _httpClient;
+
+    public GenericRoom(AuthenticatedHomeServer homeServer, string roomId) {
+        _homeServer = homeServer;
+        _httpClient = homeServer._httpClient;
+        RoomId = roomId;
+        if (GetType() != typeof(SpaceRoom))
+            AsSpace = new SpaceRoom(homeServer, RoomId);
+    }
+
+    public string RoomId { get; set; }
+
+    [Obsolete("", true)]
+    public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "") {
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/state";
+        if (!string.IsNullOrEmpty(type)) url += $"/{type}";
+        if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}";
+        return await _httpClient.GetFromJsonAsync<JsonElement>(url);
+    }
+
+    public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() {
+        var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/state");
+        var result =
+            JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(await res.Content.ReadAsStreamAsync());
+        await foreach (var resp in result) {
+            yield return resp;
+        }
+    }
+
+    public async Task<T?> GetStateAsync<T>(string type, string stateKey = "") {
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/state";
+        if (!string.IsNullOrEmpty(type)) url += $"/{type}";
+        if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}";
+        try {
+#if DEBUG && false
+            var resp = await _httpClient.GetFromJsonAsync<JsonObject>(url);
+            try {
+                _homeServer._httpClient.PostAsJsonAsync(
+                    "http://localhost:5116/validate/" + typeof(T).AssemblyQualifiedName, resp);
+            }
+            catch (Exception e) {
+                Console.WriteLine("[!!] Checking state response failed: " + e);
+            }
+
+            return resp.Deserialize<T>();
+#else
+            var resp = await _httpClient.GetFromJsonAsync<T>(url);
+            return resp;
+#endif
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_NOT_FOUND" }) {
+                throw;
+            }
+
+            Console.WriteLine(e);
+            return default;
+        }
+    }
+
+    public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b",
+        string filter = "") {
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}";
+        if (!string.IsNullOrEmpty(filter)) url += $"&filter={filter}";
+        var res = await _httpClient.GetFromJsonAsync<MessagesResponse>(url);
+        return res ?? new MessagesResponse();
+    }
+
+    public async Task<string> GetNameAsync() {
+        try {
+            var res = await GetStateAsync<RoomNameEventData>("m.room.name");
+            return res?.Name ?? RoomId;
+        }
+        catch (MatrixException e) {
+            return $"{RoomId} ({e.ErrorCode})";
+        }
+    }
+
+    public async Task JoinAsync(string[]? homeservers = null, string? reason = null) {
+        var join_url = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(RoomId)}";
+        Console.WriteLine($"Calling {join_url} with {homeservers?.Length ?? 0} via's...");
+        if (homeservers == null || homeservers.Length == 0) homeservers = new[] { RoomId.Split(':')[1] };
+        var fullJoinUrl = $"{join_url}?server_name=" + string.Join("&server_name=", homeservers);
+        var res = await _httpClient.PostAsJsonAsync(fullJoinUrl, new {
+            reason
+        });
+    }
+
+    public async IAsyncEnumerable<StateEventResponse> GetMembersAsync(bool joinedOnly = true) {
+        var res = GetFullStateAsync();
+        await foreach (var member in res) {
+            if (member.Type != "m.room.member") continue;
+            if (joinedOnly && (member.TypedContent as RoomMemberEventData).Membership is not "join") continue;
+            yield return member;
+        }
+    }
+
+    public async Task<List<string>> GetAliasesAsync() {
+        var res = await GetStateAsync<RoomAliasEventData>("m.room.aliases");
+        return res.Aliases;
+    }
+
+    public async Task<CanonicalAliasEventData?> GetCanonicalAliasAsync() =>
+        await GetStateAsync<CanonicalAliasEventData>("m.room.canonical_alias");
+
+    public async Task<RoomTopicEventData?> GetTopicAsync() =>
+        await GetStateAsync<RoomTopicEventData>("m.room.topic");
+
+    public async Task<RoomAvatarEventData?> GetAvatarUrlAsync() =>
+        await GetStateAsync<RoomAvatarEventData>("m.room.avatar");
+
+    public async Task<JoinRulesEventData> GetJoinRuleAsync() =>
+        await GetStateAsync<JoinRulesEventData>("m.room.join_rules");
+
+    public async Task<HistoryVisibilityEventData?> GetHistoryVisibilityAsync() =>
+        await GetStateAsync<HistoryVisibilityEventData>("m.room.history_visibility");
+
+    public async Task<GuestAccessEventData?> GetGuestAccessAsync() =>
+        await GetStateAsync<GuestAccessEventData>("m.room.guest_access");
+
+    public async Task<RoomCreateEventData> GetCreateEventAsync() =>
+        await GetStateAsync<RoomCreateEventData>("m.room.create");
+
+    public async Task<string?> GetRoomType() {
+        var res = await GetStateAsync<RoomCreateEventData>("m.room.create");
+        return res.Type;
+    }
+
+    public async Task ForgetAsync() =>
+        await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
+
+    public async Task LeaveAsync(string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new {
+            reason
+        });
+
+    public async Task KickAsync(string userId, string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick",
+            new UserIdAndReason() { UserId = userId, Reason = reason });
+
+    public async Task BanAsync(string userId, string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban",
+            new UserIdAndReason() { UserId = userId, Reason = reason });
+
+    public async Task UnbanAsync(string userId) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
+            new UserIdAndReason() { UserId = userId });
+
+    public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) =>
+        await (await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content))
+            .Content.ReadFromJsonAsync<EventIdResponse>();
+
+    public async Task<EventIdResponse> SendMessageEventAsync(string eventType, RoomMessageEventData content) {
+        var res = await _httpClient.PutAsJsonAsync(
+            $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content);
+        var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
+        return resu;
+    }
+
+    public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) {
+        var content = new MultipartFormDataContent();
+        content.Add(new StreamContent(fileStream), "file", fileName);
+        var res = await
+            (
+                await _httpClient.PutAsync(
+                    $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(),
+                    content
+                )
+            )
+            .Content.ReadFromJsonAsync<EventIdResponse>();
+        return res;
+    }
+
+    public readonly SpaceRoom AsSpace;
+}
diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
new file mode 100644
index 0000000..ff2c228
--- /dev/null
+++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -0,0 +1,25 @@
+using LibMatrix.Extensions;
+
+namespace LibMatrix.RoomTypes;
+
+public class SpaceRoom : GenericRoom {
+    private readonly AuthenticatedHomeServer _homeServer;
+    private readonly GenericRoom _room;
+
+    public SpaceRoom(AuthenticatedHomeServer homeServer, string roomId) : base(homeServer, roomId) {
+        _homeServer = homeServer;
+    }
+
+    private static SemaphoreSlim _semaphore = new(1, 1);
+    public async IAsyncEnumerable<GenericRoom> GetRoomsAsync(bool includeRemoved = false) {
+        await _semaphore.WaitAsync();
+        var rooms = new List<GenericRoom>();
+        var state = GetFullStateAsync();
+        await foreach (var stateEvent in state) {
+            if (stateEvent.Type != "m.space.child") continue;
+            if (stateEvent.RawContent.ToJson() != "{}" || includeRemoved)
+                yield return await _homeServer.GetRoom(stateEvent.StateKey);
+        }
+        _semaphore.Release();
+    }
+}
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
new file mode 100644
index 0000000..61c449a
--- /dev/null
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -0,0 +1,96 @@
+using System.Net.Http.Headers;
+using System.Net.Http.Json;
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Responses;
+using Microsoft.Extensions.Logging;
+
+namespace LibMatrix.Services;
+
+public class HomeserverProviderService {
+    private readonly TieredStorageService _tieredStorageService;
+    private readonly ILogger<HomeserverProviderService> _logger;
+    private readonly HomeserverResolverService _homeserverResolverService;
+
+    public HomeserverProviderService(TieredStorageService tieredStorageService,
+        ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
+        Console.WriteLine("Homeserver provider service instantiated!");
+        _tieredStorageService = tieredStorageService;
+        _logger = logger;
+        _homeserverResolverService = homeserverResolverService;
+        logger.LogDebug(
+            $"New HomeserverProviderService created with TieredStorageService<{string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name))}>!");
+    }
+
+    private static Dictionary<string, SemaphoreSlim> _authenticatedHomeserverSemaphore = new();
+    private static Dictionary<string, AuthenticatedHomeServer> _authenticatedHomeServerCache = new();
+
+    public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken,
+        string? overrideFullDomain = null) {
+        SemaphoreSlim sem = _authenticatedHomeserverSemaphore.GetOrCreate(homeserver+accessToken, _ => new SemaphoreSlim(1, 1));
+        await sem.WaitAsync();
+        if (_authenticatedHomeServerCache.ContainsKey(homeserver+accessToken)) {
+            sem.Release();
+            return _authenticatedHomeServerCache[homeserver+accessToken];
+        }
+
+        var hs = new AuthenticatedHomeServer(_tieredStorageService, homeserver, accessToken);
+        hs.FullHomeServerDomain = overrideFullDomain ??
+                                  await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
+        hs._httpClient.Dispose();
+        hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
+        hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
+        hs._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
+
+        hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
+
+        _authenticatedHomeServerCache[homeserver+accessToken] = hs;
+        sem.Release();
+
+        return hs;
+    }
+
+    public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? overrideFullDomain = null) {
+        var hs = new RemoteHomeServer(homeserver);
+        hs.FullHomeServerDomain = overrideFullDomain ??
+                                  await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
+        hs._httpClient.Dispose();
+        hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
+        hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
+        return hs;
+    }
+
+    public async Task<LoginResponse> Login(string homeserver, string user, string password,
+        string? overrideFullDomain = null) {
+        var hs = await GetRemoteHomeserver(homeserver, overrideFullDomain);
+        var payload = new LoginRequest {
+            Identifier = new() { User = user },
+            Password = password
+        };
+        var resp = await hs._httpClient.PostAsJsonAsync("/_matrix/client/v3/login", payload);
+        var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
+        return data!;
+    }
+
+    private class LoginRequest {
+        [JsonPropertyName("type")]
+        public string Type { get; set; } = "m.login.password";
+
+        [JsonPropertyName("identifier")]
+        public LoginIdentifier Identifier { get; set; } = new();
+
+        [JsonPropertyName("password")]
+        public string Password { get; set; } = "";
+
+        [JsonPropertyName("initial_device_display_name")]
+        public string InitialDeviceDisplayName { get; set; } = "Rory&::LibMatrix";
+
+        public class LoginIdentifier {
+            [JsonPropertyName("type")]
+            public string Type { get; set; } = "m.id.user";
+
+            [JsonPropertyName("user")]
+            public string User { get; set; } = "";
+        }
+    }
+}
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
new file mode 100644
index 0000000..4d3bc46
--- /dev/null
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -0,0 +1,86 @@
+using System.Text.Json;
+using LibMatrix.Extensions;
+using Microsoft.Extensions.Logging;
+
+namespace LibMatrix.Services;
+
+public class HomeserverResolverService {
+    private readonly MatrixHttpClient _httpClient = new();
+    private readonly ILogger<HomeserverResolverService> _logger;
+
+    private static readonly Dictionary<string, string> _wellKnownCache = new();
+    private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new();
+
+    public HomeserverResolverService(ILogger<HomeserverResolverService> logger) {
+        _logger = logger;
+    }
+
+    public async Task<string> ResolveHomeserverFromWellKnown(string homeserver) {
+        var res = await _resolveHomeserverFromWellKnown(homeserver);
+        if (!res.StartsWith("http")) res = "https://" + res;
+        if (res.EndsWith(":443")) res = res.Substring(0, res.Length - 4);
+        return res;
+    }
+
+    private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) {
+        if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
+        SemaphoreSlim sem = _wellKnownSemaphores.GetOrCreate(homeserver, _ => new SemaphoreSlim(1, 1));
+        await sem.WaitAsync();
+        if (_wellKnownCache.ContainsKey(homeserver)) {
+            sem.Release();
+            return _wellKnownCache[homeserver];
+        }
+
+        string? result = null;
+        _logger.LogInformation($"Attempting to resolve homeserver: {homeserver}");
+        result ??= await _tryResolveFromClientWellknown(homeserver);
+        result ??= await _tryResolveFromServerWellknown(homeserver);
+        result ??= await _tryCheckIfDomainHasHomeserver(homeserver);
+
+        if (result is not null) {
+            _logger.LogInformation($"Resolved homeserver: {homeserver} -> {result}");
+            _wellKnownCache[homeserver] = result;
+            sem.Release();
+            return result;
+        }
+
+        throw new InvalidDataException($"Failed to resolve homeserver for {homeserver}! Is it online and configured correctly?");
+    }
+
+    private async Task<string?> _tryResolveFromClientWellknown(string homeserver) {
+        if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver;
+        if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/client")) {
+            var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/client");
+            var hs = resp.GetProperty("m.homeserver").GetProperty("base_url").GetString();
+            return hs;
+        }
+
+        _logger.LogInformation("No client well-known...");
+        return null;
+    }
+
+    private async Task<string?> _tryResolveFromServerWellknown(string homeserver) {
+        if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver;
+        if (await _httpClient.CheckSuccessStatus($"{homeserver}/.well-known/matrix/server")) {
+            var resp = await _httpClient.GetFromJsonAsync<JsonElement>($"{homeserver}/.well-known/matrix/server");
+            var hs = resp.GetProperty("m.server").GetString();
+            return hs;
+        }
+
+        _logger.LogInformation("No server well-known...");
+        return null;
+    }
+
+    private async Task<string?> _tryCheckIfDomainHasHomeserver(string homeserver) {
+        _logger.LogInformation($"Checking if {homeserver} hosts a homeserver...");
+        if (await _httpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions"))
+            return homeserver;
+        _logger.LogInformation("No homeserver on shortname...");
+        return null;
+    }
+
+    private async Task<string?> _tryCheckIfSubDomainHasHomeserver(string homeserver, string subdomain) {
+        homeserver = homeserver.Replace("https://", $"https://{subdomain}.");
+        return await _tryCheckIfDomainHasHomeserver(homeserver);
+    }
+}
diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
new file mode 100644
index 0000000..96a1963
--- /dev/null
+++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.DependencyInjection;
+
+namespace LibMatrix.Services;
+
+public static class ServiceInstaller {
+
+    public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) {
+        //Check required services
+        if (!services.Any(x => x.ServiceType == typeof(TieredStorageService)))
+            throw new Exception("[MRUCore/DI] No TieredStorageService has been registered!");
+        //Add config
+        if(config is not null)
+            services.AddSingleton(config);
+        else {
+            services.AddSingleton(new RoryLibMatrixConfiguration());
+        }
+        //Add services
+        services.AddSingleton<HomeserverProviderService>();
+        services.AddSingleton<HomeserverResolverService>();
+        // services.AddScoped<MatrixHttpClient>();
+        return services;
+    }
+
+
+}
+
+public class RoryLibMatrixConfiguration {
+    public string AppName { get; set; } = "Rory&::LibMatrix";
+}
diff --git a/LibMatrix/Services/TieredStorageService.cs b/LibMatrix/Services/TieredStorageService.cs
new file mode 100644
index 0000000..954a2ce
--- /dev/null
+++ b/LibMatrix/Services/TieredStorageService.cs
@@ -0,0 +1,13 @@
+using LibMatrix.Interfaces.Services;
+
+namespace LibMatrix.Services;
+
+public class TieredStorageService {
+    public IStorageProvider CacheStorageProvider { get; }
+    public IStorageProvider DataStorageProvider { get; }
+
+    public TieredStorageService(IStorageProvider cacheStorageProvider, IStorageProvider dataStorageProvider) {
+        CacheStorageProvider = cacheStorageProvider;
+        DataStorageProvider = dataStorageProvider;
+    }
+}
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
new file mode 100644
index 0000000..5efeaf5
--- /dev/null
+++ b/LibMatrix/StateEvent.cs
@@ -0,0 +1,117 @@
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix;
+
+public class StateEvent {
+    public static List<Type> KnownStateEventTypes =
+        new ClassCollector<IStateEventType>().ResolveFromAllAccessibleAssemblies();
+
+    public static Type GetStateEventType(string type) {
+        if (type == "m.receipt") {
+            return typeof(Dictionary<string, JsonObject>);
+        }
+
+        var eventType = KnownStateEventTypes.FirstOrDefault(x =>
+            x.GetCustomAttributes<MatrixEventAttribute>()?.Any(y => y.EventName == type) ?? false);
+
+        return eventType ?? typeof(object);
+    }
+
+    public object TypedContent {
+        get {
+            try {
+                return RawContent.Deserialize(GetType)!;
+            }
+            catch (JsonException e) {
+                Console.WriteLine(e);
+                Console.WriteLine("Content:\n" + ObjectExtensions.ToJson(RawContent));
+            }
+
+            return null;
+        }
+        set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value));
+    }
+
+    [JsonPropertyName("state_key")]
+    public string StateKey { get; set; } = "";
+
+    private string _type;
+
+    [JsonPropertyName("type")]
+    public string Type {
+        get => _type;
+        set {
+            _type = value;
+            // if (RawContent is not null && this is StateEventResponse stateEventResponse) {
+            //     if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
+            //     var x = GetType.Name;
+            // }
+        }
+    }
+
+    [JsonPropertyName("replaces_state")]
+    public string? ReplacesState { get; set; }
+
+    private JsonObject? _rawContent;
+
+    [JsonPropertyName("content")]
+    public JsonObject? RawContent {
+        get => _rawContent;
+        set {
+            _rawContent = value;
+            // if (Type is not null && this is StateEventResponse stateEventResponse) {
+            //     if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
+            //     var x = GetType.Name;
+            // }
+        }
+    }
+
+    [JsonIgnore]
+    public Type GetType {
+        get {
+            var type = GetStateEventType(Type);
+
+            //special handling for some types
+            // if (type == typeof(RoomEmotesEventData)) {
+            //     RawContent["emote"] = RawContent["emote"]?.AsObject() ?? new JsonObject();
+            // }
+            //
+            // if (this is StateEventResponse stateEventResponse) {
+            //     if (type == null || type == typeof(object)) {
+            //         Console.WriteLine($"Warning: unknown event type '{Type}'!");
+            //         Console.WriteLine(RawContent.ToJson());
+            //         Directory.CreateDirectory($"unknown_state_events/{Type}");
+            //         File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
+            //             RawContent.ToJson());
+            //         Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
+            //     }
+            //     else if (RawContent is not null && RawContent.FindExtraJsonObjectFields(type)) {
+            //         Directory.CreateDirectory($"unknown_state_events/{Type}");
+            //         File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
+            //             RawContent.ToJson());
+            //         Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
+            //     }
+            // }
+
+            return type;
+        }
+    }
+
+    //debug
+    public string dtype {
+        get {
+            var res = GetType().Name switch {
+                "StateEvent`1" => $"StateEvent",
+                _ => GetType().Name
+            };
+            return res;
+        }
+    }
+
+    public string cdtype => TypedContent.GetType().Name;
+}
diff --git a/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs
new file mode 100644
index 0000000..808a0db
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Common/MjolnirShortcodeEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Common;
+
+[MatrixEvent(EventName = "org.matrix.mjolnir.shortcode")]
+public class MjolnirShortcodeEventData : IStateEventType {
+    [JsonPropertyName("shortcode")]
+    public string? Shortcode { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs
new file mode 100644
index 0000000..af1c09e
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Common/RoomEmotesEventData.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Common;
+
+[MatrixEvent(EventName = "im.ponies.room_emotes")]
+public class RoomEmotesEventData : IStateEventType {
+    [JsonPropertyName("emoticons")]
+    public Dictionary<string, EmoticonData>? Emoticons { get; set; }
+
+    [JsonPropertyName("images")]
+    public Dictionary<string, EmoticonData>? Images { get; set; }
+
+    [JsonPropertyName("pack")]
+    public PackInfo? Pack { get; set; }
+
+    public class EmoticonData {
+        [JsonPropertyName("url")]
+        public string? Url { get; set; }
+    }
+
+    public class PackInfo {
+
+    }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs
new file mode 100644
index 0000000..36cc90e
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/CanonicalAliasEventData.cs
@@ -0,0 +1,13 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.canonical_alias")]
+public class CanonicalAliasEventData : IStateEventType {
+    [JsonPropertyName("alias")]
+    public string? Alias { get; set; }
+    [JsonPropertyName("alt_aliases")]
+    public string[]? AltAliases { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs
new file mode 100644
index 0000000..b6ddd93
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/GuestAccessEventData.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.guest_access")]
+public class GuestAccessEventData : IStateEventType {
+    [JsonPropertyName("guest_access")]
+    public string GuestAccess { get; set; }
+
+    public bool IsGuestAccessEnabled {
+        get => GuestAccess == "can_join";
+        set => GuestAccess = value ? "can_join" : "forbidden";
+    }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs
new file mode 100644
index 0000000..8836fc0
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/HistoryVisibilityEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.history_visibility")]
+public class HistoryVisibilityEventData : IStateEventType {
+    [JsonPropertyName("history_visibility")]
+    public string HistoryVisibility { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs
new file mode 100644
index 0000000..0393395
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/JoinRulesEventData.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.join_rules")]
+public class JoinRulesEventData : IStateEventType {
+    private static string Public = "public";
+    private static string Invite = "invite";
+    private static string Knock = "knock";
+
+    [JsonPropertyName("join_rule")]
+    public string JoinRule { get; set; }
+
+    [JsonPropertyName("allow")]
+    public List<string> Allow { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs
new file mode 100644
index 0000000..963864f
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/PolicyRuleStateEventData.cs
@@ -0,0 +1,56 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.policy.rule.user")]
+[MatrixEvent(EventName = "m.policy.rule.server")]
+[MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")]
+public class PolicyRuleStateEventData : IStateEventType {
+    /// <summary>
+    ///     Entity this ban applies to, can use * and ? as globs.
+    /// </summary>
+    [JsonPropertyName("entity")]
+    public string Entity { get; set; }
+
+    /// <summary>
+    ///     Reason this user is banned
+    /// </summary>
+    [JsonPropertyName("reason")]
+    public string? Reason { get; set; }
+
+    /// <summary>
+    ///     Suggested action to take
+    /// </summary>
+    [JsonPropertyName("recommendation")]
+    public string? Recommendation { get; set; }
+
+    /// <summary>
+    ///     Expiry time in milliseconds since the unix epoch, or null if the ban has no expiry.
+    /// </summary>
+    [JsonPropertyName("support.feline.policy.expiry.rev.2")] //stable prefix: expiry, msc pending
+    public long? Expiry { get; set; }
+
+    //utils
+    /// <summary>
+    ///     Readable expiry time, provided for easy interaction
+    /// </summary>
+    [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")]
+    public DateTime? ExpiryDateTime {
+        get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime;
+        set => Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds();
+    }
+}
+
+public static class PolicyRecommendationTypes {
+    /// <summary>
+    ///     Ban this user
+    /// </summary>
+    public static string Ban = "m.ban";
+
+    /// <summary>
+    ///     Mute this user
+    /// </summary>
+    public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending
+}
diff --git a/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs
new file mode 100644
index 0000000..fa75a88
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/PresenceStateEventData.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.presence")]
+public class PresenceStateEventData : IStateEventType {
+    [JsonPropertyName("presence")]
+    public string Presence { get; set; }
+    [JsonPropertyName("last_active_ago")]
+    public long LastActiveAgo { get; set; }
+    [JsonPropertyName("currently_active")]
+    public bool CurrentlyActive { get; set; }
+    [JsonPropertyName("status_msg")]
+    public string StatusMessage { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs b/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs
new file mode 100644
index 0000000..d2340f5
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/ProfileResponseEventData.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+public class ProfileResponseEventData : IStateEventType {
+    [JsonPropertyName("avatar_url")]
+    public string? AvatarUrl { get; set; } = "";
+
+    [JsonPropertyName("displayname")]
+    public string? DisplayName { get; set; } = "";
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs
new file mode 100644
index 0000000..8d921b2
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomAliasEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.alias")]
+public class RoomAliasEventData : IStateEventType {
+    [JsonPropertyName("aliases")]
+    public List<string>? Aliases { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs
new file mode 100644
index 0000000..cbe41dd
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomAvatarEventData.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.avatar")]
+public class RoomAvatarEventData : IStateEventType {
+    [JsonPropertyName("url")]
+    public string? Url { get; set; }
+
+    [JsonPropertyName("info")]
+    public RoomAvatarInfo? Info { get; set; }
+
+    public class RoomAvatarInfo {
+        [JsonPropertyName("h")]
+        public int? Height { get; set; }
+
+        [JsonPropertyName("w")]
+        public int? Width { get; set; }
+
+        [JsonPropertyName("mimetype")]
+        public string? MimeType { get; set; }
+
+        [JsonPropertyName("size")]
+        public int? Size { get; set; }
+    }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs
new file mode 100644
index 0000000..b96c31e
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomCreateEventData.cs
@@ -0,0 +1,27 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.create")]
+public class RoomCreateEventData : IStateEventType {
+    [JsonPropertyName("room_version")]
+    public string? RoomVersion { get; set; }
+    [JsonPropertyName("creator")]
+    public string? Creator { get; set; }
+    [JsonPropertyName("m.federate")]
+    public bool? Federate { get; set; }
+    [JsonPropertyName("predecessor")]
+    public RoomCreatePredecessor? Predecessor { get; set; }
+    [JsonPropertyName("type")]
+    public string? Type { get; set; }
+
+    public class RoomCreatePredecessor {
+        [JsonPropertyName("room_id")]
+        public string? RoomId { get; set; }
+
+        [JsonPropertyName("event_id")]
+        public string? EventId { get; set; }
+    }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs
new file mode 100644
index 0000000..e16716e
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomEncryptionEventData.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.encryption")]
+public class RoomEncryptionEventData : IStateEventType {
+    [JsonPropertyName("algorithm")]
+    public string? Algorithm { get; set; }
+    [JsonPropertyName("rotation_period_ms")]
+    public ulong? RotationPeriodMs { get; set; }
+    [JsonPropertyName("rotation_period_msgs")]
+    public ulong? RotationPeriodMsgs { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs
new file mode 100644
index 0000000..623c43c
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomMemberEventData.cs
@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.member")]
+public class RoomMemberEventData : IStateEventType {
+    [JsonPropertyName("reason")]
+    public string? Reason { get; set; }
+
+    [JsonPropertyName("membership")]
+    public string Membership { get; set; } = null!;
+
+    [JsonPropertyName("displayname")]
+    public string? Displayname { get; set; }
+
+    [JsonPropertyName("is_direct")]
+    public bool? IsDirect { get; set; }
+
+    [JsonPropertyName("avatar_url")]
+    public string? AvatarUrl { get; set; }
+
+    [JsonPropertyName("kind")]
+    public string? Kind { get; set; }
+
+    [JsonPropertyName("join_authorised_via_users_server")]
+    public string? JoinAuthorisedViaUsersServer { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs
new file mode 100644
index 0000000..14dd67a
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomMessageEventData.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.message")]
+public class RoomMessageEventData : IStateEventType {
+    [JsonPropertyName("body")]
+    public string Body { get; set; }
+    [JsonPropertyName("msgtype")]
+    public string MessageType { get; set; } = "m.notice";
+
+    [JsonPropertyName("formatted_body")]
+    public string FormattedBody { get; set; }
+
+    [JsonPropertyName("format")]
+    public string Format { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs
new file mode 100644
index 0000000..9d13513
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomNameEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.name")]
+public class RoomNameEventData : IStateEventType {
+    [JsonPropertyName("name")]
+    public string? Name { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs
new file mode 100644
index 0000000..c7d29fa
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomPinnedEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.pinned_events")]
+public class RoomPinnedEventData : IStateEventType {
+    [JsonPropertyName("pinned")]
+    public string[]? PinnedEvents { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs
new file mode 100644
index 0000000..c5dda78
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomPowerLevelEventData.cs
@@ -0,0 +1,56 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.power_levels")]
+public class RoomPowerLevelEventData : IStateEventType {
+    [JsonPropertyName("ban")]
+    public int Ban { get; set; } // = 50;
+
+    [JsonPropertyName("events_default")]
+    public int EventsDefault { get; set; } // = 0;
+
+    [JsonPropertyName("events")]
+    public Dictionary<string, int> Events { get; set; } // = null!;
+
+    [JsonPropertyName("invite")]
+    public int Invite { get; set; } // = 50;
+
+    [JsonPropertyName("kick")]
+    public int Kick { get; set; } // = 50;
+
+    [JsonPropertyName("notifications")]
+    public NotificationsPL NotificationsPl { get; set; } // = null!;
+
+    [JsonPropertyName("redact")]
+    public int Redact { get; set; } // = 50;
+
+    [JsonPropertyName("state_default")]
+    public int StateDefault { get; set; } // = 50;
+
+    [JsonPropertyName("users")]
+    public Dictionary<string, int> Users { get; set; } // = null!;
+
+    [JsonPropertyName("users_default")]
+    public int UsersDefault { get; set; } // = 0;
+
+    [Obsolete("Historical was a key related to MSC2716, a spec change on backfill that was dropped!", true)]
+    [JsonIgnore]
+    [JsonPropertyName("historical")]
+    public int Historical { get; set; } // = 50;
+
+    public class NotificationsPL {
+        [JsonPropertyName("room")]
+        public int Room { get; set; } = 50;
+    }
+
+    public bool IsUserAdmin(string userId) {
+        return Users.TryGetValue(userId, out var level) && level >= Events.Max(x=>x.Value);
+    }
+
+    public bool UserHasPermission(string userId, string eventType) {
+        return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault);
+    }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs
new file mode 100644
index 0000000..0fd0df6
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomTopicEventData.cs
@@ -0,0 +1,12 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.topic")]
+[MatrixEvent(EventName = "org.matrix.msc3765.topic", Legacy = true)]
+public class RoomTopicEventData : IStateEventType {
+    [JsonPropertyName("topic")]
+    public string? Topic { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs
new file mode 100644
index 0000000..857338c
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/RoomTypingEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.typing")]
+public class RoomTypingEventData : IStateEventType {
+    [JsonPropertyName("user_ids")]
+    public string[]? UserIds { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs
new file mode 100644
index 0000000..68bbe6b
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/ServerACLEventData.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.room.server_acl")]
+public class ServerACLEventData : IStateEventType {
+    [JsonPropertyName("allow")]
+    public List<string> Allow { get; set; } // = null!;
+
+    [JsonPropertyName("deny")]
+    public List<string> Deny { get; set; } // = null!;
+
+    [JsonPropertyName("allow_ip_literals")]
+    public bool AllowIpLiterals { get; set; } // = false;
+}
diff --git a/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs
new file mode 100644
index 0000000..a55e941
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/SpaceChildEventData.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.space.child")]
+public class SpaceChildEventData : IStateEventType {
+    [JsonPropertyName("auto_join")]
+    public bool? AutoJoin { get; set; }
+    [JsonPropertyName("via")]
+    public string[]? Via { get; set; }
+    [JsonPropertyName("suggested")]
+    public bool? Suggested { get; set; }
+}
diff --git a/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs
new file mode 100644
index 0000000..7dc7f4c
--- /dev/null
+++ b/LibMatrix/StateEventTypes/Spec/SpaceParentEventData.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+using LibMatrix.Extensions;
+using LibMatrix.Interfaces;
+
+namespace LibMatrix.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.space.parent")]
+public class SpaceParentEventData : IStateEventType {
+    [JsonPropertyName("via")]
+    public string[]? Via { get; set; }
+
+    [JsonPropertyName("canonical")]
+    public bool? Canonical { get; set; }
+}
diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/UserIdAndReason.cs
new file mode 100644
index 0000000..a0c2acd
--- /dev/null
+++ b/LibMatrix/UserIdAndReason.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix;
+
+internal class UserIdAndReason {
+    [JsonPropertyName("user_id")]
+    public string UserId { get; set; }
+    [JsonPropertyName("reason")]
+    public string? Reason { get; set; }
+}