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