about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs125
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs17
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs44
3 files changed, 143 insertions, 43 deletions
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs

index 671566f..d14e481 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -4,13 +4,12 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Reflection; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; -using ArcaneLibs; using ArcaneLibs.Extensions; -using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; namespace LibMatrix.Extensions; @@ -169,31 +168,38 @@ public class MatrixHttpClient { }; // if (!content.StartsWith('{')) throw new InvalidDataException("Encountered invalid data:\n" + content); - if (!content.TrimStart().StartsWith('{')) { - responseMessage.EnsureSuccessStatusCode(); - throw new InvalidDataException("Encountered invalid data:\n" + content); + if (content.TrimStart().StartsWith('{')) { + //we have a matrix error, most likely + MatrixException? ex; + 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); + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); } - //we have a matrix error - MatrixException? ex; - 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) - }; + if (responseMessage.StatusCode == HttpStatusCode.BadGateway) { + // spread out retries + await Task.Delay(Random.Shared.Next(1000, 2000), cancellationToken); + request.ResetSendStatus(); + return await SendAsync(request, 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); - request.ResetSendStatus(); - return await SendAsync(request, cancellationToken); + responseMessage.EnsureSuccessStatusCode(); + throw new InvalidDataException("Encountered invalid data:\n" + content); } // GetAsync @@ -241,22 +247,16 @@ public class MatrixHttpClient { 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"); + request.Content = JsonContent.Create(value, value.GetType(), new MediaTypeHeaderValue("application/json", "utf-8"), options); 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; + options = GetJsonSerializerOptions(options); 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"); + request.Content = JsonContent.Create(value, value.GetType(), new MediaTypeHeaderValue("application/json", "utf-8"), options); return await SendAsync(request, cancellationToken); } @@ -297,5 +297,66 @@ public class MatrixHttpClient { }; return await SendAsync(request); } + + public async Task<HttpResponseMessage> PostAsyncEnumerableAsJsonAsync<T>(string url, IAsyncEnumerable<T> payload, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new AsyncEnumerableJsonContent<T>(payload, options); + return await SendAsync(request, cancellationToken); + } + + public async Task<HttpResponseMessage> PutAsyncEnumerableAsJsonAsync<T>(string url, IAsyncEnumerable<T> payload, JsonSerializerOptions? options = null, + CancellationToken cancellationToken = default) { + options = GetJsonSerializerOptions(options); + var request = new HttpRequestMessage(HttpMethod.Put, url); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Content = new AsyncEnumerableJsonContent<T>(payload, options); + return await SendAsync(request, cancellationToken); + } +} + +public class AsyncEnumerableJsonContent<T>(IAsyncEnumerable<T> payload, JsonSerializerOptions? options) : HttpContent { + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) { + await using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { + Indented = options?.WriteIndented ?? true, + Encoder = options?.Encoder, + IndentCharacter = options?.IndentCharacter ?? ' ', + IndentSize = options?.IndentSize ?? 4, + MaxDepth = options?.MaxDepth ?? 0, + NewLine = options?.NewLine ?? Environment.NewLine + }); + + writer.WriteStartArray(); + await writer.FlushAsync(); + await stream.FlushAsync(); + + await foreach (var item in payload) { + if (item is null) { + writer.WriteNullValue(); + } + else { + using var memoryStream = new MemoryStream(); + await JsonSerializer.SerializeAsync(memoryStream, item, item.GetType(), options); + + memoryStream.Position = 0; + var jsonBytes = memoryStream.ToArray(); + writer.WriteRawValue(jsonBytes, skipInputValidation: true); + } + + await writer.FlushAsync(); + await stream.FlushAsync(); + } + + writer.WriteEndArray(); + await writer.FlushAsync(); + await stream.FlushAsync(); + } + + protected override bool TryComputeLength(out long length) { + length = 0; + return false; + } } #endif \ No newline at end of file diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 4185353..e5095f1 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; +using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State.RoomInfo; @@ -409,15 +410,12 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { private Dictionary<string, string>? _namedFilterCache; private Dictionary<string, SyncFilter> _filterCache = new(); - public async Task<CapabilitiesResponse> GetCapabilitiesAsync() { - var res = await ClientHttpClient.GetAsync("/_matrix/client/v3/capabilities"); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}"); - throw new InvalidDataException($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}"); - } + private static readonly SemaphoreCache<CapabilitiesResponse> CapabilitiesCache = new(); - return await res.Content.ReadFromJsonAsync<CapabilitiesResponse>(); - } + public async Task<CapabilitiesResponse> GetCapabilitiesAsync() => + await CapabilitiesCache.GetOrAdd(ServerName, async () => + await ClientHttpClient.GetFromJsonAsync<CapabilitiesResponse>("/_matrix/client/v3/capabilities") + ); public class HsNamedCaches { internal HsNamedCaches(AuthenticatedHomeserverGeneric hs) { @@ -609,6 +607,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { [JsonPropertyName("m.set_displayname")] public BooleanCapability? SetDisplayName { get; set; } + [JsonPropertyName("gay.rory.bulk_send_events")] + public BooleanCapability? BulkSendEvents { get; set; } + [JsonExtensionData] public Dictionary<string, object>? AdditionalCapabilities { get; set; } } diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index c34dc01..2e50e69 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -406,9 +406,9 @@ public class GenericRoom { await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content)) .Content.ReadFromJsonAsync<EventIdResponse>(); - public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) => - await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content)) - .Content.ReadFromJsonAsync<EventIdResponse>(); + public async Task<EventIdResponse> SendStateEventAsync(string eventType, string stateKey, object content) => + (await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content)) + .Content.ReadFromJsonAsync<EventIdResponse>())!; public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, TimelineEventContent content) { var res = await Homeserver.ClientHttpClient.PutAsJsonAsync( @@ -418,6 +418,14 @@ public class GenericRoom { return await res.Content.ReadFromJsonAsync<EventIdResponse>() ?? throw new Exception("Failed to send event"); } + public async Task<EventIdResponse> SendRawTimelineEventAsync(string eventType, JsonObject content) { + var res = await Homeserver.ClientHttpClient.PutAsJsonAsync( + $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content, new JsonSerializerOptions { + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }); + return await res.Content.ReadFromJsonAsync<EventIdResponse>() ?? throw new Exception("Failed to send event"); + } + public async Task<EventIdResponse> SendReactionAsync(string eventId, string key) => await SendTimelineEventAsync("m.reaction", new RoomMessageReactionEventContent() { RelatesTo = new() { @@ -615,6 +623,36 @@ public class GenericRoom { } } + public async Task BulkSendEventsAsync(IEnumerable<StateEventResponse> events) { + if ((await Homeserver.GetCapabilitiesAsync()).Capabilities.BulkSendEvents?.Enabled == true) + await Homeserver.ClientHttpClient.PostAsJsonAsync( + $"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{RoomId}/bulk_send_events?_libmatrix_txn_id={Guid.NewGuid()}", events); + else { + Console.WriteLine("Homeserver does not support bulk sending events, falling back to individual sends."); + foreach (var evt in events) + await ( + evt.StateKey == null + ? SendRawTimelineEventAsync(evt.Type, evt.RawContent!) + : SendStateEventAsync(evt.Type, evt.StateKey, evt.RawContent) + ); + } + } + + public async Task BulkSendEventsAsync(IAsyncEnumerable<StateEventResponse> events) { + if ((await Homeserver.GetCapabilitiesAsync()).Capabilities.BulkSendEvents?.Enabled == true) + await Homeserver.ClientHttpClient.PostAsJsonAsync( + $"/_matrix/client/unstable/gay.rory.bulk_send_events/rooms/{RoomId}/bulk_send_events?_libmatrix_txn_id={Guid.NewGuid()}", events); + else { + Console.WriteLine("Homeserver does not support bulk sending events, falling back to individual sends."); + await foreach (var evt in events) + await ( + evt.StateKey == null + ? SendRawTimelineEventAsync(evt.Type, evt.RawContent!) + : SendStateEventAsync(evt.Type, evt.StateKey, evt.RawContent) + ); + } + } + public SpaceRoom AsSpace() => new SpaceRoom(Homeserver, RoomId); public PolicyRoom AsPolicyRoom() => new PolicyRoom(Homeserver, RoomId);