about summary refs log tree commit diff
path: root/LibMatrix/Homeservers
diff options
context:
space:
mode:
Diffstat (limited to 'LibMatrix/Homeservers')
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs124
-rw-r--r--LibMatrix/Homeservers/FederationClient.cs14
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs109
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs22
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs6
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs52
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs2
-rw-r--r--LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs170
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs54
9 files changed, 406 insertions, 147 deletions
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs

index 55899de..916780e 100644 --- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs +++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -4,6 +4,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Text.Json.Serialization; using System.Web; +using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix.EventTypes.Spec; using LibMatrix.EventTypes.Spec.State.RoomInfo; @@ -13,6 +14,7 @@ using LibMatrix.Homeservers.Extensions.NamedCaches; using LibMatrix.Responses; using LibMatrix.RoomTypes; using LibMatrix.Services; +using LibMatrix.StructuredData; using LibMatrix.Utilities; namespace LibMatrix.Homeservers; @@ -145,7 +147,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { await Task.Delay(1000); } } - }).ToAsyncEnumerable(); + }).ToAsyncResultEnumerable(); await foreach (var result in tasks) if (result is not null) @@ -215,7 +217,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { if (preserveCustomRoomProfile) { var rooms = await GetJoinedRooms(); - var roomProfiles = rooms.Select(GetOwnRoomProfileWithIdAsync).ToAsyncEnumerable(); + var roomProfiles = rooms.Select(GetOwnRoomProfileWithIdAsync).ToAsyncResultEnumerable(); targetSyncCount = rooms.Count; await foreach (var (roomId, currentRoomProfile) in roomProfiles) try { @@ -288,14 +290,26 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { public async IAsyncEnumerable<KeyValuePair<string, RoomMemberEventContent>> GetRoomProfilesAsync() { var rooms = await GetJoinedRooms(); - var results = rooms.Select(GetOwnRoomProfileWithIdAsync).ToAsyncEnumerable(); + var results = rooms.Select(GetOwnRoomProfileWithIdAsync).ToAsyncResultEnumerable(); await foreach (var res in results) yield return res; } public async Task<RoomIdResponse> JoinRoomAsync(string roomId, List<string> homeservers = null, string? reason = null) { var joinUrl = $"/_matrix/client/v3/join/{HttpUtility.UrlEncode(roomId)}"; Console.WriteLine($"Calling {joinUrl} with {homeservers?.Count ?? 0} via's..."); - if (homeservers == null || homeservers.Count == 0) homeservers = new List<string> { roomId.Split(':')[1] }; + if (homeservers is not { Count: > 0 }) { + // Legacy room IDs: !abc:server.xyz + if (roomId.Contains(':')) + homeservers = [ServerName, roomId.Split(':')[1]]; + // v12+ room IDs: !<hash> + else { + homeservers = [ServerName]; + foreach (var room in await GetJoinedRooms()) { + homeservers.Add(await room.GetOriginHomeserverAsync()); + } + } + } + var fullJoinUrl = $"{joinUrl}?server_name=" + string.Join("&server_name=", homeservers); var res = await ClientHttpClient.PostAsJsonAsync(fullJoinUrl, new { reason @@ -397,15 +411,12 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { private Dictionary<string, string>? _namedFilterCache; private Dictionary<string, SyncFilter> _filterCache = new(); - public async Task<JsonObject?> GetCapabilitiesAsync() { - var res = await ClientHttpClient.GetAsync("/_matrix/client/v3/capabilities"); - if (!res.IsSuccessStatusCode) { - Console.WriteLine($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}"); - throw new InvalidDataException($"Failed to get capabilities: {await res.Content.ReadAsStringAsync()}"); - } + private static readonly SemaphoreCache<CapabilitiesResponse> CapabilitiesCache = new(); - return await res.Content.ReadFromJsonAsync<JsonObject>(); - } + public async Task<CapabilitiesResponse> GetCapabilitiesAsync() => + await CapabilitiesCache.GetOrAdd(ServerName, async () => + await ClientHttpClient.GetFromJsonAsync<CapabilitiesResponse>("/_matrix/client/v3/capabilities") + ); public class HsNamedCaches { internal HsNamedCaches(AuthenticatedHomeserverGeneric hs) { @@ -429,7 +440,8 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { try { // Console.WriteLine($"Trying authenticated media URL: {uri}"); var res = await ClientHttpClient.SendAsync(new() { - Method = HttpMethod.Head, + // Method = HttpMethod.Head, // This apparently doesn't work with Matrix-Media-Repo... + Method = HttpMethod.Get, RequestUri = (new Uri(mxcUri.ToDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute)) }); if (res.IsSuccessStatusCode) { @@ -445,7 +457,8 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { try { // Console.WriteLine($"Trying legacy media URL: {uri}"); var res = await ClientHttpClient.SendAsync(new() { - Method = HttpMethod.Head, + // Method = HttpMethod.Head, + Method = HttpMethod.Get, RequestUri = new(mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute) }); if (res.IsSuccessStatusCode) { @@ -574,8 +587,87 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver { await SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoredUserList); } - private class CapabilitiesResponse { + public class CapabilitiesResponse { [JsonPropertyName("capabilities")] - public Dictionary<string, object>? Capabilities { get; set; } + public CapabilitiesContents Capabilities { get; set; } + + public class CapabilitiesContents { + [JsonPropertyName("m.3pid_changes")] + public BooleanCapability? ThreePidChanges { get; set; } + + [JsonPropertyName("m.change_password")] + public BooleanCapability? ChangePassword { get; set; } + + [JsonPropertyName("m.get_login_token")] + public BooleanCapability? GetLoginToken { get; set; } + + [JsonPropertyName("m.room_versions")] + public RoomVersionsCapability? RoomVersions { get; set; } + + [JsonPropertyName("m.set_avatar_url")] + public BooleanCapability? SetAvatarUrl { get; set; } + + [JsonPropertyName("m.set_displayname")] + public BooleanCapability? SetDisplayName { get; set; } + + [JsonPropertyName("gay.rory.bulk_send_events")] + public BooleanCapability? BulkSendEvents { get; set; } + + [JsonPropertyName("gay.rory.synapse_admin_extensions.room_list.query_events.v2")] + public BooleanCapability? SynapseRoomListQueryEventsV2 { get; set; } + + [JsonExtensionData] + public Dictionary<string, object>? AdditionalCapabilities { get; set; } + } + + public class BooleanCapability { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + } + + public class RoomVersionsCapability { + [JsonPropertyName("default")] + public string? Default { get; set; } + + [JsonPropertyName("available")] + public Dictionary<string, string>? Available { get; set; } + } + } + +#region Room Directory/aliases + + public async Task SetRoomAliasAsync(string roomAlias, string roomId) { + var resp = await ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/directory/room/{HttpUtility.UrlEncode(roomAlias)}", new RoomIdResponse() { + RoomId = roomId + }); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + } } + + public async Task DeleteRoomAliasAsync(string roomAlias) { + var resp = await ClientHttpClient.DeleteAsync("/_matrix/client/v3/directory/room/" + HttpUtility.UrlEncode(roomAlias)); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to set room alias: {await resp.Content.ReadAsStringAsync()}"); + } + } + + public async Task<RoomAliasesResponse> GetLocalRoomAliasesAsync(string roomId) { + var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{HttpUtility.UrlEncode(roomId)}/aliases"); + if (!resp.IsSuccessStatusCode) { + Console.WriteLine($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}"); + throw new InvalidDataException($"Failed to get room aliases: {await resp.Content.ReadAsStringAsync()}"); + } + + return await resp.Content.ReadFromJsonAsync<RoomAliasesResponse>() ?? throw new Exception("Failed to get room aliases?"); + } + + public class RoomAliasesResponse { + [JsonPropertyName("aliases")] + public required List<string> Aliases { get; set; } + } + +#endregion } \ No newline at end of file diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index 617b737..9760e20 100644 --- a/LibMatrix/Homeservers/FederationClient.cs +++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,5 +1,5 @@ -using System.Text.Json.Serialization; using LibMatrix.Extensions; +using LibMatrix.Responses.Federation; using LibMatrix.Services; namespace LibMatrix.Homeservers; @@ -17,17 +17,7 @@ public class FederationClient { public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } public async Task<ServerVersionResponse> GetServerVersionAsync() => await HttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version"); + public async Task<SignedObject<ServerKeysResponse>> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<SignedObject<ServerKeysResponse>>("/_matrix/key/v2/server"); } -public class ServerVersionResponse { - [JsonPropertyName("server")] - public required ServerInfo Server { get; set; } - public class ServerInfo { - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("version")] - public string Version { 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
index b8929a0..97c4bbf 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalRoomQueryFilter.cs
@@ -1,27 +1,90 @@ 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; } + public StringFilter RoomId { get; set; } = new(); + public StringFilter Name { get; set; } = new(); + public StringFilter CanonicalAlias { get; set; } = new(); + public StringFilter Version { get; set; } = new(); + public StringFilter Creator { get; set; } = new(); + public StringFilter Encryption { get; set; } = new(); + public StringFilter JoinRules { get; set; } = new(); + public StringFilter GuestAccess { get; set; } = new(); + public StringFilter HistoryVisibility { get; set; } = new(); + public StringFilter RoomType { get; set; } = new(); + public StringFilter Topic { get; set; } = new(); + + public IntFilter JoinedMembers { get; set; } = new() { + GreaterThan = 0, + LessThan = int.MaxValue + }; + + public IntFilter JoinedLocalMembers { get; set; } = new() { + GreaterThan = 0, + LessThan = int.MaxValue + }; + + public IntFilter StateEvents { get; set; } = new() { + GreaterThan = 0, + LessThan = int.MaxValue + }; + + public BoolFilter Federation { get; set; } = new(); + public BoolFilter Public { get; set; } = new(); + public BoolFilter Tombstone { get; set; } = new(); +} + +public class OptionalFilter { + public bool Enabled { get; set; } +} + +public class StringFilter : OptionalFilter { + public bool CheckValueContains { get; set; } + public string? ValueContains { get; set; } + + public bool CheckValueEquals { get; set; } + public string? ValueEquals { get; set; } + + public bool Matches(string? value, StringComparison comparison = StringComparison.Ordinal) { + if (!Enabled) return true; + + if (CheckValueEquals) { + if (!string.Equals(value, ValueEquals, comparison)) return false; + } + + if (CheckValueContains && ValueContains != null) { + if (value != null && !value.Contains(ValueContains, comparison)) return false; + } + + return true; + } +} + +public class IntFilter : OptionalFilter { + public bool CheckGreaterThan { get; set; } + public int GreaterThan { get; set; } + public bool CheckLessThan { get; set; } + public int LessThan { get; set; } + + public bool Matches(int value) { + if (!Enabled) return true; + + if (CheckGreaterThan) { + if (value <= GreaterThan) return false; + } + + if (CheckLessThan) { + if (value >= LessThan) return false; + } + + return true; + } +} + +public class BoolFilter : OptionalFilter { + public bool Value { get; set; } + + public bool Matches(bool value) { + if (!Enabled) return true; + return value == Value; + } } \ 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
index 62b291b..5a4acf7 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
@@ -1,27 +1,5 @@ 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/Responses/EventReportListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs
index 10fc039..0f3ee56 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/EventReportListResult.cs
@@ -97,10 +97,10 @@ public class SynapseAdminEventReportListResult : SynapseNextTokenTotalCollection [JsonPropertyName("unsigned")] public JsonObject? Unsigned { get; set; } - // Extra... copied from StateEventResponse + // Extra... copied from MatrixEventResponse [JsonIgnore] - public Type MappedType => StateEvent.GetStateEventType(Type); + public Type MappedType => MatrixEvent.GetEventType(Type); [JsonIgnore] public bool IsLegacyType => MappedType.GetCustomAttributes<MatrixEventAttribute>().FirstOrDefault(x => x.EventName == Type)?.Legacy ?? false; @@ -128,7 +128,7 @@ public class SynapseAdminEventReportListResult : SynapseNextTokenTotalCollection // return null; // } try { - var mappedType = StateEvent.GetStateEventType(Type); + var mappedType = MatrixEvent.GetEventType(Type); if (mappedType == typeof(UnknownEventContent)) Console.WriteLine($"Warning: unknown event type '{Type}'"); var deserialisedContent = (EventContent)RawContent.Deserialize(mappedType, TypedContentSerializerOptions)!; diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
index d84c89b..7006c07 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
@@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using LibMatrix.EventTypes.Spec.State.RoomInfo; namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; @@ -60,5 +61,56 @@ public class SynapseAdminRoomListResult { [JsonPropertyName("state_events")] public int StateEvents { get; set; } + + [JsonPropertyName("gay.rory.synapse_admin_extensions.tombstone")] + public MatrixEventResponse? TombstoneEvent { get; set; } + + [JsonPropertyName("gay.rory.synapse_admin_extensions.create")] + public MatrixEventResponse? CreateEvent { get; set; } + + [JsonPropertyName("gay.rory.synapse_admin_extensions.topic")] + public MatrixEventResponse? TopicEvent { get; set; } + + public async Task<MatrixEventResponse?> GetCreateEventAsync(AuthenticatedHomeserverSynapse hs) { + if (CreateEvent != null) return CreateEvent; + + try { + var events = (await hs.Admin.GetRoomStateAsync(RoomId, RoomCreateEventContent.EventId)); + CreateEvent = events.Events.SingleOrDefault(x => x.StateKey == ""); + } + catch (Exception e) { + Console.WriteLine($"Failed to fetch room create event for {RoomId}: {e}"); + } + + return null; + } + + public async Task<MatrixEventResponse?> GetTombstoneEventAsync(AuthenticatedHomeserverSynapse hs) { + if (TombstoneEvent != null) return TombstoneEvent; + + try { + var events = (await hs.Admin.GetRoomStateAsync(RoomId, RoomTombstoneEventContent.EventId)); + TombstoneEvent = events.Events.SingleOrDefault(x => x.StateKey == ""); + } + catch (Exception e) { + Console.WriteLine($"Failed to fetch room tombstone event for {RoomId}: {e}"); + } + + return null; + } + + public async Task<MatrixEventResponse?> GetTopicEventAsync(AuthenticatedHomeserverSynapse hs) { + if (TopicEvent != null) return TopicEvent; + + try { + var events = await hs.Admin.GetRoomStateAsync(RoomId, RoomTopicEventContent.EventId); + TopicEvent = events.Events.SingleOrDefault(x => x.StateKey == ""); + } + catch (Exception e) { + Console.WriteLine($"Failed to fetch room topic event for {RoomId}: {e}"); + } + + return null; + } } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs
index ae36d4e..d9d5f1a 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs
@@ -4,5 +4,5 @@ namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; public class SynapseAdminRoomStateResult { [JsonPropertyName("state")] - public required List<StateEventResponse> Events { get; set; } + public required List<MatrixEventResponse> Events { get; set; } } \ No newline at end of file diff --git a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
index 777c04a..f839e20 100644 --- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs +++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -1,21 +1,16 @@ // #define LOG_SKIP +using System.CodeDom.Compiler; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes; -using System.Net.Http.Json; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using ArcaneLibs.Extensions; +using LibMatrix.EventTypes.Spec.State.RoomInfo; using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests; using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; using LibMatrix.Responses; -using LibMatrix.Filters; -using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters; -using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses; -using LibMatrix.Responses; +using LibMatrix.StructuredData; namespace LibMatrix.Homeservers.ImplementationDetails.Synapse; @@ -27,24 +22,41 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH #region Rooms public async IAsyncEnumerable<SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom> SearchRoomsAsync(int limit = int.MaxValue, int chunkLimit = 250, - string orderBy = "name", string dir = "f", string? searchTerm = null, SynapseAdminLocalRoomQueryFilter? localFilter = null) { + string orderBy = "name", string dir = "f", string? searchTerm = null, SynapseAdminLocalRoomQueryFilter? localFilter = null, + bool fetchTombstones = false, bool fetchTopics = false, bool fetchCreateEvents = false) { + if (localFilter != null) { + fetchTombstones |= localFilter.Tombstone.Enabled; + fetchTopics |= localFilter.Topic.Enabled; + fetchCreateEvents |= localFilter.RoomType.Enabled; + } + + var serverCaps = await authenticatedHomeserver.GetCapabilitiesAsync(); + var serverSupportsQueryEventsV2 = serverCaps.Capabilities.SynapseRoomListQueryEventsV2?.Enabled ?? false; + SynapseAdminRoomListResult? res = null; var i = 0; int? totalRooms = null; do { 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 (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}"; if (res?.NextBatch is not null) url += $"&from={res.NextBatch}"; + // nonstandard stuff + if (fetchTombstones) url += "&gay.rory.synapse_admin_extensions.include_tombstone=true&emma_include_tombstone=true"; + if (fetchTopics) url += "&gay.rory.synapse_admin_extensions.include_topic=true"; + if (fetchCreateEvents) url += "&gay.rory.synapse_admin_extensions.include_create_event=true"; + Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---"); res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomListResult>(url); totalRooms ??= res.TotalRooms; // Console.WriteLine(res.ToJson(false)); + + List<SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom> keep = []; foreach (var room in res.Rooms) { if (localFilter is not null) { - if (!string.IsNullOrWhiteSpace(localFilter.RoomIdContains) && !room.RoomId.Contains(localFilter.RoomIdContains, StringComparison.OrdinalIgnoreCase)) { + if (!localFilter.RoomId.Matches(room.RoomId, StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomid."); @@ -52,7 +64,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.NameContains) && room.Name?.Contains(localFilter.NameContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.Name.Matches(room.Name ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule roomname."); @@ -60,8 +72,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.CanonicalAliasContains) && - room.CanonicalAlias?.Contains(localFilter.CanonicalAliasContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.CanonicalAlias.Matches(room.CanonicalAlias ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule alias."); @@ -69,7 +80,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.VersionContains) && !room.Version.Contains(localFilter.VersionContains, StringComparison.OrdinalIgnoreCase)) { + if (!localFilter.Version.Matches(room.Version ?? "")) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule version."); @@ -77,7 +88,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.CreatorContains) && !room.Creator.Contains(localFilter.CreatorContains, StringComparison.OrdinalIgnoreCase)) { + if (!localFilter.Creator.Matches(room.Creator ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule creator."); @@ -85,8 +96,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.EncryptionContains) && - room.Encryption?.Contains(localFilter.EncryptionContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.Encryption.Matches(room.Encryption ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule encryption."); @@ -94,8 +104,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.JoinRulesContains) && - room.JoinRules?.Contains(localFilter.JoinRulesContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.JoinRules.Matches(room.JoinRules ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joinrules."); @@ -103,8 +112,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.GuestAccessContains) && - room.GuestAccess?.Contains(localFilter.GuestAccessContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.GuestAccess.Matches(room.GuestAccess ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule guestaccess."); @@ -112,8 +120,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (!string.IsNullOrWhiteSpace(localFilter.HistoryVisibilityContains) && - room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains, StringComparison.OrdinalIgnoreCase) != true) { + if (!localFilter.HistoryVisibility.Matches(room.HistoryVisibility ?? "", StringComparison.OrdinalIgnoreCase)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule history visibility."); @@ -121,7 +128,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) { + if (!localFilter.Federation.Matches(room.Federatable)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule federation."); @@ -129,7 +136,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (localFilter.CheckPublic && room.Public != localFilter.Public) { + if (!localFilter.Public.Matches(room.Public)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule public."); @@ -137,15 +144,15 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (room.StateEvents < localFilter.StateEventsGreaterThan || room.StateEvents > localFilter.StateEventsLessThan) { + if (!localFilter.StateEvents.Matches(room.StateEvents)) { totalRooms--; #if LOG_SKIP - Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members."); + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule state events."); #endif continue; } - if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) { + if (!localFilter.JoinedMembers.Matches(room.JoinedMembers)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined members: {localFilter.JoinedMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedMembersLessThan}."); @@ -153,7 +160,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } - if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) { + if (!localFilter.JoinedLocalMembers.Matches(room.JoinedLocalMembers)) { totalRooms--; #if LOG_SKIP Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule joined local members: {localFilter.JoinedLocalMembersGreaterThan} < {room.JoinedLocalMembers} < {localFilter.JoinedLocalMembersLessThan}."); @@ -161,28 +168,99 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH continue; } } - // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) && - // !( - // room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || - // room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true || - // room.Creator?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true - // ) - // ) { - // totalRooms--; - // continue; - // } i++; + keep.Add(room); + } + + var parallelisationLimit = new SemaphoreSlim(32, 32); + List<Task<(SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom room, MatrixEventResponse?[] tasks)>> tasks = []; + + async Task<(SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom room, MatrixEventResponse?[] tasks)> fillTask( + SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom room) { + if (serverSupportsQueryEventsV2) return (room, []); + + var fillTasks = await Task.WhenAll(((Task<MatrixEventResponse?>?[]) [ + fetchTombstones && room.TombstoneEvent is null + ? parallelisationLimit.RunWithLockAsync(() => room.GetTombstoneEventAsync(authenticatedHomeserver)) + : null!, + fetchTopics && room.TopicEvent is null + ? parallelisationLimit.RunWithLockAsync(() => room.GetTopicEventAsync(authenticatedHomeserver)) + : null!, + fetchCreateEvents && room.CreateEvent is null + ? parallelisationLimit.RunWithLockAsync(() => room.GetCreateEventAsync(authenticatedHomeserver)) + : null!, + ]) + .Where(t => t != null)! + ); + return ( + room, + fillTasks + ); + } + + tasks.AddRange( + serverSupportsQueryEventsV2 + ? keep.Select(x => Task.FromResult((x, (MatrixEventResponse?[])[]))) + : keep.Select(fillTask) + ); + + // await Task.WhenAll(tasks); + + foreach (var taskRes in tasks) { + var (room, _) = await taskRes; + if (localFilter is not null) { + if (!localFilter.Tombstone.Matches(room.TombstoneEvent != null)) { + totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule tombstone."); +#endif + continue; + } + + if (!localFilter.RoomType.Matches(room.CreateEvent?.ContentAs<RoomCreateEventContent>()?.Type)) { + totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule room type."); +#endif + continue; + } + + if (!localFilter.Topic.Matches(room.TopicEvent?.ContentAs<RoomTopicEventContent>()?.Topic, StringComparison.OrdinalIgnoreCase)) { + totalRooms--; +#if LOG_SKIP + Console.WriteLine($"Skipped room {room.ToJson(indent: false)} on rule topic."); +#endif + continue; + } + } + yield return room; } } while (i < Math.Min(limit, totalRooms ?? limit)); } + public async Task<bool> CheckRoomKnownAsync(string roomId) { + try { + var createEvt = await GetRoomStateAsync(roomId, RoomCreateEventContent.EventId); + if (createEvt.Events.FirstOrDefault(e => e.StateKey == "") is null) + return false; + var members = await GetRoomMembersAsync(roomId, localOnly: true); + return members.Members.Count > 0; + } + catch (Exception e) { + if (e is HttpRequestException { StatusCode: System.Net.HttpStatusCode.NotFound }) return false; + if (e is MatrixException { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return false; + throw; + } + } + #endregion #region Users public async IAsyncEnumerable<SynapseAdminUserListResult.SynapseAdminUserListResultUser> SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250, + string orderBy = "name", string dir = "f", SynapseAdminLocalUserQueryFilter? localFilter = null) { // TODO: implement filters string? from = null; @@ -190,6 +268,9 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH 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); + if (!string.IsNullOrWhiteSpace(orderBy)) url = url.AddQuery("order_by", orderBy); + if (!string.IsNullOrWhiteSpace(dir)) url = url.AddQuery("dir", dir); + Console.WriteLine($"--- ADMIN Querying User List with URL: {url} ---"); // TODO: implement URI methods in http client var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminUserListResult>(url.ToString()); @@ -522,8 +603,13 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH $"/_synapse/admin/v2/rooms/delete_status/{deleteId}"); } - public async Task<SynapseAdminRoomMemberListResult> GetRoomMembersAsync(string roomId) { - return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomMemberListResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/members"); + public async Task<SynapseAdminRoomMemberListResult> GetRoomMembersAsync(string roomId, bool localOnly = false) { + var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomMemberListResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/members"); + if (localOnly) { + res.Members = res.Members.Where(m => m.EndsWith($":{authenticatedHomeserver.ServerName}")).ToList(); + } + + return res; } public async Task<SynapseAdminRoomStateResult> GetRoomStateAsync(string roomId, string? type = null) { diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index f0b35f9..af84be2 100644 --- a/LibMatrix/Homeservers/RemoteHomeServer.cs +++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -3,6 +3,7 @@ using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using System.Web; +using ArcaneLibs.Collections; using ArcaneLibs.Extensions; using LibMatrix.Extensions; using LibMatrix.Responses; @@ -28,7 +29,8 @@ public class RemoteHomeserver { Auth = new(this); } - private Dictionary<string, object> _profileCache { get; set; } = new(); + // private Dictionary<string, object> _profileCache { get; set; } = new(); + private SemaphoreCache<UserProfileResponse> _profileCache { get; set; } = new(); public string ServerNameOrUrl { get; } public string? Proxy { get; } @@ -40,27 +42,12 @@ public class RemoteHomeserver { public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } - public async Task<UserProfileResponse> GetProfileAsync(string mxid, bool useCache = false) { - if (mxid is null) throw new ArgumentNullException(nameof(mxid)); - if (useCache && _profileCache.TryGetValue(mxid, out var value)) { - if (value is SemaphoreSlim s) await s.WaitAsync(); - if (value is UserProfileResponse p) return p; - } - - _profileCache[mxid] = new SemaphoreSlim(1); - - var resp = await ClientHttpClient.GetAsync($"/_matrix/client/v3/profile/{HttpUtility.UrlEncode(mxid)}"); - var data = await resp.Content.ReadFromJsonAsync<UserProfileResponse>(); - if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data); - _profileCache[mxid] = data ?? throw new InvalidOperationException($"Could not get profile for {mxid}"); - - return data; - } - // TODO: Do we need to support retrieving individual profile properties? Is there any use for that besides just getting the full profile? + public async Task<UserProfileResponse> GetProfileAsync(string mxid) => + await ClientHttpClient.GetFromJsonAsync<UserProfileResponse>($"/_matrix/client/v3/profile/{HttpUtility.UrlEncode(mxid)}"); public async Task<ClientVersionsResponse> GetClientVersionsAsync() { - var resp = await ClientHttpClient.GetAsync($"/_matrix/client/versions"); + var resp = await ClientHttpClient.GetAsync("/_matrix/client/versions"); var data = await resp.Content.ReadFromJsonAsync<ClientVersionsResponse>(); if (!resp.IsSuccessStatusCode) Console.WriteLine("ClientVersions: " + data); return data ?? throw new InvalidOperationException("ClientVersionsResponse is null"); @@ -74,13 +61,27 @@ public class RemoteHomeserver { return data ?? throw new InvalidOperationException($"Could not resolve alias {alias}"); } - public Task<PublicRoomDirectoryResult> GetPublicRoomsAsync(int limit = 100, string? server = null, string? since = null) => - ClientHttpClient.GetFromJsonAsync<PublicRoomDirectoryResult>(buildUriWithParams("/_matrix/client/v3/publicRooms", (nameof(limit), true, limit), - (nameof(server), !string.IsNullOrWhiteSpace(server), server), (nameof(since), !string.IsNullOrWhiteSpace(since), since))); + public Task<PublicRoomDirectoryResult> GetPublicRoomsAsync(int limit = 100, string? server = null, string? since = null) { + var url = $"/_matrix/client/v3/publicRooms?limit={limit}"; + if (!string.IsNullOrWhiteSpace(server)) { + url += $"&server={server}"; + } - // TODO: move this somewhere else - private string buildUriWithParams(string url, params (string name, bool include, object? value)[] values) { - return url + "?" + string.Join("&", values.Where(x => x.include)); + if (!string.IsNullOrWhiteSpace(since)) { + url += $"&since={since}"; + } + + return ClientHttpClient.GetFromJsonAsync<PublicRoomDirectoryResult>(url); + } + + public async IAsyncEnumerable<PublicRoomDirectoryResult> EnumeratePublicRoomsAsync(int limit = int.MaxValue, string? server = null, string? since = null, int chunkSize = 100) { + PublicRoomDirectoryResult res; + do { + res = await GetPublicRoomsAsync(chunkSize, server, since); + yield return res; + if (res.NextBatch is null || res.NextBatch == since || res.Chunk.Count == 0) break; + since = res.NextBatch; + } while (limit > 0 && limit-- > 0); } #region Authentication @@ -117,9 +118,6 @@ public class RemoteHomeserver { #endregion - [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)] - public virtual string? ResolveMediaUri(string? mxcUri) => null; - public UserInteractiveAuthClient Auth; }