about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml4
-rw-r--r--MatrixRoomUtils.Bot/MRUBot.cs87
-rw-r--r--MatrixRoomUtils.Bot/MRUBotConfiguration.cs11
-rw-r--r--MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj6
-rw-r--r--MatrixRoomUtils.Bot/Program.cs26
-rw-r--r--MatrixRoomUtils.Bot/Properties/launchSettings.json26
-rw-r--r--MatrixRoomUtils.Bot/appsettings.Development.json9
-rw-r--r--MatrixRoomUtils.Bot/appsettings.json13
-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
-rw-r--r--MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs2
-rw-r--r--MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs5
-rw-r--r--MatrixRoomUtils.Web/Pages/DebugTools.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/DevOptions.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/LoginPage.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/MediaLocator.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor3
-rw-r--r--MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor38
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor1
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor2
-rw-r--r--MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor3
-rw-r--r--MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor7
-rw-r--r--MatrixRoomUtils.Web/Shared/InlineUserItem.razor4
-rw-r--r--MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor13
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor1
-rw-r--r--MatrixRoomUtils.Web/Shared/UserListItem.razor6
-rw-r--r--MatrixRoomUtils.Web/wwwroot/css/app.css2
-rwxr-xr-x[-rw-r--r--]MatrixRoomUtils.sln0
-rw-r--r--MatrixRoomUtils.sln.DotSettings.user16
71 files changed, 728 insertions, 461 deletions
diff --git a/.gitignore b/.gitignore
index 47d212e..33be3d4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,5 @@ MatrixRoomUtils.Web/wwwroot/MRU.tar.xz
 *.tar.xz
 matrix-sync.json
 /patches/
+MatrixRoomUtils.Bot/bot_data/
+appsettings.Local.json
diff --git a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml b/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml
index 7b08163..9c54b37 100644
--- a/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml
+++ b/.idea/.idea.MatrixRoomUtils/.idea/indexLayout.xml
@@ -1,7 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="UserContentModel">
-    <attachedFolders />
+    <attachedFolders>
+      <Path>MatrixRoomUtils.Bot/bot_data</Path>
+    </attachedFolders>
     <explicitIncludes />
     <explicitExcludes />
   </component>
diff --git a/MatrixRoomUtils.Bot/MRUBot.cs b/MatrixRoomUtils.Bot/MRUBot.cs
index 9b2a395..157673d 100644
--- a/MatrixRoomUtils.Bot/MRUBot.cs
+++ b/MatrixRoomUtils.Bot/MRUBot.cs
@@ -1,55 +1,82 @@
-using System.CodeDom.Compiler;
+using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using MatrixRoomUtils.Bot;
 using MatrixRoomUtils.Core;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Helpers;
-using MatrixRoomUtils.Core.Responses;
 using MatrixRoomUtils.Core.Services;
