about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--LibMatrix.Federation/AuthenticatedFederationClient.cs18
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs125
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs34
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs17
-rw-r--r--LibMatrix/Responses/CreateRoomRequest.cs6
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs48
6 files changed, 186 insertions, 62 deletions
diff --git a/LibMatrix.Federation/AuthenticatedFederationClient.cs b/LibMatrix.Federation/AuthenticatedFederationClient.cs

index 15dc88f..ee4bb25 100644 --- a/LibMatrix.Federation/AuthenticatedFederationClient.cs +++ b/LibMatrix.Federation/AuthenticatedFederationClient.cs
@@ -11,14 +11,14 @@ public class AuthenticatedFederationClient(string federationEndpoint, Authentica public required string OriginServerName { get; set; } } - public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { - var response = await HttpClient.SendAsync(new XMatrixAuthorizationScheme.XMatrixRequestSignature() { - OriginServerName = config.OriginServerName, - DestinationServerName = userId.Split(':', 2)[1], - Method = "GET", - Uri = $"/_matrix/federation/v1/user/devices/{userId}", - }.ToSignedHttpRequestMessage(config.PrivateKey)); - return response; - } + // public async Task<UserDeviceListResponse> GetUserDevicesAsync(string userId) { + // var response = await HttpClient.SendAsync(new XMatrixAuthorizationScheme.XMatrixRequestSignature() { + // OriginServerName = config.OriginServerName, + // DestinationServerName = userId.Split(':', 2)[1], + // Method = "GET", + // Uri = $"/_matrix/federation/v1/user/devices/{userId}", + // }.ToSignedHttpRequestMessage(config.PrivateKey)); + // return response; + // } } \ No newline at end of file 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/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index 71eed69..0db4e6f 100644 --- a/LibMatrix/Helpers/RoomBuilder.cs +++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -7,6 +7,7 @@ using LibMatrix.RoomTypes; namespace LibMatrix.Helpers; public class RoomBuilder { + private static readonly string[] V12PlusRoomVersions = ["org.matrix.hydra.11", "12"]; public string? Type { get; set; } public string Version { get; set; } = "11"; public RoomNameEventContent Name { get; set; } = new(); @@ -25,6 +26,14 @@ public class RoomBuilder { HistoryVisibility = RoomHistoryVisibilityEventContent.HistoryVisibilityTypes.Shared }; + public RoomGuestAccessEventContent GuestAccess { get; set; } = new() { + GuestAccess = "forbidden" + }; + + public RoomServerAclEventContent ServerAcls { get; set; } = new() { + AllowIpLiterals = false + }; + /// <summary> /// State events to be sent *before* room access is configured. Keep this small! /// </summary> @@ -57,14 +66,18 @@ public class RoomBuilder { { RoomCanonicalAliasEventContent.EventId, 50 }, { RoomEncryptionEventContent.EventId, 100 }, { RoomHistoryVisibilityEventContent.EventId, 100 }, + { RoomGuestAccessEventContent.EventId, 100 }, { RoomNameEventContent.EventId, 50 }, { RoomPowerLevelEventContent.EventId, 100 }, { RoomServerAclEventContent.EventId, 100 }, - { RoomTombstoneEventContent.EventId, 100 }, + { RoomTombstoneEventContent.EventId, 150 }, { RoomPolicyServerEventContent.EventId, 100 } } }; + public Dictionary<string, object> AdditionalCreationContent { get; set; } = new(); + public List<string> AdditionalCreators { get; set; } = new(); + public async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) { var crq = new CreateRoomRequest() { PowerLevelContentOverride = new() { @@ -78,20 +91,23 @@ public class RoomBuilder { NotificationsPl = new() { Room = 1000000 }, - Users = new Dictionary<string, long>() { - { homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger } - }, + Users = V12PlusRoomVersions.Contains(Version) + ? [] + : new() { + { homeserver.WhoAmI.UserId, MatrixConstants.MaxSafeJsonInteger } + }, Events = new Dictionary<string, long> { { RoomAvatarEventContent.EventId, 1000000 }, { RoomCanonicalAliasEventContent.EventId, 1000000 }, { RoomEncryptionEventContent.EventId, 1000000 }, { RoomHistoryVisibilityEventContent.EventId, 1000000 }, + { RoomGuestAccessEventContent.EventId, 1000000 }, { RoomNameEventContent.EventId, 1000000 }, { RoomPowerLevelEventContent.EventId, 1000000 }, { RoomServerAclEventContent.EventId, 1000000 }, { RoomTombstoneEventContent.EventId, 1000000 }, { RoomPolicyServerEventContent.EventId, 1000000 } - } + }, }, Visibility = "private", RoomVersion = Version @@ -103,6 +119,14 @@ public class RoomBuilder { if (!IsFederatable) crq.CreationContent.Add("m.federate", false); + if (V12PlusRoomVersions.Contains(Version) && AdditionalCreators is { Count: > 0 }) { + crq.CreationContent.Add("additional_creators", AdditionalCreators); + } + + foreach (var kvp in AdditionalCreationContent) { + crq.CreationContent.Add(kvp.Key, kvp.Value); + } + var room = await homeserver.CreateRoom(crq); await SetBasicRoomInfoAsync(room); 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/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
index 6933622..8f8af31 100644 --- a/LibMatrix/Responses/CreateRoomRequest.cs +++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -42,7 +42,7 @@ public class CreateRoomRequest { public RoomPowerLevelEventContent? PowerLevelContentOverride { get; set; } [JsonPropertyName("creation_content")] - public JsonObject CreationContent { get; set; } = new(); + public Dictionary<string, object> CreationContent { get; set; } = new(); [JsonPropertyName("invite")] public List<string>? Invite { get; set; } @@ -91,7 +91,7 @@ public class CreateRoomRequest { var request = new CreateRoomRequest { Name = name ?? "New public Room", Visibility = "public", - CreationContent = new JsonObject(), + CreationContent = new(), PowerLevelContentOverride = new RoomPowerLevelEventContent { EventsDefault = 0, UsersDefault = 0, @@ -131,7 +131,7 @@ public class CreateRoomRequest { var request = new CreateRoomRequest { Name = name ?? "New private Room", Visibility = "private", - CreationContent = new JsonObject(), + CreationContent = new(), PowerLevelContentOverride = new RoomPowerLevelEventContent { EventsDefault = 0, UsersDefault = 0, diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index c34dc01..565776c 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -393,7 +393,7 @@ public class GenericRoom { new UserIdAndReason { UserId = userId, Reason = reason }); public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) { - if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null) + if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not { Membership: "leave" or "ban" or "join" }) return; await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason)); } @@ -403,12 +403,12 @@ public class GenericRoom { #region Events public async Task<EventIdResponse?> SendStateEventAsync(string eventType, object content) => - await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content)) + await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.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> 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);