diff options
author | Emma [it/its]@Rory& <root@rory.gay> | 2024-05-30 21:39:12 +0200 |
---|---|---|
committer | Emma [it/its]@Rory& <root@rory.gay> | 2024-05-30 21:39:12 +0200 |
commit | 4bdea63982dae9c17b7a5fbda38d505655b8d4b3 (patch) | |
tree | 8ca9c6bad5f9526c5b36d707f08406fc3bbe2848 /LibMatrix | |
parent | Log warning if registering a duplicate type (diff) | |
download | LibMatrix-4bdea63982dae9c17b7a5fbda38d505655b8d4b3.tar.xz |
Diffstat (limited to 'LibMatrix')
-rw-r--r-- | LibMatrix/Events/BaseEvent.cs | 52 | ||||
-rw-r--r-- | LibMatrix/Extensions/HttpClientExtensions.cs | 248 | ||||
-rw-r--r-- | LibMatrix/Extensions/JsonConverters.cs | 29 | ||||
-rw-r--r-- | LibMatrix/Extensions/JsonElementExtensions.cs | 147 | ||||
-rw-r--r-- | LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs | 2 | ||||
-rw-r--r-- | LibMatrix/Homeservers/FederationClient.cs | 2 | ||||
-rw-r--r-- | LibMatrix/Homeservers/RemoteHomeServer.cs | 2 | ||||
-rw-r--r-- | LibMatrix/Interfaces/Services/IStorageProvider.cs | 56 | ||||
-rw-r--r-- | LibMatrix/LibMatrix.csproj | 6 | ||||
-rw-r--r-- | LibMatrix/RoomTypes/GenericRoom.cs | 47 | ||||
-rw-r--r-- | LibMatrix/RoomTypes/SpaceRoom.cs | 5 | ||||
-rw-r--r-- | LibMatrix/Services/HomeserverResolverService.cs | 2 | ||||
-rw-r--r-- | LibMatrix/Services/TieredStorageService.cs | 8 | ||||
-rw-r--r-- | LibMatrix/StateEvent.cs | 6 | ||||
-rw-r--r-- | LibMatrix/UserIdAndReason.cs | 2 |
15 files changed, 352 insertions, 262 deletions
diff --git a/LibMatrix/Events/BaseEvent.cs b/LibMatrix/Events/BaseEvent.cs new file mode 100644 index 0000000..2e27368 --- /dev/null +++ b/LibMatrix/Events/BaseEvent.cs @@ -0,0 +1,52 @@ +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace LibMatrix.Events; + +public class StateEvent { + [JsonPropertyName("state_key")] + public string? StateKey { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } +} + +public class StateEventResponse : StateEvent { + [JsonPropertyName("origin_server_ts")] + public long? OriginServerTs { get; set; } + + [JsonPropertyName("room_id")] + public string? RoomId { get; set; } + + [JsonPropertyName("sender")] + public string? Sender { get; set; } + + [JsonPropertyName("unsigned")] + public UnsignedData? Unsigned { get; set; } + + [JsonPropertyName("event_id")] + public string? EventId { get; set; } + + public class UnsignedData { + [JsonPropertyName("age")] + public ulong? Age { get; set; } + + [JsonPropertyName("redacted_because")] + public object? RedactedBecause { get; set; } + + [JsonPropertyName("transaction_id")] + public string? TransactionId { get; set; } + + [JsonPropertyName("replaces_state")] + public string? ReplacesState { get; set; } + + [JsonPropertyName("prev_sender")] + public string? PrevSender { get; set; } + + [JsonPropertyName("prev_content")] + public JsonObject? PrevContent { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs index 64b4f6a..f801e16 100644 --- a/LibMatrix/Extensions/HttpClientExtensions.cs +++ b/LibMatrix/Extensions/HttpClientExtensions.cs @@ -1,8 +1,10 @@ +#define SINGLE_HTTPCLIENT // Use a single HttpClient instance for all MatrixHttpClient instances +// #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Net.Http.Headers; using System.Reflection; +using System.Security.Cryptography.X509Certificates; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -25,7 +27,16 @@ public static class HttpClientExtensions { } } -public class MatrixHttpClient : HttpClient { +#region Per-instance HTTP client code + +#if !SINGLE_HTTPCLIENT +public class MatrixHttpClient() : HttpClient(handler) { + private static readonly SocketsHttpHandler handler = new() { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + MaxConnectionsPerServer = 256, + EnableMultipleHttp2Connections = true + }; + public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new(); internal string? AssertedUserId { get; set; } @@ -44,7 +55,7 @@ public class MatrixHttpClient : HttpClient { public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { if(debug) await _rateLimitSemaphore.WaitAsync(cancellationToken); - // 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.Content?.Headers.ContentLength ?? 0)})"); if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null"); if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri); // if (AssertedUserId is not null) request.RequestUri = request.RequestUri.AddQuery("user_id", AssertedUserId); @@ -73,6 +84,8 @@ public class MatrixHttpClient : HttpClient { finally { if(debug) _rateLimitSemaphore.Release(); } + + Console.WriteLine($"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})"); return responseMessage; } @@ -191,27 +204,220 @@ public class MatrixHttpClient : HttpClient { await foreach (var resp in result) yield return resp; } } +#endif -public class JsonFloatStringConverter : JsonConverter<float> { - public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => float.Parse(reader.GetString()!); +#endregion - public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} +#if SINGLE_HTTPCLIENT +public class MatrixHttpClient { + private static readonly SocketsHttpHandler handler; -public class JsonDoubleStringConverter : JsonConverter<double> { - public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => double.Parse(reader.GetString()!); + private static readonly HttpClient client; - public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} + static MatrixHttpClient() { + try { + handler = new SocketsHttpHandler { + PooledConnectionLifetime = TimeSpan.FromMinutes(15), + MaxConnectionsPerServer = 4096, + EnableMultipleHttp2Connections = true + }; + client = new HttpClient(handler) { + DefaultRequestVersion = new Version(3, 0) + }; + } + catch (PlatformNotSupportedException e) { + Console.WriteLine("Failed to create HttpClient with connection pooling, continuing without connection pool!"); + Console.WriteLine("Original exception (safe to ignore!):"); + Console.WriteLine(e); + + client = new HttpClient { + DefaultRequestVersion = new Version(3, 0) + }; + } + catch (Exception e) { + Console.WriteLine("Failed to create HttpClient:"); + Console.WriteLine(e); + throw; + } + } + +#if SYNC_HTTPCLIENT + internal SemaphoreSlim _rateLimitSemaphore { get; } = new(1, 1); +#endif + + public Dictionary<string, string> AdditionalQueryParameters { get; set; } = new(); + + public Uri? BaseAddress { get; set; } + + // default headers, not bound to client + public HttpRequestHeaders DefaultRequestHeaders { get; set; } = + typeof(HttpRequestHeaders).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[0], null)?.Invoke(new object[0]) as HttpRequestHeaders ?? + throw new InvalidOperationException("Failed to create HttpRequestHeaders"); + + private JsonSerializerOptions GetJsonSerializerOptions(JsonSerializerOptions? options = null) { + options ??= new JsonSerializerOptions(); + options.Converters.Add(new JsonFloatStringConverter()); + options.Converters.Add(new JsonDoubleStringConverter()); + options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + return options; + } + + public async Task<HttpResponseMessage> SendUnhandledAsync(HttpRequestMessage request, CancellationToken cancellationToken) { +#if SYNC_HTTPCLIENT + await _rateLimitSemaphore.WaitAsync(cancellationToken); +#endif + + Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})"); + + 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); + + request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true); -public class JsonDecimalStringConverter : JsonConverter<decimal> { - public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - => decimal.Parse(reader.GetString()!); + HttpResponseMessage? responseMessage; + try { + responseMessage = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + } + catch (Exception e) { + Console.WriteLine( + $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); + throw; + } +#if SYNC_HTTPCLIENT + finally { + _rateLimitSemaphore.Release(); + } +#endif + + Console.WriteLine( + $"Sending {request.Method} {request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}) -> {(int)responseMessage.StatusCode} {responseMessage.StatusCode} ({Util.BytesToString(responseMessage.Content.Headers.ContentLength ?? 0)})"); + + return responseMessage; + } + + public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { + var responseMessage = await SendUnhandledAsync(request, cancellationToken); + if (responseMessage.IsSuccessStatusCode) return responseMessage; - public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) - => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); -} \ No newline at end of file + //error handling + var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken); + if (content.Length == 0) + throw new MatrixException() { + ErrorCode = "M_UNKNOWN", + Error = "Unknown error, server returned no content" + }; + if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); + //we have a matrix error + + MatrixException? ex = null; + try { + ex = JsonSerializer.Deserialize<MatrixException>(content); + } + catch (JsonException e) { + throw new LibMatrixException() { + ErrorCode = "M_INVALID_JSON", + Error = e.Message + "\nBody:\n" + await responseMessage.Content.ReadAsStringAsync(cancellationToken) + }; + } + + Debug.Assert(ex != null, nameof(ex) + " != null"); + ex.RawContent = content; + // Console.WriteLine($"Failed to send request: {ex}"); + if (ex?.RetryAfterMs is null) throw ex!; + //we have a ratelimit error + await Task.Delay(ex.RetryAfterMs.Value, cancellationToken); + typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance) + ?.SetValue(request, 0); + return await SendAsync(request, cancellationToken); + } + + // GetAsync + public Task<HttpResponseMessage> GetAsync([StringSyntax("Uri")] string? requestUri, CancellationToken? cancellationToken = null) => + SendAsync(new HttpRequestMessage(HttpMethod.Get, requestUri), cancellationToken ?? CancellationToken.None); + + // GetFromJsonAsync + public async Task<T?> TryGetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + try { + return await GetFromJsonAsync<T>(requestUri, options, cancellationToken); + } + catch (HttpRequestException e) { + Console.WriteLine($"Failed to get {requestUri}: {e.Message}"); + return default; + } + } + + public async Task<T> GetFromJsonAsync<T>(string requestUri, JsonSerializerOptions? options = null, CancellationToken cancellationToken = default) { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken); + + return await JsonSerializer.DeserializeAsync<T>(responseStream, options, cancellationToken) ?? + throw new InvalidOperationException("Failed to deserialize response"); + } + + // GetStreamAsync + public new async Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Get, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + var response = await SendAsync(request, cancellationToken); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStreamAsync(cancellationToken); + } + + public async Task<HttpResponseMessage> PutAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Put, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async Task<HttpResponseMessage> PostAsJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, T value, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) where T : notnull { + options ??= new JsonSerializerOptions(); + options.Converters.Add(new JsonFloatStringConverter()); + options.Converters.Add(new JsonDoubleStringConverter()); + options.Converters.Add(new JsonDecimalStringConverter()); + options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + var request = new HttpRequestMessage(HttpMethod.Post, requestUri); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new StringContent(JsonSerializer.Serialize(value, value.GetType(), options), + Encoding.UTF8, "application/json"); + return await SendAsync(request, cancellationToken); + } + + public async IAsyncEnumerable<T?> GetAsyncEnumerableFromJsonAsync<T>([StringSyntax(StringSyntaxAttribute.Uri)] string? requestUri, JsonSerializerOptions? options = null) { + options = GetJsonSerializerOptions(options); + var res = await GetAsync(requestUri); + var result = JsonSerializer.DeserializeAsyncEnumerable<T>(await res.Content.ReadAsStreamAsync(), options); + await foreach (var resp in result) yield return resp; + } + + public async Task<bool> CheckSuccessStatus(string url) { + //cors causes failure, try to catch + try { + var resp = await client.GetAsync(url); + return resp.IsSuccessStatusCode; + } + catch (Exception e) { + Console.WriteLine($"Failed to check success status: {e.Message}"); + return false; + } + } + + public async Task<HttpResponseMessage> PostAsync(string uri, HttpContent? content, CancellationToken cancellationToken = default) { + var request = new HttpRequestMessage(HttpMethod.Post, uri) { + Content = content + }; + return await SendAsync(request, cancellationToken); + } +} +#endif \ No newline at end of file diff --git a/LibMatrix/Extensions/JsonConverters.cs b/LibMatrix/Extensions/JsonConverters.cs new file mode 100644 index 0000000..eed3fb2 --- /dev/null +++ b/LibMatrix/Extensions/JsonConverters.cs @@ -0,0 +1,29 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace LibMatrix.Extensions; + +public class JsonFloatStringConverter : JsonConverter<float> { + public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => float.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} + +public class JsonDoubleStringConverter : JsonConverter<double> { + public override double Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => double.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, double value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} + +public class JsonDecimalStringConverter : JsonConverter<decimal> { + public override decimal Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => decimal.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, decimal value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} \ No newline at end of file diff --git a/LibMatrix/Extensions/JsonElementExtensions.cs b/LibMatrix/Extensions/JsonElementExtensions.cs deleted file mode 100644 index c4ed743..0000000 --- a/LibMatrix/Extensions/JsonElementExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -using System.Reflection; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; - -namespace LibMatrix.Extensions; - -public static class JsonElementExtensions { - public static bool FindExtraJsonElementFields(this JsonElement obj, Type objectType, string objectPropertyName) { - if (objectPropertyName == "content" && objectType == typeof(JsonObject)) - objectType = typeof(StateEventResponse); - // if (t == typeof(JsonNode)) - // return false; - - Console.WriteLine($"{objectType.Name} {objectPropertyName}"); - var unknownPropertyFound = false; - var mappedPropsDict = objectType.GetProperties() - .Where(x => x.GetCustomAttribute<JsonPropertyNameAttribute>() is not null) - .ToDictionary(x => x.GetCustomAttribute<JsonPropertyNameAttribute>()!.Name, x => x); - objectType.GetProperties().Where(x => !mappedPropsDict.ContainsKey(x.Name)) - .ToList().ForEach(x => mappedPropsDict.TryAdd(x.Name, x)); - - foreach (var field in obj.EnumerateObject()) { - if (mappedPropsDict.TryGetValue(field.Name, out var mappedProperty)) { - //dictionary - if (mappedProperty.PropertyType.IsGenericType && - mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { - unknownPropertyFound |= _checkDictionary(field, objectType, mappedProperty.PropertyType); - continue; - } - - if (mappedProperty.PropertyType.IsGenericType && - mappedProperty.PropertyType.GetGenericTypeDefinition() == typeof(List<>)) { - unknownPropertyFound |= _checkList(field, objectType, mappedProperty.PropertyType); - continue; - } - - if (field.Name == "content" && (objectType == typeof(StateEventResponse) || objectType == typeof(StateEvent))) { - unknownPropertyFound |= field.FindExtraJsonPropertyFieldsByValueKind( - StateEvent.GetStateEventType(obj.GetProperty("type").GetString()!), // We expect type to always be present - mappedProperty.PropertyType); - continue; - } - - unknownPropertyFound |= - field.FindExtraJsonPropertyFieldsByValueKind(objectType, mappedProperty.PropertyType); - continue; - } - - Console.WriteLine($"[!!] Unknown property {field.Name} in {objectType.Name}!"); - unknownPropertyFound = true; - } - - return unknownPropertyFound; - } - - private static bool FindExtraJsonPropertyFieldsByValueKind(this JsonProperty field, Type containerType, - Type propertyType) { - if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) propertyType = propertyType.GetGenericArguments()[0]; - - var switchResult = false; - switch (field.Value.ValueKind) { - case JsonValueKind.Array: - switchResult = field.Value.EnumerateArray().Aggregate(switchResult, - (current, element) => current | element.FindExtraJsonElementFields(propertyType, field.Name)); - break; - case JsonValueKind.Object: - switchResult |= field.Value.FindExtraJsonElementFields(propertyType, field.Name); - break; - case JsonValueKind.True: - case JsonValueKind.False: - return _checkBool(field, containerType, propertyType); - case JsonValueKind.String: - return _checkString(field, containerType, propertyType); - case JsonValueKind.Number: - return _checkNumber(field, containerType, propertyType); - case JsonValueKind.Undefined: - case JsonValueKind.Null: - break; - default: - throw new ArgumentOutOfRangeException(); - } - - return switchResult; - } - - private static bool _checkBool(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(bool)) return true; - Console.WriteLine( - $"[!!] Encountered bool for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkString(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(string)) return true; - // ReSharper disable once BuiltInTypeReferenceStyle - if (propertyType == typeof(String)) return true; - Console.WriteLine( - $"[!!] Encountered string for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkNumber(this JsonProperty field, Type containerType, Type propertyType) { - if (propertyType == typeof(int) || - propertyType == typeof(double) || - propertyType == typeof(float) || - propertyType == typeof(decimal) || - propertyType == typeof(long) || - propertyType == typeof(short) || - propertyType == typeof(uint) || - propertyType == typeof(ulong) || - propertyType == typeof(ushort) || - propertyType == typeof(byte) || - propertyType == typeof(sbyte)) - return true; - Console.WriteLine( - $"[!!] Encountered number for {field.Name} in {containerType.Name}, the class defines {propertyType.Name}!"); - return false; - } - - private static bool _checkDictionary(this JsonProperty field, Type containerType, Type propertyType) { - var keyType = propertyType.GetGenericArguments()[0]; - var valueType = propertyType.GetGenericArguments()[1]; - valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; - Console.WriteLine( - $"Encountered dictionary {field.Name} with key type {keyType.Name} and value type {valueType.Name}!"); - - return field.Value.EnumerateObject() - .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) - .Aggregate(false, (current, key) => - current | key.FindExtraJsonPropertyFieldsByValueKind(containerType, valueType) - ); - } - - private static bool _checkList(this JsonProperty field, Type containerType, Type propertyType) { - var valueType = propertyType.GetGenericArguments()[0]; - valueType = Nullable.GetUnderlyingType(valueType) ?? valueType; - Console.WriteLine( - $"Encountered list {field.Name} with value type {valueType.Name}!"); - - return field.Value.EnumerateArray() - .Where(key => !valueType.IsPrimitive && valueType != typeof(string)) - .Aggregate(false, (current, key) => - current | key.FindExtraJsonElementFields(valueType, field.Name) - ); - } -} \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs index c729a44..891cc00 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs @@ -117,6 +117,8 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { } public virtual async Task Logout() { + // var res = await ClientHttpClient.PostAsync("/_matrix/client/v3/logout", null); + // TODO: investigate var res = await ClientHttpClient.PostAsync("/_matrix/client/v3/logout", null); if (!res.IsSuccessStatusCode) { Console.WriteLine($"Failed to logout: {await res.Content.ReadAsStringAsync()}"); diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs index dc0d1f6..b7f39eb 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs @@ -9,7 +9,7 @@ public class FederationClient { public FederationClient(string federationEndpoint, string? proxy = null) { HttpClient = new MatrixHttpClient { BaseAddress = new Uri(proxy?.TrimEnd('/') ?? federationEndpoint.TrimEnd('/')), - Timeout = TimeSpan.FromSeconds(120) + // Timeout = TimeSpan.FromSeconds(120) // TODO: investigate need }; if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint); } diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs index 8669ca7..680ade7 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs @@ -19,7 +19,7 @@ public class RemoteHomeserver { WellKnownUris = wellKnownUris; ClientHttpClient = new MatrixHttpClient { BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")), - Timeout = TimeSpan.FromSeconds(300) + // Timeout = TimeSpan.FromSeconds(300) // TODO: investigate need }; if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl); diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs deleted file mode 100644 index 165e7df..0000000 --- a/LibMatrix/Interfaces/Services/IStorageProvider.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace LibMatrix.Interfaces.Services; - -public interface IStorageProvider { - // save all children of a type with reflection - public Task SaveAllChildrenAsync<T>(string key, T value) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren<T>(key, value)!"); - throw new NotImplementedException(); - } - - // load all children of a type with reflection - public Task<T?> LoadAllChildrenAsync<T>(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren<T>(key)!"); - throw new NotImplementedException(); - } - - public Task SaveObjectAsync<T>(string key, T value) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!"); - throw new NotImplementedException(); - } - - // load - public Task<T?> LoadObjectAsync<T>(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject<T>(key)!"); - throw new NotImplementedException(); - } - - // check if exists - public Task<bool> ObjectExistsAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!"); - throw new NotImplementedException(); - } - - // get all keys - public Task<List<string>> GetAllKeysAsync() { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!"); - throw new NotImplementedException(); - } - - // delete - public Task DeleteObjectAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!"); - throw new NotImplementedException(); - } - - // save stream - public Task SaveStreamAsync(string key, Stream stream) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveStream(key, stream)!"); - throw new NotImplementedException(); - } - - // load stream - public Task<Stream?> LoadStreamAsync(string key) { - Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!"); - throw new NotImplementedException(); - } -} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index b85df52..131eea8 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -8,11 +8,13 @@ <Optimize>true</Optimize> <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> + + <IsAotCompatible>true</IsAotCompatible> </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0"/> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0"/> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" /> </ItemGroup> <ItemGroup> diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index f15327c..77bff71 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; } @@ -231,7 +232,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()}"); @@ -239,7 +240,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; @@ -248,14 +249,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()}"); @@ -263,7 +264,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) { @@ -273,7 +274,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(); } @@ -282,10 +283,10 @@ public class GenericRoom { public Task<EventIdResponse> SendMessageEventAsync(RoomMessageEventContent content) => SendTimelineEventAsync("m.room.message", content); - public async Task<List<string>?> GetAliasesAsync() { - var res = await GetStateAsync<RoomAliasEventContent>("m.room.aliases"); - return res.Aliases; - } + // public async Task<List<string>?> GetAliasesAsync() { + // var res = await GetStateAsync<RoomAliasEventContent>(RoomAliasEventContent.EventId); + // return res.Aliases; + // } public Task<RoomCanonicalAliasEventContent?> GetCanonicalAliasAsync() => GetStateAsync<RoomCanonicalAliasEventContent>("m.room.canonical_alias"); @@ -382,18 +383,18 @@ public class GenericRoom { public async Task KickAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick", - new UserIdAndReason { UserId = userId, Reason = reason }); + new UserIdAndReason(userId, reason)); public async Task BanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban", - new UserIdAndReason { UserId = userId, Reason = reason }); + new UserIdAndReason(userId, reason)); - public async Task UnbanAsync(string userId) => + public async Task<HttpResponseMessage> UnbanAsync(string userId, string? reason = null) => await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban", - new UserIdAndReason { UserId = userId }); + new UserIdAndReason(userId, reason)); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { - if (skipExisting && await GetStateAsync<RoomMemberEventContent>("m.room.member", userId) is not null) + if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null) return; await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason)); } @@ -524,21 +525,21 @@ 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()); if (!string.IsNullOrEmpty(to)) uri = uri.AddQuery("to", to); // Console.WriteLine($"Getting related events from {uri}"); - var result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri); + var result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.ToString()); //TODO: investigate ToString call while (result!.Chunk.Count > 0) { foreach (var resp in result.Chunk) { yield return resp; } if (result.NextBatch is null) break; - result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.AddQuery("from", result.NextBatch)); + result = await Homeserver.ClientHttpClient.GetFromJsonAsync<RecursedBatchedChunkedStateEventResponse>(uri.AddQuery("from", result.NextBatch).ToString()); //TODO: investigate ToString call } } diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs index b40ccc6..45069d9 100644 --- a/LibMatrix/RoomTypes/SpaceRoom.cs +++ b/LibMatrix/RoomTypes/SpaceRoom.cs @@ -1,9 +1,12 @@ +using System.Text.Json.Nodes; using ArcaneLibs.Extensions; 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 +34,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/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index 05ce733..27ad594 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -14,7 +14,7 @@ namespace LibMatrix.Services; public class HomeserverResolverService { private readonly MatrixHttpClient _httpClient = new() { - Timeout = TimeSpan.FromSeconds(60) + // Timeout = TimeSpan.FromSeconds(60) //TODO: investigate need }; private static readonly SemaphoreCache<WellKnownUris> WellKnownCache = new(); diff --git a/LibMatrix/Services/TieredStorageService.cs b/LibMatrix/Services/TieredStorageService.cs deleted file mode 100644 index 9e411de..0000000 --- a/LibMatrix/Services/TieredStorageService.cs +++ /dev/null @@ -1,8 +0,0 @@ -using LibMatrix.Interfaces.Services; - -namespace LibMatrix.Services; - -public class TieredStorageService(IStorageProvider? cacheStorageProvider, IStorageProvider? dataStorageProvider) { - public IStorageProvider? CacheStorageProvider { get; } = cacheStorageProvider; - public IStorageProvider? DataStorageProvider { get; } = dataStorageProvider; -} \ No newline at end of file diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs index 81ee3fe..3bd0672 100644 --- a/LibMatrix/StateEvent.cs +++ b/LibMatrix/StateEvent.cs @@ -1,6 +1,7 @@ using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; @@ -54,6 +55,8 @@ public class StateEvent { [JsonIgnore] [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] public EventContent? TypedContent { + [RequiresDynamicCode("TypedContent requires reflection to deserialize the content of the event.")] + [RequiresUnreferencedCode("TypedContent requires reflection to deserialize the content of the event.")] get { // if (Type == "m.receipt") { // return null; @@ -72,6 +75,9 @@ public class StateEvent { return null; } + + [RequiresDynamicCode("TypedContent requires reflection to deserialize the content of the event.")] + [RequiresUnreferencedCode("TypedContent requires reflection to deserialize the content of the event.")] set { if (value is null) RawContent?.Clear(); diff --git a/LibMatrix/UserIdAndReason.cs b/LibMatrix/UserIdAndReason.cs index 99c9eaf..5e52303 100644 --- a/LibMatrix/UserIdAndReason.cs +++ b/LibMatrix/UserIdAndReason.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; namespace LibMatrix; -internal class UserIdAndReason(string userId = null!, string reason = null!) { +internal class UserIdAndReason(string userId = null!, string? reason = null) { [JsonPropertyName("user_id")] public string UserId { get; set; } = userId; |