+using MatrixRoomUtils.Core.StateEventTypes;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
 public class MRUBot : IHostedService {
     private readonly HomeserverProviderService _homeserverProviderService;
     private readonly ILogger<MRUBot> _logger;
+    private readonly MRUBotConfiguration _configuration;
 
-    public MRUBot(HomeserverProviderService homeserverProviderService, ILogger<MRUBot> logger) {
+    public MRUBot(HomeserverProviderService homeserverProviderService, ILogger<MRUBot> logger,
+        MRUBotConfiguration configuration) {
         Console.WriteLine("MRUBot hosted service instantiated!");
         _homeserverProviderService = homeserverProviderService;
         _logger = logger;
+        _configuration = configuration;
     }
 
     /// <summary>Triggered when the application host is ready to start the service.</summary>
     /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
     [SuppressMessage("ReSharper", "FunctionNeverReturns")]
     public async Task StartAsync(CancellationToken cancellationToken) {
-        var hs = await _homeserverProviderService.GetAuthenticatedWithToken("rory.gay", "syt_bXJ1Y29yZXRlc3Q_XKUmPswDGZBiLAmFfAut_1iO0KD");
+        Directory.GetFiles("bot_data/cache").ToList().ForEach(File.Delete);
+        AuthenticatedHomeServer hs;
+        try {
+            hs = await _homeserverProviderService.GetAuthenticatedWithToken(_configuration.Homeserver,
+                _configuration.AccessToken);
+        }
+        catch (Exception e) {
+            _logger.LogError(e.Message);
+            throw;
+        }
+
         await (await hs.GetRoom("!DoHEdFablOLjddKWIp:rory.gay")).JoinAsync();
-        // #pragma warning disable CS4014
-        //         Task.Run(async Task? () => {
-        // #pragma warning restore CS4014
-        //             while (true) {
-        //                 var rooms = await hs.GetJoinedRooms();
-        //                 foreach (var room in rooms) {
-        //                     var states = await room.GetStateAsync<List<StateEventResponse>>("");
-        //                     foreach (var state in states) {
-        //                         // Console.WriteLine(
-        //                         //     $"{state.RoomId}: {state.Type}::{state.StateKey} = {ObjectExtensions.ToJson(state.Content, indent: false)}");
-        //                     }
-        //                 }
-        //
-        //                 await Task.Delay(1000, cancellationToken);
-        //             }
-        //         }, cancellationToken);
-        #pragma warning disable CS4014
-                Task.Run(async Task? () => {
-        #pragma warning restore CS4014
-                    SyncResult? sync = null;
-                    while (true) {
-                        sync = await hs.SyncHelper.Sync(sync?.NextBatch);
-                        _logger.LogInformation($"Got sync, next batch: {sync?.NextBatch}!");
-                    }
-                }, cancellationToken);
-        
+
+        hs.SyncHelper.InviteReceived += async (_, args) => {
+            // Console.WriteLine($"Got invite to {args.Key}:");
+            // foreach (var stateEvent in args.Value.InviteState.Events) {
+            // Console.WriteLine($"[{stateEvent.Sender}: {stateEvent.StateKey}::{stateEvent.Type}] " +
+            // ObjectExtensions.ToJson(stateEvent.Content, indent: false, ignoreNull: true));
+            // }
+
+            var inviteEvent =
+                args.Value.InviteState.Events.FirstOrDefault(x =>
+                    x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId);
+            Console.WriteLine(
+                $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as MemberEventData).Reason}");
+            if (inviteEvent.Sender == "@emma:rory.gay") {
+                try {
+                    await (await hs.GetRoom(args.Key)).JoinAsync(reason: "I was invited by Emma (Rory&)!");
+                }
+                catch (Exception e) {
+                    Console.WriteLine(e);
+                    await (await hs.GetRoom(args.Key)).LeaveAsync(reason: "I was unable to join the room: " + e);
+                }
+            }
+        };
+        hs.SyncHelper.TimelineEventReceived += async (_, @event) => {
+            Console.WriteLine(
+                $"Got timeline event in {@event.RoomId}: {@event.ToJson(indent: false, ignoreNull: true)}");
+
+            // Console.WriteLine(eventResponse.ToJson(indent: false));
+            if (@event is { Type: "m.room.message", TypedContent: MessageEventData message }) {
+                if (message is { MessageType: "m.text", Body: "!ping" }) {
+                    Console.WriteLine(
+                        $"Got ping from {@event.Sender} in {@event.RoomId} with message id {@event.EventId}!");
+                    await (await hs.GetRoom(@event.RoomId)).SendMessageEventAsync("m.room.message",
+                        new MessageEventData() { MessageType = "m.text", Body = "pong!" });
+                }
+            }
+        };
+
+        await hs.SyncHelper.RunSyncLoop(cancellationToken);
     }
 
     /// <summary>Triggered when the application host is performing a graceful shutdown.</summary>
diff --git a/MatrixRoomUtils.Bot/MRUBotConfiguration.cs b/MatrixRoomUtils.Bot/MRUBotConfiguration.cs
new file mode 100644
index 0000000..14d9b60
--- /dev/null
+++ b/MatrixRoomUtils.Bot/MRUBotConfiguration.cs
@@ -0,0 +1,11 @@
+using Microsoft.Extensions.Configuration;
+
+namespace MatrixRoomUtils.Bot; 
+
+public class MRUBotConfiguration {
+    public MRUBotConfiguration(IConfiguration config) {
+        config.GetRequiredSection("Bot").Bind(this);
+    }
+    public string Homeserver { get; set; } = "";
+    public string AccessToken { get; set; } = "";
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj b/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
index 095d0f6..a82b6aa 100644
--- a/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
+++ b/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
@@ -23,5 +23,9 @@
   <ItemGroup>

     <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0-preview.5.23280.8" />

   </ItemGroup>

-

+  <ItemGroup>

+    <Content Include="appsettings*.json">

+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>

+    </Content>

+  </ItemGroup>

 </Project>

diff --git a/MatrixRoomUtils.Bot/Program.cs b/MatrixRoomUtils.Bot/Program.cs
index 441003e..e8a5b96 100644
--- a/MatrixRoomUtils.Bot/Program.cs
+++ b/MatrixRoomUtils.Bot/Program.cs
@@ -7,20 +7,16 @@ using Microsoft.Extensions.Hosting;
 

 Console.WriteLine("Hello, World!");

 

-using IHost host = Host.CreateDefaultBuilder(args)

-    .ConfigureServices((_, services) => {

-        services.AddScoped<TieredStorageService>(x =>

-            new(

-                cacheStorageProvider: new FileStorageProvider("data/cache/"),

-                dataStorageProvider: new FileStorageProvider("data/data/")

-            )

-        );

-

-        services.AddRoryLibMatrixServices();

-

-        services.AddHostedService<MRUBot>();

-    })

-    .Build();

-

+var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {

+    services.AddScoped<TieredStorageService>(x =>

+        new(

+            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),

+            dataStorageProvider: new FileStorageProvider("bot_data/data/")

+        )

+    );

+    services.AddScoped<MRUBotConfiguration>();

+    services.AddRoryLibMatrixServices();

+    services.AddHostedService<MRUBot>();

+}).UseConsoleLifetime().Build();

 

 await host.RunAsync();
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/Properties/launchSettings.json b/MatrixRoomUtils.Bot/Properties/launchSettings.json
new file mode 100644
index 0000000..997e294
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Properties/launchSettings.json
@@ -0,0 +1,26 @@
+{
+  "$schema": "http://json.schemastore.org/launchsettings.json",
+  "profiles": {
+    "Default": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "environmentVariables": {
+
+      }
+    },
+    "Development": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "environmentVariables": {
+        "DOTNET_ENVIRONMENT": "Development"
+      }
+    },
+    "Local config": {
+      "commandName": "Project",
+      "dotnetRunMessages": true,
+      "environmentVariables": {
+        "DOTNET_ENVIRONMENT": "Local"
+      }
+    }
+  }
+}
diff --git a/MatrixRoomUtils.Bot/appsettings.Development.json b/MatrixRoomUtils.Bot/appsettings.Development.json
new file mode 100644
index 0000000..27bbd50
--- /dev/null
+++ b/MatrixRoomUtils.Bot/appsettings.Development.json
@@ -0,0 +1,9 @@
+{
+    "Logging": {
+        "LogLevel": {
+            "Default": "Debug",
+            "System": "Information",
+            "Microsoft": "Information"
+        }
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/appsettings.json b/MatrixRoomUtils.Bot/appsettings.json
new file mode 100644
index 0000000..5668b53
--- /dev/null
+++ b/MatrixRoomUtils.Bot/appsettings.json
@@ -0,0 +1,13 @@
+{
+    "Logging": {
+        "LogLevel": {
+            "Default": "Debug",
+            "System": "Information",
+            "Microsoft": "Information"
+        }
+    },
+    "Bot": {
+        "Homeserver": "rory.gay",
+        "AccessToken": "syt_xxxxxxxxxxxxxxxxx"
+    }
+}
\ No newline at end of file
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
diff --git a/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs b/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
index 878eca4..59ec4db 100644
--- a/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
+++ b/MatrixRoomUtils.Web/Classes/LocalStorageWrapper.cs
@@ -21,7 +21,7 @@ public class LocalStorageWrapper {
             Console.WriteLine("Access token is not null, creating authenticated home server");
             Console.WriteLine($"Homeserver cache: {RuntimeCache.HomeserverResolutionCache.Count} entries");
             // Console.WriteLine(RuntimeCache.HomeserverResolutionCache.ToJson());
-            RuntimeCache.CurrentHomeServer = await new AuthenticatedHomeServer(RuntimeCache.LoginSessions[RuntimeCache.LastUsedToken].LoginResponse.HomeServer, RuntimeCache.LastUsedToken, TODO).Configure();
+            RuntimeCache.CurrentHomeServer = await new AuthenticatedHomeServer(RuntimeCache.LoginSessions[RuntimeCache.LastUsedToken].LoginResponse.HomeServer, RuntimeCache.LastUsedToken, null).Configure();
             Console.WriteLine("Created authenticated home server");
         }
     }
diff --git a/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs b/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
index 77c8281..47844e3 100644
--- a/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
+++ b/MatrixRoomUtils.Web/Classes/RoomCreationTemplates/DefaultRoomCreationTemplate.cs
@@ -1,6 +1,7 @@
 using System.Text.Json.Nodes;
 using MatrixRoomUtils.Core;
 using MatrixRoomUtils.Core.Responses;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Web.Classes.RoomCreationTemplates;
 
@@ -18,9 +19,9 @@ public class DefaultRoomCreationTemplate : IRoomCreationTemplate {
                         history_visibility = "world_readable"
                     }
                 },
