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