about summary refs log tree commit diff
path: root/MatrixRoomUtils.Core
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixRoomUtils.Core')
-rw-r--r--MatrixRoomUtils.Core/AuthenticatedHomeServer.cs23
-rw-r--r--MatrixRoomUtils.Core/Authentication/MatrixAuth.cs3
-rw-r--r--MatrixRoomUtils.Core/CreateEvent.cs20
-rw-r--r--MatrixRoomUtils.Core/EventIdResponse.cs8
-rw-r--r--MatrixRoomUtils.Core/Extensions/ClassCollector.cs30
-rw-r--r--MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs36
-rw-r--r--MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs17
-rw-r--r--MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs2
-rw-r--r--MatrixRoomUtils.Core/Helpers/SyncHelper.cs88
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IHomeServer.cs6
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IStateEventType.cs5
-rw-r--r--MatrixRoomUtils.Core/JoinRules.cs15
-rw-r--r--MatrixRoomUtils.Core/MatrixException.cs2
-rw-r--r--MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj3
-rw-r--r--MatrixRoomUtils.Core/MessagesResponse.cs19
-rw-r--r--MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs234
-rw-r--r--MatrixRoomUtils.Core/Responses/CreationContentBaseType.cs18
-rw-r--r--MatrixRoomUtils.Core/Responses/LoginResponse.cs3
-rw-r--r--MatrixRoomUtils.Core/Responses/StateEventResponse.cs8
-rw-r--r--MatrixRoomUtils.Core/Room.cs90
-rw-r--r--MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs5
-rw-r--r--MatrixRoomUtils.Core/RuntimeCache.cs1
-rw-r--r--MatrixRoomUtils.Core/Services/HomeserverProviderService.cs4
-rw-r--r--MatrixRoomUtils.Core/Services/ServiceInstaller.cs15
-rw-r--r--MatrixRoomUtils.Core/StateEvent.cs55
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs16
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs9
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs23
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs3
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs43
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs5
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs (renamed from MatrixRoomUtils.Core/Responses/ProfileResponse.cs)4
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs18
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs17
-rw-r--r--MatrixRoomUtils.Core/UserIdAndReason.cs10
38 files changed, 539 insertions, 352 deletions
diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
index 74e85b1..23e98ae 100644
--- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
+++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
@@ -2,6 +2,7 @@ using System.Net.Http.Headers;
 using System.Net.Http.Json;
 using System.Text.Json;
 using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Filters;
 using MatrixRoomUtils.Core.Helpers;
