about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--MatrixRoomUtils.Bot/Bot/Commands/CmdCommand.cs49
-rw-r--r--MatrixRoomUtils.Bot/Bot/Commands/HelpCommand.cs28
-rw-r--r--MatrixRoomUtils.Bot/Bot/Commands/PingCommand.cs19
-rw-r--r--MatrixRoomUtils.Bot/Bot/FileStorageProvider.cs (renamed from MatrixRoomUtils.Bot/FileStorageProvider.cs)0
-rw-r--r--MatrixRoomUtils.Bot/Bot/Interfaces/CommandContext.cs11
-rw-r--r--MatrixRoomUtils.Bot/Bot/Interfaces/ICommand.cs12
-rw-r--r--MatrixRoomUtils.Bot/Bot/MRUBot.cs (renamed from MatrixRoomUtils.Bot/MRUBot.cs)56
-rw-r--r--MatrixRoomUtils.Bot/Bot/MRUBotConfiguration.cs (renamed from MatrixRoomUtils.Bot/MRUBotConfiguration.cs)1
-rw-r--r--MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj5
-rw-r--r--MatrixRoomUtils.Bot/Program.cs6
-rw-r--r--MatrixRoomUtils.Core/AuthenticatedHomeServer.cs10
-rw-r--r--MatrixRoomUtils.Core/Authentication/MatrixAuth.cs7
-rw-r--r--MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs31
-rw-r--r--MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs8
-rw-r--r--MatrixRoomUtils.Core/Helpers/SyncHelper.cs5
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IHomeServer.cs20
-rw-r--r--MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj7
-rw-r--r--MatrixRoomUtils.Core/RemoteHomeServer.cs16
-rw-r--r--MatrixRoomUtils.Core/Responses/StateEventResponse.cs4
-rw-r--r--MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs (renamed from MatrixRoomUtils.Core/Room.cs)81
-rw-r--r--MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs14
-rw-r--r--MatrixRoomUtils.Core/RuntimeCache.cs99
-rw-r--r--MatrixRoomUtils.Core/StateEvent.cs66
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Common/RoomEmotesEventData.cs26
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/CanonicalAliasEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs)2
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/GuestAccessData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs)0
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/HistoryVisibilityData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs)2
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/JoinRulesEventData.cs (renamed from MatrixRoomUtils.Core/JoinRules.cs)5
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/MessageEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs)8
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/PolicyRuleStateEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs)4
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/PowerLevelEvent.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs)5
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/PresenceStateEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs)0
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/ProfileResponse.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs)0
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAvatarEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs)0
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomCreateEventData.cs21
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomEncryptionEventData.cs15
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomMemberEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs)10
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomNameEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomPinnedEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTopicEventData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs)1
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTypingEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/ServerACLData.cs (renamed from MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs)0
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceChildEventData.cs15
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceParentEventData.cs14
-rw-r--r--MatrixRoomUtils.sln.DotSettings.user1
46 files changed, 471 insertions, 248 deletions
diff --git a/.gitignore b/.gitignore
index 33be3d4..5350fd3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,4 @@ MatrixRoomUtils.Web/wwwroot/MRU.tar.xz
 matrix-sync.json
 /patches/
 MatrixRoomUtils.Bot/bot_data/