-                new StateEvent<Pages.RoomManager.RoomManagerCreateRoom.GuestAccessContent> {
+                new StateEvent<GuestAccessData> {
                     Type = "m.room.guest_access",
-                    Content = new Pages.RoomManager.RoomManagerCreateRoom.GuestAccessContent {
+                    Content = new GuestAccessData {
                         GuestAccess = "can_join"
                     }
                 },
diff --git a/MatrixRoomUtils.Web/Pages/DebugTools.razor b/MatrixRoomUtils.Web/Pages/DebugTools.razor
index da5c172..4e4cec8 100644
--- a/MatrixRoomUtils.Web/Pages/DebugTools.razor
+++ b/MatrixRoomUtils.Web/Pages/DebugTools.razor
@@ -1,6 +1,5 @@
 @page "/Debug"
 @using System.Reflection
-@using MatrixRoomUtils.Core.Extensions
 @using MatrixRoomUtils.Core.Interfaces
 @inject ILocalStorageService LocalStorage
 @inject NavigationManager NavigationManager
diff --git a/MatrixRoomUtils.Web/Pages/DevOptions.razor b/MatrixRoomUtils.Web/Pages/DevOptions.razor
index 0843d5f..cdb5693 100644
--- a/MatrixRoomUtils.Web/Pages/DevOptions.razor
+++ b/MatrixRoomUtils.Web/Pages/DevOptions.razor
@@ -1,5 +1,4 @@
 @page "/DevOptions"
-@using MatrixRoomUtils.Core.Extensions
 @inject NavigationManager NavigationManager
 @inject ILocalStorageService LocalStorage
 
diff --git a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
index 1fe13bd..858fad9 100644
--- a/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
+++ b/MatrixRoomUtils.Web/Pages/HSAdmin/RoomQuery.razor
@@ -1,5 +1,4 @@
 @page "/HSAdmin/RoomQuery"
-@using MatrixRoomUtils.Core.Extensions
 @using MatrixRoomUtils.Core.Filters
 @using MatrixRoomUtils.Core.Responses.Admin
 @using MatrixRoomUtils.Web.Shared.SimpleComponents
diff --git a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
index 8031146..80dbfd1 100644
--- a/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
+++ b/MatrixRoomUtils.Web/Pages/KnownHomeserverList.razor
@@ -1,6 +1,5 @@
 @page "/KnownHomeserverList"
 @using System.Text.Json
-@using MatrixRoomUtils.Core.Extensions
 @using System.Diagnostics
 @using MatrixRoomUtils.Core.Responses
 <h3>Known Homeserver List</h3>
diff --git a/MatrixRoomUtils.Web/Pages/LoginPage.razor b/MatrixRoomUtils.Web/Pages/LoginPage.razor
index 3b78817..9df7fa6 100644
--- a/MatrixRoomUtils.Web/Pages/LoginPage.razor
+++ b/MatrixRoomUtils.Web/Pages/LoginPage.razor
@@ -52,7 +52,7 @@
             var userinfo = new UserInfo {
                 LoginResponse = result
             };
-            userinfo.Profile = await (await new AuthenticatedHomeServer(result.HomeServer, result.AccessToken, TODO).Configure()).GetProfile(result.UserId);
+            userinfo.Profile = await RuntimeCache.CurrentHomeServer.GetProfile(result.UserId);
             RuntimeCache.LastUsedToken = result.AccessToken;
 
             RuntimeCache.LoginSessions.Add(result.AccessToken, userinfo);
diff --git a/MatrixRoomUtils.Web/Pages/MediaLocator.razor b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
index 36cd8e6..38d1514 100644
--- a/MatrixRoomUtils.Web/Pages/MediaLocator.razor
+++ b/MatrixRoomUtils.Web/Pages/MediaLocator.razor
@@ -61,7 +61,7 @@
             await sem.WaitAsync();
             var httpClient = new HttpClient { BaseAddress = new Uri(hs) };
             httpClient.Timeout = TimeSpan.FromSeconds(5);
-            var rmu = mxcUrl.Replace("mxc://", $"{hs}/_matrix/media/r0/download/");
+            var rmu = mxcUrl.Replace("mxc://", $"{hs}/_matrix/media/v3/download/");
             try {
                 var res = await httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, rmu));
                 if (res.IsSuccessStatusCode) {
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
index 76b4384..0840ebf 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListEditorPage.razor
@@ -1,5 +1,4 @@
 @page "/PolicyListEditor/{RoomId}"
-@using MatrixRoomUtils.Core.Extensions
 @using MatrixRoomUtils.Core.StateEventTypes
 @using System.Text.Json
 @using MatrixRoomUtils.Core.Responses
@@ -210,7 +209,7 @@ else {
     private async Task LoadStatesAsync() {
     // using var client = new HttpClient();
     // client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", LocalStorageWrapper.AccessToken);
-    // var response = await client.GetAsync($"{LocalStorageWrapper.CurrentHomeserver}/_matrix/client/r0/rooms/{RoomId}/state");
+    // var response = await client.GetAsync($"{LocalStorageWrapper.CurrentHomeserver}/_matrix/client/v3/rooms/{RoomId}/state");
     // var Content = await response.Content.ReadAsStringAsync();
     // Console.WriteLine(JsonSerializer.Deserialize<object>(Content).ToJson());
     // var stateEvents = JsonSerializer.Deserialize<List<StateEventResponse>>(Content);
diff --git a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
index 20eab7a..8f711b5 100644
--- a/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
+++ b/MatrixRoomUtils.Web/Pages/PolicyList/PolicyListRoomList.razor
@@ -1,5 +1,4 @@
 @page "/PolicyListEditor"
-@using MatrixRoomUtils.Core.Extensions
 @inject ILocalStorageService LocalStorage
 @inject NavigationManager NavigationManager
 <h3>Policy list editor - Room list</h3>
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
index 5cfda77..80d852a 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerCreateRoom.razor
@@ -1,9 +1,8 @@
 @page "/RoomManagerCreateRoom"
-@using MatrixRoomUtils.Core.Extensions
 @using MatrixRoomUtils.Core.Responses
 @using System.Text.Json
-@using System.Text.Json.Serialization
 @using System.Reflection
+@using MatrixRoomUtils.Core.StateEventTypes
 @using MatrixRoomUtils.Web.Classes.RoomCreationTemplates
 @* ReSharper disable once RedundantUsingDirective - Must not remove this, Rider marks this as "unused" when it's not *@
 @using MatrixRoomUtils.Web.Shared.SimpleComponents
@@ -53,12 +52,12 @@
         <tr>
             <td style="padding-top: 16px;">History visibility:</td>
             <td style="padding-top: 16px;">
-                <InputSelect @bind-Value="@creationEvent.HistoryVisibility">
-                    <option value="invited">Invited</option>
-                    <option value="joined">Joined</option>
-                    <option value="shared">Shared</option>
-                    <option value="world_readable">World readable</option>
-                </InputSelect>
+                @* <InputSelect @bind-Value="@creationEvent.HistoryVisibility"> *@
+                @*     <option value="invited">Invited</option> *@
+                @*     <option value="joined">Joined</option> *@
+                @*     <option value="shared">Shared</option> *@
+                @*     <option value="world_readable">World readable</option> *@
+                @* </InputSelect> *@
             </td>
         </tr>
         <tr>
@@ -220,15 +219,15 @@
             OverwriteWrappedPropertiesFromEvent();
             creationEvent.PowerLevelContentOverride.Events = creationEvent.PowerLevelContentOverride.Events.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
             creationEvent.PowerLevelContentOverride.Users = creationEvent.PowerLevelContentOverride.Users.OrderByDescending(x => x.Value).ThenBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
-            guestAccessEvent = creationEvent["m.room.guest_access"].As<GuestAccessContent>().Content;
+            guestAccessEvent = creationEvent["m.room.guest_access"].As<GuestAccessData>().Content;
 
             Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}");
-            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessContent>().ToJson()}");
-            creationEvent["m.room.guest_access"].As<GuestAccessContent>().Content.IsGuestAccessEnabled = true;
+            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
+            creationEvent["m.room.guest_access"].As<GuestAccessData>().Content.IsGuestAccessEnabled = true;
             Console.WriteLine("-- Created new guest access content --");
             Console.WriteLine($"Creation event uncasted: {creationEvent["m.room.guest_access"].ToJson()}");
-            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessContent>().ToJson()}");
-            Console.WriteLine($"Creation event casted back: {creationEvent["m.room.guest_access"].As<GuestAccessContent>().ToJson()}");
+            Console.WriteLine($"Creation event casted: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
+            Console.WriteLine($"Creation event casted back: {creationEvent["m.room.guest_access"].As<GuestAccessData>().ToJson()}");
             StateHasChanged();
         }
     }
