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/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/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index d0511ea..e037672 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>
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 349ccb5..a1ef617 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -374,9 +374,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)
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/StateEvent.cs b/LibMatrix/StateEvent.cs
index 81ee3fe..073d26d 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>(),
|