diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 6be49b9..5fd3311 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -5,8 +5,8 @@ using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Web;
using ArcaneLibs.Extensions;
-using LibMatrix.EventTypes.Spec.State;
-using LibMatrix.Extensions;
+using LibMatrix.EventTypes.Spec;
+using LibMatrix.EventTypes.Spec.State.RoomInfo;
using LibMatrix.Filters;
using LibMatrix.Helpers;
using LibMatrix.Homeservers.Extensions.NamedCaches;
@@ -14,7 +14,6 @@ using LibMatrix.Responses;
using LibMatrix.RoomTypes;
using LibMatrix.Services;
using LibMatrix.Utilities;
-using Microsoft.Extensions.Logging.Abstractions;
namespace LibMatrix.Homeservers;
@@ -41,14 +40,14 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
public string UserId => WhoAmI.UserId;
public string UserLocalpart => UserId.Split(":")[0][1..];
public string ServerName => UserId.Split(":", 2)[1];
+ public string BaseUrl => ClientHttpClient.BaseAddress!.ToString().TrimEnd('/');
[JsonIgnore]
public string AccessToken { get; set; }
- public HsNamedCaches NamedCaches { get; set; } = null!;
+ public HsNamedCaches NamedCaches { get; set; }
public GenericRoom GetRoom(string roomId) {
- if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId));
return new GenericRoom(this, roomId);
}
@@ -171,8 +170,9 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
try {
return await GetAccountDataAsync<T>(key);
}
- catch (Exception e) {
- return default;
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND }) return default;
+ throw;
}
}
@@ -186,6 +186,16 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
#endregion
+#region MSC 4133
+
+ public async Task UpdateProfilePropertyAsync(string name, object? value) {
+ var caps = await GetCapabilitiesAsync();
+ if (caps is null) throw new Exception("Failed to get capabilities");
+ }
+
+#endregion
+
+ [Obsolete("This method assumes no support for MSC 4069 and MSC 4133")]
public async Task UpdateProfileAsync(UserProfileResponse? newProfile, bool preserveCustomRoomProfile = true) {
if (newProfile is null) return;
Console.WriteLine($"Updating profile for {WhoAmI.UserId} to {newProfile.ToJson(ignoreNull: true)} (preserving room profiles: {preserveCustomRoomProfile})");
@@ -377,11 +387,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
/// <returns>All account data.</returns>
/// <exception cref="Exception"></exception>
public async Task<EventList?> EnumerateAccountData() {
- var syncHelper = new SyncHelper(this);
- syncHelper.FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData);
+ var syncHelper = new SyncHelper(this) {
+ FilterId = await NamedCaches.FilterCache.GetOrSetValueAsync(CommonSyncFilters.GetAccountData)
+ };
var resp = await syncHelper.SyncAsync();
- if (resp is null) throw new Exception("Sync failed");
- return resp.AccountData;
+ return resp?.AccountData ?? throw new Exception("Sync failed");
}
private Dictionary<string, string>? _namedFilterCache;
@@ -410,22 +420,22 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
#region Authenticated Media
// TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig
+ private bool? _serverSupportsAuthMedia;
- private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) {
- if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri));
- var parts = mxcUri[6..].Split('/');
- if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri));
- return (parts[0], parts[1]);
- }
+ public async Task<string> GetMediaUrlAsync(MxcUri mxcUri, string? filename = null, int? timeout = null) {
+ if (_serverSupportsAuthMedia == true) return mxcUri.ToDownloadUri(BaseUrl, filename, timeout);
+ if (_serverSupportsAuthMedia == false) return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout);
- public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
- var (serverName, mediaId) = ParseMxcUri(mxcUri);
try {
- var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}";
- if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
- if (timeout is not null) uri += $"?timeout_ms={timeout}";
- var res = await ClientHttpClient.GetAsync(uri);
- return await res.Content.ReadAsStreamAsync();
+ // Console.WriteLine($"Trying authenticated media URL: {uri}");
+ var res = await ClientHttpClient.SendAsync(new() {
+ Method = HttpMethod.Head,
+ RequestUri = (new Uri(mxcUri.ToDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute))
+ });
+ if (res.IsSuccessStatusCode) {
+ _serverSupportsAuthMedia = true;
+ return mxcUri.ToDownloadUri(BaseUrl, filename, timeout);
+ }
}
catch (MatrixException e) {
if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
@@ -433,11 +443,15 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
//fallback to legacy media
try {
- var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}";
- if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
- if (timeout is not null) uri += $"?timeout_ms={timeout}";
- var res = await ClientHttpClient.GetAsync(uri);
- return await res.Content.ReadAsStreamAsync();
+ // Console.WriteLine($"Trying legacy media URL: {uri}");
+ var res = await ClientHttpClient.SendAsync(new() {
+ Method = HttpMethod.Head,
+ RequestUri = new(mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout), string.IsNullOrWhiteSpace(BaseUrl) ? UriKind.Relative : UriKind.Absolute)
+ });
+ if (res.IsSuccessStatusCode) {
+ _serverSupportsAuthMedia = false;
+ return mxcUri.ToLegacyDownloadUri(BaseUrl, filename, timeout);
+ }
}
catch (MatrixException e) {
if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
@@ -445,19 +459,25 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
throw new LibMatrixException() {
ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
- Error = "Failed to download media"
+ Error = "Failed to get media URL"
};
- // return default;
}
- public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) {
- var (serverName, mediaId) = ParseMxcUri(mxcUri);
+ public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
+ var uri = await GetMediaUrlAsync(mxcUri, filename, timeout);
+ var res = await ClientHttpClient.GetAsync(uri);
+ return await res.Content.ReadAsStreamAsync();
+ }
+
+ public async Task<Stream> GetThumbnailStreamAsync(MxcUri mxcUri, int width, int height, string? method = null, int? timeout = null) {
+ var (serverName, mediaId) = mxcUri;
+
try {
var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}");
uri = uri.AddQuery("width", width.ToString());
uri = uri.AddQuery("height", height.ToString());
if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
- if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+ if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()!);
var res = await ClientHttpClient.GetAsync(uri.ToString());
return await res.Content.ReadAsStreamAsync();
@@ -468,11 +488,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
//fallback to legacy media
try {
- var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}");
+ var uri = new Uri($"/_matrix/media/v3/thumbnail/{serverName}/{mediaId}");
uri = uri.AddQuery("width", width.ToString());
uri = uri.AddQuery("height", height.ToString());
if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
- if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+ if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString()!);
var res = await ClientHttpClient.GetAsync(uri.ToString());
return await res.Content.ReadAsStreamAsync();
@@ -496,16 +516,16 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
catch (MatrixException e) {
if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
}
-
+
//fallback to legacy media
try {
- var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}");
+ var res = await ClientHttpClient.GetAsync($"/_matrix/media/v3/preview_url?url={HttpUtility.UrlEncode(url)}");
return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
}
catch (MatrixException e) {
if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
}
-
+
throw new LibMatrixException() {
ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
Error = "Failed to download URL preview"
@@ -513,4 +533,86 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
}
#endregion
+
+ public Task ReportRoomAsync(string roomId, string reason) =>
+ ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report", new {
+ reason
+ });
+
+ public async Task ReportRoomEventAsync(string roomId, string eventId, string reason, int score = 0, bool ignoreSender = false) {
+ await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{roomId}/report/{eventId}", new {
+ reason,
+ score
+ });
+
+ if (ignoreSender) {
+ var eventContent = await GetRoom(roomId).GetEventAsync(eventId);
+ var sender = eventContent.Sender;
+ await IgnoreUserAsync(sender);
+ }
+ }
+
+ public async Task ReportUserAsync(string userId, string reason, bool ignore = false) {
+ await ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/users/{userId}/report", new {
+ reason
+ });
+
+ if (ignore) {
+ await IgnoreUserAsync(userId);
+ }
+ }
+
+ public async Task<IgnoredUserListEventContent> GetIgnoredUserListAsync() {
+ return await GetAccountDataOrNullAsync<IgnoredUserListEventContent>(IgnoredUserListEventContent.EventId) ?? new();
+ }
+
+ public async Task IgnoreUserAsync(string userId, IgnoredUserListEventContent.IgnoredUserContent? content = null) {
+ content ??= new();
+
+ var ignoredUserList = await GetIgnoredUserListAsync();
+ ignoredUserList.IgnoredUsers.TryAdd(userId, content);
+ await SetAccountDataAsync(IgnoredUserListEventContent.EventId, ignoredUserList);
+ }
+
+ private class CapabilitiesResponse {
+ [JsonPropertyName("capabilities")]
+ public Dictionary<string, object>? Capabilities { 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/AuthenticatedHomeserverHSE.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs
new file mode 100644
index 0000000..1cc8ca2
--- /dev/null
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverHSE.cs
@@ -0,0 +1,16 @@
+using LibMatrix.Homeservers.ImplementationDetails.Synapse;
+using LibMatrix.Responses;
+using LibMatrix.Services;
+
+namespace LibMatrix.Homeservers;
+
+public class AuthenticatedHomeserverHSE : AuthenticatedHomeserverGeneric {
+ public AuthenticatedHomeserverHSE(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy, string accessToken) : base(serverName,
+ wellKnownUris, proxy, accessToken) { }
+
+ public Task<Dictionary<string, LoginResponse>> GetExternalProfilesAsync() =>
+ ClientHttpClient.GetFromJsonAsync<Dictionary<string, LoginResponse>>("/_hse/client/v1/external_profiles");
+
+ public Task SetExternalProfile(string sessionName, LoginResponse session) =>
+ ClientHttpClient.PutAsJsonAsync($"/_hse/client/v1/external_profiles/{sessionName}", session);
+}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
index 83ebf20..1c0a656 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
@@ -1,7 +1,4 @@
-using ArcaneLibs.Extensions;
-using LibMatrix.Filters;
using LibMatrix.Homeservers.ImplementationDetails.Synapse;
-using LibMatrix.Responses.Admin;
using LibMatrix.Services;
namespace LibMatrix.Homeservers;
diff --git a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
index 622eef6..1f62637 100644
--- a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
+++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedCache.cs
@@ -3,35 +3,72 @@ namespace LibMatrix.Homeservers.Extensions.NamedCaches;
public class NamedCache<T>(AuthenticatedHomeserverGeneric hs, string name) where T : class {
private Dictionary<string, T>? _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;
+
+ /// <summary>
+ /// Update the cached map with the latest data from the homeserver.
+ /// </summary>
+ /// <returns>The updated data</returns>
public async Task<Dictionary<string, T>> ReadCacheMapAsync() {
- _cache = await hs.GetAccountDataOrNullAsync<Dictionary<string, T>>(name);
+ try {
+ _cache = await hs.GetAccountDataAsync<Dictionary<string, T>>(name);
+ }
+ catch (MatrixException e) {
+ if (e is { ErrorCode: MatrixException.ErrorCodes.M_NOT_FOUND })
+ _cache = [];
+ else throw;
+ }
return _cache ?? new();
}
-
- public async Task<Dictionary<string,T>> ReadCacheMapCachedAsync() {
+
+ public async Task<Dictionary<string, T>> 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<T?> GetValueAsync(string key) {
- return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key);
+
+ public virtual async Task<T?> GetValueAsync(string key, bool useCache = true) {
+ return (await (useCache ? ReadCacheMapCachedAsync() : ReadCacheMapAsync())).GetValueOrDefault(key);
}
-
- public virtual async Task<T> SetValueAsync(string key, T value) {
- var cache = await ReadCacheMapCachedAsync();
+
+ public virtual async Task<T> 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<T> GetOrSetValueAsync(string key, Func<Task<T>> value) {
- return (await ReadCacheMapCachedAsync()).GetValueOrDefault(key) ?? await SetValueAsync(key, await value());
+
+ public virtual async Task<T> 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<T> GetOrSetValueAsync(string key, Func<Task<T>> 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/Extensions/NamedCaches/NamedFilterCache.cs b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
index 76533a4..e3c5943 100644
--- a/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
+++ b/LibMatrix/Homeservers/Extensions/NamedCaches/NamedFilterCache.cs
@@ -1,3 +1,5 @@
+using System.Text.Json.Nodes;
+using ArcaneLibs.Extensions;
using LibMatrix.Filters;
using LibMatrix.Utilities;
@@ -16,7 +18,13 @@ public class NamedFilterCache(AuthenticatedHomeserverGeneric hs) : NamedCache<st
public async Task<string> GetOrSetValueAsync(string key, SyncFilter? filter = null) {
var existingValue = await GetValueAsync(key);
if (existingValue != null) {
- return existingValue;
+ try {
+ var existingFilter = await hs.GetFilterAsync(existingValue);
+ return existingValue;
+ }
+ catch {
+ // ignored
+ }
}
if (filter is null) {
diff --git a/LibMatrix/Homeservers/FederationClient.cs b/LibMatrix/Homeservers/FederationClient.cs
index 22653e4..a2cb12d 100644
--- a/LibMatrix/Homeservers/FederationClient.cs
+++ b/LibMatrix/Homeservers/FederationClient.cs
@@ -1,7 +1,7 @@
using System.Text.Json.Serialization;
using LibMatrix.Extensions;
using LibMatrix.Services;
-using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.VisualBasic.CompilerServices;
namespace LibMatrix.Homeservers;
@@ -14,10 +14,74 @@ public class FederationClient {
if (proxy is not null) HttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", federationEndpoint);
}
- public MatrixHttpClient HttpClient { get; set; } = null!;
- public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; } = null!;
+ public MatrixHttpClient HttpClient { get; set; }
+ public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; }
public async Task<ServerVersionResponse> GetServerVersionAsync() => await HttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version");
+ public async Task<ServerKeysResponse> GetServerKeysAsync() => await HttpClient.GetFromJsonAsync<ServerKeysResponse>("/_matrix/key/v2/server");
+}
+
+public class ServerKeysResponse {
+ [JsonPropertyName("server_name")]
+ public string ServerName { get; set; }
+
+ [JsonPropertyName("valid_until_ts")]
+ public ulong ValidUntilTs { get; set; }
+
+ [JsonIgnore]
+ public DateTime ValidUntil {
+ get => DateTimeOffset.FromUnixTimeMilliseconds((long)ValidUntilTs).DateTime;
+ set => ValidUntilTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
+ }
+
+ [JsonPropertyName("verify_keys")]
+ public Dictionary<string, CurrentVerifyKey> VerifyKeys { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<VersionedKeyId, CurrentVerifyKey> VerifyKeysById {
+ get => VerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
+ set => VerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
+ }
+
+ [JsonPropertyName("old_verify_keys")]
+ public Dictionary<string, ExpiredVerifyKey> OldVerifyKeys { get; set; } = new();
+
+ [JsonIgnore]
+ public Dictionary<VersionedKeyId, ExpiredVerifyKey> OldVerifyKeysById {
+ get => OldVerifyKeys.ToDictionary(key => (VersionedKeyId)key.Key, key => key.Value);
+ set => OldVerifyKeys = value.ToDictionary(key => (string)key.Key, key => key.Value);
+ }
+
+ public class VersionedKeyId {
+ public required string Algorithm { get; set; }
+ public required string KeyId { get; set; }
+
+ public static implicit operator VersionedKeyId(string key) {
+ var parts = key.Split(':', 2);
+ if (parts.Length != 2) throw new ArgumentException("Invalid key format. Expected 'algorithm:keyId'.", nameof(key));
+ return new VersionedKeyId { Algorithm = parts[0], KeyId = parts[1] };
+ }
+
+ public static implicit operator string(VersionedKeyId key) => $"{key.Algorithm}:{key.KeyId}";
+ public static implicit operator (string, string)(VersionedKeyId key) => (key.Algorithm, key.KeyId);
+ public static implicit operator VersionedKeyId((string algorithm, string keyId) key) => (key.algorithm, key.keyId);
+ }
+
+ public class CurrentVerifyKey {
+ [JsonPropertyName("key")]
+ public string Key { get; set; }
+ }
+
+ public class ExpiredVerifyKey : CurrentVerifyKey {
+ [JsonPropertyName("expired_ts")]
+ public ulong ExpiredTs { get; set; }
+
+ [JsonIgnore]
+ public DateTime Expired {
+ get => DateTimeOffset.FromUnixTimeMilliseconds((long)ExpiredTs).DateTime;
+ set => ExpiredTs = (ulong)new DateTimeOffset(value).ToUnixTimeMilliseconds();
+ }
+ }
}
public class ServerVersionResponse {
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..5a4acf7
--- /dev/null
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Filters/SynapseAdminLocalUserQueryFilter.cs
@@ -0,0 +1,5 @@
+namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters;
+
+public class SynapseAdminLocalUserQueryFilter {
+
+}
\ 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..c2b2fac
--- /dev/null
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs
@@ -0,0 +1,60 @@
+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<SynapseAdminRoomDeleteStatus> Results { get; set; }
+}
+
+public class SynapseAdminRoomDeleteStatus {
+ public const string Scheduled = "scheduled";
+ public const string Active = "active";
+ public const string Complete = "complete";
+ public const string Failed = "failed";
+
+ [JsonPropertyName("status")]
+ public string Status { get; set; } = null!;
+
+ [JsonPropertyName("shutdown_room")]
+ public RoomShutdownInfo ShutdownRoom { get; set; }
+
+ public class RoomShutdownInfo {
+ [JsonPropertyName("kicked_users")]
+ public List<string>? KickedUsers { get; set; }
+
+ [JsonPropertyName("failed_to_kick_users")]
+ public List<string>? FailedToKickUsers { get; set; }
+
+ [JsonPropertyName("local_aliases")]
+ public List<string>? 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/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<string, BackgroundUpdateInfo> 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<SynapseAdminDestinationListResultDestination> 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<SynapseAdminDestinationRoomListResultRoom> 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<SynapseAdminEventReportListResultReport> 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<string> AuthEvents { get; set; }
+
+ [JsonPropertyName("content")]
+ public JsonObject? RawContent { get; set; }
+
+ [JsonPropertyName("depth")]
+ public int Depth { get; set; }
+
+ [JsonPropertyName("hashes")]
+ public Dictionary<string, string> Hashes { get; set; }
+
+ [JsonPropertyName("origin")]
+ public string Origin { get; set; }
+
+ [JsonPropertyName("origin_server_ts")]
+ public long OriginServerTs { get; set; }
+
+ [JsonPropertyName("prev_events")]
+ public List<string> PrevEvents { get; set; }
+
+ [JsonPropertyName("prev_state")]
+ public List<object> PrevState { get; set; }
+
+ [JsonPropertyName("room_id")]
+ public string RoomId { get; set; }
+
+ [JsonPropertyName("sender")]
+ public string Sender { get; set; }
+
+ [JsonPropertyName("signatures")]
+ public Dictionary<string, Dictionary<string, string>> 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<MatrixEventAttribute>().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<EventContent>.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<JsonObject>(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<SynapseAdminRegistrationTokenListResultToken> 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/AdminRoomListingResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
index 7ab96ac..d84c89b 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListingResult.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
@@ -1,8 +1,8 @@
using System.Text.Json.Serialization;
-namespace LibMatrix.Responses.Admin;
+namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
-public class AdminRoomListingResult {
+public class SynapseAdminRoomListResult {
[JsonPropertyName("offset")]
public int Offset { get; set; }
@@ -16,9 +16,9 @@ public class AdminRoomListingResult {
public int? PrevBatch { get; set; }
[JsonPropertyName("rooms")]
- public List<AdminRoomListingResultRoom> Rooms { get; set; } = new();
+ public List<SynapseAdminRoomListResultRoom> Rooms { get; set; } = new();
- public class AdminRoomListingResultRoom {
+ public class SynapseAdminRoomListResultRoom {
[JsonPropertyName("room_id")]
public required string RoomId { get; set; }
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<string> Local { get; set; } = new();
+
+ [JsonPropertyName("remote")]
+ public List<string> 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<string> 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/SynapseAdminRoomStateResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs
new file mode 100644
index 0000000..ae36d4e
--- /dev/null
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminRoomStateResult.cs
@@ -0,0 +1,8 @@
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
+
+public class SynapseAdminRoomStateResult {
+ [JsonPropertyName("state")]
+ public required List<StateEventResponse> Events { 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 {
+ /// <summary>
+ /// One of "scheduled", "active", "completed", "failed"
+ /// </summary>
+ [JsonPropertyName("status")]
+ public string Status { get; set; }
+
+ /// <summary>
+ /// Key: Event ID, Value: Error message
+ /// </summary>
+ [JsonPropertyName("failed_redactions")]
+ public Dictionary<string, string> 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<T>(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<T> Chunk { get; set; } = [];
+
+ // TODO: figure out how to provide an IAsyncEnumerable<T> 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<T> FromJsonAsync(Stream stream) {
+ //
+ // }
+
+ public SynapseCollectionResult<T> FromJson(Stream stream, Action<T> 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<T> 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<T>(_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<T>(_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<T>(stream, ref buffer, ref reader)
+ };
+
+ object ReadObject<T>(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) {
+ if (reader.TokenType != JsonTokenType.PropertyName) {
+ throw new JsonException();
+ }
+
+ List<byte> 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<T>(objBuffer.ToArray());
+ }
+
+ return (T)call;
+
+ // return JsonSerializer.Deserialize<T>(ref reader);
+ }
+
+ private static void GetMoreBytesFromStream(Stream stream, ref byte[] buffer, ref Utf8JsonReader reader) {
+ int bytesRead;
+ if (reader.BytesConsumed < buffer.Length) {
+ ReadOnlySpan<byte> 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<T> : JsonConverter<SynapseCollectionResult<T>> {
+ public override SynapseCollectionResult<T>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+ if (reader.TokenType != JsonTokenType.StartObject) {
+ throw new JsonException();
+ }
+
+ var result = new SynapseCollectionResult<T>();
+ 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<T>(ref reader, options);
+ result.Chunk.Add(item);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ public override void Write(Utf8JsonWriter writer, SynapseCollectionResult<T> 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<MediaInfo> 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<SynapseAdminUserListResultUser> 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..cee3d8d 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -1,90 +1,163 @@
+// #define LOG_SKIP
+
+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.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.Responses.Admin;
+using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters;
+using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
+using LibMatrix.Responses;
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse;
public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedHomeserver) {
- public async IAsyncEnumerable<AdminRoomListingResult.AdminRoomListingResultRoom> 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<SynapseAdminRoomListResult.SynapseAdminRoomListResultRoom> 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<AdminRoomListingResult>(url);
+ res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomListResult>(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 +177,386 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
}
} while (i < Math.Min(limit, totalRooms ?? limit));
}
+
+#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;
+ 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);
+ 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());
+ foreach (var user in res.Users) {
+ limit--;
+ yield return user;
+ }
+
+ if (string.IsNullOrWhiteSpace(res.NextToken)) break;
+ from = res.NextToken;
+ }
+ }
+
+ public async Task<LoginResponse> 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<JsonObject>(url.ToString(), new());
+ var loginResp = await resp.Content.ReadFromJsonAsync<LoginResponse>();
+ loginResp.UserId = userId; // Synapse only returns the access token
+ return loginResp;
+ }
+
+ public async Task<AuthenticatedHomeserverSynapse> GetHomeserverForUserAsync(string userId, TimeSpan expireAfter) {
+ var loginResp = await LoginUserAsync(userId, expireAfter);
+ var homeserver = new AuthenticatedHomeserverSynapse(
+ authenticatedHomeserver.ServerName, authenticatedHomeserver.WellKnownUris, authenticatedHomeserver.Proxy, loginResp.AccessToken
+ );
+ await homeserver.Initialise();
+ return homeserver;
+ }
+
+#endregion
+
+#region Reports
+
+ public async IAsyncEnumerable<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReport> 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<SynapseAdminEventReportListResult>(url.ToString());
+ foreach (var report in res.Reports) {
+ limit--;
+ yield return report;
+ }
+
+ if (string.IsNullOrWhiteSpace(res.NextToken)) break;
+ from = res.NextToken;
+ }
+ }
+
+ public async Task<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails> GetEventReportDetailsAsync(string reportId) {
+ var url = new Uri($"/_synapse/admin/v1/event_reports/{reportId.UrlEncode()}", UriKind.Relative);
+ return await authenticatedHomeserver.ClientHttpClient
+ .GetFromJsonAsync<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails>(url.ToString());
+ }
+
+ // Utility function to get details straight away
+ public async IAsyncEnumerable<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails> GetEventReportsWithDetailsAsync(int limit = int.MaxValue,
+ int chunkLimit = 250, string dir = "f", SynapseAdminLocalEventReportQueryFilter? filter = null) {
+ Queue<Task<SynapseAdminEventReportListResult.SynapseAdminEventReportListResultReportWithDetails>> 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<bool> 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<SynapseAdminBackgroundUpdateStatusResponse>(url.ToString());
+ return resp.Enabled;
+ }
+
+ public async Task<bool> 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<JsonObject>(url.ToString(), new JsonObject {
+ ["enabled"] = enabled
+ });
+ var json = await resp.Content.ReadFromJsonAsync<SynapseAdminBackgroundUpdateStatusResponse>();
+ return json!.Enabled;
+ }
+
+ public async Task<SynapseAdminBackgroundUpdateStatusResponse> GetBackgroundUpdatesStatusAsync() {
+ var url = new Uri("/_synapse/admin/v1/background_updates/status", UriKind.Relative);
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminBackgroundUpdateStatusResponse>(url.ToString());
+ }
+
+ /// <summary>
+ /// Run a background job
+ /// </summary>
+ /// <param name="jobName">One of "populate_stats_process_rooms" or "regenerate_directory"</param>
+ 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<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination> 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<SynapseAdminDestinationListResult>(url.ToString());
+ foreach (var dest in res.Destinations) {
+ limit--;
+ yield return dest;
+ }
+ }
+ }
+
+ public async Task<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination> GetFederationDestinationDetailsAsync(string destination) {
+ var url = new Uri($"/_synapse/admin/v1/federation/destinations/{destination}", UriKind.Relative);
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminDestinationListResult.SynapseAdminDestinationListResultDestination>(url.ToString());
+ }
+
+ public async IAsyncEnumerable<SynapseAdminDestinationRoomListResult.SynapseAdminDestinationRoomListResultRoom> 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<SynapseAdminDestinationRoomListResult>(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<List<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>> GetRegistrationTokensAsync() {
+ var url = new Uri("/_synapse/admin/v1/registration_tokens", UriKind.Relative);
+ var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRegistrationTokenListResult>(url.ToString());
+ return resp.RegistrationTokens;
+ }
+
+ public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> GetRegistrationTokenAsync(string token) {
+ var url = new Uri($"/_synapse/admin/v1/registration_tokens/{token.UrlEncode()}", UriKind.Relative);
+ var resp =
+ await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>(url.ToString());
+ return resp;
+ }
+
+ public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> 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<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>();
+ return token!;
+ }
+
+ public async Task<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken> 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<SynapseAdminRegistrationTokenListResult.SynapseAdminRegistrationTokenListResultToken>();
+ }
+
+ 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<Dictionary<string, bool>> GetExperimentalFeaturesAsync(string userId) {
+ var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative);
+ var resp = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<JsonObject>(url.ToString());
+ return resp["features"]!.GetValue<Dictionary<string, bool>>();
+ }
+
+ public async Task SetExperimentalFeaturesAsync(string userId, Dictionary<string, bool> features) {
+ var url = new Uri($"/_synapse/admin/v1/experimental_features/{userId.UrlEncode()}", UriKind.Relative);
+ await authenticatedHomeserver.ClientHttpClient.PostAsJsonAsync<JsonObject>(url.ToString(), new JsonObject {
+ ["features"] = JsonSerializer.Deserialize<JsonObject>(features.ToJson())
+ });
+ }
+
+#endregion
+
+#region Media
+
+ public async Task<SynapseAdminRoomMediaListResult> GetRoomMediaAsync(string roomId) {
+ var url = new Uri($"/_synapse/admin/v1/room/{roomId.UrlEncode()}/media", UriKind.Relative);
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomMediaListResult>(url.ToString());
+ }
+
+ // This is in the user admin API section
+ // public async IAsyncEnumerable<SynapseAdminRoomMediaListResult>
+
+#endregion
+
+ public async Task<SynapseAdminUserRedactIdResponse?> DeleteAllMessages(string mxid, List<string>? rooms = null, string? reason = null, int? limit = 100000,
+ bool waitForCompletion = true) {
+ rooms ??= [];
+
+ Dictionary<string, object> 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<SynapseAdminUserRedactIdResponse>();
+
+ if (waitForCompletion) {
+ while (true) {
+ var status = await GetRedactStatus(redactId!.RedactionId);
+ if (status?.Status != "pending") break;
+ await Task.Delay(1000);
+ }
+ }
+
+ return redactId;
+ }
+
+ public async Task<SynapseAdminRedactStatusResponse?> GetRedactStatus(string redactId) {
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRedactStatusResponse>(
+ $"/_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<SynapseAdminUserMediaResult> 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<SynapseAdminUserMediaResult>(url);
+ }
+
+ public async IAsyncEnumerable<SynapseAdminUserMediaResult.MediaInfo> 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.UrlEncode()}/block", new {
+ block
+ });
+ }
+
+ public async Task<SynapseAdminRoomDeleteResponse> DeleteRoom(string roomId, SynapseAdminRoomDeleteRequest request, bool waitForCompletion = true) {
+ var resp = await authenticatedHomeserver.ClientHttpClient.DeleteAsJsonAsync($"/_synapse/admin/v2/rooms/{roomId.UrlEncode()}", request);
+ var deleteResp = await resp.Content.ReadFromJsonAsync<SynapseAdminRoomDeleteResponse>();
+
+ if (waitForCompletion) {
+ while (true) {
+ var status = await GetRoomDeleteStatus(deleteResp!.DeleteId);
+ if (status?.Status != "pending") break;
+ await Task.Delay(1000);
+ }
+ }
+
+ return deleteResp!;
+ }
+
+ public async Task<SynapseAdminRoomDeleteStatus> GetRoomDeleteStatusByRoomId(string roomId) {
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomDeleteStatus>(
+ $"/_synapse/admin/v2/rooms/{roomId.UrlEncode()}/delete_status");
+ }
+
+ public async Task<SynapseAdminRoomDeleteStatus> GetRoomDeleteStatus(string deleteId) {
+ return await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomDeleteStatus>(
+ $"/_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<SynapseAdminRoomStateResult> GetRoomStateAsync(string roomId, string? type = null) {
+ return string.IsNullOrWhiteSpace(type)
+ ? await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomStateResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/state")
+ : await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomStateResult>($"/_synapse/admin/v1/rooms/{roomId.UrlEncode()}/state?type={type}");
+ }
+
+ public async Task QuarantineMediaByRoomId(string roomId) {
+ await authenticatedHomeserver.ClientHttpClient.PutAsJsonAsync($"/_synapse/admin/v1/room/{roomId.UrlEncode()}/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/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index f9e3d04..f0b35f9 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -1,35 +1,36 @@
using System.Net.Http.Json;
+using System.Text;
using System.Text.Json;
-using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Web;
using ArcaneLibs.Extensions;
using LibMatrix.Extensions;
using LibMatrix.Responses;
using LibMatrix.Services;
-using Microsoft.Extensions.Logging.Abstractions;
namespace LibMatrix.Homeservers;
public class RemoteHomeserver {
- public RemoteHomeserver(string baseUrl, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy) {
+ public RemoteHomeserver(string serverName, HomeserverResolverService.WellKnownUris wellKnownUris, string? proxy) {
if (string.IsNullOrWhiteSpace(proxy))
proxy = null;
- BaseUrl = baseUrl;
+ ServerNameOrUrl = serverName;
WellKnownUris = wellKnownUris;
+ Proxy = proxy;
ClientHttpClient = new MatrixHttpClient {
- BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {baseUrl}!")),
+ BaseAddress = new Uri(proxy?.TrimEnd('/') ?? wellKnownUris.Client?.TrimEnd('/') ?? throw new InvalidOperationException($"No client URI for {serverName}!")),
// Timeout = TimeSpan.FromSeconds(300) // TODO: Re-implement this
};
- if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl);
+ if (proxy is not null) ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", serverName);
if (!string.IsNullOrWhiteSpace(wellKnownUris.Server))
FederationClient = new FederationClient(WellKnownUris.Server!, proxy);
Auth = new(this);
}
private Dictionary<string, object> _profileCache { get; set; } = new();
- public string BaseUrl { get; }
+ public string ServerNameOrUrl { get; }
+ public string? Proxy { get; }
[JsonIgnore]
public MatrixHttpClient ClientHttpClient { get; set; }
@@ -51,16 +52,18 @@ public class RemoteHomeserver {
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;
+ _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<ClientVersionsResponse> GetClientVersionsAsync() {
var resp = await ClientHttpClient.GetAsync($"/_matrix/client/versions");
var data = await resp.Content.ReadFromJsonAsync<ClientVersionsResponse>();
if (!resp.IsSuccessStatusCode) Console.WriteLine("ClientVersions: " + data);
- return data;
+ return data ?? throw new InvalidOperationException("ClientVersionsResponse is null");
}
public async Task<AliasResult> ResolveRoomAliasAsync(string alias) {
@@ -68,7 +71,16 @@ public class RemoteHomeserver {
var data = await resp.Content.ReadFromJsonAsync<AliasResult>();
//var text = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode) Console.WriteLine("ResolveAlias: " + data.ToJson());
- return data;
+ 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)));
+
+ // 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));
}
#region Authentication
@@ -80,12 +92,11 @@ public class RemoteHomeserver {
type = "m.id.user",
user = username
},
- password = password,
+ password,
initial_device_display_name = deviceName
});
var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
- if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToJson());
- return data;
+ return data ?? throw new InvalidOperationException("LoginResponse is null");
}
public async Task<LoginResponse> RegisterAsync(string username, string password, string? deviceName = null) {
@@ -101,26 +112,64 @@ public class RemoteHomeserver {
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
});
var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
- if (!resp.IsSuccessStatusCode) Console.WriteLine("Register: " + data.ToJson());
- return data;
+ return data ?? throw new InvalidOperationException("LoginResponse is null");
}
#endregion
[Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)]
- public string? ResolveMediaUri(string? mxcUri) {
- if (mxcUri is null) return null;
- if (mxcUri.StartsWith("https://")) return mxcUri;
- return $"{ClientHttpClient.BaseAddress}/_matrix/media/v3/download/{mxcUri.Replace("mxc://", "")}".Replace("//_matrix", "/_matrix");
- }
+ public virtual string? ResolveMediaUri(string? mxcUri) => null;
public UserInteractiveAuthClient Auth;
}
+public class PublicRoomDirectoryResult {
+ [JsonPropertyName("chunk")]
+ public List<PublicRoomListItem> Chunk { get; set; }
+
+ [JsonPropertyName("next_batch")]
+ public string? NextBatch { get; set; }
+
+ [JsonPropertyName("prev_batch")]
+ public string? PrevBatch { get; set; }
+
+ [JsonPropertyName("total_room_count_estimate")]
+ public int TotalRoomCountEstimate { get; set; }
+
+ public class PublicRoomListItem {
+ [JsonPropertyName("avatar_url")]
+ public string? AvatarUrl { get; set; }
+
+ [JsonPropertyName("canonical_alias")]
+ public string? CanonicalAlias { get; set; }
+
+ [JsonPropertyName("guest_can_join")]
+ public bool GuestCanJoin { get; set; }
+
+ [JsonPropertyName("join_rule")]
+ public string JoinRule { get; set; }
+
+ [JsonPropertyName("name")]
+ public string? Name { get; set; }
+
+ [JsonPropertyName("num_joined_members")]
+ public int NumJoinedMembers { get; set; }
+
+ [JsonPropertyName("room_id")]
+ public string RoomId { get; set; }
+
+ [JsonPropertyName("topic")]
+ public string? Topic { get; set; }
+
+ [JsonPropertyName("world_readable")]
+ public bool WorldReadable { get; set; }
+ }
+}
+
public class AliasResult {
[JsonPropertyName("room_id")]
- public string RoomId { get; set; } = null!;
+ public string RoomId { get; set; }
[JsonPropertyName("servers")]
- public List<string> Servers { get; set; } = null!;
+ public List<string> Servers { get; set; }
}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/UserInteractiveAuthClient.cs b/LibMatrix/Homeservers/UserInteractiveAuthClient.cs
index 8be2cb9..8de01d2 100644
--- a/LibMatrix/Homeservers/UserInteractiveAuthClient.cs
+++ b/LibMatrix/Homeservers/UserInteractiveAuthClient.cs
@@ -1,7 +1,5 @@
-using System.Net.Http.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
-using ArcaneLibs.Extensions;
using LibMatrix.Responses;
namespace LibMatrix.Homeservers;
@@ -51,27 +49,27 @@ public class UserInteractiveAuthClient {
internal class RegisterFlowsResponse {
[JsonPropertyName("session")]
- public string Session { get; set; } = null!;
+ public string Session { get; set; }
[JsonPropertyName("flows")]
- public List<RegisterFlow> Flows { get; set; } = null!;
+ public List<RegisterFlow> Flows { get; set; }
[JsonPropertyName("params")]
- public JsonObject Params { get; set; } = null!;
+ public JsonObject Params { get; set; }
public class RegisterFlow {
[JsonPropertyName("stages")]
- public List<string> Stages { get; set; } = null!;
+ public List<string> Stages { get; set; }
}
}
internal class LoginFlowsResponse {
[JsonPropertyName("flows")]
- public List<LoginFlow> Flows { get; set; } = null!;
+ public List<LoginFlow> Flows { get; set; }
public class LoginFlow {
[JsonPropertyName("type")]
- public string Type { get; set; } = null!;
+ public string Type { get; set; }
}
}
|