@@ -236,7 +235,7 @@
     private Dictionary<string, string> creationEventValidationErrors { get; set; } = new();
 
     private CreateRoomRequest creationEvent { get; set; }
-    GuestAccessContent guestAccessEvent { get; set; }
+    GuestAccessData guestAccessEvent { get; set; }
 
     private Dictionary<string, CreateRoomRequest> Presets { get; set; } = new();
 
@@ -275,7 +274,7 @@
         Console.WriteLine("Overwriting wrapped properties");
         Console.WriteLine($"Allow: {ServerACLAllowRules.Count}: {string.Join(", ", ServerACLAllowRules)}");
         Console.WriteLine($"Deny: {ServerACLDenyRules.Count}: {string.Join(", ", ServerACLDenyRules)}");
-        creationEvent.ServerACLs = new ServerACL {
+        creationEvent.ServerACLs = new ServerACLData {
             Allow = ServerACLAllowRules,
             Deny = ServerACLDenyRules,
             AllowIpLiterals = creationEvent.ServerACLs.AllowIpLiterals
@@ -336,14 +335,5 @@
         _ => key
         };
 
-    public class GuestAccessContent {
-        [JsonPropertyName("guest_access")]
-        public string GuestAccess { get; set; }
-
-        public bool IsGuestAccessEnabled {
-            get => GuestAccess == "can_join";
-            set => GuestAccess = value ? "can_join" : "forbidden";
-        }
-    }
     }
 