-appsettings.Local.json
+appsettings.Local*.json
diff --git a/MatrixRoomUtils.Bot/Bot/Commands/CmdCommand.cs b/MatrixRoomUtils.Bot/Bot/Commands/CmdCommand.cs
new file mode 100644
index 0000000..c267298
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Bot/Commands/CmdCommand.cs
@@ -0,0 +1,49 @@
+using System.Runtime.InteropServices;
+using System.Text;
+using MatrixRoomUtils.Bot.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MatrixRoomUtils.Bot.Commands;
+
+public class CmdCommand : ICommand {
+    public string Name { get; } = "cmd";
+    public string Description { get; } = "Runs a command on the host system";
+
+    public async Task<bool> CanInvoke(CommandContext ctx) {
+        return ctx.MessageEvent.Sender.EndsWith(":rory.gay");
+    }
+
+    public async Task Invoke(CommandContext ctx) {
+        var cmd = "\"";
+        foreach (var arg in ctx.Args) cmd += arg + " ";
+
+        cmd = cmd.Trim();
+        cmd += "\"";
+
+        await ctx.Room.SendMessageEventAsync("m.room.message", new() {
+            Body = $"Command being executed: `{cmd}`"
+        });
+
+        var output = ArcaneLibs.Util.GetCommandOutputSync(
+                Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/sh" : "cmd.exe",
+                (Environment.OSVersion.Platform == PlatformID.Unix ? "-c " : "/c ") + cmd)
+            .Replace("`", "\\`")
+            .Split("\n").ToList();
+        foreach (var _out in output) Console.WriteLine($"{_out.Length:0000} {_out}");
+
+        var msg = "";
+        while (output.Count > 0) {
+            Console.WriteLine("Adding: " + output[0]);
+            msg += output[0] + "\n";
+            output.RemoveAt(0);
+            if ((output.Count > 0 && (msg + output[0]).Length > 64000) || output.Count == 0) {
+                await ctx.Room.SendMessageEventAsync("m.room.message", new() {
+                    FormattedBody = $"```ansi\n{msg}\n```",
+                    Body = Markdig.Markdown.ToHtml(msg),
+                    Format = "org.matrix.custom.html"
+                });
+                msg = "";
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/Bot/Commands/HelpCommand.cs b/MatrixRoomUtils.Bot/Bot/Commands/HelpCommand.cs
new file mode 100644
index 0000000..af41563
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Bot/Commands/HelpCommand.cs
@@ -0,0 +1,28 @@
+using System.Text;
+using MatrixRoomUtils.Bot.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MatrixRoomUtils.Bot.Commands; 
+
+public class HelpCommand : ICommand {
+    private readonly IServiceProvider _services;
+    public HelpCommand(IServiceProvider services) {
+        _services = services;
+    }
+
+    public string Name { get; } = "help";
+    public string Description { get; } = "Displays this help message";
+
+    public async Task Invoke(CommandContext ctx) {
+        var sb = new StringBuilder();
+        sb.AppendLine("Available commands:");
+        var commands = _services.GetServices<ICommand>().ToList();
+        foreach (var command in commands) {
+            sb.AppendLine($"- {command.Name}: {command.Description}");
+        }
+
+        await ctx.Room.SendMessageEventAsync("m.room.message", new() {
+            Body = sb.ToString(),
+        });
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/Bot/Commands/PingCommand.cs b/MatrixRoomUtils.Bot/Bot/Commands/PingCommand.cs
new file mode 100644
index 0000000..a00cc8b
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Bot/Commands/PingCommand.cs
@@ -0,0 +1,19 @@
+using System.Text;
+using MatrixRoomUtils.Bot.Interfaces;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace MatrixRoomUtils.Bot.Commands; 
+
+public class PingCommand : ICommand {
+    public PingCommand() {
+    }
+
+    public string Name { get; } = "ping";
+    public string Description { get; } = "Pong!";
+
+    public async Task Invoke(CommandContext ctx) {
+        await ctx.Room.SendMessageEventAsync("m.room.message", new() {
+            Body = "pong!"
+        });
+    }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/FileStorageProvider.cs b/MatrixRoomUtils.Bot/Bot/FileStorageProvider.cs
index 8d99828..8d99828 100644
--- a/MatrixRoomUtils.Bot/FileStorageProvider.cs
+++ b/MatrixRoomUtils.Bot/Bot/FileStorageProvider.cs
diff --git a/MatrixRoomUtils.Bot/Bot/Interfaces/CommandContext.cs b/MatrixRoomUtils.Bot/Bot/Interfaces/CommandContext.cs
new file mode 100644
index 0000000..ab29554
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Bot/Interfaces/CommandContext.cs
@@ -0,0 +1,11 @@
+using MatrixRoomUtils.Core;
+using MatrixRoomUtils.Core.Responses;
+
+namespace MatrixRoomUtils.Bot.Interfaces;
+
+public class CommandContext {
+    public GenericRoom Room { get; set; }
+    public StateEventResponse MessageEvent { get; set; }
+    public string CommandName => (MessageEvent.TypedContent as MessageEventData).Body.Split(' ')[0][1..];
+    public string[] Args => (MessageEvent.TypedContent as MessageEventData).Body.Split(' ')[1..];
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/Bot/Interfaces/ICommand.cs b/MatrixRoomUtils.Bot/Bot/Interfaces/ICommand.cs
new file mode 100644
index 0000000..b57d8c9
--- /dev/null
+++ b/MatrixRoomUtils.Bot/Bot/Interfaces/ICommand.cs
@@ -0,0 +1,12 @@
+namespace MatrixRoomUtils.Bot.Interfaces; 
+
+public interface ICommand {
+    public string Name { get; }
+    public string Description { get; }
+
+    public Task<bool> CanInvoke(CommandContext ctx) {
+        return Task.FromResult(true);
+    }
+    
+    public Task Invoke(CommandContext ctx);
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/MRUBot.cs b/MatrixRoomUtils.Bot/Bot/MRUBot.cs
index 157673d..81123e0 100644
--- a/MatrixRoomUtils.Bot/MRUBot.cs
+++ b/MatrixRoomUtils.Bot/Bot/MRUBot.cs
@@ -1,11 +1,13 @@
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using MatrixRoomUtils.Bot;
+using MatrixRoomUtils.Bot.Interfaces;
 using MatrixRoomUtils.Core;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Helpers;
 using MatrixRoomUtils.Core.Services;
 using MatrixRoomUtils.Core.StateEventTypes;
+using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
 
@@ -13,13 +15,17 @@ public class MRUBot : IHostedService {
     private readonly HomeserverProviderService _homeserverProviderService;
     private readonly ILogger<MRUBot> _logger;
     private readonly MRUBotConfiguration _configuration;
+    private readonly IEnumerable<ICommand> _commands;
 
     public MRUBot(HomeserverProviderService homeserverProviderService, ILogger<MRUBot> logger,
-        MRUBotConfiguration configuration) {
+        MRUBotConfiguration configuration, IServiceProvider services) {
         Console.WriteLine("MRUBot hosted service instantiated!");
         _homeserverProviderService = homeserverProviderService;
         _logger = logger;
         _configuration = configuration;
+        Console.WriteLine("Getting commands...");
+        _commands = services.GetServices<ICommand>();
+        Console.WriteLine($"Got {_commands.Count()} commands!");
     }
 
     /// <summary>Triggered when the application host is ready to start the service.</summary>
@@ -39,18 +45,20 @@ public class MRUBot : IHostedService {
 
         await (await hs.GetRoom("!DoHEdFablOLjddKWIp:rory.gay")).JoinAsync();
 
-        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));
-            // }
+        // foreach (var room in await hs.GetJoinedRooms()) {
+        //     if(room.RoomId is "!OGEhHVWSdvArJzumhm:matrix.org") continue;
+        //     foreach (var stateEvent in await room.GetStateAsync<List<StateEvent>>("")) {
+        //         var _ = stateEvent.GetType;
+        //     }
+        //     Console.WriteLine($"Got room state for {room.RoomId}!");
+        // }
 
+        hs.SyncHelper.InviteReceived += async (_, args) => {
             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}");
+                $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventData).Reason}");
             if (inviteEvent.Sender == "@emma:rory.gay") {
                 try {
                     await (await hs.GetRoom(args.Key)).JoinAsync(reason: "I was invited by Emma (Rory&)!");
@@ -65,17 +73,37 @@ public class MRUBot : IHostedService {
             Console.WriteLine(
                 $"Got timeline event in {@event.RoomId}: {@event.ToJson(indent: false, ignoreNull: true)}");
 
+            var room = await hs.GetRoom(@event.RoomId);
             // 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!" });
+                if (message is { MessageType: "m.text" } && message.Body.StartsWith(_configuration.Prefix)) {
+                    
+                        var command = _commands.FirstOrDefault(x => x.Name == message.Body.Split(' ')[0][_configuration.Prefix.Length..]);
+                        if (command == null) {
+                            await room.SendMessageEventAsync("m.room.message",
+                                new MessageEventData() {
+                                    MessageType = "m.text",
+                                    Body = "Command not found!"
+                                });
+                            return;
+                        }
+                        var ctx = new CommandContext() {
+                            Room = room,
+                            MessageEvent = @event
+                        };
+                        if (await command.CanInvoke(ctx)) {
+                            await command.Invoke(ctx);
+                        }
+                        else {
+                            await room.SendMessageEventAsync("m.room.message",
+                                new MessageEventData() {
+                                    MessageType = "m.text",
+                                    Body = "You do not have permission to run this command!"
+                                });
+                        }
                 }
             }
         };
-
         await hs.SyncHelper.RunSyncLoop(cancellationToken);
     }
 
diff --git a/MatrixRoomUtils.Bot/MRUBotConfiguration.cs b/MatrixRoomUtils.Bot/Bot/MRUBotConfiguration.cs
index 14d9b60..c91698a 100644
--- a/MatrixRoomUtils.Bot/MRUBotConfiguration.cs
+++ b/MatrixRoomUtils.Bot/Bot/MRUBotConfiguration.cs
@@ -8,4 +8,5 @@ public class MRUBotConfiguration {
     }
     public string Homeserver { get; set; } = "";
     public string AccessToken { get; set; } = "";
+    public string Prefix { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj b/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
index a82b6aa..7012647 100644
--- a/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
+++ b/MatrixRoomUtils.Bot/MatrixRoomUtils.Bot.csproj
@@ -21,11 +21,16 @@
   </ItemGroup>

   

   <ItemGroup>

+    <PackageReference Include="ArcaneLibs" Version="1.0.0-preview3020494760.012ed3f" />

+    <PackageReference Include="Markdig" Version="0.31.0" />

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

   </ItemGroup>

   <ItemGroup>

     <Content Include="appsettings*.json">

       <CopyToOutputDirectory>Always</CopyToOutputDirectory>

     </Content>

+    <Content Update="appsettings.Local.json">

+      <CopyToOutputDirectory>Always</CopyToOutputDirectory>

+    </Content>

   </ItemGroup>

 </Project>

diff --git a/MatrixRoomUtils.Bot/Program.cs b/MatrixRoomUtils.Bot/Program.cs
index e8a5b96..0e27286 100644
--- a/MatrixRoomUtils.Bot/Program.cs
+++ b/MatrixRoomUtils.Bot/Program.cs
@@ -1,6 +1,8 @@
 // See https://aka.ms/new-console-template for more information

 

 using MatrixRoomUtils.Bot;

+using MatrixRoomUtils.Bot.Interfaces;

+using MatrixRoomUtils.Core.Extensions;

 using MatrixRoomUtils.Core.Services;

 using Microsoft.Extensions.DependencyInjection;

 using Microsoft.Extensions.Hosting;

@@ -16,6 +18,10 @@ var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {
     );

     services.AddScoped<MRUBotConfiguration>();

     services.AddRoryLibMatrixServices();

+    foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) {

+        Console.WriteLine($"Adding command {commandClass.Name}");

+        services.AddScoped(typeof(ICommand), commandClass);

+    }

     services.AddHostedService<MRUBot>();

 }).UseConsoleLifetime().Build();

 

diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
index 23e98ae..b7e01dd 100644
--- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
+++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
@@ -42,14 +42,14 @@ public class AuthenticatedHomeServer : IHomeServer {
         return this;
     }
 
-    public async Task<Room> GetRoom(string roomId) => new Room(_httpClient, roomId);
+    public async Task<GenericRoom> GetRoom(string roomId) => new(this, roomId);
 
-    public async Task<List<Room>> GetJoinedRooms() {
-        var rooms = new List<Room>();
+    public async Task<List<GenericRoom>> GetJoinedRooms() {
+        var rooms = new List<GenericRoom>();
         var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms");
 
         var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>();
-        foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new Room(_httpClient, room.GetString()));
+        foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new GenericRoom(this, room.GetString()));
 
         Console.WriteLine($"Fetched {rooms.Count} rooms");
 
@@ -67,7 +67,7 @@ public class AuthenticatedHomeServer : IHomeServer {
         return resJson.GetProperty("content_uri").GetString()!;
     }
 
-    public async Task<Room> CreateRoom(CreateRoomRequest creationEvent) {
+    public async Task<GenericRoom> CreateRoom(CreateRoomRequest creationEvent) {
         var res = await _httpClient.PostAsJsonAsync("/_matrix/client/v3/createRoom", creationEvent);
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to create room: {await res.Content.ReadAsStringAsync()}");
diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
index 83b279a..b1b0362 100644
--- a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
+++ b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
@@ -10,7 +10,7 @@ public class MatrixAuth {
     public static async Task<LoginResponse> Login(string homeserver, string username, string password) {
         Console.WriteLine($"Logging in to {homeserver} as {username}...");
         homeserver = (await new RemoteHomeServer(homeserver).Configure()).FullHomeServerDomain;
-        var hc = new HttpClient();
+        var hc = new MatrixHttpClient();
         var payload = new {
             type = "m.login.password",
             identifier = new {
@@ -25,11 +25,6 @@ public class MatrixAuth {
         Console.WriteLine($"Login: {resp.StatusCode}");
         var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
         if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data);
-        if (data.TryGetProperty("retry_after_ms", out var retryAfter)) {
-            Console.WriteLine($"Login: Waiting {retryAfter.GetInt32()}ms before retrying");
-            await Task.Delay(retryAfter.GetInt32());
-            return await Login(homeserver, username, password);
-        }
 
         Console.WriteLine($"Login: {data.ToJson()}");
         return data.Deserialize<LoginResponse>();
diff --git a/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs b/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs
index 98b0aab..8994529 100644
--- a/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs
+++ b/MatrixRoomUtils.Core/Extensions/IEnumerableExtensions.cs
@@ -1,36 +1,9 @@
-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;
-    }
-}
-
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
 public class MatrixEventAttribute : Attribute {
     public string EventName { get; set; }
+    public bool Legacy { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
index 78f4456..36da644 100644
--- a/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
+++ b/MatrixRoomUtils.Core/Extensions/JsonElementExtensions.cs
@@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
 namespace MatrixRoomUtils.Core.Extensions;
 
 public static class JsonElementExtensions {
-    public static void FindExtraJsonElementFields([DisallowNull] this JsonElement? res, Type t) {
+    public static bool FindExtraJsonElementFields([DisallowNull] this JsonElement? res, Type t) {
         var props = t.GetProperties();
         var unknownPropertyFound = false;
         foreach (var field in res.Value.EnumerateObject()) {
@@ -17,8 +17,10 @@ public static class JsonElementExtensions {
         }
 
         if (unknownPropertyFound) Console.WriteLine(res.Value.ToJson());
+
+        return unknownPropertyFound;
     }
-    public static void FindExtraJsonObjectFields([DisallowNull] this JsonObject? res, Type t) {
+    public static bool FindExtraJsonObjectFields([DisallowNull] this JsonObject? res, Type t) {
         var props = t.GetProperties();
         var unknownPropertyFound = false;
         foreach (var field in res) {
@@ -31,5 +33,7 @@ public static class JsonElementExtensions {
         }
 
         if (unknownPropertyFound) Console.WriteLine(res.ToJson());
+
+        return unknownPropertyFound;
     }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
index 04c31cd..bb4ddd5 100644
--- a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
+++ b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
@@ -38,6 +38,7 @@ public class SyncHelper {
         catch (Exception e) {
             Console.WriteLine(e);
         }
+
         return null;
     }
 
@@ -48,6 +49,7 @@ public class SyncHelper {
             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);
@@ -81,7 +83,8 @@ public class SyncHelper {
     /// <summary>
     /// Event fired when a room invite is received
     /// </summary>
-    public event EventHandler<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>>? InviteReceived;
+    public event EventHandler<KeyValuePair<string, SyncResult.RoomsDataStructure.InvitedRoomDataStructure>>?
+        InviteReceived;
 
     public event EventHandler<StateEventResponse>? TimelineEventReceived;
     public event EventHandler<StateEventResponse>? AccountDataReceived;
diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
index fcff0f2..029530c 100644
--- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
+++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
@@ -20,22 +20,6 @@ public class IHomeServer {
     }
 
     private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) {
-        if (RuntimeCache.HomeserverResolutionCache.Count == 0) {
-            // Console.WriteLine("No cached homeservers, resolving...");
-            await Task.Delay(Random.Shared.Next(1000, 5000));
-        }
-
-        if (RuntimeCache.HomeserverResolutionCache.ContainsKey(homeserver)) {
-            if (RuntimeCache.HomeserverResolutionCache[homeserver].ResolutionTime < DateTime.Now.AddHours(1)) {
-                Console.WriteLine($"Found cached homeserver: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}");
-                return RuntimeCache.HomeserverResolutionCache[homeserver].Result;
-            }
-
-            Console.WriteLine($"Cached homeserver expired, removing: {RuntimeCache.HomeserverResolutionCache[homeserver].Result}");
-            RuntimeCache.HomeserverResolutionCache.Remove(homeserver);
-        }
-        //throw new NotImplementedException();
-
         string result = null;
         Console.WriteLine($"Resolving homeserver: {homeserver}");
         if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver;
@@ -67,10 +51,6 @@ public class IHomeServer {
 
         if (result != null) {
             Console.WriteLine($"Resolved homeserver: {homeserver} -> {result}");
-            RuntimeCache.HomeserverResolutionCache.TryAdd(homeserver, new HomeServerResolutionResult {
-                Result = result,
-                ResolutionTime = DateTime.Now
-            });
             return result;
         }
 
diff --git a/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
index a043378..60f11f0 100644
--- a/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
+++ b/MatrixRoomUtils.Core/MatrixRoomUtils.Core.csproj
@@ -7,12 +7,7 @@
     </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>
+      <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
     </ItemGroup>
 
 </Project>
diff --git a/MatrixRoomUtils.Core/RemoteHomeServer.cs b/MatrixRoomUtils.Core/RemoteHomeServer.cs
index 3f50d2e..c593318 100644
--- a/MatrixRoomUtils.Core/RemoteHomeServer.cs
+++ b/MatrixRoomUtils.Core/RemoteHomeServer.cs
@@ -21,20 +21,4 @@ public class RemoteHomeServer : IHomeServer {
 
         return this;
     }
-
-    public async Task<Room> GetRoom(string roomId) => new Room(_httpClient, roomId);
-
-    public async Task<List<Room>> GetJoinedRooms() {
-        var rooms = new List<Room>();
-        var roomQuery = await _httpClient.GetAsync("/_matrix/client/v3/joined_rooms");
-        if (!roomQuery.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}");
-            throw new InvalidDataException($"Failed to get rooms: {await roomQuery.Content.ReadAsStringAsync()}");
-        }
-
-        var roomsJson = await roomQuery.Content.ReadFromJsonAsync<JsonElement>();
-        foreach (var room in roomsJson.GetProperty("joined_rooms").EnumerateArray()) rooms.Add(new Room(_httpClient, room.GetString()));
-
-        return rooms;
-    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
index 6e67887..422a557 100644
--- a/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
+++ b/MatrixRoomUtils.Core/Responses/StateEventResponse.cs
@@ -27,10 +27,10 @@ public class StateEventResponse : StateEvent {
 
     [JsonPropertyName("prev_content")]
     public dynamic PrevContent { get; set; }
-    
+
     public class UnsignedData {
         [JsonPropertyName("age")]
-        public ulong Age { get; set; }
+        public ulong? Age { get; set; }
 
         [JsonPropertyName("prev_content")]
         public dynamic? PrevContent { get; set; }
diff --git a/MatrixRoomUtils.Core/Room.cs b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs
index 59c56ed..8dc30d1 100644
--- a/MatrixRoomUtils.Core/Room.cs
+++ b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs
@@ -7,14 +7,16 @@ using MatrixRoomUtils.Core.RoomTypes;
 
 namespace MatrixRoomUtils.Core;
 
-public class Room {
-    private readonly HttpClient _httpClient;
+public class GenericRoom {
+    internal readonly AuthenticatedHomeServer _homeServer;
+    internal readonly HttpClient _httpClient;
 
-    public Room(HttpClient httpClient, string roomId) {
-        _httpClient = httpClient;
+    public GenericRoom(AuthenticatedHomeServer homeServer, string roomId) {
+        _homeServer = homeServer;
+        _httpClient = homeServer._httpClient;
         RoomId = roomId;
-        if(GetType() != typeof(SpaceRoom))
-            AsSpace = new SpaceRoom(_httpClient, RoomId);
+        if (GetType() != typeof(SpaceRoom))
+            AsSpace = new SpaceRoom(homeServer, RoomId);
     }
 
     public string RoomId { get; set; }
@@ -40,7 +42,8 @@ public class Room {
         return res.Value.Deserialize<T>();
     }
 
-    public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b", string filter = "") {
+    public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b",
+        string filter = "") {
         var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}";
         if (!string.IsNullOrEmpty(filter)) url += $"&filter={filter}";
         var res = await _httpClient.GetAsync(url);
@@ -83,7 +86,8 @@ public class Room {
             if (member.GetProperty("type").GetString() != "m.room.member") continue;
             if (joinedOnly && member.GetProperty("content").GetProperty("membership").GetString() != "join") continue;
             var memberId = member.GetProperty("state_key").GetString();
-            members.Add(memberId ?? throw new InvalidOperationException("Event type was member but state key was null!"));
+            members.Add(
+                memberId ?? throw new InvalidOperationException("Event type was member but state key was null!"));
         }
 
         return members;
@@ -116,10 +120,10 @@ public class Room {
         return res.Value.GetProperty("url").GetString() ?? "";
     }
 
-    public async Task<JoinRules> GetJoinRuleAsync() {
+    public async Task<JoinRulesEventData> GetJoinRuleAsync() {
         var res = await GetStateAsync("m.room.join_rules");
-        if (!res.HasValue) return new JoinRules();
-        return res.Value.Deserialize<JoinRules>() ?? new JoinRules();
+        if (!res.HasValue) return new JoinRulesEventData();
+        return res.Value.Deserialize<JoinRulesEventData>() ?? new JoinRulesEventData();
     }
 
     public async Task<string> GetHistoryVisibilityAsync() {
@@ -149,7 +153,7 @@ public class Room {
         if (res.Value.TryGetProperty("type", out var type)) return type.GetString();
         return null;
     }
-    
+
     public async Task ForgetAsync() {
         var res = await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
         if (!res.IsSuccessStatusCode) {
@@ -157,7 +161,7 @@ public class Room {
             throw new Exception($"Failed to forget room {RoomId} - got status: {res.StatusCode}");
         }
     }
-    
+
     public async Task LeaveAsync(string? reason = null) {
         var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new {
             reason
@@ -167,50 +171,73 @@ public class Room {
             throw new Exception($"Failed to leave room {RoomId} - got status: {res.StatusCode}");
         }
     }
-    
+
     public async Task KickAsync(string userId, string? reason = null) {
-       
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", new UserIdAndReason() { UserId = 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}");
         }
     }
-    
+
     public async Task BanAsync(string userId, string? reason = null) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", new UserIdAndReason() { UserId = 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}");
         }
     }
-    
+
     public async Task UnbanAsync(string userId) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", new UserIdAndReason() { UserId = 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}");
+            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);
+
+    public async Task<EventIdResponse> SendMessageEventAsync(string eventType, MessageEventData content) {
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid();
+        var res = await _httpClient.PutAsJsonAsync(url, 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>();
+
+        var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
+
+        return resu;
     }
 
+    public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) {
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid();
+        var content = new MultipartFormDataContent();
+        content.Add(new StreamContent(fileStream), "file", fileName);
+        var res = await _httpClient.PutAsync(url, 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}");
+        }
+
+        var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
+
+        return resu;
+    }
 
     public readonly SpaceRoom AsSpace;
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
index 6eaa73b..6b586c7 100644
--- a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
+++ b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
@@ -5,18 +5,22 @@ using MatrixRoomUtils.Core.Responses;
 
 namespace MatrixRoomUtils.Core.RoomTypes;
 
-public class SpaceRoom : Room {
-    public SpaceRoom(HttpClient httpClient, string roomId) : base(httpClient, roomId) { }
+public class SpaceRoom : GenericRoom {
+    private readonly AuthenticatedHomeServer _homeServer;
+    private readonly GenericRoom _room;
+    public SpaceRoom(AuthenticatedHomeServer homeServer, string roomId) : base(homeServer, roomId) {
+        _homeServer = homeServer;
+    }
 
-    public async Task<List<Room>> GetRoomsAsync(bool includeRemoved = false) {
-        var rooms = new List<Room>();
+    public async Task<List<GenericRoom>> GetRoomsAsync(bool includeRemoved = false) {
+        var rooms = new List<GenericRoom>();
         var state = await GetStateAsync("");
         if (state != null) {
             var states = state.Value.Deserialize<StateEventResponse[]>()!;
             foreach (var stateEvent in states.Where(x => x.Type == "m.space.child")) {
                 var roomId = stateEvent.StateKey;
                 if(stateEvent.TypedContent.ToJson() != "{}" || includeRemoved)
-                    rooms.Add(await RuntimeCache.CurrentHomeServer.GetRoom(roomId));
+                    rooms.Add(await _homeServer.GetRoom(roomId));
             }
         }
 
diff --git a/MatrixRoomUtils.Core/RuntimeCache.cs b/MatrixRoomUtils.Core/RuntimeCache.cs
deleted file mode 100644
index 7ab3952..0000000
--- a/MatrixRoomUtils.Core/RuntimeCache.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using MatrixRoomUtils.Core.Extensions;
-using MatrixRoomUtils.Core.Responses;
-using MatrixRoomUtils.Core.StateEventTypes;
-
-namespace MatrixRoomUtils.Core;
-
-public class RuntimeCache {
-    public static bool WasLoaded = false;
-
-    static RuntimeCache() =>
-        Task.Run(async () => {
-            while (true) {
-                await Task.Delay(1000);
-                foreach (var (key, value) in GenericResponseCache)
-                    if (value.Cache.Any())
-                        SaveObject("rory.matrixroomutils.generic_cache:" + key, value);
-                    else
-                        RemoveObject("rory.matrixroomutils.generic_cache:" + key);
-            }
-        });
-
-    public static string? LastUsedToken { get; set; }
-    public static AuthenticatedHomeServer CurrentHomeServer { get; set; }
-    public static Dictionary<string, UserInfo> LoginSessions { get; set; } = new();
-
-    public static Dictionary<string, HomeServerResolutionResult> HomeserverResolutionCache { get; set; } = new();
-    // public static Dictionary<string, (DateTime cachedAt, ProfileResponse response)> ProfileCache { get; set; } = new();
-
-    public static Dictionary<string, ObjectCache<object>> GenericResponseCache { get; set; } = new();
-
-    public static Task Save { get; set; } = new Task(() => { Console.WriteLine("RuntimeCache.Save() was called, but no callback was set!"); });
-    public static Action<string, object> SaveObject { get; set; } = (key, value) => { Console.WriteLine($"RuntimeCache.SaveObject({key}, {value}) was called, but no callback was set!"); };
-    public static Action<string> RemoveObject { get; set; } = key => { Console.WriteLine($"RuntimeCache.RemoveObject({key}) was called, but no callback was set!"); };
-}
-
-public class UserInfo {
-    public ProfileResponse Profile { get; set; } = new();
-    public LoginResponse LoginResponse { get; set; }
-
-    public string AccessToken => LoginResponse.AccessToken;
-}
-
-public class HomeServerResolutionResult {
-    public string Result { get; set; }
-    public DateTime ResolutionTime { get; set; }
-}
-
-public class ObjectCache<T> where T : class {
-    public ObjectCache() =>
-        //expiry timer
-        Task.Run(async () => {
-            while (Cache.Any()) {
-                await Task.Delay(1000);
-                foreach (var x in Cache.Where(x => x.Value.ExpiryTime < DateTime.Now).OrderBy(x => x.Value.ExpiryTime).Take(15).ToList())
-                    // Console.WriteLine($"Removing {x.Key} from cache");
-                    Cache.Remove(x.Key);
-                //RuntimeCache.SaveObject("rory.matrixroomutils.generic_cache:" + Name, this);
-            }
-        });
-
-    public Dictionary<string, GenericResult<T>> Cache { get; set; } = new();
-    public string Name { get; set; } = null!;
-
-    public GenericResult<T> this[string key] {
-        get {
-            if (Cache.ContainsKey(key)) {
-                // Console.WriteLine($"cache.get({key}): hit");
-                // Console.WriteLine($"Found item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}");
-                if (Cache[key].ExpiryTime < DateTime.Now)
-                    Console.WriteLine($"WARNING: item {key} in cache {Name} expired at {Cache[key].ExpiryTime}:\n{Cache[key].Result.ToJson(false)}");
-                return Cache[key];
-            }
-
-            Console.WriteLine($"cache.get({key}): miss");
-            return null;
-        }
-        set => Cache[key] = value;
-        // Console.WriteLine($"set({key}) = {Cache[key].Result.ToJson(indent:false)}");
-        // Console.WriteLine($"new_state: {this.ToJson(indent:false)}");
-        // Console.WriteLine($"New item in cache: {key} - {Cache[key].Result.ToJson(indent: false)}");
-        // Console.Error.WriteLine("Full cache: " + Cache.ToJson());
-    }
-
-    public bool ContainsKey(string key) => Cache.ContainsKey(key);
-}
-
-public class GenericResult<T> {
-    public GenericResult() {
-        //expiry timer
-    }
-
-    public GenericResult(T? result, DateTime? expiryTime = null) : this() {
-        Result = result;
-        ExpiryTime = expiryTime;
-    }
-
-    public T? Result { get; set; }
-    public DateTime? ExpiryTime { get; set; } = DateTime.Now;
-}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs
index cb8f0b4..f2c8701 100644
--- a/MatrixRoomUtils.Core/StateEvent.cs
+++ b/MatrixRoomUtils.Core/StateEvent.cs
@@ -1,9 +1,12 @@
+using System.Diagnostics;
 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;
+using MatrixRoomUtils.Core.Responses;
+using MatrixRoomUtils.Core.StateEventTypes;
 
 namespace MatrixRoomUtils.Core;
 
@@ -19,14 +22,36 @@ public class StateEvent {
     [JsonPropertyName("state_key")]
     public string StateKey { get; set; } = "";
 
+    private string _type;
+
     [JsonPropertyName("type")]
-    public string Type { get; set; }
+    public string Type {
+        get => _type;
+        set {
+            _type = value;
+            if (RawContent != null && this is StateEventResponse stateEventResponse) {
+                if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
+                var x = GetType.Name;
+            }
+        }
+    }
 
     [JsonPropertyName("replaces_state")]
     public string? ReplacesState { get; set; }
 
+    private JsonObject? _rawContent;
+
     [JsonPropertyName("content")]
-    public JsonObject? RawContent { get; set; }
+    public JsonObject? RawContent {
+        get => _rawContent;
+        set {
+            _rawContent = value;
+            if (Type != null && this is StateEventResponse stateEventResponse) {
+                if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
+                var x = GetType.Name;
+            }
+        }
+    }
 
     public T1 GetContent<T1>() where T1 : IStateEventType {
         return RawContent.Deserialize<T1>();
@@ -35,17 +60,36 @@ public class StateEvent {
     [JsonIgnore]
     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);
+            if (Type == "m.receipt") {
+                return typeof(Dictionary<string, JsonObject>);
+            }
+
+            var type = KnownStateEventTypes.FirstOrDefault(x =>
+                x.GetCustomAttributes<MatrixEventAttribute>()?.Any(y => y.EventName == Type) ?? false);
+
+            //special handling for some types
+            // if (type == typeof(RoomEmotesEventData)) {
+            //     RawContent["emote"] = RawContent["emote"]?.AsObject() ?? new JsonObject();
+            // }
+
+            if (this is StateEventResponse stateEventResponse) {
+                if (type == null || type == typeof(object)) {
+                    Console.WriteLine($"Warning: unknown event type '{Type}'!");
+                    Console.WriteLine(RawContent.ToJson());
+                    Directory.CreateDirectory($"unknown_state_events/{Type}");
+                    File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
+                        RawContent.ToJson());
+                    Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
+                }
+                else if (RawContent.FindExtraJsonObjectFields(type)) {
+                    Directory.CreateDirectory($"unknown_state_events/{Type}");
+                    File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
+                        RawContent.ToJson());
+                    Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
+                }
             }
 
-            RawContent.FindExtraJsonObjectFields(type);
-            
-            return type;
+            return type ?? typeof(object);
         }
     }
 
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Common/RoomEmotesEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Common/RoomEmotesEventData.cs
new file mode 100644
index 0000000..4a75b98
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Common/RoomEmotesEventData.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "im.ponies.room_emotes")]
+public class RoomEmotesEventData : IStateEventType {
+    [JsonPropertyName("emoticons")]
+    public Dictionary<string, EmoticonData>? Emoticons { get; set; }
+    
+    [JsonPropertyName("images")]
+    public Dictionary<string, EmoticonData>? Images { get; set; }
+    
+    [JsonPropertyName("pack")]
+    public PackInfo? Pack { get; set; }
+    
+    public class EmoticonData {
+        [JsonPropertyName("url")]
+        public string? Url { get; set; }
+    }
+}
+
+public class PackInfo {
+    
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/CanonicalAliasEventData.cs
index 2f9502e..4d6f9c3 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/CanonicalAliasEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/CanonicalAliasEventData.cs
@@ -8,4 +8,6 @@ namespace MatrixRoomUtils.Core.StateEventTypes;
 public class CanonicalAliasEventData : IStateEventType {
     [JsonPropertyName("alias")]
     public string? Alias { get; set; }
+    [JsonPropertyName("alt_aliases")]
+    public string[]? AltAliases { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/GuestAccessData.cs
index 1727ce9..1727ce9 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/GuestAccessData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/GuestAccessData.cs
diff --git a/MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/HistoryVisibilityData.cs
index 481cc08..2bae838 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/HistoryVisibilityData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/HistoryVisibilityData.cs
@@ -1,3 +1,4 @@
+using System.Text.Json.Serialization;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Interfaces;
 
@@ -5,5 +6,6 @@ namespace MatrixRoomUtils.Core.StateEventTypes;
 
 [MatrixEvent(EventName = "m.room.history_visibility")]
 public class HistoryVisibilityData : IStateEventType {
+    [JsonPropertyName("history_visibility")]
     public string HistoryVisibility { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/JoinRules.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/JoinRulesEventData.cs
index 7ce56c4..590835b 100644
--- a/MatrixRoomUtils.Core/JoinRules.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/JoinRulesEventData.cs
@@ -1,8 +1,11 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core;
 
-public class JoinRules {
+[MatrixEvent(EventName = "m.room.join_rules")]
+public class JoinRulesEventData : IStateEventType {
     private static string Public = "public";
     private static string Invite = "invite";
     private static string Knock = "knock";
diff --git a/MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/MessageEventData.cs
index ad99709..bc1c52b 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/MessageEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/MessageEventData.cs
@@ -7,5 +7,11 @@ public class MessageEventData : IStateEventType {
     [JsonPropertyName("body")]
     public string Body { get; set; }
     [JsonPropertyName("msgtype")]
-    public string MessageType { get; set; }
+    public string MessageType { get; set; } = "m.notice";
+
+    [JsonPropertyName("formatted_body")]
+    public string FormattedBody { get; set; }
+
+    [JsonPropertyName("format")]
+    public string Format { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/PolicyRuleStateEventData.cs
index e67639b..debbef0 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/PolicyRuleStateEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/PolicyRuleStateEventData.cs
@@ -1,8 +1,12 @@
 using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Interfaces;
 
 namespace MatrixRoomUtils.Core.StateEventTypes;
 
+[MatrixEvent(EventName = "m.policy.rule.user")]
+[MatrixEvent(EventName = "m.policy.rule.server")]
+[MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")]
 public class PolicyRuleStateEventData : IStateEventType {
     /// <summary>
     ///     Entity this ban applies to, can use * and ? as globs.
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/PowerLevelEvent.cs
index a3e44d1..c6100bb 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/PowerLevelEvent.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/PowerLevelEvent.cs
@@ -36,6 +36,11 @@ public class PowerLevelEvent : IStateEventType {
     [JsonPropertyName("users_default")]
     public int UsersDefault { get; set; } // = 0;
     
+    [Obsolete("Historical was a key related to MSC2716, a spec change on backfill that was dropped!", true)]
+    [JsonIgnore]
+    [JsonPropertyName("historical")]
+    public int Historical { get; set; } // = 50;
+    
     public class NotificationsPL {
         [JsonPropertyName("room")]
         public int Room { get; set; } = 50;
diff --git a/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/PresenceStateEventData.cs
index a17b6f9..a17b6f9 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/PresenceStateEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/PresenceStateEventData.cs
diff --git a/MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/ProfileResponse.cs
index d36ef74..d36ef74 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/ProfileResponse.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/ProfileResponse.cs
diff --git a/MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAvatarEventData.cs
index 03ce16b..03ce16b 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/RoomAvatarEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAvatarEventData.cs
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomCreateEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomCreateEventData.cs
new file mode 100644
index 0000000..2e4bb5a
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomCreateEventData.cs
@@ -0,0 +1,21 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "m.room.create")]
+public class RoomCreateEventData : IStateEventType {
+    [JsonPropertyName("room_version")]
+    public string? RoomVersion { get; set; }
+    [JsonPropertyName("creator")]
+    public string? Creator { get; set; }
+    [JsonPropertyName("m.federate")]
+    public bool? Federate { get; set; }
+    [JsonPropertyName("predecessor")]
+    public RoomCreatePredecessor? Predecessor { get; set; }
+    [JsonPropertyName("type")]
+    public string? Type { get; set; }
+    
+    public class RoomCreatePredecessor { }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomEncryptionEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomEncryptionEventData.cs
new file mode 100644
index 0000000..8d0576d
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomEncryptionEventData.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "m.room.encryption")]
+public class RoomEncryptionEventData : IStateEventType {
+    [JsonPropertyName("algorithm")]
+    public string? Algorithm { get; set; }
+    [JsonPropertyName("rotation_period_ms")]
+    public ulong? RotationPeriodMs { get; set; }
+    [JsonPropertyName("rotation_period_msgs")]
+    public ulong? RotationPeriodMsgs { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomMemberEventData.cs
index acf7777..50d9dd2 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/MemberEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomMemberEventData.cs
@@ -5,7 +5,7 @@ using MatrixRoomUtils.Core.Interfaces;
 namespace MatrixRoomUtils.Core.StateEventTypes;
 
 [MatrixEvent(EventName = "m.room.member")]
-public class MemberEventData : IStateEventType {
+public class RoomMemberEventData : IStateEventType {
     [JsonPropertyName("reason")]
     public string? Reason { get; set; }
 
@@ -17,7 +17,13 @@ public class MemberEventData : IStateEventType {
 
     [JsonPropertyName("is_direct")]
     public bool? IsDirect { get; set; }
-    
+
     [JsonPropertyName("avatar_url")]
     public string? AvatarUrl { get; set; }
+
+    [JsonPropertyName("kind")]
+    public string? Kind { get; set; }
+    
+    [JsonPropertyName("join_authorised_via_users_server")]
+    public string? JoinAuthorisedViaUsersServer { get; set; }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomNameEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomNameEventData.cs
new file mode 100644
index 0000000..642b5f9
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomNameEventData.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.name")]
+public class RoomNameEventData : IStateEventType {
+    [JsonPropertyName("name")]
+    public string? Name { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomPinnedEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomPinnedEventData.cs
new file mode 100644
index 0000000..05c0048
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomPinnedEventData.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.pinned_events")]
+public class RoomPinnedEventData : IStateEventType {
+    [JsonPropertyName("pinned")]
+    public string[]? PinnedEvents { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTopicEventData.cs
index 72651c8..cc5b35b 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/RoomTopicEventData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTopicEventData.cs
@@ -5,6 +5,7 @@ using MatrixRoomUtils.Core.Interfaces;
 namespace MatrixRoomUtils.Core.StateEventTypes; 
 
 [MatrixEvent(EventName = "m.room.topic")]
+[MatrixEvent(EventName = "org.matrix.msc3765.topic", Legacy = true)]
 public class RoomTopicEventData : IStateEventType {
     [JsonPropertyName("topic")]
     public string? Topic { get; set; }
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTypingEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTypingEventData.cs
new file mode 100644
index 0000000..eac4af2
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomTypingEventData.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.typing")]
+public class RoomTypingEventData : IStateEventType {
+    [JsonPropertyName("user_ids")]
+    public string[]? UserIds { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/ServerACLData.cs
index 41bf0a8..41bf0a8 100644
--- a/MatrixRoomUtils.Core/StateEventTypes/ServerACLData.cs
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/ServerACLData.cs
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceChildEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceChildEventData.cs
new file mode 100644
index 0000000..f65cd5b
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceChildEventData.cs
@@ -0,0 +1,15 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes; 
+
+[MatrixEvent(EventName = "m.space.child")]
+public class SpaceChildEventData : IStateEventType {
+    [JsonPropertyName("auto_join")]
+    public bool? AutoJoin { get; set; }
+    [JsonPropertyName("via")]
+    public string[]? Via { get; set; }
+    [JsonPropertyName("suggested")]
+    public bool? Suggested { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceParentEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceParentEventData.cs
new file mode 100644
index 0000000..a40f7ae
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/SpaceParentEventData.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes.Spec;
+
+[MatrixEvent(EventName = "m.space.parent")]
+public class SpaceParentEventData : IStateEventType {
+    [JsonPropertyName("via")]
+    public string[]? Via { get; set; }
+
+    [JsonPropertyName("canonical")]
+    public bool? Canonical { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.sln.DotSettings.user b/MatrixRoomUtils.sln.DotSettings.user
index 93b1fca..8c6a223 100644
--- a/MatrixRoomUtils.sln.DotSettings.user
+++ b/MatrixRoomUtils.sln.DotSettings.user
@@ -10,6 +10,7 @@
 	<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>