about summary refs log tree commit diff
diff options
context:
space:
mode:
m---------ArcaneLibs0
-rw-r--r--LibMatrix.EventTypes/Interop/Draupnir/DraupnirWatchedListsData.cs28
-rw-r--r--LibMatrix.EventTypes/Interop/Draupnir/DraupnirZtdManagementRoomData.cs19
-rw-r--r--LibMatrix.EventTypes/LibMatrix.EventTypes.csproj2
-rw-r--r--LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs28
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPolicyServerEventContent.cs3
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationBackfillResponse.cs14
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationEvent.cs30
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationGetMissingEventsRequest.cs34
-rw-r--r--LibMatrix.Federation/FederationTypes/FederationTransaction.cs26
-rw-r--r--LibMatrix.Federation/FederationTypes/RoomInvite.cs14
-rw-r--r--LibMatrix.Federation/LibMatrix.Federation.csproj2
-rw-r--r--LibMatrix.Federation/XMatrixAuthorizationScheme.cs29
-rw-r--r--LibMatrix.Federation/deps.json17
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs10
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs2
-rw-r--r--LibMatrix/Helpers/RoomBuilder.cs64
-rw-r--r--LibMatrix/Helpers/RoomUpgradeBuilder.cs15
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs35
-rw-r--r--LibMatrix/LibMatrix.csproj6
-rw-r--r--LibMatrix/LibMatrixException.cs7
-rw-r--r--LibMatrix/Responses/Federation/SignedObject.cs3
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs5
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs16
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs1
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs23
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs9
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/PolicyServerWellKnownResolver.cs28
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs7
-rw-r--r--LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs4
-rw-r--r--LibMatrix/Utilities/CommonSyncFilters.cs10
-rw-r--r--LibMatrix/deps.json13
-rw-r--r--README.MD10
-rw-r--r--Tests/LibMatrix.Tests/LibMatrix.Tests.csproj10
-rw-r--r--Utilities/LibMatrix.DebugDataValidationApi/LibMatrix.DebugDataValidationApi.csproj4
-rw-r--r--Utilities/LibMatrix.E2eeTestKit/LibMatrix.E2eeTestKit.csproj4
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/DirectoryController.cs51
-rw-r--r--Utilities/LibMatrix.FederationTest/Controllers/Spec/MembershipsController.cs41
-rw-r--r--Utilities/LibMatrix.FederationTest/FedTest.http13
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml19
-rw-r--r--Utilities/LibMatrix.FederationTest/Pages/IndexPage.cshtml.cs9
-rw-r--r--Utilities/LibMatrix.FederationTest/Program.cs26
-rw-r--r--Utilities/LibMatrix.FederationTest/Services/ServerAuthService.cs58
-rw-r--r--Utilities/LibMatrix.HomeserverEmulator/LibMatrix.HomeserverEmulator.csproj4
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/LibMatrix.TestDataGenerator.csproj2
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/LibMatrix.Utilities.Bot.csproj6
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/deps.json117
-rw-r--r--flake.lock12
48 files changed, 728 insertions, 162 deletions
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": {