diff --git a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
index ab650d1..a9c71c4 100644
--- a/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomManager/RoomManagerSpace.razor
@@ -1,5 +1,4 @@
 @page "/RoomManager/Space/{RoomId}"
-@using MatrixRoomUtils.Core.Extensions
 @using System.Text.Json
 @using MatrixRoomUtils.Core.Responses
 <h3>Room manager - Viewing Space</h3>
diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
index fa5b6a8..296514c 100644
--- a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateEditorPage.razor
@@ -63,7 +63,7 @@
         using var client = new HttpClient();
     //TODO: can this be improved?
         client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeCache.CurrentHomeServer.AccessToken);
-        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/r0/rooms/{RoomId}/state");
+        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/v3/rooms/{RoomId}/state");
     // var response = await client.GetAsync($"http://localhost:5117/matrix-hq-state.json");
     //var _events = await response.Content.ReadFromJsonAsync<Queue<StateEventStruct>>();
         var _data = await response.Content.ReadAsStreamAsync();
diff --git a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
index bfd4d10..ff1d9ac 100644
--- a/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
+++ b/MatrixRoomUtils.Web/Pages/RoomState/RoomStateViewerPage.razor
@@ -1,5 +1,4 @@
 @page "/RoomStateViewer/{RoomId}"
