diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject 270012a7527345a914003c98bf97c7b221812cc
+Subproject 66fa066a90fb1a836f08cd1f62591ed537c0da5
diff --git a/LibMatrix.EventTypes/Interop/Draupnir/DraupnirWatchedListsData.cs b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirWatchedListsData.cs
new file mode 100644
index 0000000..bf5f148
--- /dev/null
+++ b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirWatchedListsData.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+using System.Web;
+
+namespace LibMatrix.EventTypes.Interop.Draupnir;
+
+[MatrixEvent(EventName = EventId)]
+public class DraupnirWatchedListsData : EventContent {
+ public const string EventId = "org.matrix.mjolnir.watched_lists";
+
+ [JsonPropertyName("references")]
+ public List<string> References { get; set; }
+
+ public List<(string RoomId, List<string>? Vias)> GetReferenceRooms() {
+ List<(string RoomId, List<string>? Vias)> results = [];
+ foreach (var reference in References) {
+ var id = HttpUtility.UrlDecode(reference.Split("/#/")[1].Split("?via")[0]);
+ var vias =
+ reference.Contains('?')
+ ? reference.Split('?')[1].Split('&').Select(x => HttpUtility.UrlDecode(x.Replace("via=", "")))
+ : id.Contains(':')
+ ? [id.Split(':')[1]]
+ : null;
+ results.Add((id, vias?.ToList()));
+ }
+
+ return results;
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix.EventTypes/Interop/Draupnir/DraupnirZtdManagementRoomData.cs b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirZtdManagementRoomData.cs
new file mode 100644
index 0000000..33f44a5
--- /dev/null
+++ b/LibMatrix.EventTypes/Interop/Draupnir/DraupnirZtdManagementRoomData.cs
@@ -0,0 +1,19 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Text.Encodings.Web;
+using System.Text.Json.Serialization;
+using System.Web;
+
+namespace LibMatrix.EventTypes.Interop.Draupnir;
+
+[MatrixEvent(EventName = EventId)]
+public class DraupnirZtdManagementRoomData : EventContent {
+ public const string EventId = "space.draupnir.zero_touch_deploy_room";
+
+ [JsonPropertyName("room")]
+ public string? Room { get; set; }
+
+ public string? GetPlainRoomId() {
+ var val = Room?.Split("/#/")[1].Split("?via")[0];
+ return HttpUtility.UrlDecode(val);
+ }
+}
\ No newline at end of file
diff --git a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj
index e633aef..25c9f64 100644
--- a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj
+++ b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj
@@ -15,7 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/>
- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.2025*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/>
+ <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.2026*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/>
</ItemGroup>
</Project>
diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
index d1cf8be..ccb5d42 100644
--- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -11,6 +11,9 @@ public class RoomMessageEventContent : TimelineEventContent {
Body = body ?? "";
}
+ // TODO: https://spec.matrix.org/v1.16/client-server-api/#mimage
+ // TODO: add `file` for e2ee files
+
[JsonPropertyName("body")]
public string Body { get; set; }
@@ -53,7 +56,7 @@ public class RoomMessageEventContent : TimelineEventContent {
public class MentionsStruct {
[JsonPropertyName("user_ids")]
public List<string>? Users { get; set; }
-
+
[JsonPropertyName("room")]
public bool? Room { get; set; }
}
@@ -68,10 +71,33 @@ public class RoomMessageEventContent : TimelineEventContent {
[JsonPropertyName("thumbnail_url")]
public string? ThumbnailUrl { get; set; }
+ [JsonPropertyName("thumbnail_info")]
+ public ThumbnailInfoStruct? ThumbnailInfo { get; set; }
+
[JsonPropertyName("w")]
public int? Width { get; set; }
[JsonPropertyName("h")]
public int? Height { get; set; }
+
+ /// <summary>
+ /// Duration of the audio/video in milliseconds, if applicable
+ /// </summary>
+ [JsonPropertyName("duration")]
+ public long? Duration { get; set; }
+
+ public class ThumbnailInfoStruct {
+ [JsonPropertyName("w")]
+ public int? Width { get; set; }
+
+ [JsonPropertyName("h")]
+ public int? Height { get; set; }
+
+ [JsonPropertyName("mimetype")]
+ public string? MimeType { get; set; }
+
+ [JsonPropertyName("size")]
+ public long? Size { get; set; }
+ }
}
}
\ No newline at end of file
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
index 80e254f..78fdc8e 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs
@@ -8,4 +8,7 @@ public class RoomPolicyServerEventContent : EventContent {
[JsonPropertyName("via")]
public string? Via { get; set; }
+
+ [JsonPropertyName("public_key")]
+ public string? PublicKey { get; set; }
}
\ No newline at end of file
diff --git a/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs
new file mode 100644
index 0000000..0fe72bd
--- /dev/null
+++ b/LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Federation.FederationTypes;
+
+public class FederationBackfillResponse {
+ [JsonPropertyName("origin")]
+ public required string Origin { get; set; }
+
+ [JsonPropertyName("origin_server_ts")]
+ public required long OriginServerTs { get; set; }
+
+ [JsonPropertyName("pdus")]
+ public required List<SignedFederationEvent> Pdus { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/FederationTypes/FederationEvent.cs b/LibMatrix.Federation/FederationTypes/FederationEvent.cs
new file mode 100644
index 0000000..05bdcc9
--- /dev/null
+++ b/LibMatrix.Federation/FederationTypes/FederationEvent.cs
@@ -0,0 +1,30 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Federation.FederationTypes;
+
+public class FederationEvent : MatrixEventResponse {
+ [JsonPropertyName("auth_events")]
+ public required List<string> AuthEvents { get; set; } = [];
+
+ [JsonPropertyName("prev_events")]
+ public required List<string> PrevEvents { get; set; } = [];
+
+ [JsonPropertyName("depth")]
+ public required int Depth { get; set; }
+}
+
+public class SignedFederationEvent : FederationEvent {
+ [JsonPropertyName("signatures")]
+ public required Dictionary<string, Dictionary<string, string>> Signatures { get; set; } = new();
+
+ [JsonPropertyName("hashes")]
+ public required Dictionary<string, string> Hashes { get; set; } = new();
+}
+
+public class FederationEphemeralEvent {
+ [JsonPropertyName("edu_type")]
+ public required string Type { get; set; }
+
+ [JsonPropertyName("content")]
+ public required Dictionary<string, object> Content { get; set; } = new();
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs
new file mode 100644
index 0000000..f43dd49
--- /dev/null
+++ b/LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs
@@ -0,0 +1,34 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Federation.FederationTypes;
+
+public class FederationGetMissingEventsRequest {
+ /// <summary>
+ /// Latest event IDs we already have (aka earliest to return)
+ /// </summary>
+ [JsonPropertyName("earliest_events")]
+ public required List<string> EarliestEvents { get; set; }
+
+ /// <summary>
+ /// Events we want to get events before
+ /// </summary>
+ [JsonPropertyName("latest_events")]
+ public required List<string> LatestEvents { get; set; }
+
+ /// <summary>
+ /// 10 by default
+ /// </summary>
+ [JsonPropertyName("limit")]
+ public int Limit { get; set; }
+
+ /// <summary>
+ /// 0 by default
+ /// </summary>
+ [JsonPropertyName("min_depth")]
+ public long MinDepth { get; set; }
+}
+
+public class FederationGetMissingEventsResponse {
+ [JsonPropertyName("events")]
+ public required List<SignedFederationEvent> Events { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/FederationTypes/FederationTransaction.cs b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs
new file mode 100644
index 0000000..0581a08
--- /dev/null
+++ b/LibMatrix.Federation/FederationTypes/FederationTransaction.cs
@@ -0,0 +1,26 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Federation.FederationTypes;
+
+/// <summary>
+/// This only covers v12 rooms for now?
+/// </summary>
+public class FederationTransaction {
+ /// <summary>
+ /// Up to 100 EDUs per transaction
+ /// </summary>
+ [JsonPropertyName("edus")]
+ public List<FederationEvent>? EphemeralEvents { get; set; }
+
+ [JsonPropertyName("origin")]
+ public required string Origin { get; set; }
+
+ [JsonPropertyName("origin_server_ts")]
+ public required long OriginServerTs { get; set; }
+
+ /// <summary>
+ /// Up to 50 PDUs per transaction
+ /// </summary>
+ [JsonPropertyName("pdus")]
+ public List<SignedFederationEvent>? PersistentEvents { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/FederationTypes/RoomInvite.cs b/LibMatrix.Federation/FederationTypes/RoomInvite.cs
new file mode 100644
index 0000000..dc550f3
--- /dev/null
+++ b/LibMatrix.Federation/FederationTypes/RoomInvite.cs
@@ -0,0 +1,14 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Federation.FederationTypes;
+
+public class RoomInvite {
+ [JsonPropertyName("event")]
+ public required SignedFederationEvent Event { get; set; }
+
+ [JsonPropertyName("invite_room_state")]
+ public required List<MatrixEventResponse> InviteRoomState { get; set; } = [];
+
+ [JsonPropertyName("room_version")]
+ public required string RoomVersion { get; set; }
+}
\ No newline at end of file
diff --git a/LibMatrix.Federation/LibMatrix.Federation.csproj b/LibMatrix.Federation/LibMatrix.Federation.csproj
index 2a9a0d8..3ba02ae 100644
--- a/LibMatrix.Federation/LibMatrix.Federation.csproj
+++ b/LibMatrix.Federation/LibMatrix.Federation.csproj
@@ -21,7 +21,7 @@
<ItemGroup>
<PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2"/>
- <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.Primitives" Version="10.0.9" />
</ItemGroup>
</Project>
diff --git a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
index 392cd93..c6be906 100644
--- a/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
+++ b/LibMatrix.Federation/XMatrixAuthorizationScheme.cs
@@ -3,6 +3,7 @@ using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using ArcaneLibs.Extensions;
using LibMatrix.Abstractions;
+using LibMatrix.Extensions;
using LibMatrix.Responses.Federation;
using Microsoft.Extensions.Primitives;
@@ -37,17 +38,27 @@ public class XMatrixAuthorizationScheme {
ErrorCode = MatrixException.ErrorCodes.M_UNAUTHORIZED
};
- var headerValues = new StringValues(header.Parameter);
- foreach (var value in headerValues) {
- Console.WriteLine(headerValues.ToJson());
+ var headerValues = new Dictionary<string, string>();
+ var parts = header.Parameter.Split(',');
+ foreach (var part in parts) {
+ var kv = part.Split('=', 2);
+ if (kv.Length != 2)
+ continue;
+ var key = kv[0].Trim();
+ var value = kv[1].Trim().Trim('"');
+ headerValues[key] = value;
}
- return new() {
- Destination = "",
- Key = "",
- Origin = "",
- Signature = ""
+ Console.WriteLine("X-Matrix parts: " + headerValues.ToJson(unsafeContent: true));
+
+ var xma = new XMatrixAuthorizationHeader() {
+ Destination = headerValues["destination"],
+ Key = headerValues["key"],
+ Origin = headerValues["origin"],
+ Signature = headerValues["sig"]
};
+ Console.WriteLine("Parsed X-Matrix Auth Header: " + xma.ToJson());
+ return xma;
}
public static XMatrixAuthorizationHeader FromSignedObject(SignedObject<XMatrixRequestSignature> signedObj, VersionedHomeserverPrivateKey currentKey) =>
@@ -74,7 +85,7 @@ public class XMatrixAuthorizationScheme {
[JsonPropertyName("destination")]
public required string DestinationServerName { get; set; }
- [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ [JsonPropertyName("content"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public JsonObject? Content { get; set; }
}
}
\ No newline at end of file
diff --git a/LibMatrix.Federation/deps.json b/LibMatrix.Federation/deps.json
index 6c90cd1..4ca83b7 100644
--- a/LibMatrix.Federation/deps.json
+++ b/LibMatrix.Federation/deps.json
@@ -1,22 +1,27 @@
[
{
+ "pname": "ArcaneLibs",
+ "version": "1.0.1-preview.20260619-035441",
+ "hash": "sha256-jNEGd8Ccgkk4A8ZMaJO4QDExmsf9q7TeuhiNESos3Q4="
+ },
+ {
"pname": "BouncyCastle.Cryptography",
"version": "2.6.2",
"hash": "sha256-Yjk2+x/RcVeccGOQOQcRKCiYzyx1mlFnhS5auCII+Ms="
},
{
"pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps="
+ "version": "10.0.9",
+ "hash": "sha256-YzQpGAsrjLU18s016LmM7DMJKml0MzKl1bPPYJ/erEk="
},
{
"pname": "Microsoft.Extensions.Logging.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk="
+ "version": "10.0.9",
+ "hash": "sha256-su2q/OZuG7nB3wnUTVcimy3oHSdetFh4hhmjI3f/ym8="
},
{
"pname": "Microsoft.Extensions.Primitives",
- "version": "10.0.0",
- "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I="
+ "version": "10.0.9",
+ "hash": "sha256-WEPtmlEexm6QSFR4ITlBHuUD5/bqMfecOxFe5hkbAPU="
}
]
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index cd82071..ae18b2d 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -1,5 +1,6 @@
#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.Collections.Concurrent;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
@@ -70,7 +71,7 @@ public class MatrixHttpClient {
public int MaxRetryIntervalMs { get; set; } = DefaultMaxRetryIntervalMs;
public int MaxRetries { get; set; } = DefaultMaxRetries;
- private Dictionary<HttpRequestMessage, int> _retries = [];
+ private readonly ConcurrentDictionary<HttpRequestMessage, int> _retries = [];
// default headers, not bound to client
public HttpRequestHeaders DefaultRequestHeaders { get; set; } =
@@ -224,7 +225,12 @@ public class MatrixHttpClient {
}
if (responseMessage.IsSuccessStatusCode) {
- _retries.Remove(request);
+ while (!_retries.TryRemove(request, out _)) {
+ Console.WriteLine("[MatrixHttpClient] Race - failed to remove retries entry, retrying...");
+ // ReSharper disable once MethodSupportsCancellation - this shouldn't be cancellable as it would be a memory leak
+ await Task.Delay(5); // hopefully helps resolve contention?
+ }
+
return responseMessage;
}
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index f753bf7..c6f8ea1 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -96,7 +96,7 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
}
public MessageBuilder WithMention(string id, string? displayName = null, string[]? vias = null, bool useIdInPlainText = false, bool useLinkInPlainText = false) {
- if (!useLinkInPlainText) Content.Body += $"@{(useIdInPlainText ? id : displayName ?? id)}";
+ if (!useLinkInPlainText) Content.Body += $"{(useIdInPlainText ? id : displayName ?? id)}";
else {
Content.Body += $"https://matrix.to/#/{id}";
if (vias is { Length: > 0 }) Content.Body += $"?via={string.Join("&via=", vias)}";
diff --git a/LibMatrix/Helpers/RoomBuilder.cs b/LibMatrix/Helpers/RoomBuilder.cs
index a292f33..ed47eb2 100644
--- a/LibMatrix/Helpers/RoomBuilder.cs
+++ b/LibMatrix/Helpers/RoomBuilder.cs
@@ -2,7 +2,9 @@ using System.Diagnostics;
using System.Runtime.Intrinsics.X86;
using System.Text.RegularExpressions;
using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec;
using LibMatrix.EventTypes.Spec.State.RoomInfo;
+using LibMatrix.EventTypes.Spec.State.Space;
using LibMatrix.Homeservers;
using LibMatrix.Responses;
using LibMatrix.RoomTypes;
@@ -85,6 +87,11 @@ public class RoomBuilder {
{ RoomTombstoneEventContent.EventId, 150 },
{ RoomPolicyServerEventContent.EventId, 100 },
{ RoomPinnedEventContent.EventId, 50 },
+ { RoomTopicEventContent.EventId, 50 },
+ { SpaceChildEventContent.EventId, 100 },
+ { SpaceParentEventContent.EventId, 100 },
+ { RoomMessageReactionEventContent.EventId, 0 },
+ { RoomRedactionEventContent.EventId, 0 },
// recommended extensions
{ "im.vector.modular.widgets", 50 },
// { "m.reaction", 0 }, // we probably don't want these to end up as room state
@@ -99,6 +106,7 @@ public class RoomBuilder {
public List<string> AdditionalCreators { get; set; } = new();
public virtual async Task<GenericRoom> Create(AuthenticatedHomeserverGeneric homeserver) {
+ Console.WriteLine($"Creating room on {homeserver.ServerName}...");
var crq = new CreateRoomRequest {
PowerLevelContentOverride = new() {
EventsDefault = 1000000,
@@ -154,8 +162,6 @@ public class RoomBuilder {
var room = await homeserver.CreateRoom(crq);
- Console.WriteLine("Press any key to continue...");
- Console.ReadKey(true);
await SetBasicRoomInfoAsync(room);
await SetStatesAsync(room, ImportantState);
await SetAccessAsync(room);
@@ -167,6 +173,7 @@ public class RoomBuilder {
private async Task SendInvites(GenericRoom room) {
if (Invites.Count == 0) return;
+ Console.WriteLine($"Sending {Invites.Count} invites for room {room.RoomId}");
if (SynapseAdminAutoAcceptLocalInvites && room.Homeserver is AuthenticatedHomeserverSynapse synapse) {
var localJoinTasks = Invites.Where(u => UserId.Parse(u.Key).ServerName == synapse.ServerName).Select(async entry => {
@@ -192,14 +199,15 @@ public class RoomBuilder {
catch (MatrixException e) {
Console.Error.WriteLine("Failed to invite {0} to {1}: {2}", kvp.Key, room.RoomId, e.Message);
}
- });
+ }).ToList();
await Task.WhenAll(inviteTasks);
}
private async Task SetStatesAsync(GenericRoom room, List<MatrixEvent> state) {
if (state.Count == 0) return;
- await room.BulkSendEventsAsync(state);
+ Console.WriteLine($"Setting {state.Count} state events for {room.RoomId}...");
+ // await room.BulkSendEventsAsync(state);
// We chunk this up to try to avoid hitting reverse proxy timeouts
// foreach (var group in state.Chunk(chunkSize)) {
// var sw = Stopwatch.StartNew();
@@ -209,30 +217,35 @@ public class RoomBuilder {
// Console.WriteLine($"Warning: Sending {group.Length} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}.");
// }
// }
- // int chunkSize = 50;
- // for (int i = 0; i < state.Count; i += chunkSize) {
- // var chunk = state.Skip(i).Take(chunkSize).ToList();
- // if (chunk.Count == 0) continue;
- //
- // var sw = Stopwatch.StartNew();
- // await room.BulkSendEventsAsync(chunk, forceSyncInterval: chunk.Count + 1);
- // Console.WriteLine($"Sent {chunk.Count} state events in {sw.ElapsedMilliseconds}ms. {state.Count - (i + chunk.Count)} remaining.");
- // // if (sw.ElapsedMilliseconds > 45000) {
- // // chunkSize = Math.Max(chunkSize / 3, 1);
- // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is dangerously long. Reducing chunk size to {chunkSize}.");
- // // }
- // // else if (sw.ElapsedMilliseconds > 30000) {
- // // chunkSize = Math.Max(chunkSize / 2, 1);
- // // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}.");
- // // }
- // // else if (sw.ElapsedMilliseconds < 10000) {
- // // chunkSize = Math.Min((int)(chunkSize * 1.2), 1000);
- // // Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}.");
- // // }
- // }
+ int chunkSize = 767;
+ for (int i = 0; i < state.Count; i += chunkSize) {
+ var chunk = state.Skip(i).Take(chunkSize).ToList();
+ if (chunk.Count == 0) continue;
+
+ var sw = Stopwatch.StartNew();
+ await room.BulkSendEventsAsync(chunk, forceSyncInterval: chunk.Count + 1);
+ Console.WriteLine($"Sent {chunk.Count} state events in {sw.ElapsedMilliseconds}ms. {state.Count - (i + chunk.Count)} remaining.");
+ if (sw.ElapsedMilliseconds > 50000) {
+ chunkSize = Math.Max((int)(chunkSize / 1.2), 1);
+ Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is dangerously long. Reducing chunk size to {chunkSize}.");
+ }
+ // else if (sw.ElapsedMilliseconds > 30000) {
+ // chunkSize = Math.Max(chunkSize / 2, 1);
+ // Console.WriteLine($"Warning: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, which is quite long. Reducing chunk size to {chunkSize}.");
+ // }
+ else if (sw.ElapsedMilliseconds < 5000) {
+ chunkSize = Math.Min((int)(chunkSize * 1.5), 1000);
+ Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}.");
+ }
+ else if (sw.ElapsedMilliseconds < 10000) {
+ chunkSize = Math.Min((int)(chunkSize * 1.2), 1000);
+ Console.WriteLine($"Info: Sending {chunk.Count} state events took {sw.ElapsedMilliseconds}ms, increasing chunk size to {chunkSize}.");
+ }
+ }
}
private async Task SetBasicRoomInfoAsync(GenericRoom room) {
+ Console.WriteLine($"Setting basic room info for {room.RoomId}...");
if (!string.IsNullOrWhiteSpace(Name.Name))
await room.SendStateEventAsync(RoomNameEventContent.EventId, Name);
@@ -255,6 +268,7 @@ public class RoomBuilder {
}
private async Task SetAccessAsync(GenericRoom room) {
+ Console.WriteLine($"Setting access settings for {room.RoomId}...");
if (!V12PlusRoomVersions.Contains(Version))
PowerLevels.Users![room.Homeserver.WhoAmI.UserId] = OwnPowerLevel;
else {
diff --git a/LibMatrix/Helpers/RoomUpgradeBuilder.cs b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
index ced0ef3..ae71f8a 100644
--- a/LibMatrix/Helpers/RoomUpgradeBuilder.cs
+++ b/LibMatrix/Helpers/RoomUpgradeBuilder.cs
@@ -15,7 +15,7 @@ namespace LibMatrix.Helpers;
public class RoomUpgradeBuilder : RoomBuilder {
public RoomUpgradeOptions UpgradeOptions { get; set; } = new();
public string OldRoomId { get; set; } = string.Empty;
- public bool CanUpgrade { get; private set; }
+ public bool CanUpgrade { get; set; }
public Dictionary<string, object> AdditionalTombstoneContent { get; set; } = new();
private List<Type> basePolicyTypes = [];
@@ -27,7 +27,7 @@ public class RoomUpgradeBuilder : RoomBuilder {
basePolicyTypes = ClassCollector<PolicyRuleEventContent>.ResolveFromAllAccessibleAssemblies().ToList();
Console.WriteLine($"Found {basePolicyTypes.Count} policy types in {sw.ElapsedMilliseconds}ms");
CanUpgrade = (
- (await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId)
+ (await OldRoom.GetPowerLevelsAsync())?.UserHasStatePermission(OldRoom.Homeserver.UserId, RoomTombstoneEventContent.EventId, true)
?? (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId)
)
|| (OldRoom.IsV12PlusRoomId && (await OldRoom.GetRoomCreatorsAsync()).Contains(OldRoom.Homeserver.UserId));
@@ -186,6 +186,16 @@ public class RoomUpgradeBuilder : RoomBuilder {
await oldRoom.SendStateEventAsync(RoomTombstoneEventContent.EventId, tombstoneContent);
}
+ var oldPls = await oldRoom.GetPowerLevelsAsync();
+ if (oldPls?.UserHasStatePermission(oldRoom.Homeserver.UserId, RoomJoinRulesEventContent.EventId, true) ?? true) {
+ var oldJoinRules = await oldRoom.GetJoinRuleAsync();
+ var restrictContent = new RoomJoinRulesEventContent {
+ JoinRule = RoomJoinRulesEventContent.JoinRules.Restricted,
+ Allow = (oldJoinRules?.Allow ?? []).Append(new() { RoomId = room.RoomId, Type = "m.room_membership" }).ToList()
+ };
+ await oldRoom.SendStateEventAsync(RoomJoinRulesEventContent.EventId, restrictContent);
+ }
+
return room;
}
@@ -198,6 +208,7 @@ public class RoomUpgradeBuilder : RoomBuilder {
public bool UpgradeUnstableValues { get; set; }
public bool ForceUpgrade { get; set; }
public bool NoopUpgrade { get; set; }
+ public bool RestrictOldRoom { get; set; }
public Msc4321PolicyListUpgradeOptions Msc4321PolicyListUpgradeOptions { get; set; } = new();
[JsonIgnore]
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index b453d87..b1e655d 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -186,6 +186,34 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
}
}
+ public virtual async Task<T> GetRoomAccountDataAsync<T>(string roomId, string key) =>
+ // var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}");
+ // if (!res.IsSuccessStatusCode) {
+ // Console.WriteLine($"Failed to get account data: {await res.Content.ReadAsStringAsync()}");
+ // throw new InvalidDataException($"Failed to get account data: {await res.Content.ReadAsStringAsync()}");
+ // }
+ //
+ // return await res.Content.ReadFromJsonAsync<T>();
+ await ClientHttpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{WhoAmI.UserId}/rooms/{HttpUtility.UrlEncode(roomId)}/account_data/{key}");
+
+ public virtual async Task<T?> GetRoomAccountDataOrNullAsync<T>(string roomId, string key) {
+ try {
+ return await GetRoomAccountDataAsync<T>(roomId, key);
+ }
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return default;
+ throw;
+ }
+ }
+
+ public virtual async Task SetRoomAccountDataAsync(string roomId, string key, object data) {
+ var res = await ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{WhoAmI.UserId}/rooms/{HttpUtility.UrlEncode(roomId)}/account_data/{key}", data);
+ if (!res.IsSuccessStatusCode) {
+ Console.WriteLine($"Failed to set account data: {await res.Content.ReadAsStringAsync()}");
+ throw new InvalidDataException($"Failed to set account data: {await res.Content.ReadAsStringAsync()}");
+ }
+ }
+
#endregion
#region MSC 4133
@@ -378,11 +406,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
/// <b>Warning</b>: This uses /sync!
/// </summary>
/// <param name="includeGlobal">Include non-room account data</param>
+ /// <param name="rooms">Scope sync request to given room ID(s)</param>
/// <returns>Dictionary of room IDs and their account data.</returns>
/// <exception cref="Exception"></exception>
- public async Task<Dictionary<string, EventList?>> EnumerateAccountDataPerRoom(bool includeGlobal = false) {
+ public async Task<Dictionary<string, EventList?>> EnumerateAccountDataPerRoom(bool includeGlobal = false, List<string>? rooms = null) {
var syncHelper = new SyncHelper(this);
- syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountDataWithRooms);
+ syncHelper.FilterId = rooms == null
+ ? await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountDataWithRooms)
+ : (await UploadFilterAsync(CommonSyncFilters.GetAccountDataForRoomsFilter(rooms))).FilterId;
var resp = await syncHelper.SyncAsync();
if (resp is null) throw new Exception("Sync failed");
var perRoomAccountData = new Dictionary<string, EventList?>();
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index f9e5ce3..2179817 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -19,14 +19,14 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0"/>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
<ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" Condition="'$(ContinuousIntegrationBuild)'!='true'"/>
- <PackageReference Include="ArcaneLibs" Version="1.0.0-preview.2025*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/>
+ <PackageReference Include="ArcaneLibs" Version="1.0.1-preview.2026*" Condition="'$(ContinuousIntegrationBuild)'=='true'"/>
</ItemGroup>
</Project>
diff --git a/LibMatrix/LibMatrixException.cs b/LibMatrix/LibMatrixException.cs
index 27cfc2a..e066d6c 100644
--- a/LibMatrix/LibMatrixException.cs
+++ b/LibMatrix/LibMatrixException.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;
using ArcaneLibs.Extensions;
+
// ReSharper disable MemberCanBePrivate.Global
namespace LibMatrix;
@@ -12,13 +13,15 @@ public class LibMatrixException : Exception {
[JsonPropertyName("error")]
public required string Error { get; set; }
-
public object GetAsObject() => new { errcode = ErrorCode, error = Error };
public string GetAsJson() => GetAsObject().ToJson(ignoreNull: true);
public override string Message =>
$"{ErrorCode}: {ErrorCode switch {
+ "M_NOT_FOUND" => "The specified entity could not be found",
"M_UNSUPPORTED" => "The requested feature is not supported",
+ "RLM_NO_CLIENT_URL" => "Could not resolve client URL",
+ "RLM_NO_SERVER_URL" => "Could not resolve server URL",
_ => $"Unknown error: {GetAsObject().ToJson(ignoreNull: true)}"
}}\nError: {Error}";
@@ -26,5 +29,7 @@ public class LibMatrixException : Exception {
public static class ErrorCodes {
public const string M_NOT_FOUND = "M_NOT_FOUND";
public const string M_UNSUPPORTED = "M_UNSUPPORTED";
+ public const string RLM_NO_CLIENT_URL = "RLM_NO_CLIENT_URL";
+ public const string RLM_NO_SERVER_URL = "RLM_NO_SERVER_URL";
}
}
\ No newline at end of file
diff --git a/LibMatrix/Responses/Federation/SignedObject.cs b/LibMatrix/Responses/Federation/SignedObject.cs
index 3f6ffd6..517bb1f 100644
--- a/LibMatrix/Responses/Federation/SignedObject.cs
+++ b/LibMatrix/Responses/Federation/SignedObject.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@@ -19,7 +20,7 @@ public class SignedObject<T> {
}
[JsonExtensionData]
- public required JsonObject Content { get; set; }
+ public JsonObject Content { get; set; } = null!;
[JsonIgnore]
public T TypedContent {
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 6d9a499..550eb58 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -321,6 +321,9 @@ public class GenericRoom {
public Task<RoomCreateEventContent?> GetCreateEventAsync() =>
GetStateAsync<RoomCreateEventContent>("m.room.create");
+ public Task<RoomPolicyServerEventContent?> GetPolicyServerAsync() =>
+ GetStateAsync<RoomPolicyServerEventContent>(RoomPolicyServerEventContent.EventId);
+
public async Task<string?> GetRoomType() {
var res = await GetStateAsync<RoomCreateEventContent>("m.room.create");
return res.Type;
@@ -393,7 +396,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 { Membership: "leave" or "ban" or "join" })
+ if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is { Membership: "ban" or "join" })
return;
await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
}
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index ed1d2e3..700cfbb 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -37,16 +37,22 @@ public class HomeserverResolverService {
var res = new WellKnownUris();
if (client != null)
- res.Client = (await client)?.TrimEnd('/') ?? throw new Exception($"Could not resolve client URL for {homeserver}.");
+ res.Client = (await client)?.TrimEnd('/') ?? throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.RLM_NO_CLIENT_URL,
+ Error = $"Could not resolve client URL for {homeserver}."
+ };
if (server != null)
- res.Server = (await server)?.TrimEnd('/') ?? throw new Exception($"Could not resolve server URL for {homeserver}.");
+ res.Server = (await server)?.TrimEnd('/') ?? throw new LibMatrixException() {
+ ErrorCode = LibMatrixException.ErrorCodes.RLM_NO_SERVER_URL,
+ Error = $"Could not resolve server URL for {homeserver}."
+ };
_logger.LogInformation("Resolved well-knowns for {hs}: {json}", homeserver, res.ToJson(indent: false));
return res;
});
}
-
+
private async Task<T?> GetFromJsonAsync<T>(string url) {
try {
return await _httpClient.GetFromJsonAsync<T>(url);
@@ -56,7 +62,7 @@ public class HomeserverResolverService {
return default;
}
}
-
+
private async Task<string?> _tryResolveClientEndpoint(string homeserver) {
ArgumentNullException.ThrowIfNull(homeserver);
_logger.LogTrace("Resolving client well-known: {homeserver}", homeserver);
@@ -71,7 +77,7 @@ public class HomeserverResolverService {
}
else if (homeserver.StartsWith("http://")) {
clientWellKnown = await GetFromJsonAsync<ClientWellKnown>($"{homeserver}/.well-known/matrix/client");
-
+
if (clientWellKnown is null && await MatrixHttpClient.CheckSuccessStatus($"{homeserver}/_matrix/client/versions"))
return homeserver;
}
diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
index 5ffd43a..7f15cd2 100644
--- a/LibMatrix/Services/ServiceInstaller.cs
+++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -13,6 +13,7 @@ public static class ServiceInstaller {
services.AddSingleton<ClientWellKnownResolver>();
services.AddSingleton<ServerWellKnownResolver>();
services.AddSingleton<SupportWellKnownResolver>();
+ services.AddSingleton<PolicyServerWellKnownResolver>();
if (!services.Any(x => x.ServiceType == typeof(WellKnownResolverConfiguration)))
services.AddSingleton<WellKnownResolverConfiguration>();
services.AddSingleton<WellKnownResolverService>();
diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
index c5e9d9c..8764096 100644
--- a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
+++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs
@@ -14,15 +14,17 @@ public class WellKnownResolverService {
private readonly ClientWellKnownResolver _clientWellKnownResolver;
private readonly SupportWellKnownResolver _supportWellKnownResolver;
private readonly ServerWellKnownResolver _serverWellKnownResolver;
+ private readonly PolicyServerWellKnownResolver _policyServerWellKnownResolver;
private readonly WellKnownResolverConfiguration _configuration;
public WellKnownResolverService(ILogger<WellKnownResolverService> logger, ClientWellKnownResolver clientWellKnownResolver, SupportWellKnownResolver supportWellKnownResolver,
- WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver) {
+ WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver, PolicyServerWellKnownResolver policyServerWellKnownResolver) {
_logger = logger;
_clientWellKnownResolver = clientWellKnownResolver;
_supportWellKnownResolver = supportWellKnownResolver;
_configuration = configuration;
_serverWellKnownResolver = serverWellKnownResolver;
+ _policyServerWellKnownResolver = policyServerWellKnownResolver;
if (logger is NullLogger<WellKnownResolverService>) {
var stackFrame = new StackTrace(true).GetFrame(1);
Console.WriteLine(
@@ -31,16 +33,26 @@ public class WellKnownResolverService {
}
public async Task<WellKnownRecords> TryResolveWellKnownRecords(string homeserver, bool includeClient = true, bool includeServer = true, bool includeSupport = true,
- WellKnownResolverConfiguration? config = null) {
+ bool includePolicyServer = true, WellKnownResolverConfiguration? config = null) {
WellKnownRecords records = new();
_logger.LogDebug($"Resolving well-knowns for {homeserver}");
- var clientTask = _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration);
- var serverTask = _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration);
- var supportTask = _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration);
+ var clientTask = includeClient
+ ? _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration)
+ : Task.FromResult<WellKnownResolutionResult<ClientWellKnown?>>(null!);
+ var serverTask = includeServer
+ ? _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration)
+ : Task.FromResult<WellKnownResolutionResult<ServerWellKnown?>>(null!);
+ var supportTask = includeSupport
+ ? _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration)
+ : Task.FromResult<WellKnownResolutionResult<SupportWellKnown?>>(null!);
+ var policyServerTask = includePolicyServer
+ ? _policyServerWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration)
+ : Task.FromResult<WellKnownResolutionResult<PolicyServerWellKnown?>>(null!);
if (includeClient && await clientTask is { } clientResult) records.ClientWellKnown = clientResult;
if (includeServer && await serverTask is { } serverResult) records.ServerWellKnown = serverResult;
if (includeSupport && await supportTask is { } supportResult) records.SupportWellKnown = supportResult;
+ if (includePolicyServer && await policyServerTask is { } policyServerResult) records.PolicyServerWellKnown = policyServerResult;
return records;
}
@@ -49,6 +61,7 @@ public class WellKnownResolverService {
public WellKnownResolutionResult<ClientWellKnown?>? ClientWellKnown { get; set; }
public WellKnownResolutionResult<ServerWellKnown?>? ServerWellKnown { get; set; }
public WellKnownResolutionResult<SupportWellKnown?>? SupportWellKnown { get; set; }
+ public WellKnownResolutionResult<PolicyServerWellKnown?>? PolicyServerWellKnown { get; set; }
}
public class WellKnownResolutionResult<T> {
diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
index f52b217..678c077 100644
--- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
+++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs
@@ -1,10 +1,10 @@
using System.Text.Json.Serialization;
using ArcaneLibs.Collections;
-using LibMatrix.Extensions;
using Microsoft.Extensions.Logging;
using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown;
-using ResultType =
- LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown?>;
+using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<
+ LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ClientWellKnown?
+>;
namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
@@ -14,7 +14,7 @@ public class ClientWellKnownResolver(ILogger<ClientWellKnownResolver> logger, We
StoreNulls = false
};
- public Task<WellKnownResolverService.WellKnownResolutionResult<ClientWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) {
+ public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) {
config ??= configuration;
return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => {
logger.LogTrace($"Resolving client well-known: {homeserver}");
@@ -23,7 +23,6 @@ public class ClientWellKnownResolver(ILogger<ClientWellKnownResolver> logger, We
await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/client", WellKnownResolverService.WellKnownSource.Https);
if (result.Content != null) return result;
-
return result;
});
}
diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs
new file mode 100644
index 0000000..f7ffd62
--- /dev/null
+++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs
@@ -0,0 +1,28 @@
+using System.Text.Json.Serialization;
+using Microsoft.Extensions.Logging;
+using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.PolicyServerWellKnown;
+using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<
+ LibMatrix.Services.WellKnownResolver.WellKnownResolvers.PolicyServerWellKnown?
+>;
+
+namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
+
+public class PolicyServerWellKnownResolver(ILogger<PolicyServerWellKnownResolver> logger, WellKnownResolverConfiguration configuration) : BaseWellKnownResolver<WellKnownType> {
+ public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) {
+ config ??= configuration;
+ return WellKnownCache.TryGetOrAdd(homeserver, async () => {
+ logger.LogTrace($"Resolving support well-known: {homeserver}");
+
+ ResultType result = await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/policy_server", WellKnownResolverService.WellKnownSource.Https);
+ if (result.Content != null)
+ return result;
+
+ return null;
+ });
+ }
+}
+
+public class PolicyServerWellKnown {
+ [JsonPropertyName("public_key")]
+ public string PublicKey { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
index a48d846..f4be57d 100644
--- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
+++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs
@@ -2,8 +2,9 @@ using System.Text.Json.Serialization;
using ArcaneLibs.Collections;
using Microsoft.Extensions.Logging;
using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown;
-using ResultType =
- LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown?>;
+using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult<
+ LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown?
+>;
namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers;
@@ -13,7 +14,7 @@ public class ServerWellKnownResolver(ILogger<ServerWellKnownResolver> logger, We
StoreNulls = false
};
- public Task<WellKnownResolverService.WellKnownResolutionResult<ServerWellKnown>> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) {
+ public Task<ResultType> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) {
config ??= configuration;
return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => {
logger.LogTrace($"Resolving client well-known: {homeserver}");
diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
index 99313db..4faff62 100644
--- a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
+++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs
@@ -1,5 +1,3 @@
-using System.Diagnostics;
-using System.Net.Http.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.Logging;
using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown;
@@ -16,7 +14,7 @@ public class SupportWellKnownResolver(ILogger<SupportWellKnownResolver> logger,
logger.LogTrace($"Resolving support well-known: {homeserver}");
ResultType result = await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/support", WellKnownResolverService.WellKnownSource.Https);
- if (result.Content != null)
+ if (result.Content != null)
return result;
return null;
diff --git a/LibMatrix/Utilities/CommonSyncFilters.cs b/LibMatrix/Utilities/CommonSyncFilters.cs
index 503cc1f..ec52e72 100644
--- a/LibMatrix/Utilities/CommonSyncFilters.cs
+++ b/LibMatrix/Utilities/CommonSyncFilters.cs
@@ -91,4 +91,14 @@ public static class CommonSyncFilters {
[GetSpaceRelations] = GetSpaceRelationsFilter,
[GetOwnMemberEvents] = GetOwnMemberEventsFilter
}.ToFrozenDictionary();
+
+ public static SyncFilter GetAccountDataForRoomsFilter(List<string> rooms) => new() {
+ Presence = new SyncFilter.EventFilter(notTypes: ["*"]),
+ Room = new SyncFilter.RoomFilter() {
+ State = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]),
+ Ephemeral = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]),
+ Timeline = new SyncFilter.RoomFilter.StateFilter(notTypes: ["*"]),
+ AccountData = new SyncFilter.RoomFilter.StateFilter(rooms: rooms)
+ }
+ };
}
\ No newline at end of file
diff --git a/LibMatrix/deps.json b/LibMatrix/deps.json
index ce0dfbf..6989072 100644
--- a/LibMatrix/deps.json
+++ b/LibMatrix/deps.json
@@ -1,12 +1,17 @@
[
{
+ "pname": "ArcaneLibs",
+ "version": "1.0.1-preview.20260619-035441",
+ "hash": "sha256-jNEGd8Ccgkk4A8ZMaJO4QDExmsf9q7TeuhiNESos3Q4="
+ },
+ {
"pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps="
+ "version": "10.0.9",
+ "hash": "sha256-YzQpGAsrjLU18s016LmM7DMJKml0MzKl1bPPYJ/erEk="
},
{
"pname": "Microsoft.Extensions.Logging.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk="
+ "version": "10.0.9",
+ "hash": "sha256-su2q/OZuG7nB3wnUTVcimy3oHSdetFh4hhmjI3f/ym8="
}
]
diff --git a/README.MD b/README.MD
index 85a8137..f5ede59 100644
--- a/README.MD
+++ b/README.MD
@@ -1,14 +1,18 @@
# Rory&::LibMatrix
An extensible C# library for the Matrix protocol. Primarily built around our own project needs, but we're open to contributions and improvements, especially around spec compliance.
-The library currently targets .NET 8. We like to follow the latest release of .NET.
+The library currently targets .NET 10. We like to follow the latest release of .NET.
ArcaneLibs can be found on [GitHub](https://github.com/TheArcaneBrony/ArcaneLibs.git). Personally we use the [MatrixRoomUtils project](https://cgit.rory.gay/matrix/tools/MatrixRoomUtils.git/) as workspace, though improvements to make the library more easy to build outside of this would be appreciated.
# Installation
-Probably add as a submodule for now? NuGet packaging still has to be implemented.
+You can find the packages under the RoryLibMatrix namespace on NuGet.
+https://www.nuget.org/packages/RoryLibMatrix/
+https://www.nuget.org/packages/RoryLibMatrix.EventTypes/
+https://www.nuget.org/packages/RoryLibMatrix.Federation/
+https://www.nuget.org/packages/RoryLibMatrix.Utilities.Bot/
# Contributing
-See the [contributing guidelines](CONTRIBUTING.md) for more information.
\ No newline at end of file
+See the [contributing guidelines](CONTRIBUTING.md) for more information.
diff --git a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj
index a82471b..69048fc 100644
--- a/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj
+++ b/Tests/LibMatrix.Tests/LibMatrix.Tests.csproj
@@ -10,20 +10,20 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
- <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
+ <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.7.0" />
<PackageReference Include="xunit" Version="2.9.3"/>
- <PackageReference Include="Xunit.Microsoft.DependencyInjection" Version="10.0.0" />
+ <PackageReference Include="Xunit.Microsoft.DependencyInjection" Version="10.0.4" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="coverlet.collector" Version="6.0.4">
+ <PackageReference Include="coverlet.collector" Version="10.0.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="Xunit.SkippableFact" Version="1.5.23"/>
+ <PackageReference Include="Xunit.SkippableFact" Version="1.5.61" />
</ItemGroup>
<ItemGroup>
diff --git a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj
index a7b81e0..e9db191 100644
--- a/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj
+++ b/Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj
@@ -9,8 +9,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
</ItemGroup>
<ItemGroup>
diff --git a/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj b/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj
index 8c3f405..4750c31 100644
--- a/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj
+++ b/Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj
@@ -8,8 +8,8 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.0" />
- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.0" PrivateAssets="all" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.9" />
+ <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="10.0.9" PrivateAssets="all" />
</ItemGroup>
<ItemGroup>
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs
new file mode 100644
index 0000000..707a149
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs
@@ -0,0 +1,51 @@
+using System.Net.Http.Headers;
+using LibMatrix.Federation;
+using LibMatrix.FederationTest.Services;
+using LibMatrix.Homeservers;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/federation/")]
+public class DirectoryController(ServerAuthService serverAuth) : ControllerBase {
+ [HttpGet("v1/publicRooms")]
+ [HttpPost("v1/publicRooms")]
+ public async Task<IActionResult> GetPublicRooms() {
+ if (Request.Headers.ContainsKey("Authorization")) {
+ Console.WriteLine("INFO | Authorization header found.");
+ await serverAuth.AssertValidAuthentication();
+ }
+ else Console.WriteLine("INFO | Room directory request without auth");
+
+ var rooms = new List<PublicRoomDirectoryResult.PublicRoomListItem> {
+ new() {
+ GuestCanJoin = false,
+ RoomId = "!tuiLEoMqNOQezxILzt:rory.gay",
+ NumJoinedMembers = Random.Shared.Next(),
+ WorldReadable = false,
+ CanonicalAlias = "#libmatrix:rory.gay",
+ Name = "Rory&::LibMatrix",
+ Topic = $"A .NET {Environment.Version.Major} library for interacting with Matrix"
+ }
+ };
+ return Ok(new PublicRoomDirectoryResult() {
+ Chunk = rooms,
+ TotalRoomCountEstimate = rooms.Count
+ });
+ }
+
+ [HttpGet("v1/query/profile")]
+ public async Task<IActionResult> GetProfile([FromQuery(Name = "user_id")] string userId) {
+ if (Request.Headers.ContainsKey("Authorization")) {
+ Console.WriteLine("INFO | Authorization header found.");
+ await serverAuth.AssertValidAuthentication();
+ }
+ else Console.WriteLine("INFO | Profile request without auth");
+
+ return Ok(new {
+ avatar_url = "mxc://rory.gay/ocRVanZoUTCcifcVNwXgbtTg",
+ displayname = "Rory&::LibMatrix.FederationTest"
+ });
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs
new file mode 100644
index 0000000..7c561ad
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs
@@ -0,0 +1,41 @@
+using System.Net.Http.Headers;
+using LibMatrix.Federation;
+using LibMatrix.Federation.FederationTypes;
+using LibMatrix.FederationTest.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace LibMatrix.FederationTest.Controllers.Spec;
+
+[ApiController]
+[Route("_matrix/federation/")]
+public class MembershipsController(ServerAuthService sas) : ControllerBase {
+ [HttpGet("v1/make_join/{roomId}/{userId}")]
+ [HttpPut("v1/send_join/{roomId}/{eventId}")]
+ [HttpPut("v2/send_join/{roomId}/{eventId}")]
+ [HttpGet("v1/make_knock/{roomId}/{userId}")]
+ [HttpPut("v1/send_knock/{roomId}/{eventId}")]
+ [HttpGet("v1/make_leave/{roomId}/{eventId}")]
+ [HttpPut("v1/send_leave/{roomId}/{eventId}")]
+ [HttpPut("v2/send_leave/{roomId}/{eventId}")]
+ public async Task<IActionResult> JoinKnockMemberships() {
+ await sas.AssertValidAuthentication();
+ return NotFound(new MatrixException() {
+ ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND,
+ Error = "Rory&::LibMatrix.FederationTest does not support membership events."
+ }.GetAsObject());
+ }
+
+ // [HttpPut("v1/invite/{roomId}/{eventId}")]
+ [HttpPut("v2/invite/{roomId}/{eventId}")]
+ public async Task<IActionResult> InviteHandler([FromBody] RoomInvite invite) {
+ await sas.AssertValidAuthentication();
+
+ Console.WriteLine($"Received invite event from {invite.Event.Sender} for room {invite.Event.RoomId} (version {invite.RoomVersion})\n" +
+ $"{invite.InviteRoomState.Count} invite room state events.");
+
+ return NotFound(new MatrixException() {
+ ErrorCode = MatrixException.ErrorCodes.M_NOT_FOUND,
+ Error = "Rory&::LibMatrix.FederationTest does not support membership events."
+ }.GetAsObject());
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/FedTest.http b/Utilities/LibMatrix.FederationTest/FedTest.http
new file mode 100644
index 0000000..26b1cd0
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/FedTest.http
@@ -0,0 +1,13 @@
+POST https://libmatrix-fed-test.rory.gay/ping
+Accept: application/json
+Content-Type: application/json
+
+[
+ "matrix.org",
+ "rory.gay",
+ "element.io",
+ "4d2.org",
+ "mozilla.org",
+ "fedora.im",
+ "opensuse.org"
+]
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml
new file mode 100644
index 0000000..283c13e
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml
@@ -0,0 +1,19 @@
+@page "/"
+@model LibMatrix.FederationTest.Pages.IndexPage
+
+@{
+ Layout = null;
+}
+
+<!DOCTYPE html>
+
+<html>
+ <head>
+ <title>LibMatrix.FederationTest</title>
+ </head>
+ <body>
+ <div>
+ If you're seeing this, LibMatrix.FederationTest is running!
+ </div>
+ </body>
+</html>
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs
new file mode 100644
index 0000000..0d372b0
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs
@@ -0,0 +1,9 @@
+using Microsoft.AspNetCore.Mvc.RazorPages;
+
+namespace LibMatrix.FederationTest.Pages;
+
+public class IndexPage : PageModel {
+ public void OnGet() {
+
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Program.cs b/Utilities/LibMatrix.FederationTest/Program.cs
index 18d3421..3e9cb80 100644
--- a/Utilities/LibMatrix.FederationTest/Program.cs
+++ b/Utilities/LibMatrix.FederationTest/Program.cs
@@ -1,12 +1,33 @@
+using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
using LibMatrix.FederationTest.Services;
using LibMatrix.Services;
+using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers()
+ .ConfigureApiBehaviorOptions(options => {
+ options.InvalidModelStateResponseFactory = context => {
+ var problemDetails = new ValidationProblemDetails(context.ModelState) {
+ Status = StatusCodes.Status400BadRequest,
+ Title = "One or more validation errors occurred.",
+ Detail = "See the errors property for more details.",
+ Instance = context.HttpContext.Request.Path
+ };
+
+ Console.WriteLine("Model validation failed: " + problemDetails.ToJson());
+
+ return new BadRequestObjectResult(problemDetails) {
+ ContentTypes = { "application/problem+json", "application/problem+xml" }
+ };
+ };
+ })
.AddJsonOptions(options => {
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.JsonSerializerOptions.WriteIndented = true;
@@ -20,13 +41,15 @@ builder.Services.AddHttpLogging(options => {
options.RequestHeaders.Add("X-Forwarded-Host");
options.RequestHeaders.Add("X-Forwarded-Port");
});
+builder.Services.AddRazorPages();
+builder.Services.AddHttpContextAccessor();
builder.Services.AddRoryLibMatrixServices();
builder.Services.AddSingleton<FederationTestConfiguration>();
builder.Services.AddSingleton<FederationKeyStore>();
+builder.Services.AddScoped<ServerAuthService>();
var app = builder.Build();
-
// Configure the HTTP request pipeline.
if (true || app.Environment.IsDevelopment()) {
app.MapOpenApi();
@@ -35,6 +58,7 @@ if (true || app.Environment.IsDevelopment()) {
// app.UseAuthorization();
app.MapControllers();
+app.MapRazorPages();
// app.UseHttpLogging();
app.Run();
\ No newline at end of file
diff --git a/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs
new file mode 100644
index 0000000..58274eb
--- /dev/null
+++ b/Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs
@@ -0,0 +1,58 @@
+using System.Net.Http.Headers;
+using System.Text.Json.Nodes;
+using LibMatrix.Extensions;
+using LibMatrix.Federation;
+using LibMatrix.FederationTest.Utilities;
+using LibMatrix.Responses.Federation;
+using LibMatrix.Services;
+using Microsoft.AspNetCore.Http.Extensions;
+using Microsoft.AspNetCore.Http.Features;
+using Org.BouncyCastle.Math.EC.Rfc8032;
+
+namespace LibMatrix.FederationTest.Services;
+
+public class ServerAuthService(HomeserverProviderService hsProvider, IHttpContextAccessor httpContextAccessor) {
+ private static Dictionary<string, SignedObject<ServerKeysResponse>> _serverKeysCache = new();
+
+ public async Task AssertValidAuthentication(XMatrixAuthorizationScheme.XMatrixAuthorizationHeader authHeader) {
+ var httpContext = httpContextAccessor.HttpContext!;
+ var hs = await hsProvider.GetFederationClient(authHeader.Origin, "");
+ var serverKeys = (_serverKeysCache.TryGetValue(authHeader.Origin, out var sk) && sk.TypedContent.ValidUntil > DateTimeOffset.UtcNow)
+ ? sk
+ : _serverKeysCache[authHeader.Origin] = await hs.GetServerKeysAsync();
+ var publicKeyBase64 = serverKeys.TypedContent.VerifyKeys[authHeader.Key].Key;
+ var publicKey = Ed25519Utils.LoadPublicKeyFromEncoded(publicKeyBase64);
+ var requestAuthenticationData = new XMatrixAuthorizationScheme.XMatrixRequestSignature() {
+ Method = httpContext.Request.Method,
+ Uri = httpContext.Features.Get<IHttpRequestFeature>()!.RawTarget,
+ OriginServerName = authHeader.Origin,
+ DestinationServerName = authHeader.Destination,
+ Content = httpContext.Request.HasJsonContentType() ? await httpContext.Request.ReadFromJsonAsync<JsonObject?>() : null
+ };
+ var contentBytes = CanonicalJsonSerializer.SerializeToUtf8Bytes(requestAuthenticationData);
+ var signatureBytes = UnpaddedBase64.Decode(authHeader.Signature);
+
+ Console.WriteLine($"Validating X-Matrix authorized request\n" +
+ $" - From: {requestAuthenticationData.OriginServerName}, To: {requestAuthenticationData.DestinationServerName}\n" +
+ $" - Key: {authHeader.Key} ({publicKeyBase64})\n" +
+ $" - Signature: {authHeader.Signature}\n" +
+ $" - Request: {requestAuthenticationData.Method} {requestAuthenticationData.Uri}\n" +
+ $" - Has request body: {requestAuthenticationData.Content is not null}\n" +
+ // $" - Canonicalized request body (or null if missing): {(requestAuthenticationData.Content is null ? "(null)" : CanonicalJsonSerializer.Serialize(requestAuthenticationData.Content))}\n" +
+ $" - Canonicalized message to verify: {System.Text.Encoding.UTF8.GetString(contentBytes)}");
+
+ if (!publicKey.Verify(Ed25519.Algorithm.Ed25519, null, contentBytes, 0, contentBytes.Length, signatureBytes, 0)) {
+ throw new UnauthorizedAccessException("Invalid signature in X-Matrix authorization header.");
+ }
+
+ Console.WriteLine("INFO | Valid X-Matrix authorization header.");
+ }
+
+ public async Task AssertValidAuthentication() {
+ await AssertValidAuthentication(
+ XMatrixAuthorizationScheme.XMatrixAuthorizationHeader.FromHeaderValue(
+ httpContextAccessor.HttpContext!.Request.GetTypedHeaders().Get<AuthenticationHeaderValue>("Authorization")!
+ )
+ );
+ }
+}
\ No newline at end of file
diff --git a/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj
index 733cd7f..c587204 100644
--- a/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj
+++ b/Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj
@@ -11,8 +11,8 @@
<ItemGroup>
<PackageReference Include="EasyCompressor.LZMA" Version="2.1.0"/>
- <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
- <PackageReference Include="Swashbuckle.AspNetCore" Version="10.0.1" />
+ <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.9" />
+ <PackageReference Include="Swashbuckle.AspNetCore" Version="10.2.3" />
</ItemGroup>
<ItemGroup>
diff --git a/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj b/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj
index 090b515..1262f8a 100644
--- a/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj
+++ b/Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj
@@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
</ItemGroup>
<ItemGroup>
<Content Include="appsettings*.json">
diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj
index 8c3bfcb..dbc1983 100644
--- a/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj
+++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj
@@ -20,9 +20,9 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0"/>
- <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0"/>
- <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0"/>
+ <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.9" />
+ <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.9" />
</ItemGroup>
diff --git a/Utilities/LibMatrix.Utilities.Bot/deps.json b/Utilities/LibMatrix.Utilities.Bot/deps.json
index 566f6a9..f9e7bfe 100644
--- a/Utilities/LibMatrix.Utilities.Bot/deps.json
+++ b/Utilities/LibMatrix.Utilities.Bot/deps.json
@@ -1,142 +1,147 @@
[
{
+ "pname": "ArcaneLibs",
+ "version": "1.0.1-preview.20260619-035441",
+ "hash": "sha256-jNEGd8Ccgkk4A8ZMaJO4QDExmsf9q7TeuhiNESos3Q4="
+ },
+ {
"pname": "Microsoft.Extensions.Configuration",
- "version": "10.0.0",
- "hash": "sha256-MsLskVPpkCvov5+DWIaALCt1qfRRX4u228eHxvpE0dg="
+ "version": "10.0.9",
+ "hash": "sha256-4d8r6Vyune1Qb0kwqzfGPA7BK2nvaiOenTiJedzq6sU="
},
{
"pname": "Microsoft.Extensions.Configuration.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-GcgrnTAieCV7AVT13zyOjfwwL86e99iiO/MiMOxPGG0="
+ "version": "10.0.9",
+ "hash": "sha256-pliaksEAQdxyPURUVSlXpfF3LzJB93zHaeEDsaF5QJE="
},
{
"pname": "Microsoft.Extensions.Configuration.Binder",
- "version": "10.0.0",
- "hash": "sha256-YSiWoA3VQR22k6+bSEAUqeG7UDzZlJfHWDTubUO5V8U="
+ "version": "10.0.9",
+ "hash": "sha256-ouJa+84H/bfMi67v25hjEWhTIyBHaWTJAoEvArwbrGA="
},
{
"pname": "Microsoft.Extensions.Configuration.CommandLine",
- "version": "10.0.0",
- "hash": "sha256-ldTiRFqnv8/pA0gl6UR+4DDGAIZOf9+MhaLWOuKOXOI="
+ "version": "10.0.9",
+ "hash": "sha256-Dsq7uQtcOf5pA3OR8Hc7X311OsBpSU/Dpdr481KaDg4="
},
{
"pname": "Microsoft.Extensions.Configuration.EnvironmentVariables",
- "version": "10.0.0",
- "hash": "sha256-UayfeqrAmNyfOkuhcBKfj8UpjQqV/ZMqWrDyxCSG1MA="
+ "version": "10.0.9",
+ "hash": "sha256-TqmySse14myFWrrLbNSQD7bTvzT/ZQ24wcpykIe8XOo="
},
{
"pname": "Microsoft.Extensions.Configuration.FileExtensions",
- "version": "10.0.0",
- "hash": "sha256-rN+3rqrHiTaBfHgP+E4dA8Qm2cFJPfbEcd93yKLsqlQ="
+ "version": "10.0.9",
+ "hash": "sha256-r120E0PUCGIh8CsZnzqDlZ7ikbT8Bg/wVYB/g+JJ7D4="
},
{
"pname": "Microsoft.Extensions.Configuration.Json",
- "version": "10.0.0",
- "hash": "sha256-VCFukgsxiQ2MFGE6RDMFTGopBHbcZL2t0ER7ENaFXRY="
+ "version": "10.0.9",
+ "hash": "sha256-vlH4uDfclV9i7zLeeSF0a/pCiFDdg4mDyHjk0O2KM80="
},
{
"pname": "Microsoft.Extensions.Configuration.UserSecrets",
- "version": "10.0.0",
- "hash": "sha256-uIoIpbDPRMfFqT8Y6j/wHbFCAly6H1N9qpxnomRbHIo="
+ "version": "10.0.9",
+ "hash": "sha256-OAs5+LE5Tz6FDQ/iBsVf6YP5h60yi6zzHXcMT4sKNkw="
},
{
"pname": "Microsoft.Extensions.DependencyInjection",
- "version": "10.0.0",
- "hash": "sha256-LYm9hVlo/R9c2aAKHsDYJ5vY9U0+3Jvclme3ou3BtvQ="
+ "version": "10.0.9",
+ "hash": "sha256-YIqgDknwTq48X88Imdrfav3tmQ6tZwIOYsLt6dT40M8="
},
{
"pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-9iodXP39YqgxomnOPOxd/mzbG0JfOSXzFoNU3omT2Ps="
+ "version": "10.0.9",
+ "hash": "sha256-YzQpGAsrjLU18s016LmM7DMJKml0MzKl1bPPYJ/erEk="
},
{
"pname": "Microsoft.Extensions.Diagnostics",
- "version": "10.0.0",
- "hash": "sha256-o7QkCisEcFIh227qBUfWFci2ns4cgEpLqpX7YvHGToQ="
+ "version": "10.0.9",
+ "hash": "sha256-nnhs4+Z8gNL89Dttjqt0FR6uWY5U6bQ1dIa0wwMiUno="
},
{
"pname": "Microsoft.Extensions.Diagnostics.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-cix7QxQ/g3sj6reXu3jn0cRv2RijzceaLLkchEGTt5E="
+ "version": "10.0.9",
+ "hash": "sha256-OoBCatkSA+QWbMfyFxeuEOIHb04ukPH32gNfCUsar2M="
},
{
"pname": "Microsoft.Extensions.FileProviders.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-CHDs2HCN8QcfuYQpgNVszZ5dfXFe4yS9K2GoQXecc20="
+ "version": "10.0.9",
+ "hash": "sha256-slMiJSmykMXcy0pu/uvoCaOw7BiyBUZQWZ216ERhZok="
},
{
"pname": "Microsoft.Extensions.FileProviders.Physical",
- "version": "10.0.0",
- "hash": "sha256-2Rw/cwBO+/A3QY2IjN/c8Y0LhtC1qTBL7VdJiD1J2UQ="
+ "version": "10.0.9",
+ "hash": "sha256-14V+vnaNyZeNf3pSqFQkxEpuy9WXm+Jo3llmm4UE2Y4="
},
{
"pname": "Microsoft.Extensions.FileSystemGlobbing",
- "version": "10.0.0",
- "hash": "sha256-ETfVTdsdBtp69EggLg/AARTQW4lLQYVdVldXIQrsjZA="
+ "version": "10.0.9",
+ "hash": "sha256-+8gYl7xPmeTD5UazeddRKSfMttCDCoLyhNssP0Ddk04="
},
{
"pname": "Microsoft.Extensions.Hosting",
- "version": "10.0.0",
- "hash": "sha256-tY0g6lCy2yFprE+NmriiU6FGmwpzxV8LqE0ZFNKIwuM="
+ "version": "10.0.9",
+ "hash": "sha256-EXHh7tiz/YJx3y6zhyxou4nTANBmjUyGcxOrypcJRjY="
},
{
"pname": "Microsoft.Extensions.Hosting.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-Sub3Thay/+eR84cEODk/nPh1oYIYtawvDX6r0duReqo="
+ "version": "10.0.9",
+ "hash": "sha256-wbVJw22JPxh8YXSeXCQdrbYE4ofPi0h6O8ZN/gyQ6CE="
},
{
"pname": "Microsoft.Extensions.Logging",
- "version": "10.0.0",
- "hash": "sha256-P+zPAadLL63k/GqK34/qChqQjY9aIRxZfxlB9lqsSrs="
+ "version": "10.0.9",
+ "hash": "sha256-644xYxPB+PtwGUTAKJV9wByXHWFkR5Ol0p08wFQwMm4="
},
{
"pname": "Microsoft.Extensions.Logging.Abstractions",
- "version": "10.0.0",
- "hash": "sha256-BnhgGZc01HwTSxogavq7Ueq4V7iMA3wPnbfRwQ4RhGk="
+ "version": "10.0.9",
+ "hash": "sha256-su2q/OZuG7nB3wnUTVcimy3oHSdetFh4hhmjI3f/ym8="
},
{
"pname": "Microsoft.Extensions.Logging.Configuration",
- "version": "10.0.0",
- "hash": "sha256-7/TWO1aq8hdgbcTEKDBWIjgSC9KpFN3kRnMX+12bOkU="
+ "version": "10.0.9",
+ "hash": "sha256-fDtKa36XFBzdjwf0g9+mL4y2xE6Dce+YTO6dQaAYYZ8="
},
{
"pname": "Microsoft.Extensions.Logging.Console",
- "version": "10.0.0",
- "hash": "sha256-Rsblo7GSMTOr43876KkdvqS6wU9Typ1yoFK3tL50CBk="
+ "version": "10.0.9",
+ "hash": "sha256-i6C3zk+s/ux+tCOnx7gDpoj2hFzBz61hqh0zfbZvHHo="
},
{
"pname": "Microsoft.Extensions.Logging.Debug",
- "version": "10.0.0",
- "hash": "sha256-n/+KRVlsgKm17cJImaoAPHAObHpApW/hf6mMsQFGrvY="
+ "version": "10.0.9",
+ "hash": "sha256-Weu1JE68fQGbIccHU5lI+KRJHiM5GD2VOgeyvhtXtSY="
},
{
"pname": "Microsoft.Extensions.Logging.EventLog",
- "version": "10.0.0",
- "hash": "sha256-4RJ2r80RtI3QUAhCAYbGnA0YcTmouqtZvQU9o3CrB38="
+ "version": "10.0.9",
+ "hash": "sha256-2sx6D7642undaHjFxx3K3CsZD53sXW12cPTx7Fd8sr0="
},
{
"pname": "Microsoft.Extensions.Logging.EventSource",
- "version": "10.0.0",
- "hash": "sha256-tqC13Qwkf4x14iGxOYlXTyeoN8KPVX+mupv2LdpzGHo="
+ "version": "10.0.9",
+ "hash": "sha256-nDfYJm2mY0o3ShcktFBVYmretrds4SP/RHxQWwI+6Lc="
},
{
"pname": "Microsoft.Extensions.Options",
- "version": "10.0.0",
- "hash": "sha256-j5MOqZSKeUtxxzmZjzZMGy0vELHdvPraqwTQQQNVsYA="
+ "version": "10.0.9",
+ "hash": "sha256-/fc1g1SDJI81nOZRS7W4LZIAC0ssmasqaWhgJK+9rRs="
},
{
"pname": "Microsoft.Extensions.Options.ConfigurationExtensions",
- "version": "10.0.0",
- "hash": "sha256-XGAs5DxMvWnmjX8dqRwKY0vsuS40SHvsfJqB1rO4L7k="
+ "version": "10.0.9",
+ "hash": "sha256-XVKJrNjPo0hojY5V4xoLxedpvkqtr4GJqkCUhLKSTcQ="
},
{
"pname": "Microsoft.Extensions.Primitives",
- "version": "10.0.0",
- "hash": "sha256-Dup08KcptLjlnpN5t5//+p4n8FUTgRAq4n/w1s6us+I="
+ "version": "10.0.9",
+ "hash": "sha256-WEPtmlEexm6QSFR4ITlBHuUD5/bqMfecOxFe5hkbAPU="
},
{
"pname": "System.Diagnostics.EventLog",
- "version": "10.0.0",
- "hash": "sha256-pN3tld926Fp0n5ZNjjzIJviUQrynlOAB0vhc1aoso6E="
+ "version": "10.0.9",
+ "hash": "sha256-asuR1KqI8IdI83alSGSvPmcfNuPHK5WweZ2uAhuxe2U="
}
]
diff --git a/flake.lock b/flake.lock
index d70f9b9..c1e4cb3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -8,11 +8,11 @@
]
},
"locked": {
- "lastModified": 1765126047,
- "narHash": "sha256-c+IuteUQJI9Apm4z64XjEXZZyJS62THHmmceftYb6xk=",
+ "lastModified": 1781647411,
+ "narHash": "sha256-sbn+Sd0dJLEDSgaIzYD0kJD7zAgCSFtR6looEe2CkWg=",
"owner": "TheArcaneBrony",
"repo": "ArcaneLibs",
- "rev": "270012a7527345a914003c98bf97c7b221812cc1",
+ "rev": "66fa066a90fb1a836f08cd1f62591ed537c0da59",
"type": "github"
},
"original": {
@@ -59,11 +59,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1764950072,
- "narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
+ "lastModified": 1783224372,
+ "narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "f61125a668a320878494449750330ca58b78c557",
+ "rev": "d407951447dcd00442e97087bf374aad70c04cea",
"type": "github"
},
"original": {
|