about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--LibMatrix.EventTypes/EventContent.cs16
-rw-r--r--LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs1
-rw-r--r--LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs4
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs4
-rw-r--r--LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj13
-rw-r--r--LibMatrix.sln86
-rw-r--r--LibMatrix/Extensions/CanonicalJsonSerializer.cs91
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs16
-rw-r--r--LibMatrix/Extensions/UnicodeJsonEncoder.cs173
-rw-r--r--LibMatrix/Helpers/HomeserverWeightEstimation.cs58
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs12
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs8
-rw-r--r--LibMatrix/LibMatrix.csproj5
-rw-r--r--LibMatrix/Responses/SyncResponse.cs2
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs58
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs4
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs10
-rw-r--r--LibMatrix/StateEvent.cs41
18 files changed, 461 insertions, 141 deletions
diff --git a/LibMatrix.EventTypes/EventContent.cs b/LibMatrix.EventTypes/EventContent.cs
index c582cf2..a837252 100644
--- a/LibMatrix.EventTypes/EventContent.cs
+++ b/LibMatrix.EventTypes/EventContent.cs
@@ -1,10 +1,20 @@
+using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 
 namespace LibMatrix.EventTypes;
 
-public abstract class EventContent;
+public abstract class EventContent {
+    public static List<string> GetMatchingEventTypes<T>() where T : EventContent {
+        var type = typeof(T);
+        var eventTypes = new List<string>();
+        foreach (var attr in type.GetCustomAttributes<MatrixEventAttribute>(true)) {
+            eventTypes.Add(attr.EventName);
+        }
+        return eventTypes;
+    }
+}
 
 public class UnknownEventContent : TimelineEventContent;
 