@@ -17,15 +18,16 @@ public class AuthenticatedHomeServer : IHomeServer {
     public readonly HomeserverAdminApi Admin;
     public readonly SyncHelper SyncHelper;
 
-    public AuthenticatedHomeServer(string canonicalHomeServerDomain, string accessToken, TieredStorageService storage) {
+    public AuthenticatedHomeServer(TieredStorageService storage, string canonicalHomeServerDomain, string accessToken) {
         _storage = storage;
-        AccessToken = accessToken;
-        HomeServerDomain = canonicalHomeServerDomain;
+        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 { get; }
     public string AccessToken { get; set; }
 
@@ -35,6 +37,7 @@ public class AuthenticatedHomeServer : IHomeServer {
         _httpClient = new MatrixHttpClient { BaseAddress = new Uri(FullHomeServerDomain) };
         _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", AccessToken);
         Console.WriteLine("[AHS] Finished setting up http client");
+        WhoAmI = (await _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
 
         return this;
     }
@@ -54,7 +57,7 @@ public class AuthenticatedHomeServer : IHomeServer {
     }
 
     public async Task<string> UploadFile(string fileName, Stream fileStream, string contentType = "application/octet-stream") {
-        var res = await _httpClient.PostAsync($"/_matrix/media/r0/upload?filename={fileName}", new StreamContent(fileStream));
+        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()}");
@@ -65,7 +68,7 @@ public class AuthenticatedHomeServer : IHomeServer {
     }
 
     public async Task<Room> CreateRoom(CreateRoomRequest creationEvent) {
-        var res = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/createRoom", 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()}");
@@ -168,4 +171,14 @@ public class AuthenticatedHomeServer : IHomeServer {
             } 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; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
index 0f9eb58..83b279a 100644
--- a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
+++ b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
 using System.Text.Json;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Responses;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core.Authentication;
 
@@ -20,7 +21,7 @@ public class MatrixAuth {
             initial_device_display_name = "Rory&::MatrixRoomUtils"
         };
         Console.WriteLine($"Sending login request to {homeserver}...");
-        var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/r0/login", payload);
+        var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/v3/login", payload);
         Console.WriteLine($"Login: {resp.StatusCode}");
         var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
         if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data);
diff --git a/MatrixRoomUtils.Core/CreateEvent.cs b/MatrixRoomUtils.Core/CreateEvent.cs
new file mode 100644
index 0000000..a7022c5
--- /dev/null
+++ b/MatrixRoomUtils.Core/CreateEvent.cs
@@ -0,0 +1,20 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Core;
+
+public class CreateEvent {
+    [JsonPropertyName("creator")]
+    public string Creator { get; set; }
+
+    [JsonPropertyName("room_version")]
+    public string RoomVersion { get; set; }
+
+    [JsonPropertyName("type")]
+    public string? Type { get; set; }
+
+    [JsonPropertyName("predecessor")]
+    public object? Predecessor { get; set; }
+
+    [JsonPropertyName("m.federate")]
+    public bool Federate { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/EventIdResponse.cs b/MatrixRoomUtils.Core/EventIdResponse.cs
new file mode 100644
index 0000000..77dc7f8
--- /dev/null
+++ b/MatrixRoomUtils.Core/EventIdResponse.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Core;
+
+public class EventIdResponse {
+    [JsonPropertyName("event_id")]
+    public string EventId { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/ClassCollector.cs b/MatrixRoomUtils.Core/Extensions/ClassCollector.cs
new file mode 100644
index 0000000..9d3d3c0
--- /dev/null
+++ b/MatrixRoomUtils.Core/Extensions/ClassCollector.cs
@@ -0,0 +1,30 @@
+using System.Reflection;
+
+namespace MatrixRoomUtils.Core.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();
+    // {
+    //     List<Type> ret = new();
+    //     foreach (var x in assembly.GetTypes().Where(x => x is { IsClass: true, IsAbstract: false } && x.GetInterfaces().Contains(typeof(T))).ToList()) {
+    //         // Console.WriteLine($"[!!] Found class {x.FullName}");
+    //         ret.Add(x);
+    //     }
+    //     return ret;
+    // }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs b/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs
new file mode 100644
index 0000000..98b0aab
--- /dev/null
+++ b/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs
@@ -0,0 +1,36 @@
+using System.Reflection;
+using System.Text.Json;
+using MatrixRoomUtils.Core.Interfaces;
+using MatrixRoomUtils.Core.Responses;
+
+namespace MatrixRoomUtils.Core.Extensions;
+
+public static class IEnumerableExtensions {
+    public static List<StateEventResponse> DeserializeMatrixTypes(this List<JsonElement> stateEvents) {
+        return stateEvents.Select(DeserializeMatrixType).ToList();
+    }
+    
+    public static StateEventResponse DeserializeMatrixType(this JsonElement stateEvent) {
+        var type = stateEvent.GetProperty("type").GetString();
+        var knownType = StateEvent.KnownStateEventTypes.FirstOrDefault(x => x.GetCustomAttribute<MatrixEventAttribute>()?.EventName == type);
+        if (knownType == null) {
+            Console.WriteLine($"Warning: unknown event type '{type}'!");
+            return new StateEventResponse();
+        }
+        
+        var eventInstance = Activator.CreateInstance(typeof(StateEventResponse).MakeGenericType(knownType))!;
+        stateEvent.Deserialize(eventInstance.GetType());
+        
+        return (StateEventResponse) eventInstance;
+    }
+
+    public static void Replace(this List<StateEvent> stateEvents, StateEvent old, StateEvent @new) {
+        var index = stateEvents.IndexOf(old);
+        if (index == -1) return;
+        stateEvents[index] = @new;
+    }
+}
+
+public class MatrixEventAttribute : Attribute {
+    public string EventName { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
index b007136..78f4456 100644
--- a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
+++ b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
@@ -1,12 +1,13 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Reflection;
 using System.Text.Json;
+using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 
 namespace MatrixRoomUtils.Core.Extensions;
 
 public static class JsonElementExtensions {
-    public static void FindExtraJsonFields([DisallowNull] this JsonElement? res, Type t) {
+    public static void FindExtraJsonElementFields([DisallowNull] this JsonElement? res, Type t) {
         var props = t.GetProperties();
         var unknownPropertyFound = false;
         foreach (var field in res.Value.EnumerateObject()) {
@@ -17,4 +18,18 @@ public static class JsonElementExtensions {
 
         if (unknownPropertyFound) Console.WriteLine(res.Value.ToJson());
     }
+    public static void FindExtraJsonObjectFields([DisallowNull] this JsonObject? res, Type t) {
+        var props = t.GetProperties();
+        var unknownPropertyFound = false;
+        foreach (var field in res) {
+            if (props.Any(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name == field.Key)) continue;
+            Console.WriteLine($"[!!] Unknown property {field.Key} in {t.Name}!");
+            unknownPropertyFound = true;
+            // foreach (var propertyInfo in props) {
+            //     Console.WriteLine($"[!!] Known property {propertyInfo.GetCustomAttribute<JsonPropertyNameAttribute>()?.Name} in {t.Name}!");
+            // }
+        }
+
+        if (unknownPropertyFound) Console.WriteLine(res.ToJson());
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs
index 812c81c..a4b0791 100644
--- a/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs
+++ b/MatrixRoomUtils.Core/Extensions/ObjectExtensions.cs
@@ -1,5 +1,7 @@
 using System.Text.Encodings.Web;
 using System.Text.Json;
+using MatrixRoomUtils.Core.Interfaces;
+using MatrixRoomUtils.Core.Responses;
 
 namespace MatrixRoomUtils.Core.Extensions;
 
diff --git a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
index edbb646..04c31cd 100644
--- a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
+++ b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
@@ -1,5 +1,7 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Net.Http.Json;
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Interfaces;
 using MatrixRoomUtils.Core.Responses;
 using MatrixRoomUtils.Core.Services;
 using MatrixRoomUtils.Core.StateEventTypes;
@@ -15,7 +17,7 @@ public class SyncHelper {
         _storageService = storageService;
     }
 
-    public async Task<SyncResult?> Sync(string? since = null) {
+    public async Task<SyncResult?> Sync(string? since = null, CancellationToken? cancellationToken = null) {
         var outFileName = "sync-" +
                           (await _storageService.CacheStorageProvider.GetAllKeys()).Count(x => x.StartsWith("sync")) +
                           ".json";
@@ -23,12 +25,66 @@ public class SyncHelper {
         if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}";
         else url += "&full_state=true";
         Console.WriteLine("Calling: " + url);
-        var res = await _homeServer._httpClient.GetFromJsonAsync<SyncResult>(url);
-        await _storageService.CacheStorageProvider.SaveObject(outFileName, res);
-        return res;
+        try {
+            var res = await _homeServer._httpClient.GetFromJsonAsync<SyncResult>(url,
+                cancellationToken: cancellationToken ?? CancellationToken.None);
+            await _storageService.CacheStorageProvider.SaveObject(outFileName, res);
+            Console.WriteLine($"Wrote file: {outFileName}");
+            return res;
+        }
+        catch (TaskCanceledException) {
+            Console.WriteLine("Sync cancelled!");
+        }
+        catch (Exception e) {
+            Console.WriteLine(e);
+        }
+        return null;
+    }
+
+    [SuppressMessage("ReSharper", "FunctionNeverReturns")]
+    public async Task RunSyncLoop(CancellationToken? cancellationToken = null, bool skipInitialSyncEvents = true) {
+        SyncResult? sync = null;
+        while (cancellationToken is null || !cancellationToken.Value.IsCancellationRequested) {
+            sync = await Sync(sync?.NextBatch, cancellationToken);
+            Console.WriteLine($"Got sync, next batch: {sync?.NextBatch}!");
+            if (sync == null) continue;
+            if (sync.Rooms is { Invite.Count: > 0 }) {
+                foreach (var roomInvite in sync.Rooms.Invite) {
+                    Console.WriteLine(roomInvite.Value.GetType().Name);
+                    InviteReceived?.Invoke(this, roomInvite);
+                }
+            }
+
+            if (sync.AccountData is { Events: { Count: > 0 } }) {
+                foreach (var accountDataEvent in sync.AccountData.Events) {
+                    AccountDataReceived?.Invoke(this, accountDataEvent);
+                }
+            }
+
+            // 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;
+                        TimelineEventReceived?.Invoke(this, stateEventResponse);
+                    }
+                }
+            }
+        }
     }
-    
-    public event EventHandler<SyncResult>? ;
+
+    /// <summary>
+    /// Event fired when a room invite is received
+    /// </summary>
+    public event EventHandler<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>>? InviteReceived;
+
+    public event EventHandler<StateEventResponse>? TimelineEventReceived;
+    public event EventHandler<StateEventResponse>? AccountDataReceived;
 }
 
 public class SyncResult {
@@ -36,30 +92,30 @@ public class SyncResult {
     public string NextBatch { get; set; }
 
     [JsonPropertyName("account_data")]
-    public EventList AccountData { get; set; }
+    public EventList? AccountData { get; set; }
 
     [JsonPropertyName("presence")]
-    public PresenceDataStructure Presence { get; set; }
+    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; }
+    public RoomsDataStructure? Rooms { get; set; }
 
     // supporting classes
     public class PresenceDataStructure {
         [JsonPropertyName("events")]
-        public List<StateEventResponse<PresenceStateEventData>> Events { get; set; }
+        public List<StateEventResponse> Events { get; set; }
     }
 
     public class RoomsDataStructure {
         [JsonPropertyName("join")]
-        public Dictionary<string, JoinedRoomDataStructure> Join { get; set; }
+        public Dictionary<string, JoinedRoomDataStructure>? Join { get; set; }
 
         [JsonPropertyName("invite")]
-        public Dictionary<string, InvitedRoomDataStructure> Invite { get; set; }
-        
+        public Dictionary<string, InvitedRoomDataStructure>? Invite { get; set; }
+
         public class JoinedRoomDataStructure {
             [JsonPropertyName("timeline")]
             public TimelineDataStructure Timeline { get; set; }
@@ -75,7 +131,7 @@ public class SyncResult {
 
             [JsonPropertyName("unread_notifications")]
             public UnreadNotificationsDataStructure UnreadNotifications { get; set; }
-            
+
             [JsonPropertyName("summary")]
             public SummaryDataStructure Summary { get; set; }
 
@@ -97,12 +153,14 @@ public class SyncResult {
                 [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; }
             }
diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
index c5645e6..fcff0f2 100644
--- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
+++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
@@ -1,7 +1,7 @@
 using System.Net.Http.Json;
 using System.Text.Json;
 using MatrixRoomUtils.Core.Extensions;
-using MatrixRoomUtils.Core.Responses;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core.Interfaces;
 
@@ -91,7 +91,7 @@ public class IHomeServer {
         }
 
         _profileCache.Add(mxid, null);
-        var resp = await _httpClient.GetAsync($"/_matrix/client/r0/profile/{mxid}");
+        var resp = await _httpClient.GetAsync($"/_matrix/client/v3/profile/{mxid}");
         var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
         if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data);
         var profile = data.Deserialize<ProfileResponse>();
@@ -99,5 +99,5 @@ public class IHomeServer {
         return profile;
     }
 
-    public string? ResolveMediaUri(string mxc) => mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/r0/download/");
+    public string? ResolveMediaUri(string mxc) => mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/v3/download/");
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Interfaces/IStateEventType.cs b/MatrixRoomUtils.Core/Interfaces/IStateEventType.cs
new file mode 100644
index 0000000..053f50c
--- /dev/null
+++ b/MatrixRoomUtils.Core/Interfaces/IStateEventType.cs
@@ -0,0 +1,5 @@
+namespace MatrixRoomUtils.Core.Interfaces; 
+
+public interface IStateEventType {
+    
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/JoinRules.cs b/MatrixRoomUtils.Core/JoinRules.cs
new file mode 100644
index 0000000..7ce56c4
--- /dev/null
+++ b/MatrixRoomUtils.Core/JoinRules.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Core;
+
+public class JoinRules {
+    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; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/MatrixException.cs b/MatrixRoomUtils.Core/MatrixException.cs
index 3df70e1..50fae20 100644
--- a/MatrixRoomUtils.Core/MatrixException.cs
+++ b/MatrixRoomUtils.Core/MatrixException.cs
@@ -52,6 +52,6 @@ public class MatrixException : Exception {
             "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()
+            _ => "Unknown error: " + new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true)
         };
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
index 5ca40bd..a043378 100644
--- a/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
+++ b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
@@ -7,6 +7,9 @@
     </PropertyGroup>
 
     <ItemGroup>
+      <Reference Include="Microsoft.AspNetCore.Mvc.Core">
+        <HintPath>..\..\..\.cache\NuGetPackages\microsoft.aspnetcore.app.ref\7.0.5\ref\net7.0\Microsoft.AspNetCore.Mvc.Core.dll</HintPath>
+      </Reference>
       <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions">
         <HintPath>..\..\..\.cache\NuGetPackages\microsoft.extensions.dependencyinjection.abstractions\7.0.0\lib\net7.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll</HintPath>
       </Reference>
diff --git a/MatrixRoomUtils.Core/MessagesResponse.cs b/MatrixRoomUtils.Core/MessagesResponse.cs
new file mode 100644
index 0000000..7a303bc
--- /dev/null
+++ b/MatrixRoomUtils.Core/MessagesResponse.cs
@@ -0,0 +1,19 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Interfaces;
+using MatrixRoomUtils.Core.Responses;
+
+namespace MatrixRoomUtils.Core;
+
+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();
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
index da7d569..8719b5a 100644
--- a/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
+++ b/MatrixRoomUtils.Core/Responses/CreateRoomRequest.cs
@@ -1,8 +1,8 @@
-using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 using System.Text.RegularExpressions;
-using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core.Responses;
 
@@ -20,6 +20,7 @@ public class CreateRoomRequest {
     //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!;
 
@@ -47,237 +48,12 @@ public class CreateRoomRequest {
         }
     }
 
-    //extra properties
-    [JsonIgnore]
-    public string HistoryVisibility {
-        get {
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility");
-            if (stateEvent == null) {
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.history_visibility",
-                    Content = new JsonObject {
-                        ["history_visibility"] = "shared"
-                    }
-                });
-                return "shared";
-            }
-
-            return stateEvent.ContentAsJsonNode["history_visibility"].GetValue<string>();
-        }
-        set {
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.history_visibility");
-            if (stateEvent == null)
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.history_visibility",
-                    Content = new JsonObject {
-                        ["history_visibility"] = value
-                    }
-                });
-            else {
-                var v = stateEvent.ContentAsJsonNode;
-                v["history_visibility"] = value;
-                stateEvent.ContentAsJsonNode = v;
-            }
-        }
-    }
-
-    [JsonIgnore]
-    public string RoomIcon {
-        get {
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar");
-            if (stateEvent == null) {
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.avatar",
-                    Content = new JsonObject {
-                        ["url"] = ""
-                    }
-                });
-                return "";
-            }
-
-            return stateEvent.ContentAsJsonNode["url"].GetValue<string>();
-        }
-        set {
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.avatar");
-            if (stateEvent == null)
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.avatar",
-                    Content = new JsonObject {
-                        ["url"] = value
-                    }
-                });
-            else {
-                var v = stateEvent.ContentAsJsonNode;
-                v["url"] = value;
-                stateEvent.ContentAsJsonNode = v;
-            }
-        }
-    }
-
-    // [JsonIgnore]
-    // public string GuestAccess
-    // {
-    //     get
-    //     {
-    //         var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access");
-    //         if (stateEvent == null)
-    //         {
-    //             InitialState.Add(new StateEvent()
-    //             {
-    //                 Type = "m.room.guest_access",
-    //                 Content = new JsonObject()
-    //                 {
-    //                     ["guest_access"] = "can_join"
-    //                 }
-    //             });
-    //             return "can_join";
-    //         }
-    //
-    //         return stateEvent.ContentAsJsonNode["guest_access"].GetValue<string>();
-    //     }
-    //     set
-    //     {
-    //         var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.guest_access");
-    //         if (stateEvent == null)
-    //         {
-    //             InitialState.Add(new StateEvent()
-    //             {
-    //                 Type = "m.room.guest_access",
-    //                 Content = new JsonObject()
-    //                 {
-    //                     ["guest_access"] = value
-    //                 }
-    //             });
-    //         }
-    //         else
-    //         {
-    //             var v = stateEvent.ContentAsJsonNode;
-    //             v["guest_access"] = value;
-    //             stateEvent.ContentAsJsonNode = v;
-    //         }
-    //     }
-    // }
-
-    public ServerACL ServerACLs {
-        get {
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl");
-            if (stateEvent == null) {
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.server_acl",
-                    Content = new JsonObject {
-                        ["allow"] = new JsonArray {
-                            "*"
-                        },
-                        ["deny"] = new JsonArray()
-                    }
-                });
-                return new ServerACL {
-                    Allow = new List<string> {
-                        "*"
-                    },
-                    Deny = new List<string>(),
-                    AllowIpLiterals = true
-                };
-            }
-
-            return new ServerACL {
-                Allow = stateEvent.ContentAsJsonNode["allow"].Deserialize<List<string>>(),
-                Deny = stateEvent.ContentAsJsonNode["deny"].Deserialize<List<string>>(),
-                AllowIpLiterals = true
-            };
-        }
-        set {
-            Console.WriteLine($"Setting server acl to {value.ToJson()}");
-            var stateEvent = InitialState.FirstOrDefault(x => x.Type == "m.room.server_acl");
-            if (stateEvent == null)
-                InitialState.Add(new StateEvent {
-                    Type = "m.room.server_acl",
-                    Content = new JsonObject {
-                        ["allow"] = JsonNode.Parse(JsonSerializer.Serialize(value.Allow)),
-                        ["deny"] = JsonNode.Parse(JsonSerializer.Serialize(value.Deny))
-                            ["allow_ip_literals"] = value.AllowIpLiterals
-                    }
-                });
-            else {
-                var v = stateEvent.ContentAsJsonNode;
-                v["allow"] = JsonNode.Parse(JsonSerializer.Serialize(value.Allow));
-                v["deny"] = JsonNode.Parse(JsonSerializer.Serialize(value.Deny));
-                v["allow_ip_literals"] = value.AllowIpLiterals;
-                stateEvent.ContentAsJsonNode = v;
-                Console.WriteLine($"v={v.ToJson()}");
-                Console.WriteLine($"stateEvent.ContentAsJsonNode={stateEvent.ContentAsJsonNode.ToJson()}");
-            }
-        }
-    }
-
     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.");
+            errors.Add("room_alias_name",
+                "Room alias name must only contain letters, numbers, underscores, and hyphens.");
 
         return errors;
     }
-}
-
-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;
-        }
-    }
-}
-
-public class PowerLevelEvent {
-    [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;
-}
-
-public class NotificationsPL {
-    [JsonPropertyName("room")]
-    public int Room { get; set; } = 50;
-}
-
-public class ServerACL {
-    [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;
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Responses/CreationContentBaseType.cs b/MatrixRoomUtils.Core/Responses/CreationContentBaseType.cs
new file mode 100644
index 0000000..743c552
--- /dev/null
+++ b/MatrixRoomUtils.Core/Responses/CreationContentBaseType.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Core.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;
+        }
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Responses/LoginResponse.cs b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
index 3259e44..8d0d94f 100644
--- a/MatrixRoomUtils.Core/Responses/LoginResponse.cs
+++ b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
@@ -1,6 +1,7 @@
 using System.Net.Http.Json;
 using System.Text.Json;
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core.Responses;
 
@@ -19,7 +20,7 @@ public class LoginResponse {
 
     public async Task<ProfileResponse> GetProfile() {
         var hc = new HttpClient();
-        var resp = await hc.GetAsync($"{HomeServer}/_matrix/client/r0/profile/{UserId}");
+        var resp = await hc.GetAsync($"{HomeServer}/_matrix/client/v3/profile/{UserId}");
         var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
         if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data);
         return data.Deserialize<ProfileResponse>();
diff --git a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
index 7b138e0..6e67887 100644
--- a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
+++ b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
@@ -1,4 +1,5 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core.Responses;
 
@@ -26,7 +27,7 @@ public class StateEventResponse : StateEvent {
 
     [JsonPropertyName("prev_content")]
     public dynamic PrevContent { get; set; }
-
+    
     public class UnsignedData {
         [JsonPropertyName("age")]
         public ulong Age { get; set; }
@@ -40,9 +41,4 @@ public class StateEventResponse : StateEvent {
         [JsonPropertyName("transaction_id")]
         public string? TransactionId { get; set; }
     }
-}
-
-public class StateEventResponse<T> : StateEventResponse where T : class {
-    [JsonPropertyName("content")]
-    public T Content { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Room.cs b/MatrixRoomUtils.Core/Room.cs
index 1568746..59c56ed 100644
--- a/MatrixRoomUtils.Core/Room.cs
+++ b/MatrixRoomUtils.Core/Room.cs
@@ -1,10 +1,8 @@
 using System.Net.Http.Json;
 using System.Text;
 using System.Text.Json;
-using System.Text.Json.Serialization;
 using System.Web;
 using MatrixRoomUtils.Core.Extensions;
-using MatrixRoomUtils.Core.Responses;
 using MatrixRoomUtils.Core.RoomTypes;
 
 namespace MatrixRoomUtils.Core;
@@ -43,7 +41,7 @@ public class Room {
     }
 
     public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b", string filter = "") {
-        var url = $"/_matrix/client/r0/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}";
+        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.GetAsync(url);
         if (!res.IsSuccessStatusCode) {
@@ -67,12 +65,14 @@ public class Room {
         return resn;
     }
 
-    public async Task JoinAsync(string[]? homeservers = null) {
-        var join_url = $"/_matrix/client/r0/join/{HttpUtility.UrlEncode(RoomId)}";
+    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.PostAsync(fullJoinUrl, null);
+        var res = await _httpClient.PostAsJsonAsync(fullJoinUrl, new {
+            reason
+        });
     }
 
     public async Task<List<string>> GetMembersAsync(bool joinedOnly = true) {
@@ -138,7 +138,7 @@ public class Room {
         var res = await GetStateAsync("m.room.create");
         if (!res.HasValue) return new CreateEvent();
 
-        res.FindExtraJsonFields(typeof(CreateEvent));
+        res.FindExtraJsonElementFields(typeof(CreateEvent));
 
         return res.Value.Deserialize<CreateEvent>() ?? new CreateEvent();
     }
@@ -151,7 +151,7 @@ public class Room {
     }
     
     public async Task ForgetAsync() {
-        var res = await _httpClient.PostAsync($"/_matrix/client/r0/rooms/{RoomId}/forget", null);
+        var res = await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to forget room {RoomId} - got status: {res.StatusCode}");
             throw new Exception($"Failed to forget room {RoomId} - got status: {res.StatusCode}");
@@ -159,7 +159,9 @@ public class Room {
     }
     
     public async Task LeaveAsync(string? reason = null) {
-        var res = await _httpClient.PostAsync($"/_matrix/client/r0/rooms/{RoomId}/leave", string.IsNullOrWhiteSpace(reason) ? null : new StringContent($"{{\"reason\":\"{reason}\"}}", Encoding.UTF8, "application/json"));
+        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new {
+            reason
+        });
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to leave room {RoomId} - got status: {res.StatusCode}");
             throw new Exception($"Failed to leave room {RoomId} - got status: {res.StatusCode}");
@@ -168,7 +170,7 @@ public class Room {
     
     public async Task KickAsync(string userId, string? reason = null) {
        
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/r0/rooms/{RoomId}/kick", new UserIdAndReason() { user_id = userId, reason = reason });
+        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", new UserIdAndReason() { UserId = userId, Reason = reason });
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}");
             throw new Exception($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}");
@@ -176,7 +178,7 @@ public class Room {
     }
     
     public async Task BanAsync(string userId, string? reason = null) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/r0/rooms/{RoomId}/ban", new UserIdAndReason() { user_id = userId, reason = reason });
+        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", new UserIdAndReason() { UserId = userId, Reason = reason });
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}");
             throw new Exception($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}");
@@ -184,61 +186,31 @@ public class Room {
     }
     
     public async Task UnbanAsync(string userId) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/r0/rooms/{RoomId}/unban", new UserIdAndReason() { user_id = userId });
+        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", new UserIdAndReason() { UserId = userId });
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}");
             throw new Exception($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}");
         }
     }
     
+    public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) {
+        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content);
+        if (!res.IsSuccessStatusCode) {
+            Console.WriteLine($"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}");
+            throw new Exception($"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}");
+        }
+        return await res.Content.ReadFromJsonAsync<EventIdResponse>();
+    }
+    
+    public async Task<EventIdResponse> SendMessageEventAsync(string eventType, object content) {
+        var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/"+new Guid(), content);
+        if (!res.IsSuccessStatusCode) {
+            Console.WriteLine($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
+            throw new Exception($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
+        }
+        return await res.Content.ReadFromJsonAsync<EventIdResponse>();
+    }
 
 
     public readonly SpaceRoom AsSpace;
-}
-
-internal class UserIdAndReason {
-    public string user_id { get; set; }
-    public string? reason { get; set; }
-}
-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();
-}
-
-public class CreateEvent {
-    [JsonPropertyName("creator")]
-    public string Creator { get; set; }
-
-    [JsonPropertyName("room_version")]
-    public string RoomVersion { get; set; }
-
-    [JsonPropertyName("type")]
-    public string? Type { get; set; }
-
-    [JsonPropertyName("predecessor")]
-    public object? Predecessor { get; set; }
-
-    [JsonPropertyName("m.federate")]
-    public bool Federate { get; set; }
-}
-
-public class JoinRules {
-    private const string Public = "public";
-    private const string Invite = "invite";
-    private const string Knock = "knock";
-
-    [JsonPropertyName("join_rule")]
-    public string JoinRule { get; set; }
-
-    [JsonPropertyName("allow")]
-    public List<string> Allow { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
index 7f634dc..6eaa73b 100644
--- a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
+++ b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
@@ -1,5 +1,6 @@
 using System.Text.Json;
 using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
 using MatrixRoomUtils.Core.Responses;
 
 namespace MatrixRoomUtils.Core.RoomTypes;
@@ -11,10 +12,10 @@ public class SpaceRoom : Room {
         var rooms = new List<Room>();
         var state = await GetStateAsync("");
         if (state != null) {
-            var states = state.Value.Deserialize<StateEventResponse<object>[]>()!;
+            var states = state.Value.Deserialize<StateEventResponse[]>()!;
             foreach (var stateEvent in states.Where(x => x.Type == "m.space.child")) {
                 var roomId = stateEvent.StateKey;
-                if(stateEvent.Content.ToJson() != "{}" || includeRemoved)
+                if(stateEvent.TypedContent.ToJson() != "{}" || includeRemoved)
                     rooms.Add(await RuntimeCache.CurrentHomeServer.GetRoom(roomId));
             }
         }
diff --git a/MatrixRoomUtils.Core/RuntimeCache.cs b/MatrixRoomUtils.Core/RuntimeCache.cs
index db71ee5..7ab3952 100644
--- a/MatrixRoomUtils.Core/RuntimeCache.cs
+++ b/MatrixRoomUtils.Core/RuntimeCache.cs
@@ -1,5 +1,6 @@
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Responses;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core;
 
diff --git a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
index 3db4584..0f09a45 100644
--- a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
+++ b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
@@ -1,5 +1,3 @@
-using MatrixRoomUtils.Core.Attributes;
-
 namespace MatrixRoomUtils.Core.Services;
 
 public class HomeserverProviderService {
@@ -13,6 +11,6 @@ public class HomeserverProviderService {
     }
 
     public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken) {
-        return await new AuthenticatedHomeServer(homeserver, accessToken, _tieredStorageService).Configure();
+        return await new AuthenticatedHomeServer(_tieredStorageService, homeserver, accessToken).Configure();
     }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
index 43255d8..1b275c5 100644
--- a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
+++ b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
@@ -1,16 +1,27 @@
-using MatrixRoomUtils.Core.Interfaces.Services;
 using Microsoft.Extensions.DependencyInjection;
 
 namespace MatrixRoomUtils.Core.Services; 
 
 public static class ServiceInstaller {
     
-    public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services) {
+    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 != null)
+            services.AddSingleton(config);
+        else {
+            services.AddSingleton(new RoryLibMatrixConfiguration());
+        }
+        //Add services
         services.AddScoped<HomeserverProviderService>();
         return services;
     }
     
     
+}
+
+public class RoryLibMatrixConfiguration {
+    public string AppName { get; set; } = "Rory&::LibMatrix";
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs
index a8c1fac..cb8f0b4 100644
--- a/MatrixRoomUtils.Core/StateEvent.cs
+++ b/MatrixRoomUtils.Core/StateEvent.cs
@@ -1,12 +1,20 @@
+using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core;
 
 public class StateEvent {
-    [JsonPropertyName("content")]
-    public dynamic Content { get; set; } = new { };
+    public static List<Type> KnownStateEventTypes =
+        new ClassCollector<IStateEventType>().ResolveFromAllAccessibleAssemblies();
+
+    public object TypedContent {
+        get => RawContent.Deserialize(GetType)!;
+        set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value));
+    }
 
     [JsonPropertyName("state_key")]
     public string StateKey { get; set; } = "";
@@ -17,35 +25,40 @@ public class StateEvent {
     [JsonPropertyName("replaces_state")]
     public string? ReplacesState { get; set; }
 
-    //extra properties
+    [JsonPropertyName("content")]
+    public JsonObject? RawContent { get; set; }
+
+    public T1 GetContent<T1>() where T1 : IStateEventType {
+        return RawContent.Deserialize<T1>();
+    }
+
     [JsonIgnore]
-    public JsonNode ContentAsJsonNode {
-        get => JsonSerializer.SerializeToNode(Content);
-        set => Content = value;
+    public Type GetType {
+        get {
+            var type = StateEvent.KnownStateEventTypes.FirstOrDefault(x =>
+                x.GetCustomAttribute<MatrixEventAttribute>()?.EventName == Type);
+            if (type == null) {
+                Console.WriteLine($"Warning: unknown event type '{Type}'!");
+                Console.WriteLine(RawContent.ToJson());
+                return typeof(object);
+            }
+
+            RawContent.FindExtraJsonObjectFields(type);
+            
+            return type;
+        }
     }
 
+    //debug
     public string dtype {
         get {
             var res = GetType().Name switch {
-                "StateEvent`1" => $"StateEvent<{Content.GetType().Name}>",
+                "StateEvent`1" => $"StateEvent",
                 _ => GetType().Name
             };
             return res;
         }
     }
 
-    public StateEvent<T> As<T>() where T : class => (StateEvent<T>)this;
-}
-
-public class StateEvent<T> : StateEvent where T : class {
-    public StateEvent() {
-        //import base content if not an empty object
-        if (base.Content.GetType() == typeof(T)) {
-            Console.WriteLine($"StateEvent<{typeof(T)}> created with base content of type {base.Content.GetType()}. Importing base content.");
-            Content = base.Content;
-        }
-    }
-
-    [JsonPropertyName("content")]
-    public new T Content { get; set; }
+    public string cdtype => TypedContent.GetType().Name;
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs
new file mode 100644
index 0000000..2f9502e
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "m.room.canonical_alias")]
+public class CanonicalAliasEventData : IStateEventType {
+    [JsonPropertyName("alias")]
+    public string? Alias { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs b/MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs
new file mode 100644
index 0000000..1727ce9
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "m.room.guest_access")]
+public class GuestAccessData : IStateEventType {
+    [JsonPropertyName("guest_access")]
+    public string GuestAccess { get; set; }
+
+    public bool IsGuestAccessEnabled {
+        get => GuestAccess == "can_join";
+        set => GuestAccess = value ? "can_join" : "forbidden";
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs b/MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs
new file mode 100644
index 0000000..481cc08
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs
@@ -0,0 +1,9 @@
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "m.room.history_visibility")]
+public class HistoryVisibilityData : IStateEventType {
+    public string HistoryVisibility { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs
new file mode 100644
index 0000000..acf7777
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs
@@ -0,0 +1,23 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "m.room.member")]
+public class MemberEventData : 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; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs
new file mode 100644
index 0000000..ad99709
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+[MatrixEvent(EventName = "m.room.message")]
+public class MessageEventData : IStateEventType {
+    [JsonPropertyName("body")]
+    public string Body { get; set; }
+    [JsonPropertyName("msgtype")]
+    public string MessageType { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
index 6f6d082..e67639b 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
@@ -1,8 +1,9 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core.StateEventTypes;
 
-public class PolicyRuleStateEventData {
+public class PolicyRuleStateEventData : IStateEventType {
     /// <summary>
     ///     Entity this ban applies to, can use * and ? as globs.
     /// </summary>
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs b/MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs
new file mode 100644
index 0000000..a3e44d1
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs
@@ -0,0 +1,43 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "m.room.power_levels")]
+public class PowerLevelEvent : 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;
+    
+    public class NotificationsPL {
+        [JsonPropertyName("room")]
+        public int Room { get; set; } = 50;
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs
index d835c95..a17b6f9 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs
@@ -1,8 +1,11 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core.StateEventTypes; 
 
-public class PresenceStateEventData {
+[MatrixEvent(EventName = "m.presence")]
+public class PresenceStateEventData : IStateEventType {
     [JsonPropertyName("presence")]
     public string Presence { get; set; }
     [JsonPropertyName("last_active_ago")]
diff --git a/MatrixRoomUtils.Core/Responses/ProfileResponse.cs b/MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs
index db72386..d36ef74 100644
--- a/MatrixRoomUtils.Core/Responses/ProfileResponse.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs
@@ -1,7 +1,9 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
 
-namespace MatrixRoomUtils.Core.Responses;
+namespace MatrixRoomUtils.Core.StateEventTypes;
 
+[MatrixEvent(EventName = "m.room.member")]
 public class ProfileResponse {
     [JsonPropertyName("avatar_url")]
     public string? AvatarUrl { get; set; } = "";
diff --git a/MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs
new file mode 100644
index 0000000..03ce16b
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs
@@ -0,0 +1,18 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[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 {
+        
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs
new file mode 100644
index 0000000..72651c8
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "m.room.topic")]
+public class RoomTopicEventData : IStateEventType {
+    [JsonPropertyName("topic")]
+    public string? Topic { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs b/MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs
new file mode 100644
index 0000000..41bf0a8
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs
@@ -0,0 +1,17 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "m.room.server_acl")]
+public class ServerACLData : 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;
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/UserIdAndReason.cs b/MatrixRoomUtils.Core/UserIdAndReason.cs
new file mode 100644
index 0000000..3801077
--- /dev/null
+++ b/MatrixRoomUtils.Core/UserIdAndReason.cs
@@ -0,0 +1,10 @@
+using System.Text.Json.Serialization;
+
+namespace MatrixRoomUtils.Core;
+
+internal class UserIdAndReason {
+    [JsonPropertyName("user_id")]
+    public string UserId { get; set; }
+    [JsonPropertyName("reason")]
+    public string? Reason { get; set; }
+}
\ No newline at end of file