diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject c2e278bd7aa8d340cdd7738b92da0ee61de4950
+Subproject 825a60576cca6fc921c683036da568251da632d
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);
|