From db835755e01b13dcb8d33a91f57ae8f20b931c57 Mon Sep 17 00:00:00 2001 From: Rory& Date: Sun, 9 Mar 2025 17:24:34 +0100 Subject: Well known resolver work, synapse admin work --- ArcaneLibs | 2 +- LibMatrix.EventTypes/LibMatrix.EventTypes.csproj | 2 +- LibMatrix/Extensions/MatrixHttpClient.Single.cs | 22 +- .../Extensions/NamedCaches/NamedCache.cs | 58 ++- .../SynapseAdminLocalEventReportQueryFilter.cs | 27 ++ .../Filters/SynapseAdminLocalRoomQueryFilter.cs | 27 ++ .../Filters/SynapseAdminLocalUserQueryFilter.cs | 27 ++ .../Models/Requests/AdminRoomDeleteRequest.cs | 23 - .../SynapseAdminRegistrationTokenCreateRequest.cs | 31 ++ .../Requests/SynapseAdminRoomDeleteRequest.cs | 54 +++ .../Models/Responses/AdminRoomListingResult.cs | 64 --- .../Synapse/Models/Responses/BackgroundUpdates.cs | 28 ++ .../Synapse/Models/Responses/Destinations.cs | 56 +++ .../Models/Responses/EventReportListResult.cs | 169 ++++++++ .../Responses/RegistrationTokenListResult.cs | 31 ++ .../Synapse/Models/Responses/RoomListResult.cs | 64 +++ .../Models/Responses/RoomMediaListResult.cs | 11 + .../Responses/SynapseAdminRoomMemberListResult.cs | 11 + .../Responses/SynapseAdminUserRedactIdResponse.cs | 22 + .../Models/Responses/SynapseCollectionResult.cs | 250 +++++++++++ .../Models/Responses/SynapseUserMediaResult.cs | 40 ++ .../Synapse/Models/Responses/UserListResult.cs | 71 ++++ .../Synapse/SynapseAdminApiClient.cs | 462 ++++++++++++++++++++- .../Synapse/SynapseAdminUserCleanupExecutor.cs | 27 ++ LibMatrix/LibMatrix.csproj | 2 +- LibMatrix/MxcUri.cs | 10 +- LibMatrix/RoomTypes/GenericRoom.cs | 2 +- LibMatrix/Services/HomeserverResolverService.cs | 6 +- LibMatrix/Services/ServiceInstaller.cs | 7 +- .../WellKnownResolverConfiguration.cs | 49 +++ .../WellKnownResolver/WellKnownResolverService.cs | 91 ++++ .../WellKnownResolvers/BaseWellKnownResolver.cs | 52 +++ .../WellKnownResolvers/ClientWellKnownResolver.cs | 42 ++ .../WellKnownResolvers/ServerWellKnownResolver.cs | 38 ++ .../WellKnownResolvers/SupportWellKnownResolver.cs | 44 ++ LibMatrix/Services/WellKnownResolverService.cs | 88 ---- .../WellKnownResolvers/ClientWellKnownResolver.cs | 53 --- .../WellKnownResolvers/SupportWellKnownResolver.cs | 54 --- .../ClientWellKnownResolverTests.cs | 10 +- .../LibMatrix.DevTestBot.csproj | 2 +- 40 files changed, 1797 insertions(+), 332 deletions(-) create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs delete mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs delete mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs create mode 100644 LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs create mode 100644 LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs delete mode 100644 LibMatrix/Services/WellKnownResolverService.cs delete mode 100644 LibMatrix/Services/WellKnownResolvers/ClientWellKnownResolver.cs delete mode 100644 LibMatrix/Services/WellKnownResolvers/SupportWellKnownResolver.cs diff --git a/ArcaneLibs b/ArcaneLibs index 209108f..2956a7c 160000 --- a/ArcaneLibs +++ b/ArcaneLibs @@ -1 +1 @@ -Subproject commit 209108f67cddce1b7b94eabcd8ab38805bcfe3e2 +Subproject commit 2956a7ce4e8d12034322a91b6afa449e70354858 diff --git a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj index ae2f4de..647b68f 100644 --- a/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj +++ b/LibMatrix.EventTypes/LibMatrix.EventTypes.csproj @@ -7,7 +7,7 @@ - + diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs index b42d94e..42f81a8 100644 --- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs +++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Text.Json.Serialization; using ArcaneLibs; using ArcaneLibs.Extensions; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; namespace LibMatrix.Extensions; @@ -91,7 +92,14 @@ public class MatrixHttpClient { responseMessage = await Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); } catch (Exception e) { - if (!e.Message.StartsWith("TypeError: NetworkError")) + if (e is TaskCanceledException or TimeoutException) { + if (request.Method == HttpMethod.Get && !cancellationToken.IsCancellationRequested) { + await Task.Delay(Random.Shared.Next(500, 2500), cancellationToken); + request.ResetSendStatus(); + return await SendAsync(request, cancellationToken); + } + } + else if (!e.ToString().StartsWith("TypeError: NetworkError")) Console.WriteLine( $"Failed to send request {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)}):\n{e}"); throw; @@ -238,5 +246,17 @@ public class MatrixHttpClient { }; return await SendAsync(request, cancellationToken); } + + public async Task DeleteAsync(string url) { + var request = new HttpRequestMessage(HttpMethod.Delete, url); + await SendAsync(request); + } + + public async Task DeleteAsJsonAsync(string url, T payload) { + var request = new HttpRequestMessage(HttpMethod.Delete, url) { + Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json") + }; + return await SendAsync(request); + } } #endif \ No newline at end of file diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs index 622eef6..9f11fa0 100644 --- a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs +++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs @@ -3,35 +3,65 @@ namespace LibMatrix.Homeservers.Extensions.NamedCaches; public class NamedCache(AuthenticatedHomeserverGeneric hs, string name) where T : class { private Dictionary? _cache = new(); private DateTime _expiry = DateTime.MinValue; - + private SemaphoreSlim _lock = new(1, 1); + + public TimeSpan ExpiryTime { get; set; } = TimeSpan.FromMinutes(5); + public DateTime GetCurrentExpiryTime() => _expiry; + + /// + /// Update the cached map with the latest data from the homeserver. + /// + /// The updated data public async Task> ReadCacheMapAsync() { - _cache = await hs.GetAccountDataOrNullAsync>(name); + _cache = await hs.GetAccountDataAsync>(name); return _cache ?? new(); } - - public async Task> ReadCacheMapCachedAsync() { + + public async Task> ReadCacheMapCachedAsync() { + await _lock.WaitAsync(); if (_expiry < DateTime.Now || _cache == null) { _cache = await ReadCacheMapAsync(); - _expiry = DateTime.Now.AddMinutes(5); + _expiry = DateTime.Now.Add(ExpiryTime); } + _lock.Release(); + return _cache; } - - public virtual async Task GetValueAsync(string key) { - return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key); + + public virtual async Task GetValueAsync(string key, bool useCache = true) { + return (await (useCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync())).GetValueOrDefault(key); } - - public virtual async Task SetValueAsync(string key, T value) { - var cache = await ReadCacheMapCachedAsync(); + + public virtual async Task SetValueAsync(string key, T value, bool unsafeUseCache = false) { + if (!unsafeUseCache) + await _lock.WaitAsync(); + var cache = await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync()); cache[key] = value; await hs.SetAccountDataAsync(name, cache); + if (!unsafeUseCache) + _lock.Release(); + return value; } - - public virtual async Task GetOrSetValueAsync(string key, Func> value) { - return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key) ?? await SetValueAsync(key, await value()); + + public virtual async Task RemoveValueAsync(string key, bool unsafeUseCache = false) { + if (!unsafeUseCache) + await _lock.WaitAsync(); + var cache = await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync()); + var removedValue = cache[key]; + cache.Remove(key); + await hs.SetAccountDataAsync(name, cache); + + if (!unsafeUseCache) + _lock.Release(); + + return removedValue; + } + + public virtual async Task GetOrSetValueAsync(string key, Func> value, bool unsafeUseCache = false) { + return (await (unsafeUseCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync())).GetValueOrDefault(key) ?? await SetValueAsync(key, await value()); } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs new file mode 100644 index 0000000..c34ad7c --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalEventReportQueryFilter.cs @@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; + +public class SynapseAdminLocalEventReportQueryFilter { + public string UserIdContains { get; set; } = ""; + public string NameContains { get; set; } = ""; + public string CanonicalAliasContains { get; set; } = ""; + public string VersionContains { get; set; } = ""; + public string CreatorContains { get; set; } = ""; + public string EncryptionContains { get; set; } = ""; + public string JoinRulesContains { get; set; } = ""; + public string GuestAccessContains { get; set; } = ""; + public string HistoryVisibilityContains { get; set; } = ""; + + public bool Federatable { get; set; } = true; + public bool Public { get; set; } = true; + + public int JoinedMembersGreaterThan { get; set; } + public int JoinedMembersLessThan { get; set; } = int.MaxValue; + + public int JoinedLocalMembersGreaterThan { get; set; } + public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue; + public int StateEventsGreaterThan { get; set; } + public int StateEventsLessThan { get; set; } = int.MaxValue; + + public bool CheckFederation { get; set; } + public bool CheckPublic { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs new file mode 100644 index 0000000..b8929a0 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs @@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; + +public class SynapseAdminLocalRoomQueryFilter { + public string RoomIdContains { get; set; } = ""; + public string NameContains { get; set; } = ""; + public string CanonicalAliasContains { get; set; } = ""; + public string VersionContains { get; set; } = ""; + public string CreatorContains { get; set; } = ""; + public string EncryptionContains { get; set; } = ""; + public string JoinRulesContains { get; set; } = ""; + public string GuestAccessContains { get; set; } = ""; + public string HistoryVisibilityContains { get; set; } = ""; + + public bool Federatable { get; set; } = true; + public bool Public { get; set; } = true; + + public int JoinedMembersGreaterThan { get; set; } + public int JoinedMembersLessThan { get; set; } = int.MaxValue; + + public int JoinedLocalMembersGreaterThan { get; set; } + public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue; + public int StateEventsGreaterThan { get; set; } + public int StateEventsLessThan { get; set; } = int.MaxValue; + + public bool CheckFederation { get; set; } + public bool CheckPublic { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs new file mode 100644 index 0000000..62b291b --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs @@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; + +public class SynapseAdminLocalUserQueryFilter { + public string UserIdContains { get; set; } = ""; + public string NameContains { get; set; } = ""; + public string CanonicalAliasContains { get; set; } = ""; + public string VersionContains { get; set; } = ""; + public string CreatorContains { get; set; } = ""; + public string EncryptionContains { get; set; } = ""; + public string JoinRulesContains { get; set; } = ""; + public string GuestAccessContains { get; set; } = ""; + public string HistoryVisibilityContains { get; set; } = ""; + + public bool Federatable { get; set; } = true; + public bool Public { get; set; } = true; + + public int JoinedMembersGreaterThan { get; set; } + public int JoinedMembersLessThan { get; set; } = int.MaxValue; + + public int JoinedLocalMembersGreaterThan { get; set; } + public int JoinedLocalMembersLessThan { get; set; } = int.MaxValue; + public int StateEventsGreaterThan { get; set; } + public int StateEventsLessThan { get; set; } = int.MaxValue; + + public bool CheckFederation { get; set; } + public bool CheckPublic { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs deleted file mode 100644 index f4c927a..0000000 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Text.Json.Serialization; - -namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; - -public class AdminRoomDeleteRequest { - [JsonPropertyName("new_room_user_id")] - public string? NewRoomUserId { get; set; } - - [JsonPropertyName("room_name")] - public string? RoomName { get; set; } - - [JsonPropertyName("block")] - public bool Block { get; set; } - - [JsonPropertyName("purge")] - public bool Purge { get; set; } - - [JsonPropertyName("message")] - public string? Message { get; set; } - - [JsonPropertyName("force_purge")] - public bool ForcePurge { get; set; } -} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs new file mode 100644 index 0000000..197fd5d --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRegistrationTokenCreateRequest.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRegistrationTokenUpdateRequest { + [JsonPropertyName("uses_allowed")] + public int? UsesAllowed { get; set; } + + [JsonPropertyName("expiry_time")] + public long? ExpiryTime { get; set; } + + [JsonIgnore] + public DateTime? ExpiresAt { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime : null; + set => ExpiryTime = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + + [JsonIgnore] + public TimeSpan? ExpiresAfter { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime - DateTimeOffset.Now : null; + set => ExpiryTime = value.HasValue ? (DateTimeOffset.Now + value.Value).ToUnixTimeMilliseconds() : null; + } +} + +public class SynapseAdminRegistrationTokenCreateRequest : SynapseAdminRegistrationTokenUpdateRequest { + [JsonPropertyName("token")] + public string? Token { get; set; } + + [JsonPropertyName("length")] + public int? Length { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs new file mode 100644 index 0000000..aee2a7e --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; + +public class SynapseAdminRoomDeleteRequest { + [JsonPropertyName("new_room_user_id")] + public string? NewRoomUserId { get; set; } + + [JsonPropertyName("room_name")] + public string? RoomName { get; set; } + + [JsonPropertyName("block")] + public bool Block { get; set; } + + [JsonPropertyName("purge")] + public bool Purge { get; set; } + + [JsonPropertyName("message")] + public string? Message { get; set; } + + [JsonPropertyName("force_purge")] + public bool ForcePurge { get; set; } +} + +public class SynapseAdminRoomDeleteResponse { + [JsonPropertyName("delete_id")] + public string DeleteId { get; set; } = null!; +} + +public class SynapseAdminRoomDeleteStatusList { + [JsonPropertyName("results")] + public List Results { get; set; } +} +public class SynapseAdminRoomDeleteStatus { + [JsonPropertyName("status")] + public string Status { get; set; } = null!; + + [JsonPropertyName("shutdown_room")] + public RoomShutdownInfo ShutdownRoom { get; set; } + + public class RoomShutdownInfo { + [JsonPropertyName("kicked_users")] + public List? KickedUsers { get; set; } + + [JsonPropertyName("failed_to_kick_users")] + public List? FailedToKickUsers { get; set; } + + [JsonPropertyName("local_aliases")] + public List? LocalAliasses { get; set; } + + [JsonPropertyName("new_room_id")] + public string? NewRoomId { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs deleted file mode 100644 index 7ab96ac..0000000 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System.Text.Json.Serialization; - -namespace LibMatrix.Responses.Admin; - -public class AdminRoomListingResult { - [JsonPropertyName("offset")] - public int Offset { get; set; } - - [JsonPropertyName("total_rooms")] - public int TotalRooms { get; set; } - - [JsonPropertyName("next_batch")] - public int? NextBatch { get; set; } - - [JsonPropertyName("prev_batch")] - public int? PrevBatch { get; set; } - - [JsonPropertyName("rooms")] - public List Rooms { get; set; } = new(); - - public class AdminRoomListingResultRoom { - [JsonPropertyName("room_id")] - public required string RoomId { get; set; } - - [JsonPropertyName("name")] - public string? Name { get; set; } - - [JsonPropertyName("canonical_alias")] - public string? CanonicalAlias { get; set; } - - [JsonPropertyName("joined_members")] - public int JoinedMembers { get; set; } - - [JsonPropertyName("joined_local_members")] - public int JoinedLocalMembers { get; set; } - - [JsonPropertyName("version")] - public string? Version { get; set; } - - [JsonPropertyName("creator")] - public string? Creator { get; set; } - - [JsonPropertyName("encryption")] - public string? Encryption { get; set; } - - [JsonPropertyName("federatable")] - public bool Federatable { get; set; } - - [JsonPropertyName("public")] - public bool Public { get; set; } - - [JsonPropertyName("join_rules")] - public string? JoinRules { get; set; } - - [JsonPropertyName("guest_access")] - public string? GuestAccess { get; set; } - - [JsonPropertyName("history_visibility")] - public string? HistoryVisibility { get; set; } - - [JsonPropertyName("state_events")] - public int StateEvents { get; set; } - } -} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs new file mode 100644 index 0000000..2394b98 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/BackgroundUpdates.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminBackgroundUpdateStatusResponse { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("current_updates")] + public Dictionary CurrentUpdates { get; set; } + + public class BackgroundUpdateInfo { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("total_item_count")] + public int TotalItemCount { get; set; } + + [JsonPropertyName("total_duration_ms")] + public double TotalDurationMs { get; set; } + + [JsonPropertyName("average_items_per_ms")] + public double AverageItemsPerMs { get; set; } + + [JsonIgnore] + public TimeSpan TotalDuration => TimeSpan.FromMilliseconds(TotalDurationMs); + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs new file mode 100644 index 0000000..646a4b5 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/Destinations.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminDestinationListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("destinations")] + public List Destinations { get; set; } = new(); + + public class SynapseAdminDestinationListResultDestination { + [JsonPropertyName("destination")] + public string Destination { get; set; } + + [JsonPropertyName("retry_last_ts")] + public long RetryLastTs { get; set; } + + [JsonPropertyName("retry_interval")] + public long RetryInterval { get; set; } + + [JsonPropertyName("failure_ts")] + public long? FailureTs { get; set; } + + [JsonPropertyName("last_successful_stream_ordering")] + public long? LastSuccessfulStreamOrdering { get; set; } + + [JsonIgnore] + public DateTime? FailureTsDateTime { + get => FailureTs.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(FailureTs.Value).DateTime : null; + set => FailureTs = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + + [JsonIgnore] + public DateTime? RetryLastTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(RetryLastTs).DateTime; + set => RetryLastTs = new DateTimeOffset(value.Value).ToUnixTimeMilliseconds(); + } + + [JsonIgnore] + public TimeSpan RetryIntervalTimeSpan { + get => TimeSpan.FromMilliseconds(RetryInterval); + set => RetryInterval = (long)value.TotalMilliseconds; + } + } +} + +public class SynapseAdminDestinationRoomListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("rooms")] + public List Rooms { get; set; } = new(); + + public class SynapseAdminDestinationRoomListResultRoom { + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("stream_ordering")] + public int StreamOrdering { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs new file mode 100644 index 0000000..10fc039 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs @@ -0,0 +1,169 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using ArcaneLibs; +using ArcaneLibs.Attributes; +using ArcaneLibs.Extensions; +using LibMatrix.EventTypes; +using LibMatrix.Extensions; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminEventReportListResult : SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("event_reports")] + public List Reports { get; set; } = new(); + + public class SynapseAdminEventReportListResultReport { + [JsonPropertyName("event_id")] + public string EventId { get; set; } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("reason")] + public string? Reason { get; set; } + + [JsonPropertyName("score")] + public int? Score { get; set; } + + [JsonPropertyName("received_ts")] + public long ReceivedTs { get; set; } + + [JsonPropertyName("canonical_alias")] + public string? CanonicalAlias { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("user_id")] + public string UserId { get; set; } + + [JsonIgnore] + public DateTime ReceivedTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(ReceivedTs).DateTime; + set => ReceivedTs = new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + } + + public class SynapseAdminEventReportListResultReportWithDetails : SynapseAdminEventReportListResultReport { + [JsonPropertyName("event_json")] + public SynapseEventJson EventJson { get; set; } + + public class SynapseEventJson { + [JsonPropertyName("auth_events")] + public List AuthEvents { get; set; } + + [JsonPropertyName("content")] + public JsonObject? RawContent { get; set; } + + [JsonPropertyName("depth")] + public int Depth { get; set; } + + [JsonPropertyName("hashes")] + public Dictionary Hashes { get; set; } + + [JsonPropertyName("origin")] + public string Origin { get; set; } + + [JsonPropertyName("origin_server_ts")] + public long OriginServerTs { get; set; } + + [JsonPropertyName("prev_events")] + public List PrevEvents { get; set; } + + [JsonPropertyName("prev_state")] + public List PrevState { get; set; } + + [JsonPropertyName("room_id")] + public string RoomId { get; set; } + + [JsonPropertyName("sender")] + public string Sender { get; set; } + + [JsonPropertyName("signatures")] + public Dictionary> Signatures { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("unsigned")] + public JsonObject? Unsigned { get; set; } + + // Extra... copied from StateEventResponse + + [JsonIgnore] + public Type MappedType => StateEvent.GetStateEventType(Type); + + [JsonIgnore] + public bool IsLegacyType => MappedType.GetCustomAttributes().FirstOrDefault(x => x.EventName == Type)?.Legacy ?? false; + + [JsonIgnore] + public string FriendlyTypeName => MappedType.GetFriendlyNameOrNull() ?? Type; + + [JsonIgnore] + public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type; + + private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() { + Converters = { + new JsonFloatStringConverter(), + new JsonDoubleStringConverter(), + new JsonDecimalStringConverter() + } + }; + + [JsonIgnore] + [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")] + public EventContent? TypedContent { + get { + ClassCollector.ResolveFromAllAccessibleAssemblies(); + // if (Type == "m.receipt") { + // return null; + // } + try { + var mappedType = StateEvent.GetStateEventType(Type); + if (mappedType == typeof(UnknownEventContent)) + Console.WriteLine($"Warning: unknown event type '{Type}'"); + var deserialisedContent = (EventContent)RawContent.Deserialize(mappedType, TypedContentSerializerOptions)!; + return deserialisedContent; + } + catch (JsonException e) { + Console.WriteLine(e); + Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null")); + } + + return null; + } + set { + if (value is null) + RawContent?.Clear(); + else + RawContent = JsonSerializer.Deserialize(JsonSerializer.Serialize(value, value.GetType(), + new JsonSerializerOptions() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull })); + } + } + + //debug + [JsonIgnore] + public string InternalSelfTypeName { + get { + var res = GetType().Name switch { + "StateEvent`1" => "StateEvent", + _ => GetType().Name + }; + return res; + } + } + + [JsonIgnore] + public string InternalContentTypeName => TypedContent?.GetType().Name ?? "null"; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs new file mode 100644 index 0000000..fa92ef9 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RegistrationTokenListResult.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRegistrationTokenListResult { + [JsonPropertyName("registration_tokens")] + public List RegistrationTokens { get; set; } = new(); + + public class SynapseAdminRegistrationTokenListResultToken { + [JsonPropertyName("token")] + public string Token { get; set; } + + [JsonPropertyName("uses_allowed")] + public int? UsesAllowed { get; set; } + + [JsonPropertyName("pending")] + public int Pending { get; set; } + + [JsonPropertyName("completed")] + public int Completed { get; set; } + + [JsonPropertyName("expiry_time")] + public long? ExpiryTime { get; set; } + + [JsonIgnore] + public DateTime? ExpiryTimeDateTime { + get => ExpiryTime.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(ExpiryTime.Value).DateTime : null; + set => ExpiryTime = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs new file mode 100644 index 0000000..d84c89b --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs @@ -0,0 +1,64 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomListResult { + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("total_rooms")] + public int TotalRooms { get; set; } + + [JsonPropertyName("next_batch")] + public int? NextBatch { get; set; } + + [JsonPropertyName("prev_batch")] + public int? PrevBatch { get; set; } + + [JsonPropertyName("rooms")] + public List Rooms { get; set; } = new(); + + public class SynapseAdminRoomListResultRoom { + [JsonPropertyName("room_id")] + public required string RoomId { get; set; } + + [JsonPropertyName("name")] + public string? Name { get; set; } + + [JsonPropertyName("canonical_alias")] + public string? CanonicalAlias { get; set; } + + [JsonPropertyName("joined_members")] + public int JoinedMembers { get; set; } + + [JsonPropertyName("joined_local_members")] + public int JoinedLocalMembers { get; set; } + + [JsonPropertyName("version")] + public string? Version { get; set; } + + [JsonPropertyName("creator")] + public string? Creator { get; set; } + + [JsonPropertyName("encryption")] + public string? Encryption { get; set; } + + [JsonPropertyName("federatable")] + public bool Federatable { get; set; } + + [JsonPropertyName("public")] + public bool Public { get; set; } + + [JsonPropertyName("join_rules")] + public string? JoinRules { get; set; } + + [JsonPropertyName("guest_access")] + public string? GuestAccess { get; set; } + + [JsonPropertyName("history_visibility")] + public string? HistoryVisibility { get; set; } + + [JsonPropertyName("state_events")] + public int StateEvents { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs new file mode 100644 index 0000000..97e85ad --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomMediaListResult.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomMediaListResult { + [JsonPropertyName("local")] + public List Local { get; set; } = new(); + + [JsonPropertyName("remote")] + public List Remote { get; set; } = new(); +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs new file mode 100644 index 0000000..cb2ec08 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomMemberListResult.cs @@ -0,0 +1,11 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminRoomMemberListResult { + [JsonPropertyName("members")] + public List Members { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs new file mode 100644 index 0000000..3f5f865 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminUserRedactIdResponse.cs @@ -0,0 +1,22 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserRedactIdResponse { + [JsonPropertyName("redact_id")] + public string RedactionId { get; set; } +} + +public class SynapseAdminRedactStatusResponse { + /// + /// One of "scheduled", "active", "completed", "failed" + /// + [JsonPropertyName("status")] + public string Status { get; set; } + + /// + /// Key: Event ID, Value: Error message + /// + [JsonPropertyName("failed_redactions")] + public Dictionary FailedRedactions { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs new file mode 100644 index 0000000..36a5596 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseCollectionResult.cs @@ -0,0 +1,250 @@ +using System.Buffers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using ArcaneLibs.Extensions; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseNextTokenTotalCollectionResult { + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } +} + +// [JsonConverter(typeof(SynapseCollectionJsonConverter<>))] +public class SynapseCollectionResult(string chunkKey = "chunk", string prevTokenKey = "prev_token", string nextTokenKey = "next_token", string totalKey = "total") { + public int? Total { get; set; } + public string? PrevToken { get; set; } + public string? NextToken { get; set; } + public List Chunk { get; set; } = []; + + // TODO: figure out how to provide an IAsyncEnumerable for this + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-utf8jsonreader#read-from-a-stream-using-utf8jsonreader + + // public async IAsyncEnumerable FromJsonAsync(Stream stream) { + // + // } + + public SynapseCollectionResult FromJson(Stream stream, Action action) { + byte[] buffer = new byte[4096]; + _ = stream.Read(buffer); + var reader = new Utf8JsonReader(buffer, isFinalBlock: false, state: default); + + try { + FromJsonInternal(stream, ref buffer, ref reader, action); + } + catch (JsonException e) { + Console.WriteLine($"Caught a JsonException: {e}"); + int hexdumpWidth = 64; + Console.WriteLine($"Check hexdump line {reader.BytesConsumed / hexdumpWidth} index {reader.BytesConsumed % hexdumpWidth}"); + buffer.HexDump(64); + } + finally { } + + return this; + } + + private void FromJsonInternal(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader, Action action) { + while (!reader.IsFinalBlock) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + if (reader.TokenType == JsonTokenType.PropertyName) { + var propName = reader.GetString(); + Console.WriteLine($"SynapseCollectionResult: encountered property name: {propName}"); + + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + Console.WriteLine($"{reader.BytesConsumed}/{stream.Position} {reader.TokenType}"); + + if (propName == totalKey && reader.TokenType == JsonTokenType.Number) { + Total = reader.GetInt32(); + } + else if (propName == prevTokenKey && reader.TokenType == JsonTokenType.String) { + PrevToken = reader.GetString(); + } + else if (propName == nextTokenKey && reader.TokenType == JsonTokenType.String) { + NextToken = reader.GetString(); + } + else if (propName == chunkKey) { + if (reader.TokenType == JsonTokenType.StartArray) { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) { + // if (reader.TokenType == JsonTokenType.EndArray) { + // break; + // } + // Console.WriteLine($"Encountered token in chunk: {reader.TokenType}"); + // var _buf = reader.ValueSequence.ToArray(); + // try { + // var item = JsonSerializer.Deserialize(_buf); + // action(item); + // Chunk.Add(item); + // } + // catch(JsonException e) { + // Console.WriteLine($"Caught a JsonException: {e}"); + // int hexdumpWidth = 64; + // + // // Console.WriteLine($"Check hexdump line {reader.BytesConsumed / hexdumpWidth} index {reader.BytesConsumed % hexdumpWidth}"); + // Console.WriteLine($"Buffer length: {_buf.Length}"); + // _buf.HexDump(64); + // throw; + // } + var item = ReadItem(stream, ref buffer, ref reader); + action(item); + Chunk.Add(item); + } + } + } + } + } + } + + private T ReadItem(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + // handle nullable types + if (typeof(T).IsGenericType && typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>)) { + if (reader.TokenType == JsonTokenType.Null) { + return default(T); + } + } + + // if(typeof(T) == typeof(string)) { + // return (T)(object)reader.GetString(); + // } + // else if(typeof(T) == typeof(int)) { + // return (T)(object)reader.GetInt32(); + // } + // else { + // var _buf = reader.ValueSequence.ToArray(); + // return JsonSerializer.Deserialize(_buf); + // } + + // default branch uses "object?" cast to avoid compiler error + // add more branches here as nessesary + // reader.Read(); + var call = typeof(T) switch { + Type t when t == typeof(string) => reader.GetString(), + _ => ReadObject(stream, ref buffer, ref reader) + }; + + object ReadObject(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new JsonException(); + } + + List objBuffer = [(byte)'{', ..reader.ValueSequence.ToArray()]; + var currentDepth = reader.CurrentDepth; + while (reader.CurrentDepth >= currentDepth) { + while (!reader.Read()) { + GetMoreBytesFromStream(stream, ref buffer, ref reader); + } + + if (reader.TokenType == JsonTokenType.EndObject && reader.CurrentDepth == currentDepth) { + break; + } + + objBuffer.AddRange(reader.ValueSpan); + } + + return JsonSerializer.Deserialize(objBuffer.ToArray()); + } + + return (T)call; + + // return JsonSerializer.Deserialize(ref reader); + } + + private static void GetMoreBytesFromStream(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) { + int bytesRead; + if (reader.BytesConsumed < buffer.Length) { + ReadOnlySpan leftover = buffer.AsSpan((int)reader.BytesConsumed); + + if (leftover.Length == buffer.Length) { + Array.Resize(ref buffer, buffer.Length * 2); + Console.WriteLine($"Increased buffer size to {buffer.Length}"); + } + + leftover.CopyTo(buffer); + bytesRead = stream.Read(buffer.AsSpan(leftover.Length)); + } + else { + bytesRead = stream.Read(buffer); + } + + // Console.WriteLine($"String in buffer is: {Encoding.UTF8.GetString(buffer)}"); + reader = new Utf8JsonReader(buffer, isFinalBlock: bytesRead == 0, reader.CurrentState); + } +} + +public partial class SynapseCollectionJsonConverter : JsonConverter> { + public override SynapseCollectionResult? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType != JsonTokenType.StartObject) { + throw new JsonException(); + } + + var result = new SynapseCollectionResult(); + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType != JsonTokenType.PropertyName) { + throw new JsonException(); + } + + var propName = reader.GetString(); + reader.Read(); + if (propName == "total") { + result.Total = reader.GetInt32(); + } + else if (propName == "prev_token") { + result.PrevToken = reader.GetString(); + } + else if (propName == "next_token") { + result.NextToken = reader.GetString(); + } + else if (propName == "chunk") { + if (reader.TokenType != JsonTokenType.StartArray) { + throw new JsonException(); + } + + while (reader.Read()) { + if (reader.TokenType == JsonTokenType.EndArray) { + break; + } + + var item = JsonSerializer.Deserialize(ref reader, options); + result.Chunk.Add(item); + } + } + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, SynapseCollectionResult value, JsonSerializerOptions options) { + writer.WriteStartObject(); + if (value.Total is not null) + writer.WriteNumber("total", value.Total ?? 0); + if (value.PrevToken is not null) + writer.WriteString("prev_token", value.PrevToken); + if (value.NextToken is not null) + writer.WriteString("next_token", value.NextToken); + + writer.WriteStartArray("chunk"); + foreach (var item in value.Chunk) { + JsonSerializer.Serialize(writer, item, options); + } + + writer.WriteEndArray(); + writer.WriteEndObject(); + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs new file mode 100644 index 0000000..5530cc3 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseUserMediaResult.cs @@ -0,0 +1,40 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserMediaResult { + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } + + [JsonPropertyName("media")] + public List Media { get; set; } = new(); + + public class MediaInfo { + [JsonPropertyName("created_ts")] + public long CreatedTimestamp { get; set; } + + [JsonPropertyName("last_access_ts")] + public long? LastAccessTimestamp { get; set; } + + [JsonPropertyName("media_id")] + public string MediaId { get; set; } + + [JsonPropertyName("media_length")] + public int MediaLength { get; set; } + + [JsonPropertyName("media_type")] + public string MediaType { get; set; } + + [JsonPropertyName("quarantined_by")] + public string? QuarantinedBy { get; set; } + + [JsonPropertyName("safe_from_quarantine")] + public bool SafeFromQuarantine { get; set; } + + [JsonPropertyName("upload_name")] + public string UploadName { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs new file mode 100644 index 0000000..3132906 --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; + +public class SynapseAdminUserListResult { + [JsonPropertyName("offset")] + public int Offset { get; set; } + + [JsonPropertyName("total")] + public int Total { get; set; } + + [JsonPropertyName("next_token")] + public string? NextToken { get; set; } + + [JsonPropertyName("users")] + public List Users { get; set; } = new(); + + public class SynapseAdminUserListResultUser { + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("is_guest")] + public bool? IsGuest { get; set; } + + [JsonPropertyName("admin")] + public bool? Admin { get; set; } + + [JsonPropertyName("user_type")] + public string? UserType { get; set; } + + [JsonPropertyName("deactivated")] + public bool Deactivated { get; set; } + + [JsonPropertyName("erased")] + public bool Erased { get; set; } + + [JsonPropertyName("shadow_banned")] + public bool ShadowBanned { get; set; } + + [JsonPropertyName("displayname")] + public string? DisplayName { get; set; } + + [JsonPropertyName("avatar_url")] + public string? AvatarUrl { get; set; } + + [JsonPropertyName("creation_ts")] + public long CreationTs { get; set; } + + [JsonPropertyName("last_seen_ts")] + public long? LastSeenTs { get; set; } + + [JsonPropertyName("locked")] + public bool Locked { get; set; } + + // Requires enabling MSC3866 + [JsonPropertyName("approved")] + public bool? Approved { get; set; } + + [JsonIgnore] + public DateTime CreationTsDateTime { + get => DateTimeOffset.FromUnixTimeMilliseconds(CreationTs).DateTime; + set => CreationTs = new DateTimeOffset(value).ToUnixTimeMilliseconds(); + } + + [JsonIgnore] + public DateTime? LastSeenTsDateTime { + get => LastSeenTs.HasValue ? DateTimeOffset.FromUnixTimeMilliseconds(LastSeenTs.Value).DateTime : null; + set => LastSeenTs = value.HasValue ? new DateTimeOffset(value.Value).ToUnixTimeMilliseconds() : null; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs index ac94a7a..3ed7311 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs @@ -1,90 +1,155 @@ +// #define LOG_SKIP + +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; using ArcaneLibs.Extensions; -using LibMatrix.Filters; -using LibMatrix.Responses.Admin; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; +using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; +using LibMatrix.Responses; namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedHomeserver) { - public async IAsyncEnumerable SearchRoomsAsync(int limit = int.MaxValue, string orderBy = "name", string dir = "f", - string? searchTerm = null, LocalRoomQueryFilter? localFilter = null) { - AdminRoomListingResult? res = null; + private SynapseAdminUserCleanupExecutor UserCleanupExecutor { get; } = new(authenticatedHomeserver); + // https://github.com/element-hq/synapse/tree/develop/docs/admin_api + // https://github.com/element-hq/synapse/tree/develop/docs/usage/administration/admin_api + +#region Rooms + + public async IAsyncEnumerable SearchRoomsAsync(int limit = int.MaxValue, int chunkLimit = 250, + string orderBy = "name", string dir = "f", string? searchTerm = null, SynapseAdminLocalRoomQueryFilter? localFilter = null) { + SynapseAdminRoomListResult? res = null; var i = 0; int? totalRooms = null; do { - var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 250)}&dir={dir}&order_by={orderBy}"; + var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, chunkLimit)}&dir={dir}&order_by={orderBy}"; if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); - res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url); + res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url); totalRooms ??= res.TotalRooms; - Console.WriteLine(res.ToJson(false)); + // Console.WriteLine(res.ToJson(false)); foreach (var room in res.Rooms) { if (localFilter is not null) { - if (!room.RoomId.Contains(localFilter.RoomIdContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.RoomIdContains) && !room.RoomId.Contains(localFilter.RoomIdContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomid."); +#endif continue; } - if (!room.Name?.Contains(localFilter.NameContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.NameContains) && room.Name?.Contains(localFilter.NameContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomname."); +#endif continue; } - if (!room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.CanonicalAliasContains) && + room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule alias."); +#endif continue; } - if (!room.Version.Contains(localFilter.VersionContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.VersionContains) && !room.Version.Contains(localFilter.VersionContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule version."); +#endif continue; } - if (!room.Creator.Contains(localFilter.CreatorContains)) { + if (!string.IsNullOrWhiteSpace(localFilter.CreatorContains) && !room.Creator.Contains(localFilter.CreatorContains, StringComparison.OrdinalIgnoreCase)) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule creator."); +#endif continue; } - if (!room.Encryption?.Contains(localFilter.EncryptionContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.EncryptionContains) && + room.Encryption?.Contains(localFilter.EncryptionContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule encryption."); +#endif continue; } - if (!room.JoinRules?.Contains(localFilter.JoinRulesContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.JoinRulesContains) && + room.JoinRules?.Contains(localFilter.JoinRulesContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joinrules."); +#endif continue; } - if (!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.GuestAccessContains) && + room.GuestAccess?.Contains(localFilter.GuestAccessContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule guestaccess."); +#endif continue; } - if (!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) { + if (!string.IsNullOrWhiteSpace(localFilter.HistoryVisibilityContains) && + room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains, StringComparison.OrdinalIgnoreCase) != true) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule history visibility."); +#endif continue; } if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule federation."); +#endif continue; } if (localFilter.CheckPublic && room.Public != localFilter.Public) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule public."); +#endif continue; } + if (room.StateEvents < localFilter.StateEventsGreaterThan || room.StateEvents > localFilter.StateEventsLessThan) { + totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members."); +#endif + continue; + } + if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined members: {localFilter.JoinedMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedMembersLessThan}."); +#endif continue; } if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members: {localFilter.JoinedLocalMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedLocalMembersLessThan}."); +#endif continue; } } @@ -104,4 +169,367 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH } } while (i < Math.Min(limit, totalRooms ?? limit)); } + +#endregion + +#region Users + + public async IAsyncEnumerable SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250, + SynapseAdminLocalUserQueryFilter? localFilter = null) { + // TODO: implement filters + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v3/users", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying User List with URL: {url} ---"); + // TODO: implement URI methods in http client + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + foreach (var user in res.Users) { + limit--; + yield return user; + } + + if (string.IsNullOrWhiteSpace(res.NextToken)) break; + from = res.NextToken; + } + } + + public async Task LoginUserAsync(string userId, TimeSpan expireAfter) { + var url = new Uri($"/_synapse/admin/v1/users/{userId.UrlEncode()}/login", UriKind.Relative); + url.AddQuery("valid_until_ms", DateTimeOffset.UtcNow.Add(expireAfter).ToUnixTimeMilliseconds().ToString()); + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), new()); + var loginResp = await resp.Content.ReadFromJsonAsync(); + loginResp.UserId = userId; // Synapse only returns the access token + return loginResp; + } + +#endregion + +#region Reports + + public async IAsyncEnumerable GetEventReportsAsync(int limit = int.MaxValue, int chunkLimit = 250, + string dir = "f", SynapseAdminLocalEventReportQueryFilter? filter = null) { + // TODO: implement filters + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v1/event_reports", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Reports with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + foreach (var report in res.Reports) { + limit--; + yield return report; + } + + if (string.IsNullOrWhiteSpace(res.NextToken)) break; + from = res.NextToken; + } + } + + public async Task GetEventReportDetailsAsync(string reportId) { + var url = new Uri($"/_synapse/admin/v1/event_reports/{reportId.UrlEncode()}", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient + .GetFromJsonAsync(url.ToString()); + } + + // Utility function to get details straight away + public async IAsyncEnumerable GetEventReportsWithDetailsAsync(int limit = int.MaxValue, + int chunkLimit = 250, string dir = "f", SynapseAdminLocalEventReportQueryFilter? filter = null) { + Queue> tasks = []; + await foreach (var report in GetEventReportsAsync(limit, chunkLimit, dir, filter)) { + tasks.Enqueue(GetEventReportDetailsAsync(report.Id)); + while (tasks.Peek().IsCompleted) yield return await tasks.Dequeue(); // early return if possible + } + + while (tasks.Count > 0) yield return await tasks.Dequeue(); + } + + public async Task DeleteEventReportAsync(string reportId) { + var url = new Uri($"/_synapse/admin/v1/event_reports/{reportId.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.DeleteAsync(url.ToString()); + } + +#endregion + +#region Background Updates + + public async Task GetBackgroundUpdatesEnabledAsync() { + var url = new Uri("/_synapse/admin/v1/background_updates/enabled", UriKind.Relative); + // The return type is technically wrong, but includes the field we want. + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + return resp.Enabled; + } + + public async Task SetBackgroundUpdatesEnabledAsync(bool enabled) { + var url = new Uri("/_synapse/admin/v1/background_updates/enabled", UriKind.Relative); + // The used types are technically wrong, but include the field we want. + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), new JsonObject { + ["enabled"] = enabled + }); + var json = await resp.Content.ReadFromJsonAsync(); + return json!.Enabled; + } + + public async Task GetBackgroundUpdatesStatusAsync() { + var url = new Uri("/_synapse/admin/v1/background_updates/status", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + } + + /// + /// Run a background job + /// + /// One of "populate_stats_process_rooms" or "regenerate_directory" + public async Task RunBackgroundJobsAsync(string jobName) { + var url = new Uri("/_synapse/admin/v1/background_updates/run", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), new JsonObject() { + ["job_name"] = jobName + }); + } + +#endregion + +#region Federation + + public async IAsyncEnumerable GetFederationDestinationsAsync(int limit = int.MaxValue, + int chunkLimit = 250) { + string? from = null; + while (limit > 0) { + var url = new Uri("/_synapse/admin/v1/federation/destinations", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Federation Destinations with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + foreach (var dest in res.Destinations) { + limit--; + yield return dest; + } + } + } + + public async Task GetFederationDestinationDetailsAsync(string destination) { + var url = new Uri($"/_synapse/admin/v1/federation/destinations/{destination}", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + } + + public async IAsyncEnumerable GetFederationDestinationRoomsAsync(string destination, + int limit = int.MaxValue, int chunkLimit = 250) { + string? from = null; + while (limit > 0) { + var url = new Uri($"/_synapse/admin/v1/federation/destinations/{destination}/rooms", UriKind.Relative); + url = url.AddQuery("limit", Math.Min(limit, chunkLimit).ToString()); + if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from); + Console.WriteLine($"--- ADMIN Querying Federation Destination Rooms with URL: {url} ---"); + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + foreach (var room in res.Rooms) { + limit--; + yield return room; + } + } + } + + public async Task ResetFederationConnectionTimeoutAsync(string destination) { + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/federation/destinations/{destination}/reset_connection", new JsonObject()); + } + +#endregion + +#region Registration Tokens + + // does not support pagination + public async Task> GetRegistrationTokensAsync() { + var url = new Uri("/_synapse/admin/v1/registration_tokens", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + return resp.RegistrationTokens; + } + + public async Task GetRegistrationTokenAsync(string token) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + var resp = + await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + return resp; + } + + public async Task CreateRegistrationTokenAsync( + SynapseAdminRegistrationTokenCreateRequest request) { + var url = new Uri("/_synapse/admin/v1/", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), request); + var token = await resp.Content.ReadFromJsonAsync(); + return token!; + } + + public async Task UpdateRegistrationTokenAsync(string token, + SynapseAdminRegistrationTokenUpdateRequest request) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync(url.ToString(), request); + return await resp.Content.ReadFromJsonAsync(); + } + + public async Task DeleteRegistrationTokenAsync(string token) { + var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.DeleteAsync(url.ToString()); + } + +#endregion + +#region Account Validity + + // Does anyone even use this? + // README: https://github.com/matrix-org/synapse/issues/15271 + // -> Don't implement unless requested, if not for this feature almost never being used. + +#endregion + +#region Experimental Features + + public async Task> GetExperimentalFeaturesAsync(string userId) { + var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative); + var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + return resp["features"]!.GetValue>(); + } + + public async Task SetExperimentalFeaturesAsync(string userId, Dictionary features) { + var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative); + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync(url.ToString(), new JsonObject { + ["features"] = JsonSerializer.Deserialize(features.ToJson()) + }); + } + +#endregion + +#region Media + + public async Task GetRoomMediaAsync(string roomId) { + var url = new Uri($"/_synapse/admin/v1/room/{roomId.UrlEncode()}/media", UriKind.Relative); + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url.ToString()); + } + + // This is in the user admin API section + // public async IAsyncEnumerable + +#endregion + + public async Task DeleteAllMessages(string mxid, List? rooms = null, string? reason = null, int? limit = 100000, + bool waitForCompletion = true) { + rooms ??= []; + + Dictionary payload = new(); + if (rooms.Count > 0) payload["rooms"] = rooms; + if (!string.IsNullOrEmpty(reason)) payload["reason"] = reason; + if (limit.HasValue) payload["limit"] = limit.Value; + + var redactIdResp = await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/user/{mxid}/redact", payload); + var redactId = await redactIdResp.Content.ReadFromJsonAsync(); + + if (waitForCompletion) { + while (true) { + var status = await GetRedactStatus(redactId!.RedactionId); + if (status?.Status != "pending") break; + await Task.Delay(1000); + } + } + + return redactId; + } + + public async Task GetRedactStatus(string redactId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync( + $"/_synapse/admin/v1/user/redact_status/{redactId}"); + } + + public async Task DeactivateUserAsync(string mxid, bool erase = false, bool eraseMessages = false, bool extraCleanup = false) { + if (eraseMessages) { + await DeleteAllMessages(mxid); + } + + if (extraCleanup) { + await UserCleanupExecutor.CleanupUser(mxid); + } + + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/deactivate", new { erase }); + } + + public async Task ResetPasswordAsync(string mxid, string newPassword, bool logoutDevices = false) { + await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync($"/_synapse/admin/v1/reset_password/{mxid}", + new { new_password = newPassword, logout_devices = logoutDevices }); + } + + public async Task GetUserMediaAsync(string mxid, int? limit = 100, string? from = null, string? orderBy = null, string? dir = null) { + var url = $"/_synapse/admin/v1/users/{mxid}/media"; + if (limit.HasValue) url += $"?limit={limit}"; + if (!string.IsNullOrEmpty(from)) url += $"&from={from}"; + if (!string.IsNullOrEmpty(orderBy)) url += $"&order_by={orderBy}"; + if (!string.IsNullOrEmpty(dir)) url += $"&dir={dir}"; + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync(url); + } + + public async IAsyncEnumerable GetUserMediaEnumerableAsync(string mxid, int chunkSize = 100, string? orderBy = null, string? dir = null) { + SynapseAdminUserMediaResult? res = null; + do { + res = await GetUserMediaAsync(mxid, chunkSize, res?.NextToken, orderBy, dir); + foreach (var media in res.Media) { + yield return media; + } + } while (!string.IsNullOrEmpty(res.NextToken)); + } + + public async Task BlockRoom(string roomId, bool block = true) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/rooms/{roomId}/block", new { + block + }); + } + + public async Task DeleteRoom(string roomId, SynapseAdminRoomDeleteRequest request, bool waitForCompletion = true) { + var resp = await authenticatedHomeserver.ClientHttpClient.DeleteAsJsonAsync($"/_synapse/admin/v2/rooms/{roomId}", request); + var deleteResp = await resp.Content.ReadFromJsonAsync(); + + if (waitForCompletion) { + while (true) { + var status = await GetRoomDeleteStatus(deleteResp!.DeleteId); + if (status?.Status != "pending") break; + await Task.Delay(1000); + } + } + + return deleteResp!; + } + + public async Task GetRoomDeleteStatusByRoomId(string roomId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync( + $"/_synapse/admin/v2/rooms/{roomId}/delete_status"); + } + + public async Task GetRoomDeleteStatus(string deleteId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync( + $"/_synapse/admin/v2/rooms/delete_status/{deleteId}"); + } + + public async Task GetRoomMembersAsync(string roomId) { + return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync($"/_synapse/admin/v1/rooms/{roomId}/members"); + } + + public async Task QuarantineMediaByRoomId(string roomId) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/room/{roomId}/media/quarantine", new { }); + } + + public async Task QuarantineMediaByUserId(string mxid) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/user/{mxid}/media/quarantine", new { }); + } + + public async Task QuarantineMediaById(string serverName, string mediaId) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/media/quarantine/{serverName}/{mediaId}", new { }); + } + + public async Task QuarantineMediaById(MxcUri mxcUri) { + await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/media/quarantine/{mxcUri.ServerName}/{mxcUri.MediaId}", new { }); + } + + public async Task DeleteMediaById(string serverName, string mediaId) { + await authenticatedHomeserver.ClientHttpClient.DeleteAsync($"/_synapse/admin/v1/media/{serverName}/{mediaId}"); + } + + public async Task DeleteMediaById(MxcUri mxcUri) { + await authenticatedHomeserver.ClientHttpClient.DeleteAsync($"/_synapse/admin/v1/media/{mxcUri.ServerName}/{mxcUri.MediaId}"); + } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs new file mode 100644 index 0000000..6edf40c --- /dev/null +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminUserCleanupExecutor.cs @@ -0,0 +1,27 @@ +namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; + +public class SynapseAdminUserCleanupExecutor(AuthenticatedHomeserverSynapse homeserver) { + /* + Remove mappings of SSO IDs + Delete media uploaded by user (included avatar images) + Delete sent and received messages + Remove the user's creation (registration) timestamp + Remove rate limit overrides + Remove from monthly active users + Remove user's consent information (consent version and timestamp) + */ + public async Task CleanupUser(string mxid) { + // change the user's password to a random one + var newPassword = Guid.NewGuid().ToString(); + await homeserver.Admin.ResetPasswordAsync(mxid, newPassword, true); + await homeserver.Admin.DeleteAllMessages(mxid); + + } + private async Task RunUserTasks(string mxid) { + var auth = await homeserver.Admin.LoginUserAsync(mxid, TimeSpan.FromDays(1)); + var userHs = new AuthenticatedHomeserverSynapse(homeserver.ServerName, homeserver.WellKnownUris, null, auth.AccessToken); + await userHs.Initialise(); + + + } +} \ No newline at end of file diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj index 4814a18..042d943 100644 --- a/LibMatrix/LibMatrix.csproj +++ b/LibMatrix/LibMatrix.csproj @@ -18,7 +18,7 @@ - + diff --git a/LibMatrix/MxcUri.cs b/LibMatrix/MxcUri.cs index 02a8fa6..875ae53 100644 --- a/LibMatrix/MxcUri.cs +++ b/LibMatrix/MxcUri.cs @@ -3,9 +3,9 @@ using System.Diagnostics.CodeAnalysis; namespace LibMatrix; public class MxcUri { - public required string ServerName; - public required string MediaId; - + public required string ServerName { get; set; } + public required string MediaId { get; set; } + public static MxcUri Parse([StringSyntax("Uri")] string mxcUri) { if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri)); var parts = mxcUri[6..].Split('/'); @@ -15,13 +15,13 @@ public class MxcUri { MediaId = parts[1] }; } - + public static implicit operator MxcUri(string mxcUri) => Parse(mxcUri); public static implicit operator string(MxcUri mxcUri) => $"mxc://{mxcUri.ServerName}/{mxcUri.MediaId}"; public static implicit operator (string, string)(MxcUri mxcUri) => (mxcUri.ServerName, mxcUri.MediaId); public static implicit operator MxcUri((string serverName, string mediaId) mxcUri) => (mxcUri.serverName, mxcUri.mediaId); // public override string ToString() => $"mxc://{ServerName}/{MediaId}"; - + public string ToDownloadUri(string? baseUrl = null, string? filename = null, int? timeout = null) { var uri = $"{baseUrl}/_matrix/client/v1/media/download/{ServerName}/{MediaId}"; if (filename is not null) uri += $"/{filename}"; diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs index 8f1b56d..fb56f2e 100644 --- a/LibMatrix/RoomTypes/GenericRoom.cs +++ b/LibMatrix/RoomTypes/GenericRoom.cs @@ -143,7 +143,7 @@ public class GenericRoom { var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?dir={dir}"; if (!string.IsNullOrWhiteSpace(from)) url += $"&from={from}"; if (limit is not null) url += $"&limit={limit}"; - else if (!string.IsNullOrWhiteSpace(filter)) url += $"&filter={filter}"; + if (!string.IsNullOrWhiteSpace(filter)) url += $"&filter={filter}"; var res = await Homeserver.ClientHttpClient.GetFromJsonAsync(url); return res; diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs index fa75f1e..01b11cc 100644 --- a/LibMatrix/Services/HomeserverResolverService.cs +++ b/LibMatrix/Services/HomeserverResolverService.cs @@ -49,6 +49,7 @@ public class HomeserverResolverService { ArgumentNullException.ThrowIfNull(homeserver); _logger.LogTrace("Resolving client well-known: {homeserver}", homeserver); ClientWellKnown? clientWellKnown = null; + homeserver = homeserver.TrimEnd('/'); // check if homeserver has a client well-known if (homeserver.StartsWith("https://")) { clientWellKnown = await _httpClient.TryGetFromJsonAsync($"{homeserver}/.well-known/matrix/client"); @@ -80,6 +81,7 @@ public class HomeserverResolverService { ArgumentNullException.ThrowIfNull(homeserver); _logger.LogTrace($"Resolving server well-known: {homeserver}"); ServerWellKnown? serverWellKnown = null; + homeserver = homeserver.TrimEnd('/'); // check if homeserver has a server well-known if (homeserver.StartsWith("https://")) { serverWellKnown = await _httpClient.TryGetFromJsonAsync($"{homeserver}/.well-known/matrix/server"); @@ -95,7 +97,7 @@ public class HomeserverResolverService { _logger.LogInformation("Server well-known for {hs}: {json}", homeserver, serverWellKnown?.ToJson() ?? "null"); if (!string.IsNullOrWhiteSpace(serverWellKnown?.Homeserver)) { - var resolved = serverWellKnown.Homeserver; + var resolved = serverWellKnown.Homeserver.TrimEnd('/'); if (resolved.StartsWith("https://") || resolved.StartsWith("http://")) return resolved; if (await _httpClient.CheckSuccessStatus($"https://{resolved}/_matrix/federation/v1/version")) @@ -106,7 +108,7 @@ public class HomeserverResolverService { } // fallback: most servers host C2S and S2S on the same domain - var clientUrl = await _tryResolveClientEndpoint(homeserver); + var clientUrl = (await _tryResolveClientEndpoint(homeserver)).TrimEnd('/'); if (clientUrl is not null && await _httpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version")) return clientUrl; diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs index ecc3f09..5ffd43a 100644 --- a/LibMatrix/Services/ServiceInstaller.cs +++ b/LibMatrix/Services/ServiceInstaller.cs @@ -1,4 +1,5 @@ -using LibMatrix.Services.WellKnownResolvers; +using LibMatrix.Services.WellKnownResolver; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; using Microsoft.Extensions.DependencyInjection; namespace LibMatrix.Services; @@ -10,6 +11,10 @@ public static class ServiceInstaller { //Add services services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + if (!services.Any(x => x.ServiceType == typeof(WellKnownResolverConfiguration))) + services.AddSingleton(); services.AddSingleton(); // Legacy services.AddSingleton(); diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs new file mode 100644 index 0000000..26a4c43 --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverConfiguration.cs @@ -0,0 +1,49 @@ +using System.Text.Json.Serialization; + +namespace LibMatrix.Services.WellKnownResolver; + +public class WellKnownResolverConfiguration { + /// + /// Allow transparent downgrades to plaintext HTTP if HTTPS fails + /// Enabling this is unsafe! + /// + [JsonPropertyName("allow_http")] + public bool AllowHttp { get; set; } = false; + + /// + /// Use DNS resolution if available, for resolving SRV records + /// + [JsonPropertyName("allow_dns")] + public bool AllowDns { get; set; } = true; + + /// + /// Use system resolver(s) if empty + /// + [JsonPropertyName("dns_servers")] + public List DnsServers { get; set; } = new(); + + /// + /// Same as AllowDns, but for DNS over HTTPS - useful in browser contexts + /// + [JsonPropertyName("allow_doh")] + public bool AllowDoh { get; set; } = true; + + /// + /// Use DNS over HTTPS - useful in browser contexts + /// Disabled if empty + /// + [JsonPropertyName("doh_servers")] + public List DohServers { get; set; } = new(); + + /// + /// Whether to allow fallback subdomain lookups + /// + [JsonPropertyName("allow_fallback_subdomains")] + public bool AllowFallbackSubdomains { get; set; } = true; + + /// + /// Fallback subdomains to try if the homeserver is not found + /// + [JsonPropertyName("fallback_subdomains")] + public List FallbackSubdomains { get; set; } = ["matrix", "chat", "im"]; +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs new file mode 100644 index 0000000..4c78347 --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolverService.cs @@ -0,0 +1,91 @@ +using System.Diagnostics; +using System.Text.Json.Serialization; +using LibMatrix.Extensions; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; + +namespace LibMatrix.Services.WellKnownResolver; + +public class WellKnownResolverService { + private readonly MatrixHttpClient _httpClient = new(); + + private readonly ILogger _logger; + private readonly ClientWellKnownResolver _clientWellKnownResolver; + private readonly SupportWellKnownResolver _supportWellKnownResolver; + private readonly ServerWellKnownResolver _serverWellKnownResolver; + private readonly WellKnownResolverConfiguration _configuration; + + public WellKnownResolverService(ILogger logger, ClientWellKnownResolver clientWellKnownResolver, SupportWellKnownResolver supportWellKnownResolver, + WellKnownResolverConfiguration configuration, ServerWellKnownResolver serverWellKnownResolver) { + _logger = logger; + _clientWellKnownResolver = clientWellKnownResolver; + _supportWellKnownResolver = supportWellKnownResolver; + _configuration = configuration; + _serverWellKnownResolver = serverWellKnownResolver; + if (logger is NullLogger) { + var stackFrame = new StackTrace(true).GetFrame(1); + Console.WriteLine( + $"WARN | Null logger provided to WellKnownResolverService!\n{stackFrame?.GetMethod()?.DeclaringType?.ToString() ?? "null"} at {stackFrame?.GetFileName() ?? "null"}:{stackFrame?.GetFileLineNumber().ToString() ?? "null"}"); + } + } + + public async Task TryResolveWellKnownRecords(string homeserver, bool includeClient = true, bool includeServer = true, bool includeSupport = true, + WellKnownResolverConfiguration? config = null) { + WellKnownRecords records = new(); + _logger.LogDebug($"Resolving well-knowns for {homeserver}"); + if (includeClient && await _clientWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } clientResult) { + records.ClientWellKnown = clientResult; + } + + if (includeServer && await _serverWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } serverResult) { + records.ServerWellKnown = serverResult; + } + + if (includeSupport && await _supportWellKnownResolver.TryResolveWellKnown(homeserver, config ?? _configuration) is { } supportResult) { + records.SupportWellKnown = supportResult; + } + + return records; + } + + public class WellKnownRecords { + public WellKnownResolutionResult? ClientWellKnown { get; set; } + public WellKnownResolutionResult? ServerWellKnown { get; set; } + public WellKnownResolutionResult? SupportWellKnown { get; set; } + } + + public class WellKnownResolutionResult { + public WellKnownSource Source { get; set; } + public string? SourceUri { get; set; } + public T? Content { get; set; } + public List Warnings { get; set; } = []; + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WellKnownSource { + None, + Https, + Dns, + Http, + ManualCheck, + Search + } + + public struct WellKnownResolutionWarning { + public WellKnownResolutionWarningType Type { get; set; } + public string Message { get; set; } + [JsonIgnore] + public Exception? Exception { get; set; } + public string? ExceptionMessage => Exception?.Message; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum WellKnownResolutionWarningType { + None, + Exception, + InvalidResponse, + Timeout, + SlowResponse + } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs new file mode 100644 index 0000000..cbe5b0a --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/BaseWellKnownResolver.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Net.Http.Json; +using ArcaneLibs.Collections; +using LibMatrix.Extensions; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class BaseWellKnownResolver where T : class, new() { + internal static readonly SemaphoreCache> WellKnownCache = new() { + StoreNulls = false + }; + + internal static readonly MatrixHttpClient HttpClient = new(); + + internal async Task> TryGetWellKnownFromUrl(string url, + WellKnownResolverService.WellKnownSource source) { + var sw = Stopwatch.StartNew(); + try { + var request = await HttpClient.GetAsync(url); + sw.Stop(); + var result = new WellKnownResolverService.WellKnownResolutionResult { + Content = await request.Content.ReadFromJsonAsync(), + Source = source, + SourceUri = url, + Warnings = [] + }; + + if (sw.ElapsedMilliseconds > 1000) { + // logger.LogWarning($"Support well-known resolution took {sw.ElapsedMilliseconds}ms: {url}"); + result.Warnings.Add(new() { + Type = WellKnownResolverService.WellKnownResolutionWarning.WellKnownResolutionWarningType.SlowResponse, + Message = $"Well-known resolution took {sw.ElapsedMilliseconds}ms" + }); + } + + return result; + } + catch (Exception e) { + return new WellKnownResolverService.WellKnownResolutionResult { + Source = source, + SourceUri = url, + Warnings = [ + new() { + Exception = e, + Type = WellKnownResolverService.WellKnownResolutionWarning.WellKnownResolutionWarningType.Exception, + Message = e.Message + } + ] + }; + } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs new file mode 100644 index 0000000..f8de38d --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ClientWellKnownResolver.cs @@ -0,0 +1,42 @@ +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; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class ClientWellKnownResolver(ILogger logger, WellKnownResolverConfiguration configuration) + : BaseWellKnownResolver { + private static readonly SemaphoreCache> ClientWellKnownCache = new() { + StoreNulls = false + }; + + private static readonly MatrixHttpClient HttpClient = new(); + + public Task> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving client well-known: {homeserver}"); + + WellKnownResolverService.WellKnownResolutionResult result = + await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/client", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) return result; + + + return result; + }); + } +} + +public class ClientWellKnown { + [JsonPropertyName("m.homeserver")] + public WellKnownHomeserver Homeserver { get; set; } + + public class WellKnownHomeserver { + [JsonPropertyName("base_url")] + public required string BaseUrl { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs new file mode 100644 index 0000000..a99185c --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/ServerWellKnownResolver.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; +using ArcaneLibs.Collections; +using LibMatrix.Extensions; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.ServerWellKnown; +using ResultType = + LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class ServerWellKnownResolver(ILogger logger, WellKnownResolverConfiguration configuration) + : BaseWellKnownResolver { + private static readonly SemaphoreCache> ClientWellKnownCache = new() { + StoreNulls = false + }; + + private static readonly MatrixHttpClient HttpClient = new(); + + public Task> TryResolveWellKnown(string homeserver, WellKnownResolverConfiguration? config = null) { + config ??= configuration; + return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { + logger.LogTrace($"Resolving client well-known: {homeserver}"); + + WellKnownResolverService.WellKnownResolutionResult result = + await TryGetWellKnownFromUrl($"https://{homeserver}/.well-known/matrix/server", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) return result; + + + return result; + }); + } +} + + +public class ServerWellKnown { + [JsonPropertyName("m.server")] + public string Homeserver { get; set; } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs new file mode 100644 index 0000000..99313db --- /dev/null +++ b/LibMatrix/Services/WellKnownResolver/WellKnownResolvers/SupportWellKnownResolver.cs @@ -0,0 +1,44 @@ +using System.Diagnostics; +using System.Net.Http.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Logging; +using WellKnownType = LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown; +using ResultType = LibMatrix.Services.WellKnownResolver.WellKnownResolverService.WellKnownResolutionResult< + LibMatrix.Services.WellKnownResolver.WellKnownResolvers.SupportWellKnown? +>; + +namespace LibMatrix.Services.WellKnownResolver.WellKnownResolvers; + +public class SupportWellKnownResolver(ILogger logger, WellKnownResolverConfiguration configuration) : BaseWellKnownResolver { + public Task 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/support", WellKnownResolverService.WellKnownSource.Https); + if (result.Content != null) + return result; + + return null; + }); + } +} + +public class SupportWellKnown { + [JsonPropertyName("contacts")] + public List? Contacts { get; set; } + + [JsonPropertyName("support_page")] + public Uri? SupportPage { get; set; } + + public class WellKnownContact { + [JsonPropertyName("email_address")] + public string? EmailAddress { get; set; } + + [JsonPropertyName("matrix_id")] + public string? MatrixId { get; set; } + + [JsonPropertyName("role")] + public required string Role { get; set; } + } +} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolverService.cs b/LibMatrix/Services/WellKnownResolverService.cs deleted file mode 100644 index ab2660f..0000000 --- a/LibMatrix/Services/WellKnownResolverService.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System.Diagnostics; -using System.Text.Json.Serialization; -using ArcaneLibs.Collections; -using ArcaneLibs.Extensions; -using LibMatrix.Extensions; -using LibMatrix.Services.WellKnownResolvers; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; - -namespace LibMatrix.Services; - -public class WellKnownResolverService { - private readonly MatrixHttpClient _httpClient = new(); - - private readonly ILogger _logger; - - public WellKnownResolverService(ILogger logger) { - _logger = logger; - if (logger is NullLogger) { - var stackFrame = new StackTrace(true).GetFrame(1); - Console.WriteLine( - $"WARN | Null logger provided to WellKnownResolverService!\n{stackFrame?.GetMethod()?.DeclaringType?.ToString() ?? "null"} at {stackFrame?.GetFileName() ?? "null"}:{stackFrame?.GetFileLineNumber().ToString() ?? "null"}"); - } - } - - public async Task TryResolveWellKnownRecords(string homeserver) { - WellKnownRecords records = new(); - _logger.LogDebug($"Resolving well-knowns for {homeserver}"); - - return records; - } - - - - public class ServerWellKnown { - [JsonPropertyName("m.server")] - public required string Homeserver { get; set; } - } - - public class WellKnownRecords { - public ClientWellKnownResolver.ClientWellKnown? ClientWellKnown { get; set; } - public ServerWellKnown? ServerWellKnown { get; set; } - public SupportWellKnownResolver.SupportWellKnown? SupportWellKnown { get; set; } - - /// - /// Reports the source of the client well-known data. - /// - public WellKnownSource? ClientWellKnownSource { get; set; } - - /// - /// Reports the source of the server well-known data. - /// - public WellKnownSource? ServerWellKnownSource { get; set; } - - /// - /// Reports the source of the support well-known data. - /// - public WellKnownSource? SupportWellKnownSource { get; set; } - } - - public struct WellKnownResolutionResult { - public WellKnownResolverService.WellKnownSource Source { get; set; } - public T WellKnown { get; set; } - public List Warnings { get; set; } - } - - public enum WellKnownSource { - None, - Https, - Dns, - Http, - ManualCheck, - Search - } - - public struct WellKnownResolutionWarning { - public WellKnownResolutionWarningType Type { get; set; } - public string Message { get; set; } - public Exception? Exception { get; set; } - - public enum WellKnownResolutionWarningType { - None, - Exception, - InvalidResponse, - Timeout - } - } -} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolvers/ClientWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolvers/ClientWellKnownResolver.cs deleted file mode 100644 index d4d0166..0000000 --- a/LibMatrix/Services/WellKnownResolvers/ClientWellKnownResolver.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.Text.Json.Serialization; -using ArcaneLibs.Collections; -using LibMatrix.Extensions; -using Microsoft.Extensions.Logging; - -namespace LibMatrix.Services.WellKnownResolvers; - -public class ClientWellKnownResolver(ILogger logger) { - private static readonly SemaphoreCache ClientWellKnownCache = new() { - StoreNulls = false - }; - private static readonly MatrixHttpClient HttpClient = new(); - - public Task TryResolveClientWellKnown(string homeserver) { - return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { - logger.LogTrace($"Resolving client well-known: {homeserver}"); - if ((await TryGetClientWellKnownFromHttps(homeserver)) is { } clientWellKnown) - return new() { - Source = WellKnownResolverService.WellKnownSource.Https, - WellKnown = clientWellKnown - }; - - return default!; - }); - } - - private async Task TryGetClientWellKnownFromHttps(string homeserver) { - try { - return await HttpClient.TryGetFromJsonAsync($"https://{homeserver}/.well-known/matrix/client"); - } - catch { - return null; - } - } - - - - public class ClientWellKnown { - [JsonPropertyName("m.homeserver")] - public required WellKnownHomeserver Homeserver { get; set; } - - public class WellKnownHomeserver { - [JsonPropertyName("base_url")] - public required string BaseUrl { get; set; } - } - } - - public struct WellKnownResolutionResult { - public WellKnownResolverService.WellKnownSource Source { get; set; } - public ClientWellKnown WellKnown { get; set; } - public List Warnings { get; set; } - } -} \ No newline at end of file diff --git a/LibMatrix/Services/WellKnownResolvers/SupportWellKnownResolver.cs b/LibMatrix/Services/WellKnownResolvers/SupportWellKnownResolver.cs deleted file mode 100644 index 1d7567a..0000000 --- a/LibMatrix/Services/WellKnownResolvers/SupportWellKnownResolver.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.Text.Json.Serialization; -using ArcaneLibs.Collections; -using LibMatrix.Extensions; -using Microsoft.Extensions.Logging; - -namespace LibMatrix.Services.WellKnownResolvers; - -public class SupportWellKnownResolver(ILogger logger) { - private static readonly SemaphoreCache> ClientWellKnownCache = new() { - StoreNulls = false - }; - - private static readonly MatrixHttpClient HttpClient = new(); - - public Task> TryResolveClientWellKnown(string homeserver) { - return ClientWellKnownCache.TryGetOrAdd(homeserver, async () => { - logger.LogTrace($"Resolving client well-known: {homeserver}"); - if ((await TryGetClientWellKnownFromHttps(homeserver)) is { } clientWellKnown) - return new() { - Source = WellKnownResolverService.WellKnownSource.Https, - WellKnown = clientWellKnown - }; - return default!; - }); - } - - private async Task TryGetClientWellKnownFromHttps(string homeserver) { - try { - return await HttpClient.TryGetFromJsonAsync($"https://{homeserver}/.well-known/matrix/support"); - } - catch { - return null; - } - } - - public struct SupportWellKnown { - [JsonPropertyName("contacts")] - public List? Contacts { get; set; } - - [JsonPropertyName("support_page")] - public Uri? SupportPage { get; set; } - - public class WellKnownContact { - [JsonPropertyName("email_address")] - public string? EmailAddress { get; set; } - - [JsonPropertyName("matrix_id")] - public string? MatrixId { get; set; } - - [JsonPropertyName("role")] - public required string Role { get; set; } - } - } -} \ No newline at end of file diff --git a/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs index f6dfd97..ea494fa 100644 --- a/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs +++ b/Tests/LibMatrix.Tests/Tests/HomeserverResolverTests/ClientWellKnownResolverTests.cs @@ -1,5 +1,5 @@ using LibMatrix.Services; -using LibMatrix.Services.WellKnownResolvers; +using LibMatrix.Services.WellKnownResolver.WellKnownResolvers; using LibMatrix.Tests.Fixtures; using Xunit.Abstractions; using Xunit.Microsoft.DependencyInjection.Abstracts; @@ -18,16 +18,16 @@ public class ClientWellKnownResolverTests : TestBed { [Fact] public async Task ResolveServerClient() { var tasks = _config.ExpectedHomeserverClientMappings.Select(async mapping => { - var server = await _resolver.TryResolveClientWellKnown(mapping.Key); - Assert.Equal(mapping.Value, server.WellKnown.Homeserver.BaseUrl); + var server = await _resolver.TryResolveWellKnown(mapping.Key); + Assert.Equal(mapping.Value, server.Content.Homeserver.BaseUrl); return server; }).ToList(); await Task.WhenAll(tasks); } private async Task AssertClientWellKnown(string homeserver, string expected) { - var server = await _resolver.TryResolveClientWellKnown(homeserver); - Assert.Equal(expected, server.WellKnown.Homeserver.BaseUrl); + var server = await _resolver.TryResolveWellKnown(homeserver); + Assert.Equal(expected, server.Content.Homeserver.BaseUrl); } [Fact] diff --git a/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj b/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj index 8817e9c..7897e28 100644 --- a/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj +++ b/Utilities/LibMatrix.DevTestBot/LibMatrix.DevTestBot.csproj @@ -18,7 +18,7 @@ - + -- cgit 1.5.1