@@ -37,6 +47,10 @@ public abstract class TimelineEventContent : EventContent {
         [JsonPropertyName("rel_type")]
         public string? RelationType { get; set; }
 
+        // used for reactions
+        [JsonPropertyName("key")]
+        public string? Key { get; set; }
+
         public class EventInReplyTo {
             [JsonPropertyName("event_id")]
             public string? EventId { get; set; }
diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
index ae893f8..9602bf3 100644
--- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -29,6 +29,7 @@ public class RoomMessageEventContent : TimelineEventContent {
     [JsonPropertyName("url")]
     public string? Url { get; set; }
 
+    [JsonPropertyName("filename")]
     public string? FileName { get; set; }
 
     [JsonPropertyName("info")]
diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 89e2fdb..5bfd77b 100644
--- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -1,5 +1,7 @@
+using System.Security.Cryptography;
 using System.Text.Json.Serialization;
 using ArcaneLibs.Attributes;
+using ArcaneLibs.Extensions;
 
 namespace LibMatrix.EventTypes.Spec.State.Policy;
 
@@ -87,6 +89,8 @@ public abstract class PolicyRuleEventContent : EventContent {
                 Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds();
         }
     }
+
+    public string GetDraupnir2StateKey() => Convert.ToBase64String(SHA256.HashData($"{Entity}{Recommendation}".AsBytes().ToArray()));
 }
 
 public static class PolicyRecommendationTypes {
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
index c619d0e..f26b8e5 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
@@ -15,8 +15,8 @@ public class RoomCreateEventContent : EventContent {
     [JsonPropertyName("m.federate")]
     public bool? Federate { get; set; }
 
-    [JsonPropertyName("predecessor")]
-    public RoomCreatePredecessor? Predecessor { get; set; }
+    // [JsonPropertyName("predecessor")]
+    // public RoomCreatePredecessor? Predecessor { get; set; }
 
     [JsonPropertyName("type")]
     public string? Type { get; set; }
diff --git a/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj b/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj
deleted file mode 100644
index 4df0c6b..0000000
--- a/LibMatrix.MxApiExtensions/LibMatrix.MxApiExtensions.csproj
+++ /dev/null
@@ -1,13 +0,0 @@
-<Project Sdk="Microsoft.NET.Sdk">
-
-    <PropertyGroup>
-        <TargetFramework>net8.0</TargetFramework>
-        <ImplicitUsings>enable</ImplicitUsings>
-        <Nullable>enable</Nullable>
-    </PropertyGroup>
-
-    <ItemGroup>
-      <Folder Include="Classes\" />
-    </ItemGroup>
-
-</Project>
diff --git a/LibMatrix.sln b/LibMatrix.sln
index c068216..3294f77 100644
--- a/LibMatrix.sln
+++ b/LibMatrix.sln
@@ -5,22 +5,14 @@ VisualStudioVersion = 17.0.31903.59
 MinimumVisualStudioVersion = 10.0.40219.1

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

 EndProject

-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ExampleBots", "ExampleBots", "{840309F0-435B-43A7-8471-8C2BE643889D}"

-EndProject

 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{A6345ECE-4C5E-400F-9130-886E343BF314}"

 EndProject

 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.MxApiExtensions", "LibMatrix.MxApiExtensions\LibMatrix.MxApiExtensions.csproj", "{32D9616B-91BB-4B43-97C6-2C3840C12EA6}"

 EndProject

-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.ExampleBot", "ExampleBots\LibMatrix.ExampleBot\LibMatrix.ExampleBot.csproj", "{1B1B2197-61FB-416F-B6C8-845F2E5A0442}"

-EndProject

-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ModerationBot", "ExampleBots\ModerationBot\ModerationBot.csproj", "{8F0A820E-F6AE-45A2-970E-7A3759693919}"

-EndProject

 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DebugDataValidationApi", "Utilities\LibMatrix.DebugDataValidationApi\LibMatrix.DebugDataValidationApi.csproj", "{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}"

 EndProject

 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Utilities.Bot", "Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj", "{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}"

 EndProject

-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluralContactBotPoC", "ExampleBots\PluralContactBotPoC\PluralContactBotPoC.csproj", "{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}"

-EndProject

 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BFE16D8E-EFC5-49F6-9854-DB001309B3B4}"

 EndProject

 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.Tests", "Tests\LibMatrix.Tests\LibMatrix.Tests.csproj", "{345934FF-CA81-4A4B-B137-9F198102C65F}"

@@ -37,6 +29,24 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.EventTypes", "Lib
 EndProject

 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.HomeserverEmulator", "Tests\LibMatrix.HomeserverEmulator\LibMatrix.HomeserverEmulator.csproj", "{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}"

 EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Blazor.Components", "ArcaneLibs\ArcaneLibs.Blazor.Components\ArcaneLibs.Blazor.Components.csproj", "{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Legacy", "ArcaneLibs\ArcaneLibs.Legacy\ArcaneLibs.Legacy.csproj", "{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Logging", "ArcaneLibs\ArcaneLibs.Logging\ArcaneLibs.Logging.csproj", "{711D1579-9228-47BA-9CB3-C237F7E8F403}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.StringNormalisation", "ArcaneLibs\ArcaneLibs.StringNormalisation\ArcaneLibs.StringNormalisation.csproj", "{87270607-F95C-4EED-AE69-57666863EFDF}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.Timings", "ArcaneLibs\ArcaneLibs.Timings\ArcaneLibs.Timings.csproj", "{5EE6E02C-A63F-4864-91F3-7375B4C50189}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLibs.UsageTest", "ArcaneLibs\ArcaneLibs.UsageTest\ArcaneLibs.UsageTest.csproj", "{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaneLib.Tests", "ArcaneLibs\ArcaneLib.Tests\ArcaneLib.Tests.csproj", "{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.DevTestBot", "Utilities\LibMatrix.DevTestBot\LibMatrix.DevTestBot.csproj", "{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}"

+EndProject

+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LibMatrix.E2eeTestKit", "Utilities\LibMatrix.E2eeTestKit\LibMatrix.E2eeTestKit.csproj", "{A1066F8F-CE8A-4F60-9EFB-553C9E794435}"

+EndProject

 Global

 	GlobalSection(SolutionConfigurationPlatforms) = preSolution

 		Debug|Any CPU = Debug|Any CPU

@@ -54,14 +64,6 @@ Global
 		{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.ActiveCfg = Release|Any CPU

 		{32D9616B-91BB-4B43-97C6-2C3840C12EA6}.Release|Any CPU.Build.0 = Release|Any CPU

-		{1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

-		{1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Debug|Any CPU.Build.0 = Debug|Any CPU

-		{1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.ActiveCfg = Release|Any CPU

-		{1B1B2197-61FB-416F-B6C8-845F2E5A0442}.Release|Any CPU.Build.0 = Release|Any CPU

-		{8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

-		{8F0A820E-F6AE-45A2-970E-7A3759693919}.Debug|Any CPU.Build.0 = Debug|Any CPU

-		{8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.ActiveCfg = Release|Any CPU

-		{8F0A820E-F6AE-45A2-970E-7A3759693919}.Release|Any CPU.Build.0 = Release|Any CPU

 		{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

 		{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{35DF9A1A-D988-4225-AFA3-06BB8EDEB559}.Release|Any CPU.ActiveCfg = Release|Any CPU

@@ -70,10 +72,6 @@ Global
 		{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.ActiveCfg = Release|Any CPU

 		{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7}.Release|Any CPU.Build.0 = Release|Any CPU

-		{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

-		{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Debug|Any CPU.Build.0 = Debug|Any CPU

-		{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.ActiveCfg = Release|Any CPU

-		{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B}.Release|Any CPU.Build.0 = Release|Any CPU

 		{345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

 		{345934FF-CA81-4A4B-B137-9F198102C65F}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{345934FF-CA81-4A4B-B137-9F198102C65F}.Release|Any CPU.ActiveCfg = Release|Any CPU

@@ -98,17 +96,59 @@ Global
 		{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Debug|Any CPU.Build.0 = Debug|Any CPU

 		{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.ActiveCfg = Release|Any CPU

 		{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2}.Release|Any CPU.Build.0 = Release|Any CPU

+		{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C}.Release|Any CPU.Build.0 = Release|Any CPU

+		{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD}.Release|Any CPU.Build.0 = Release|Any CPU

+		{711D1579-9228-47BA-9CB3-C237F7E8F403}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{711D1579-9228-47BA-9CB3-C237F7E8F403}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{711D1579-9228-47BA-9CB3-C237F7E8F403}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{711D1579-9228-47BA-9CB3-C237F7E8F403}.Release|Any CPU.Build.0 = Release|Any CPU

+		{87270607-F95C-4EED-AE69-57666863EFDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{87270607-F95C-4EED-AE69-57666863EFDF}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{87270607-F95C-4EED-AE69-57666863EFDF}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{87270607-F95C-4EED-AE69-57666863EFDF}.Release|Any CPU.Build.0 = Release|Any CPU

+		{5EE6E02C-A63F-4864-91F3-7375B4C50189}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{5EE6E02C-A63F-4864-91F3-7375B4C50189}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{5EE6E02C-A63F-4864-91F3-7375B4C50189}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{5EE6E02C-A63F-4864-91F3-7375B4C50189}.Release|Any CPU.Build.0 = Release|Any CPU

+		{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF}.Release|Any CPU.Build.0 = Release|Any CPU

+		{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF}.Release|Any CPU.Build.0 = Release|Any CPU

+		{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5}.Release|Any CPU.Build.0 = Release|Any CPU

+		{A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Debug|Any CPU.ActiveCfg = Debug|Any CPU

+		{A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Debug|Any CPU.Build.0 = Debug|Any CPU

+		{A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Release|Any CPU.ActiveCfg = Release|Any CPU

+		{A1066F8F-CE8A-4F60-9EFB-553C9E794435}.Release|Any CPU.Build.0 = Release|Any CPU

 	EndGlobalSection

 	GlobalSection(NestedProjects) = preSolution

-		{1B1B2197-61FB-416F-B6C8-845F2E5A0442} = {840309F0-435B-43A7-8471-8C2BE643889D}

-		{8F0A820E-F6AE-45A2-970E-7A3759693919} = {840309F0-435B-43A7-8471-8C2BE643889D}

 		{35DF9A1A-D988-4225-AFA3-06BB8EDEB559} = {A6345ECE-4C5E-400F-9130-886E343BF314}

 		{3ACF2613-E23F-42C2-925E-0BB4FC3AB1F7} = {A6345ECE-4C5E-400F-9130-886E343BF314}

-		{FB8FE4EB-B53B-464B-A5FD-9BF9D0F3EF9B} = {840309F0-435B-43A7-8471-8C2BE643889D}

 		{345934FF-CA81-4A4B-B137-9F198102C65F} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}

 		{0B9B34D1-9362-45A9-9C21-816FD6959110} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}

 		{4D9B5227-48DC-4A30-9263-AFB51DC01ABB} = {A6345ECE-4C5E-400F-9130-886E343BF314}

 		{13A797D1-7E13-4789-A167-8628B1641AC0} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

 		{D44DB78D-9BAD-4AB6-A054-839ECA9D68D2} = {BFE16D8E-EFC5-49F6-9854-DB001309B3B4}

+		{ABAB8F42-A4BC-4ABF-AF1D-FDB40D87A91C} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{F94D35FF-E90E-4B86-B26E-A8E46EB54BDD} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{711D1579-9228-47BA-9CB3-C237F7E8F403} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{87270607-F95C-4EED-AE69-57666863EFDF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{5EE6E02C-A63F-4864-91F3-7375B4C50189} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{7E7BBBDD-CE09-4298-B839-5EBDCA5D0DAF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{E6BF8A7E-92F6-4761-A793-1EC709F7E2BF} = {01A126FE-9D50-40F2-817B-E55F4065EA76}

+		{6F4A4ABC-5E42-4293-80E6-0C38FE8C9EC5} = {A6345ECE-4C5E-400F-9130-886E343BF314}

+		{A1066F8F-CE8A-4F60-9EFB-553C9E794435} = {A6345ECE-4C5E-400F-9130-886E343BF314}

 	EndGlobalSection

 EndGlobal

diff --git a/LibMatrix/Extensions/CanonicalJsonSerializer.cs b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
new file mode 100644
index 0000000..a6fbcf4
--- /dev/null
+++ b/LibMatrix/Extensions/CanonicalJsonSerializer.cs
@@ -0,0 +1,91 @@
+using System.Collections.Frozen;
+using System.Reflection;
+using System.Security.Cryptography;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
+using System.Text.Unicode;
+using ArcaneLibs.Extensions;
+
+namespace LibMatrix.Extensions;
+
+public static class CanonicalJsonSerializer {
+    // TODO: Alphabetise dictionaries
+    private static JsonSerializerOptions _options => new() {
+        WriteIndented = false,
+        Encoder = UnicodeJsonEncoder.Singleton,
+    };
+
+    private static readonly FrozenSet<PropertyInfo> JsonSerializerOptionsProperties = typeof(JsonSerializerOptions)
+        .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+        .Where(x => x.SetMethod != null && x.GetMethod != null)
+        .ToFrozenSet();
+
+    private static JsonSerializerOptions MergeOptions(JsonSerializerOptions? inputOptions) {
+        var newOptions = _options;
+        if (inputOptions == null)
+            return newOptions;
+        
+        foreach (var property in JsonSerializerOptionsProperties) {
+            if(property.Name == nameof(JsonSerializerOptions.Encoder))
+                continue;
+            if (property.Name == nameof(JsonSerializerOptions.WriteIndented))
+                continue;
+                
+            var value = property.GetValue(inputOptions);
+            // if (value == null)
+                // continue;
+            property.SetValue(newOptions, value);
+        }
+
+        return newOptions;
+    }
+
+#region STJ API
+
+    public static String Serialize<TValue>(TValue value, JsonSerializerOptions? options = null) {
+        var newOptions = MergeOptions(options);
+
+        return System.Text.Json.JsonSerializer.SerializeToNode(value, options) // We want to allow passing custom converters for eg. double/float -> string here...
+            .SortProperties()!
+            .CanonicalizeNumbers()!
+            .ToJsonString(newOptions);
+        
+            
+        // System.Text.Json.JsonSerializer.SerializeToNode(System.Text.Json.JsonSerializer.Deserialize<dynamic>("{\n    \"a\": -0,\n    \"b\": 1e10\n}")).ToJsonString();
+        
+    }
+
+    public static String Serialize(object value, Type inputType, JsonSerializerOptions? options = null) => JsonSerializer.Serialize(value, inputType, _options);
+    // public static String Serialize<TValue>(TValue value, JsonTypeInfo<TValue> jsonTypeInfo) => JsonSerializer.Serialize(value, jsonTypeInfo, _options);
+    // public static String Serialize(Object value, JsonTypeInfo jsonTypeInfo) 
+
+#endregion
+
+    private static partial class JsonExtensions {
+        public static Action<JsonTypeInfo> AlphabetizeProperties(Type type) {
+            return typeInfo => {
+                if (typeInfo.Kind != JsonTypeInfoKind.Object || !type.IsAssignableFrom(typeInfo.Type))
+                    return;
+                AlphabetizeProperties()(typeInfo);
+            };
+        }
+
+        public static Action<JsonTypeInfo> AlphabetizeProperties() {
+            return static typeInfo => {
+                if (typeInfo.Kind == JsonTypeInfoKind.Dictionary) { }
+
+                if (typeInfo.Kind != JsonTypeInfoKind.Object)
+                    return;
+                var properties = typeInfo.Properties.OrderBy(p => p.Name, StringComparer.Ordinal).ToList();
+                typeInfo.Properties.Clear();
+                for (int i = 0; i < properties.Count; i++) {
+                    properties[i].Order = i;
+                    typeInfo.Properties.Add(properties[i]);
+                }
+            };
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index 39eb7e5..4145a16 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -2,6 +2,7 @@
 // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Net;
 using System.Net.Http.Headers;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
@@ -73,12 +74,15 @@ public class MatrixHttpClient {
         await _rateLimitSemaphore.WaitAsync(cancellationToken);
 #endif
 
-        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})");
+        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.GetContentLength())})");
 
         if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
         if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri);
         foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
-        foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value);
+        foreach (var (key, value) in DefaultRequestHeaders) {
+            if (request.Headers.Contains(key)) continue;
+            request.Headers.Add(key, value);
+        }
 
         request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
 
@@ -106,7 +110,13 @@ public class MatrixHttpClient {
     public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
         var responseMessage = await SendUnhandledAsync(request, cancellationToken);
         if (responseMessage.IsSuccessStatusCode) return responseMessage;
-
+        
+        //retry on gateway timeout
+        if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
+            request.ResetSendStatus();
+            return await SendAsync(request, cancellationToken);
+        }
+        
         //error handling
         var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
         if (content.Length == 0)
diff --git a/LibMatrix/Extensions/UnicodeJsonEncoder.cs b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
new file mode 100644
index 0000000..ae58263
--- /dev/null
+++ b/LibMatrix/Extensions/UnicodeJsonEncoder.cs
@@ -0,0 +1,173 @@
+// LibMatrix: File sourced from https://github.com/dotnet/runtime/pull/87147/files under the MIT license.
+
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Text;
+using System.Text.Encodings.Web;
+
+namespace LibMatrix.Extensions;
+
+internal sealed class UnicodeJsonEncoder : JavaScriptEncoder
+{
+    internal static readonly UnicodeJsonEncoder Singleton = new UnicodeJsonEncoder();
+
+    private readonly bool _preferHexEscape;
+    private readonly bool _preferUppercase;
+
+    public UnicodeJsonEncoder()
+        : this(preferHexEscape: false, preferUppercase: false)
+    {
+    }
+
+    public UnicodeJsonEncoder(bool preferHexEscape, bool preferUppercase)
+    {
+        _preferHexEscape = preferHexEscape;
+        _preferUppercase = preferUppercase;
+    }
+
+    public override int MaxOutputCharactersPerInputCharacter => 6; // "\uXXXX" for a single char ("\uXXXX\uYYYY" [12 chars] for supplementary scalar value)
+
+    public override unsafe int FindFirstCharacterToEncode(char* text, int textLength)
+    {
+        for (int index = 0; index < textLength; ++index)
+        {
+            char value = text[index];
+
+            if (NeedsEncoding(value))
+            {
+                return index;
+            }
+        }
+
+        return -1;
+    }
+
+    public override unsafe bool TryEncodeUnicodeScalar(int unicodeScalar, char* buffer, int bufferLength, out int numberOfCharactersWritten)
+    {
+        bool encode = WillEncode(unicodeScalar);
+
+        if (!encode)
+        {
+            Span<char> span = new Span<char>(buffer, bufferLength);
+            int spanWritten;
+            bool succeeded = new Rune(unicodeScalar).TryEncodeToUtf16(span, out spanWritten);
+            numberOfCharactersWritten = spanWritten;
+            return succeeded;
+        }
+
+        if (!_preferHexEscape && unicodeScalar <= char.MaxValue && HasTwoCharacterEscape((char)unicodeScalar))
+        {
+            if (bufferLength < 2)
+            {
+                numberOfCharactersWritten = 0;
+                return false;
+            }
+
+            buffer[0] = '\\';
+            buffer[1] = GetTwoCharacterEscapeSuffix((char)unicodeScalar);
+            numberOfCharactersWritten = 2;
+            return true;
+        }
+        else
+        {
+            if (bufferLength < 6)
+            {
+                numberOfCharactersWritten = 0;
+                return false;
+            }
+
+            buffer[0] = '\\';
+            buffer[1] = 'u';
+            buffer[2] = '0';
+            buffer[3] = '0';
+            buffer[4] = ToHexDigit((unicodeScalar & 0xf0) >> 4, _preferUppercase);
+            buffer[5] = ToHexDigit(unicodeScalar & 0xf, _preferUppercase);
+            numberOfCharactersWritten = 6;
+            return true;
+        }
+    }
+
+    public override bool WillEncode(int unicodeScalar)
+    {
+        if (unicodeScalar > char.MaxValue)
+        {
+            return false;
+        }
+
+        return NeedsEncoding((char)unicodeScalar);
+    }
+
+    // https://datatracker.ietf.org/doc/html/rfc8259#section-7
+    private static bool NeedsEncoding(char value)
+    {
+        if (value == '"' || value == '\\')
+        {
+            return true;
+        }
+
+        return value <= '\u001f';
+    }
+
+    private static bool HasTwoCharacterEscape(char value)
+    {
+        // RFC 8259, Section 7, "char = " BNF
+        switch (value)
+        {
+            case '"':
+            case '\\':
+            case '/':
+            case '\b':
+            case '\f':
+            case '\n':
+            case '\r':
+            case '\t':
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    private static char GetTwoCharacterEscapeSuffix(char value)
+    {
+        // RFC 8259, Section 7, "char = " BNF
+        switch (value)
+        {
+            case '"':
+                return '"';
+            case '\\':
+                return '\\';
+            case '/':
+                return '/';
+            case '\b':
+                return 'b';
+            case '\f':
+                return 'f';
+            case '\n':
+                return 'n';
+            case '\r':
+                return 'r';
+            case '\t':
+                return 't';
+            default:
+                throw new ArgumentOutOfRangeException(nameof(value));
+        }
+    }
+
+    private static char ToHexDigit(int value, bool uppercase)
+    {
+        if (value > 0xf)
+        {
+            throw new ArgumentOutOfRangeException(nameof(value));
+        }
+
+        if (value < 10)
+        {
+            return (char)(value + '0');
+        }
+        else
+        {
+            return (char)(value - 0xa + (uppercase ? 'A' : 'a'));
+        }
+    }
+}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/HomeserverWeightEstimation.cs b/LibMatrix/Helpers/HomeserverWeightEstimation.cs
deleted file mode 100644
index 5735af3..0000000
--- a/LibMatrix/Helpers/HomeserverWeightEstimation.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-namespace LibMatrix.Helpers;
-
-public class HomeserverWeightEstimation {
-    public static Dictionary<string, int> EstimatedSize = new() {
-        { "matrix.org", 843870 },
-        { "anontier.nl", 44809 },
-        { "nixos.org", 8195 },
-        { "the-apothecary.club", 6983 },
-        { "waifuhunter.club", 3953 },
-        { "neko.dev", 2666 },
-        { "nerdsin.space", 2647 },
-        { "feline.support", 2633 },
-        { "gitter.im", 2584 },
-        { "midov.pl", 2219 },
-        { "no.lgbtqia.zone", 2083 },
-        { "nheko.im", 1883 },
-        { "fachschaften.org", 1849 },
-        { "pixelthefox.net", 1478 },
-        { "arcticfoxes.net", 981 },
-        { "pixie.town", 817 },
-        { "privacyguides.org", 809 },
-        { "rory.gay", 653 },
-        { "artemislena.eu", 599 },
-        { "alchemi.dev", 445 },
-        { "jameskitt616.one", 390 },
-        { "hackint.org", 382 },
-        { "pikaviestin.fi", 368 },
-        { "matrix.nomagic.uk", 337 },
-        { "thearcanebrony.net", 178 },
-        { "fairydust.space", 176 },
-        { "grin.hu", 176 },
-        { "envs.net", 165 },
-        { "tastytea.de", 143 },
-        { "koneko.chat", 121 },
-        { "vscape.tk", 115 },
-        { "funklause.de", 112 },
-        { "seirdy.one", 107 },
-        { "pcg.life", 72 },
-        { "draupnir.midnightthoughts.space", 22 },
-        { "tchncs.de", 19 },
-        { "catgirl.cloud", 16 },
-        { "possum.city", 16 },
-        { "tu-dresden.de", 9 },
-        { "fosscord.com", 9 },
-        { "nightshade.fun", 8 },
-        { "matrix.eclipse.org", 8 },
-        { "masfloss.net", 8 },
-        { "e2e.zone", 8 },
-        { "hyteck.de", 8 }
-    };
-
-    public static Dictionary<string, int> LargeRooms = new() {
-        { "!ehXvUhWNASUkSLvAGP:matrix.org", 21957 },
-        { "!fRRqjOaQcUbKOfCjvc:anontier.nl", 19117 },
-        { "!OGEhHVWSdvArJzumhm:matrix.org", 101457 },
-        { "!YTvKGNlinIzlkMTVRl:matrix.org", 30164 }
-    };
-}
\ No newline at end of file
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index d897078..b639e1f 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -91,6 +91,18 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
         return this;
     }
 
+    public MessageBuilder WithMention(string id, string? displayName = null) {
+        Content.Body += $"@{displayName ?? id}";
+        Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
+        return this;
+    }
+
+    public MessageBuilder WithNewline() {
+        Content.Body += "\n";
+        Content.FormattedBody += "<br>";
+        return this;
+    }
+
     public MessageBuilder WithTable(Action<TableBuilder> tableBuilder) {
         var tb = new TableBuilder(this);
         this.WithHtmlTag("table", msb => tableBuilder(tb));
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 1833bd0..c9ca85d 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -4,6 +4,7 @@ using ArcaneLibs.Extensions;
 using LibMatrix.Filters;
 using LibMatrix.Homeservers;
 using LibMatrix.Responses;
+using LibMatrix.Utilities;
 using Microsoft.Extensions.Logging;
 
 namespace LibMatrix.Helpers;
@@ -42,6 +43,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
             _filter = value;
             _filterIsDirty = true;
             _filterId = null;
+            _namedFilterName = null;
         }
     }
 
@@ -81,16 +83,16 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
         if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}";
         if (_filterId is not null) url += $"&filter={_filterId}";
 
-        logger?.LogInformation("SyncHelper: Calling: {}", url);
+        // logger?.LogInformation("SyncHelper: Calling: {}", url);
 
         try {
             var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None);
             if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request");
-            logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.Content.Headers.ContentLength ?? -1, sw.Elapsed);
+            logger?.LogTrace("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed);
             var deserializeSw = Stopwatch.StartNew();
             var resp = await httpResp.Content.ReadFromJsonAsync<SyncResponse>(cancellationToken: cancellationToken ?? CancellationToken.None,
                 jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse);
-            logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.Content.Headers.ContentLength ?? -1, deserializeSw.Elapsed, sw.Elapsed);
+            logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed);
             var timeToWait = MinimumDelay.Subtract(sw.Elapsed);
             if (timeToWait.TotalMilliseconds > 0)
                 await Task.Delay(timeToWait);
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index d0511ea..6158ff8 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -8,6 +8,7 @@
 
         <Optimize>true</Optimize>
         <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
+        <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <!-- Required for UnicodeJsonEncoder... -->
     </PropertyGroup>
 
     <ItemGroup>
@@ -22,12 +23,14 @@
                 Using the NuGet version in development is annoying due to delays between pushing and being able to consume.
                 If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git
                 instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. -->
-        <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/>
+        <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview.202*"/>
         <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/>
     </ItemGroup>
 
+    <!-- 
     <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild">
         <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/>
     </Target>
+    -->
 
 </Project>
diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs
index e4addb6..b2308c5 100644
--- a/LibMatrix/Responses/SyncResponse.cs
+++ b/LibMatrix/Responses/SyncResponse.cs
@@ -39,7 +39,7 @@ public class SyncResponse {
     // supporting classes
     public class PresenceDataStructure {
         [JsonPropertyName("events")]
-        public List<StateEventResponse> Events { get; set; } = new();
+        public List<StateEventResponse>? Events { get; set; }
     }
 
     public class RoomsDataStructure {
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 349ccb5..8398ab9 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -106,7 +106,7 @@ public class GenericRoom {
                 Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync");
                 var sh = new SyncHelper(Homeserver);
                 var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]);
-                var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]);
+                var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []);
                 sh.Filter = new() {
                     Presence = emptyFilter,
                     AccountData = emptyFilter,
@@ -121,10 +121,11 @@ public class GenericRoom {
                 var sync = await sh.SyncAsync();
                 var state = sync.Rooms.Join[RoomId].State.Events;
                 var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey);
-                if (stateEvent is null) throw new LibMatrixException() {
-                    ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND,
-                    Error = "State event not found in sync response"
-                };
+                if (stateEvent is null)
+                    throw new LibMatrixException() {
+                        ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND,
+                        Error = "State event not found in sync response"
+                    };
                 return stateEvent.EventId;
             }
 
@@ -232,7 +233,7 @@ public class GenericRoom {
         // var sw = Stopwatch.StartNew();
         var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members");
         // if (sw.ElapsedMilliseconds > 1000)
-            // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         // var resText = await res.Content.ReadAsStringAsync();
         // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}");
@@ -240,7 +241,7 @@ public class GenericRoom {
             TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default
         });
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         foreach (var resp in result.Chunk) {
             if (resp?.Type != "m.room.member") continue;
@@ -249,14 +250,14 @@ public class GenericRoom {
         }
 
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
     }
 
     public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(bool joinedOnly = true) {
         // var sw = Stopwatch.StartNew();
         var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members");
         // if (sw.ElapsedMilliseconds > 1000)
-            // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         // var resText = await res.Content.ReadAsStringAsync();
         // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}");
@@ -264,7 +265,7 @@ public class GenericRoom {
             TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default
         });
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         var members = new List<StateEventResponse>();
         foreach (var resp in result.Chunk) {
@@ -274,7 +275,7 @@ public class GenericRoom {
         }
 
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
         return members.ToFrozenSet();
     }
 
@@ -320,7 +321,9 @@ public class GenericRoom {
     [Obsolete("This method will be merged into GetNameAsync() in the future.")]
     public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) {
         try {
-            return await GetNameAsync();
+            var name = await GetNameAsync();
+            if (!string.IsNullOrEmpty(name)) return name;
+            throw new();
         }
         catch {
             try {
@@ -374,9 +377,9 @@ public class GenericRoom {
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban",
             new UserIdAndReason { UserId = userId, Reason = reason });
 
-    public async Task UnbanAsync(string userId) =>
+    public async Task UnbanAsync(string userId, string? reason = null) =>
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
-            new UserIdAndReason { UserId = userId });
+            new UserIdAndReason { UserId = userId, Reason = reason });
 
     public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) {
         if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
@@ -393,7 +396,7 @@ public class GenericRoom {
             .Content.ReadFromJsonAsync<EventIdResponse>();
 
     public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) =>
-        await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content))
+        await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content))
             .Content.ReadFromJsonAsync<EventIdResponse>();
 
     public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, TimelineEventContent content) {
@@ -428,6 +431,16 @@ public class GenericRoom {
 
         return await res.Content.ReadFromJsonAsync<T>();
     }
+    
+    public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) {
+        try {
+            return await GetRoomAccountDataAsync<T>(key);
+        }
+        catch (MatrixException e) {
+            if (e.ErrorCode == "M_NOT_FOUND") return default;
+            throw;
+        }
+    }
 
     public async Task SetRoomAccountDataAsync(string key, object data) {
         var res = await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data);
@@ -440,10 +453,17 @@ public class GenericRoom {
     public Task<StateEventResponse> GetEventAsync(string eventId) =>
         Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}");
 
-    public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) {
+    public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) {
         var data = new { reason };
-        return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(
-            $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid().ToString()}";
+        while (true) {
+            try {
+                return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(url, data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
+            } catch (MatrixException e) {
+                if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw;
+                throw;
+            }
+        }
     }
 
 #endregion
@@ -510,7 +530,7 @@ public class GenericRoom {
 
         var uri = new Uri(path, UriKind.Relative);
         if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir);
-        else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir));
+        else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir));
         if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from);
         if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString());
         if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString());
diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
index b40ccc6..4563ed3 100644
--- a/LibMatrix/RoomTypes/SpaceRoom.cs
+++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -4,6 +4,8 @@ using LibMatrix.Homeservers;
 namespace LibMatrix.RoomTypes;
 
 public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) {
+    public const string TypeName = "m.space";
+
     public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) {
         // var rooms = new List<GenericRoom>();
         var state = GetFullStateAsync();
@@ -31,7 +33,7 @@ public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId)
         });
         return resp;
     }
-    
+
     public async Task<EventIdResponse> AddChildByIdAsync(string id) {
         return await AddChildAsync(Homeserver.GetRoom(id));
     }
diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
index 06ea9de..8b7e54b 100644
--- a/LibMatrix/Services/ServiceInstaller.cs
+++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -5,23 +5,13 @@ namespace LibMatrix.Services;
 
 public static class ServiceInstaller {
     public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) {
-        //Check required services
-        // if (!services.Any(x => x.ServiceType == typeof(TieredStorageService)))
-        // throw new Exception("[RMUCore/DI] No TieredStorageService has been registered!");
         //Add config
         services.AddSingleton(config ?? new RoryLibMatrixConfiguration());
 
         //Add services
         services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>()));
-
-        // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) {
         services.AddSingleton<HomeserverProviderService>();
-        // }
-        // else {
-        // services.AddScoped<HomeserverProviderService>();
-        // }
 
-        // services.AddScoped<MatrixHttpClient>();
         return services;
     }
 }
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 81ee3fe..cc870e4 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -13,7 +13,7 @@ using LibMatrix.Extensions;
 namespace LibMatrix;
 
 public class StateEvent {
-    public static FrozenSet<Type> KnownStateEventTypes { get; } = new ClassCollector<EventContent>().ResolveFromAllAccessibleAssemblies().ToFrozenSet();
+    public static FrozenSet<Type> KnownStateEventTypes { get; } = ClassCollector<EventContent>.ResolveFromAllAccessibleAssemblies().ToFrozenSet();
 
     public static FrozenDictionary<string, Type> KnownStateEventTypesByName { get; } = KnownStateEventTypes.Aggregate(
         new Dictionary<string, Type>(),
@@ -44,6 +44,7 @@ public class StateEvent {
     public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type;
 
     private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() {
+        // We need these, NumberHandling covers other number types that we don't want to convert
         Converters = {
             new JsonFloatStringConverter(),
             new JsonDoubleStringConverter(),
@@ -55,9 +56,6 @@ public class StateEvent {
     [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")]
     public EventContent? TypedContent {
         get {
-            // if (Type == "m.receipt") {
-            // return null;
-            // }
             try {
                 var mappedType = GetStateEventType(Type);
                 if (mappedType == typeof(UnknownEventContent))
@@ -81,6 +79,18 @@ public class StateEvent {
         }
     }
 
+    public T? ContentAs<T>() {
+        try {
+            return RawContent.Deserialize<T>(TypedContentSerializerOptions)!;
+        }
+        catch (JsonException e) {
+            Console.WriteLine(e);
+            Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null"));
+        }
+
+        return default;
+    }
+
     [JsonPropertyName("state_key")]
     public string? StateKey { get; set; }
 
@@ -156,7 +166,7 @@ public class StateEventResponse : StateEvent {
     public string? Sender { get; set; }
 
     [JsonPropertyName("unsigned")]
-    public UnsignedData? Unsigned { get; set; }
+    public JsonObject? Unsigned { get; set; }
 
     [JsonPropertyName("event_id")]
     public string? EventId { get; set; }
@@ -254,4 +264,23 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR
 }
 */
 
-#endregion
\ No newline at end of file
+#endregion
+
+/*
+public class ForgivingObjectConverter<T> : JsonConverter<T> where T : new() {
+    public override T? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) {
+        try {
+            var text = JsonDocument.ParseValue(ref reader).RootElement.GetRawText();
+            return JsonSerializer.Deserialize<T>(text, options);
+        }
+        catch (JsonException ex) {
+            Console.WriteLine(ex);
+            return null;
+        }
+    }
+
+    public override bool CanConvert(Type typeToConvert) => true;
+
+    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+        => JsonSerializer.Serialize<T>(writer, value, options);
+}*/
\ No newline at end of file