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;
|