about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/Extensions/CanonicalJsonSerializer.cs91
-rw-r--r--LibMatrix/Extensions/UnicodeJsonEncoder.cs173
-rw-r--r--LibMatrix/Helpers/HomeserverWeightEstimation.cs58
-rw-r--r--LibMatrix/LibMatrix.csproj1
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs4
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs4
-rw-r--r--LibMatrix/StateEvent.cs2
7 files changed, 271 insertions, 62 deletions
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>(),