-@using MatrixRoomUtils.Core.Extensions
 @using System.Net.Http.Headers
 @using System.Text.Json
 @inject ILocalStorageService LocalStorage
@@ -88,7 +87,7 @@
     //TODO: can we improve this?
         using var client = new HttpClient();
         client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", RuntimeCache.CurrentHomeServer.AccessToken);
-        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/r0/rooms/{RoomId}/state");
+        var response = await client.GetAsync($"{RuntimeCache.CurrentHomeServer.FullHomeServerDomain}/_matrix/client/v3/rooms/{RoomId}/state");
     // var response = await client.GetAsync($"http://localhost:5117/matrix-hq-state.json");
     //var _events = await response.Content.ReadFromJsonAsync<Queue<StateEventStruct>>();
         var _data = await response.Content.ReadAsStreamAsync();
diff --git a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
index 8ab44fb..975da43 100644
--- a/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
+++ b/MatrixRoomUtils.Web/Shared/IndexComponents/IndexUserItem.razor
@@ -22,12 +22,13 @@
     private int _roomCount { get; set; } = 0;
 
     protected override async Task OnInitializedAsync() {
-        var hs = await new AuthenticatedHomeServer(User.LoginResponse.HomeServer, User.AccessToken, TODO).Configure();
+        await LocalStorageWrapper.LoadFromLocalStorage(LocalStorage);
+
         if (User.Profile.AvatarUrl != null && User.Profile.AvatarUrl != "")
-            _avatarUrl = hs.ResolveMediaUri(User.Profile.AvatarUrl);
+            _avatarUrl = RuntimeCache.CurrentHomeServer.ResolveMediaUri(User.Profile.AvatarUrl);
         else _avatarUrl = "https://api.dicebear.com/6.x/identicon/svg?seed=" + User.LoginResponse.UserId;
         try {
-            _roomCount = (await hs.GetJoinedRooms()).Count;
+            _roomCount = (await RuntimeCache.CurrentHomeServer.GetJoinedRooms()).Count;
         }
         catch {
             _roomCount = -1;
diff --git a/MatrixRoomUtils.Web/Shared/InlineUserItem.razor b/MatrixRoomUtils.Web/Shared/InlineUserItem.razor
index 43a4111..0a58b4d 100644
--- a/MatrixRoomUtils.Web/Shared/InlineUserItem.razor
+++ b/MatrixRoomUtils.Web/Shared/InlineUserItem.razor
@@ -36,13 +36,11 @@
 
         await _semaphoreSlim.WaitAsync();
 
-        var hs = await new AuthenticatedHomeServer(RuntimeCache.CurrentHomeServer.HomeServerDomain, RuntimeCache.CurrentHomeServer.AccessToken, TODO).Configure();
-
         if (User == null) {
             if (UserId == null) {
                 throw new ArgumentNullException(nameof(UserId));
             }
-            User = await hs.GetProfile(UserId);
+            User = await RuntimeCache.CurrentHomeServer.GetProfile(UserId);
         }
 
     // UserId = User.;
diff --git a/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor b/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor
index 42a5f64..ea7d2ec 100644
--- a/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor
+++ b/MatrixRoomUtils.Web/Shared/SimpleComponents/DictionaryEditor.razor
@@ -1,4 +1,3 @@
-@using MatrixRoomUtils.Core.Extensions
 <table>
     @foreach (var i in Items.Keys) {
         var key = i;
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
index 4fb5596..f89dc44 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMemberItem.razor
@@ -1,22 +1,21 @@
-@using MatrixRoomUtils.Core.Extensions
-@if (Event.ContentAsJsonNode["membership"]!.GetValue<string>() == "ban") {
+@if (Event.membership"]!.GetValue<string>() == "ban") {
     <i>@Event.StateKey was banned</i>
 }
-else if (Event.ContentAsJsonNode["membership"]!.GetValue<string>() == "invite") {
+else if (Event.membership"]!.GetValue<string>() == "invite") {
     <i>@Event.StateKey was invited</i>
 }
-else if (Event.ContentAsJsonNode["membership"]!.GetValue<string>() == "join") {
+else if (Event.membership"]!.GetValue<string>() == "join") {
     @if (Event.ReplacesState != null) {
-        <i>@Event.StateKey changed their display name to @(Event.ContentAsJsonNode["displayname"]!.GetValue<string>())</i>
+        <i>@Event.StateKey changed their display name to @(Event.displayname"]!.GetValue<string>())</i>
     }
     else {
         <i><InlineUserItem UserId="@Event.StateKey"></InlineUserItem> joined</i>
     }
 }
-else if (Event.ContentAsJsonNode["membership"]!.GetValue<string>() == "leave") {
+else if (Event.membership"]!.GetValue<string>() == "leave") {
     <i>@Event.StateKey left</i>
 }
-else if (Event.ContentAsJsonNode["membership"]!.GetValue<string>() == "knock") {
+else if (Event.membership"]!.GetValue<string>() == "knock") {
     <i>@Event.StateKey knocked</i>
 }
 else {
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
index 80a432b..6c26dc2 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineMessageItem.razor
@@ -1,4 +1,3 @@
-@using MatrixRoomUtils.Core.Extensions
 @using MatrixRoomUtils.Core.Responses
 <pre>
     @ObjectExtensions.ToJson(Event.Content, indent: false)
diff --git a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
index f78bdc9..d8ea7e2 100644
--- a/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
+++ b/MatrixRoomUtils.Web/Shared/TimelineComponents/TimelineUnknownItem.razor
@@ -1,4 +1,3 @@
-@using MatrixRoomUtils.Core.Extensions
 <div>
 
     <details style="display: inline;">
diff --git a/MatrixRoomUtils.Web/Shared/UserListItem.razor b/MatrixRoomUtils.Web/Shared/UserListItem.razor
index b99671a..52f398a 100644
--- a/MatrixRoomUtils.Web/Shared/UserListItem.razor
+++ b/MatrixRoomUtils.Web/Shared/UserListItem.razor
@@ -25,7 +25,7 @@
     private string? profileAvatar { get; set; } = "/icon-192.png";
     private string? profileName { get; set; } = "Loading...";
 
-    private static SemaphoreSlim _semaphoreSlim = new(128);
+    private static SemaphoreSlim _semaphoreSlim = new(8);
 
     protected override async Task OnInitializedAsync() {
         await base.OnInitializedAsync();
@@ -33,13 +33,11 @@
 
         await _semaphoreSlim.WaitAsync();
 
-        var hs = await new AuthenticatedHomeServer(RuntimeCache.CurrentHomeServer.HomeServerDomain, RuntimeCache.CurrentHomeServer.AccessToken, TODO).Configure();
-
         if (User == null) {
             if (UserId == null) {
                 throw new ArgumentNullException(nameof(UserId));
             }
-            User = await hs.GetProfile(UserId);
+            User = await RuntimeCache.CurrentHomeServer.GetProfile(UserId);
         }
 
     // UserId = User.;
diff --git a/MatrixRoomUtils.Web/wwwroot/css/app.css b/MatrixRoomUtils.Web/wwwroot/css/app.css
index cb7ba63..b3a8cf3 100644
--- a/MatrixRoomUtils.Web/wwwroot/css/app.css
+++ b/MatrixRoomUtils.Web/wwwroot/css/app.css
@@ -74,7 +74,7 @@ a, .btn-link {
 }
 
 .blazor-error-boundary {
-    background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
+    background: url(data:image/svg+xml;base64,PHN2ZyB3aWv3aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
     padding: 1rem 1rem 1rem 3.7rem;
     color: white;
 }
diff --git a/MatrixRoomUtils.sln b/MatrixRoomUtils.sln
index a29cf89..a29cf89 100644..100755
--- a/MatrixRoomUtils.sln
+++ b/MatrixRoomUtils.sln
diff --git a/MatrixRoomUtils.sln.DotSettings.user b/MatrixRoomUtils.sln.DotSettings.user
index 033da4f..93b1fca 100644
--- a/MatrixRoomUtils.sln.DotSettings.user
+++ b/MatrixRoomUtils.sln.DotSettings.user
@@ -1,7 +1,15 @@
 <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
-	<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">ShowAndRun</s:String>
+	<s:String x:Key="/Default/CodeInspection/Highlighting/SweaWarningsMode/@EntryValue">DoNotShowAndRun</s:String>
+	<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsBuildSolutionLoadingNodeCount/@EntryValue">12</s:Int64>
+	<s:Boolean x:Key="/Default/Environment/Hierarchy/Build/BuildTool/MsBuildSolutionLoadingOrderingEnabled/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue">No</s:String>
+	<s:Int64 x:Key="/Default/Environment/Hierarchy/Build/SolutionBuilderNext/ParallelProcessesCount2/@EntryValue">12</s:Int64>
+	
+	<s:Boolean x:Key="/Default/Environment/Hierarchy/Build/SolutionBuilderNext/ShouldRestoreNugetPackages/@EntryValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=244e90fe_002Dee26_002D4f78_002D86eb_002D27529ae48905_0023MatrixRoomUtils/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=d38da95d_002Ddd83_002D4340_002D96a4_002D6f59fc6ae3d9_0023MatrixRoomUtils_002EWeb/@EntryIndexedValue">True</s:Boolean>
+	
+	
 	<s:Boolean x:Key="/Default/UnloadedProject/UnloadedProjects/=f997f26f_002D2ec1_002D4d18_002Db3dd_002Dc46fb2ad65c0_0023MatrixRoomUtils_002EWeb_002EServer/@EntryIndexedValue">True</s:Boolean>
 	
 	
@@ -10,4 +18,10 @@
 	
 	
 	
+	
+	
+	
+	
+	
+	
 	</wpf:ResourceDictionary>
\ No newline at end of file