diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index 771f41e..cdc0dca 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -255,5 +255,10 @@ public class MatrixHttpClient {
};
return await SendAsync(request, cancellationToken);
}
+
+ public async Task DeleteAsync(string url) {
+ var request = new HttpRequestMessage(HttpMethod.Delete, url);
+ await SendAsync(request);
+ }
}
#endif
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index ae033f1..a7010ee 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -56,6 +56,12 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
public TimeSpan MinimumDelay { get; set; } = new(0);
+ public async Task<int> GetUnoptimisedStoreCount() {
+ if (storageProvider is null) return -1;
+ var keys = await storageProvider.GetAllKeysAsync();
+ return keys.Count(x => !x.StartsWith("old/")) - 1;
+ }
+
private async Task UpdateFilterAsync() {
if (!string.IsNullOrWhiteSpace(NamedFilterName)) {
_filterId = await homeserver.NamedCaches.FilterCache.GetOrSetValueAsync(NamedFilterName);
diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs
index e2dbdee..e9c5938 100644
--- a/LibMatrix/Helpers/SyncStateResolver.cs
+++ b/LibMatrix/Helpers/SyncStateResolver.cs
@@ -1,3 +1,5 @@
+using System.Collections.Frozen;
+using System.Diagnostics;
using ArcaneLibs.Extensions;
using LibMatrix.Extensions;
using LibMatrix.Filters;
@@ -26,16 +28,10 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge
_syncHelper.SetPresence = SetPresence;
_syncHelper.Filter = Filter;
_syncHelper.FullState = FullState;
- // run sync or grab from storage if available
- // var sync = storageProvider != null && await storageProvider.ObjectExistsAsync(Since ?? "init")
- // ? await storageProvider.LoadObjectAsync<SyncResponse>(Since ?? "init")
- // : await _syncHelper.SyncAsync(cancellationToken);
+
var sync = await _syncHelper.SyncAsync(cancellationToken);
if (sync is null) return await ContinueAsync(cancellationToken);
- // if (storageProvider != null && !await storageProvider.ObjectExistsAsync(Since ?? "init"))
- // await storageProvider.SaveObjectAsync(Since ?? "init", sync);
-
if (MergedState is null) MergedState = sync;
else MergedState = MergeSyncs(MergedState, sync);
Since = sync.NextBatch;
@@ -45,22 +41,98 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge
public async Task OptimiseStore() {
if (storageProvider is null) return;
+ if (!await storageProvider.ObjectExistsAsync("init")) return;
+
+ Console.Write("Optimising sync store...");
+ var initLoadTask = storageProvider.LoadObjectAsync<SyncResponse>("init");
+ var keys = (await storageProvider.GetAllKeysAsync()).ToFrozenSet();
+ var count = keys.Count(x => !x.StartsWith("old/")) - 1;
+ Console.WriteLine($"Found {count} entries to optimise.");
- var keys = await storageProvider.GetAllKeysAsync();
- var count = keys.Count - 2;
- var merged = await storageProvider.LoadObjectAsync<SyncResponse>("init");
+ var merged = await initLoadTask;
if (merged is null) return;
+ if (!keys.Contains(merged.NextBatch)) {
+ Console.WriteLine("Next response after initial sync is not present, not checkpointing!");
+ return;
+ }
+
+ // We back up old entries
+ var oldPath = $"old/{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}";
+ await storageProvider.MoveObjectAsync("init", $"{oldPath}/init");
+
+ var moveTasks = new List<Task>();
while (keys.Contains(merged.NextBatch)) {
+ Console.Write($"Merging {merged.NextBatch}, {--count} remaining... ");
+ var sw = Stopwatch.StartNew();
+ var swt = Stopwatch.StartNew();
var next = await storageProvider.LoadObjectAsync<SyncResponse>(merged.NextBatch);
- if (next is null) break;
+ Console.Write($"Load {sw.GetElapsedAndRestart().TotalMilliseconds}ms... ");
+ if (next is null || merged.NextBatch == next.NextBatch) break;
+
+ Console.Write($"Check {sw.GetElapsedAndRestart().TotalMilliseconds}ms... ");
+ // back up old entry
+ moveTasks.Add(storageProvider.MoveObjectAsync(merged.NextBatch, $"{oldPath}/{merged.NextBatch}"));
+ Console.Write($"Move {sw.GetElapsedAndRestart().TotalMilliseconds}ms... ");
+
merged = MergeSyncs(merged, next);
- Console.WriteLine($"Merged {merged.NextBatch}, {--count} remaining...");
+ Console.Write($"Merge {sw.GetElapsedAndRestart().TotalMilliseconds}ms... ");
+ Console.WriteLine($"Total {swt.Elapsed.TotalMilliseconds}ms");
+ // Console.WriteLine($"Merged {merged.NextBatch}, {--count} remaining...");
+ }
+
+ await storageProvider.SaveObjectAsync("init", merged);
+ await Task.WhenAll(moveTasks);
+ }
+
+ public async Task UnrollOptimisedStore() {
+ if (storageProvider is null) return;
+ Console.WriteLine("WARNING: Unrolling sync store!");
+ }
+
+ public async Task SquashOptimisedStore(int targetCountPerCheckpoint) {
+ Console.Write($"Balancing optimised store to {targetCountPerCheckpoint} per checkpoint...");
+ var checkpoints = await GetCheckpointMap();
+ if (checkpoints is null) return;
+
+ Console.WriteLine(
+ $" Stats: {checkpoints.Count} checkpoints with [{checkpoints.Min(x => x.Value.Count)} < ~{checkpoints.Average(x => x.Value.Count)} < {checkpoints.Max(x => x.Value.Count)}] entries");
+ Console.WriteLine($"Found {checkpoints?.Count ?? 0} checkpoints.");
+ }
+
+ public async Task dev() {
+ var keys = (await storageProvider?.GetAllKeysAsync()).ToFrozenSet();
+ var times = new Dictionary<long, List<string>>();
+ var values = keys.Select(async x => Task.Run(async () => (x, await storageProvider?.LoadObjectAsync<SyncResponse>(x)))).ToAsyncEnumerable();
+ await foreach (var task in values) {
+ var (key, data) = await task;
+ if (data is null) continue;
+ var derivTime = data.GetDerivedSyncTime();
+ if (!times.ContainsKey(derivTime)) times[derivTime] = new();
+ times[derivTime].Add(key);
}
- await storageProvider.SaveObjectAsync("merged", merged);
+ foreach (var (time, ckeys) in times.OrderBy(x => x.Key)) {
+ Console.WriteLine($"{time}: {ckeys.Count} keys");
+ }
+ }
+
+ private async Task<Dictionary<ulong, List<string>>?> GetCheckpointMap() {
+ if (storageProvider is null) return null;
+ var keys = (await storageProvider.GetAllKeysAsync()).ToFrozenSet();
+ var map = new Dictionary<ulong, List<string>>();
+ foreach (var key in keys) {
+ if (!key.StartsWith("old/")) continue;
+ var parts = key.Split('/');
+ if (parts.Length < 3) continue;
+ // if (!map.ContainsKey(parts[1])) map[parts[1]] = new();
+ // map[parts[1]].Add(parts[2]);
+ if (!ulong.TryParse(parts[1], out var checkpoint)) continue;
+ if (!map.ContainsKey(checkpoint)) map[checkpoint] = new();
+ map[checkpoint].Add(parts[2]);
+ }
- Environment.Exit(0);
+ return map.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);
}
private SyncResponse MergeSyncs(SyncResponse oldSync, SyncResponse newSync) {
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/AdminRoomDeleteRequest.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs
index f4c927a..67a3104 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/AdminRoomDeleteRequest.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Requests/SynapseAdminRoomDeleteRequest.cs
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Requests;
-public class AdminRoomDeleteRequest {
+public class SynapseAdminRoomDeleteRequest {
[JsonPropertyName("new_room_user_id")]
public string? NewRoomUserId { get; set; }
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/AdminRoomListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
index c9d7e52..d84c89b 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminRoomListResult.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/RoomListResult.cs
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
-public class AdminRoomListResult {
+public class SynapseAdminRoomListResult {
[JsonPropertyName("offset")]
public int Offset { get; set; }
@@ -16,9 +16,9 @@ public class AdminRoomListResult {
public int? PrevBatch { get; set; }
[JsonPropertyName("rooms")]
- public List<AdminRoomListResultRoom> Rooms { get; set; } = new();
+ public List<SynapseAdminRoomListResultRoom> Rooms { get; set; } = new();
- public class AdminRoomListResultRoom {
+ 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/SynapseAdminEventReportListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminEventReportListResult.cs
deleted file mode 100644
index 030108a..0000000
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/SynapseAdminEventReportListResult.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
-
-public class SynapseAdminEventReportListResult {
- [JsonPropertyName("offset")]
- public int Offset { get; set; }
-
- [JsonPropertyName("total")]
- public int Total { get; set; }
-
- [JsonPropertyName("next_token")]
- public string? NextToken { get; set; }
-
- [JsonPropertyName("event_reports")]
- public List<SynapseAdminEventReportListResultReport> Reports { get; set; } = new();
-
- public class SynapseAdminEventReportListResultReport {
- [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; }
-
- [JsonPropertyName("approved")]
- public bool Approved { 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/AdminUserListResult.cs b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs
index 9b0c481..3132906 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/AdminUserListResult.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/Models/Responses/UserListResult.cs
@@ -2,7 +2,7 @@ using System.Text.Json.Serialization;
namespace LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Responses;
-public class AdminUserListResult {
+public class SynapseAdminUserListResult {
[JsonPropertyName("offset")]
public int Offset { get; set; }
@@ -13,9 +13,9 @@ public class AdminUserListResult {
public string? NextToken { get; set; }
[JsonPropertyName("users")]
- public List<AdminUserListResultUser> Users { get; set; } = new();
+ public List<SynapseAdminUserListResultUser> Users { get; set; } = new();
- public class AdminUserListResultUser {
+ public class SynapseAdminUserListResultUser {
[JsonPropertyName("name")]
public string Name { get; set; }
@@ -52,7 +52,20 @@ public class AdminUserListResult {
[JsonPropertyName("locked")]
public bool Locked { get; set; }
+ // Requires enabling MSC3866
[JsonPropertyName("approved")]
- public bool Approved { get; set; }
+ 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 b3902eb..4d8a577 100644
--- a/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
+++ b/LibMatrix/Homeservers/ImplementationDetails/Synapse/SynapseAdminApiClient.cs
@@ -1,5 +1,7 @@
using System.Net.Http.Json;
+using System.Text.Json;
using System.Text.Json.Nodes;
+using System.Text.Json.Serialization;
using ArcaneLibs.Extensions;
using LibMatrix.Filters;
using LibMatrix.Homeservers.ImplementationDetails.Synapse.Models.Filters;
@@ -10,12 +12,13 @@ namespace LibMatrix.Homeservers.ImplementationDetails.Synapse;
public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse 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<AdminRoomListResult.AdminRoomListResultRoom> SearchRoomsAsync(int limit = int.MaxValue, int chunkLimit = 250, string orderBy = "name",
- string dir = "f", string? searchTerm = null, SynapseAdminLocalRoomQueryFilter? localFilter = null) {
- AdminRoomListResult? res = null;
+ 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 {
@@ -26,7 +29,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---");
- res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<AdminRoomListResult>(url);
+ res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminRoomListResult>(url);
totalRooms ??= res.TotalRooms;
Console.WriteLine(res.ToJson(false));
foreach (var room in res.Rooms) {
@@ -117,7 +120,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
#region Users
- public async IAsyncEnumerable<AdminUserListResult.AdminUserListResultUser> SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250,
+ public async IAsyncEnumerable<SynapseAdminUserListResult.SynapseAdminUserListResultUser> SearchUsersAsync(int limit = int.MaxValue, int chunkLimit = 250,
SynapseAdminLocalUserQueryFilter? localFilter = null) {
// TODO: implement filters
string? from = null;
@@ -127,7 +130,7 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
if (!string.IsNullOrWhiteSpace(from)) url = url.AddQuery("from", from);
Console.WriteLine($"--- ADMIN Querying User List with URL: {url} ---");
// TODO: implement URI methods in http client
- var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<AdminUserListResult>(url.ToString());
+ var res = await authenticatedHomeserver.ClientHttpClient.GetFromJsonAsync<SynapseAdminUserListResult>(url.ToString());
foreach (var user in res.Users) {
limit--;
yield return user;
@@ -171,5 +174,185 @@ public class SynapseAdminApiClient(AuthenticatedHomeserverSynapse authenticatedH
}
}
+ 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/rooms/{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
}
\ No newline at end of file
diff --git a/LibMatrix/Interfaces/Services/IStorageProvider.cs b/LibMatrix/Interfaces/Services/IStorageProvider.cs
index 165e7df..fb7bb6d 100644
--- a/LibMatrix/Interfaces/Services/IStorageProvider.cs
+++ b/LibMatrix/Interfaces/Services/IStorageProvider.cs
@@ -31,7 +31,7 @@ public interface IStorageProvider {
}
// get all keys
- public Task<List<string>> GetAllKeysAsync() {
+ public Task<IEnumerable<string>> GetAllKeysAsync() {
Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!");
throw new NotImplementedException();
}
@@ -53,4 +53,18 @@ public interface IStorageProvider {
Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadStream(key)!");
throw new NotImplementedException();
}
+
+ // copy
+ public async Task CopyObjectAsync(string sourceKey, string destKey) {
+ Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement CopyObject(sourceKey, destKey), using load + save!");
+ var data = await LoadObjectAsync<object>(sourceKey);
+ await SaveObjectAsync(destKey, data);
+ }
+
+ // move
+ public async Task MoveObjectAsync(string sourceKey, string destKey) {
+ Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement MoveObject(sourceKey, destKey), using copy + delete!");
+ await CopyObjectAsync(sourceKey, destKey);
+ await DeleteObjectAsync(sourceKey);
+ }
}
\ No newline at end of file
diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs
index e7fe109..9b4ce05 100644
--- a/LibMatrix/Responses/SyncResponse.cs
+++ b/LibMatrix/Responses/SyncResponse.cs
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
+using LibMatrix.EventTypes.Spec.Ephemeral;
using LibMatrix.EventTypes.Spec.State;
namespace LibMatrix.Responses;
@@ -64,10 +65,9 @@ public class SyncResponse {
public EventList? State { get; set; }
public override string ToString() {
- var lastEvent = Timeline?.Events?.LastOrDefault(x=>x.Type == "m.room.member");
+ var lastEvent = Timeline?.Events?.LastOrDefault(x => x.Type == "m.room.member");
var membership = (lastEvent?.TypedContent as RoomMemberEventContent);
return $"LeftRoomDataStructure: {lastEvent?.Sender} {membership?.Membership} ({membership?.Reason})";
-
}
}
@@ -133,4 +133,15 @@ public class SyncResponse {
public EventList? InviteState { get; set; }
}
}
+
+ public long GetDerivedSyncTime() {
+ return ((long[]) [
+ AccountData?.Events?.Max(x => x.OriginServerTs) ?? 0,
+ Presence?.Events?.Max(x => x.OriginServerTs) ?? 0,
+ ToDevice?.Events?.Max(x => x.OriginServerTs) ?? 0,
+ Rooms?.Join?.Values?.Max(x => x.Timeline?.Events?.Max(y => y.OriginServerTs)) ?? 0,
+ Rooms?.Invite?.Values?.Max(x => x.InviteState?.Events?.Max(y => y.OriginServerTs)) ?? 0,
+ Rooms?.Leave?.Values?.Max(x => x.Timeline?.Events?.Max(y => y.OriginServerTs)) ?? 0
+ ]).Max();
+ }
}
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 8398ab9..02bd555 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -328,8 +328,8 @@ public class GenericRoom {
catch {
try {
var alias = await GetCanonicalAliasAsync();
- if (alias?.Alias is not null) return alias.Alias;
- throw new Exception("No name or alias");
+ if (!string.IsNullOrWhiteSpace(alias?.Alias)) return alias.Alias;
+ throw new Exception("No alias");
}
catch {
try {
@@ -337,7 +337,8 @@ public class GenericRoom {
var memberList = new List<string>();
var memberCount = 0;
await foreach (var member in members)
- memberList.Add(member.RawContent?["displayname"]?.GetValue<string>() ?? "");
+ if (member.StateKey != Homeserver.UserId)
+ memberList.Add(member.RawContent?["displayname"]?.GetValue<string>() ?? "");
memberCount = memberList.Count;
memberList.RemoveAll(string.IsNullOrWhiteSpace);
memberList = memberList.OrderBy(x => x).ToList();
@@ -431,7 +432,7 @@ public class GenericRoom {
return await res.Content.ReadFromJsonAsync<T>();
}
-
+
public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) {
try {
return await GetRoomAccountDataAsync<T>(key);
@@ -554,4 +555,4 @@ public class GenericRoom {
public class RoomIdResponse {
[JsonPropertyName("room_id")]
public string RoomId { get; set; } = null!;
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 8f99e10..87050cc 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -31,6 +31,23 @@ public class StateEvent {
public static Type GetStateEventType(string? type) =>
string.IsNullOrWhiteSpace(type) ? typeof(UnknownEventContent) : KnownStateEventTypesByName.GetValueOrDefault(type) ?? typeof(UnknownEventContent);
+ [JsonPropertyName("state_key")]
+ public string? StateKey { get; set; }
+
+ [JsonPropertyName("type")]
+ public string Type { get; set; }
+
+ [JsonPropertyName("replaces_state")]
+ public string? ReplacesState { get; set; }
+
+ private JsonObject? _rawContent;
+
+ [JsonPropertyName("content")]
+ public JsonObject? RawContent {
+ get => _rawContent;
+ set => _rawContent = value;
+ }
+
[JsonIgnore]
public Type MappedType => GetStateEventType(Type);
@@ -79,6 +96,7 @@ public class StateEvent {
}
}
+<<<<<<< HEAD
public T? ContentAs<T>() {
try {
return RawContent.Deserialize<T>(TypedContentSerializerOptions)!;
@@ -107,37 +125,6 @@ public class StateEvent {
get => _rawContent;
set => _rawContent = value;
}
- //
- // [JsonIgnore]
- // public new Type GetType {
- // get {
- // var type = GetStateEventType(Type);
- //
- // //special handling for some types
- // // if (type == typeof(RoomEmotesEventContent)) {
- // // RawContent["emote"] = RawContent["emote"]?.AsObject() ?? new JsonObject();
- // // }
- // //
- // // if (this is StateEventResponse stateEventResponse) {
- // // if (type == null || type == typeof(object)) {
- // // Console.WriteLine($"Warning: unknown event type '{Type}'!");
- // // Console.WriteLine(RawContent.ToJson());
- // // Directory.CreateDirectory($"unknown_state_events/{Type}");
- // // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
- // // RawContent.ToJson());
- // // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
- // // }
- // // else if (RawContent is not null && RawContent.FindExtraJsonObjectFields(type)) {
- // // Directory.CreateDirectory($"unknown_state_events/{Type}");
- // // File.WriteAllText($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json",
- // // RawContent.ToJson());
- // // Console.WriteLine($"Saved to unknown_state_events/{Type}/{stateEventResponse.EventId}.json");
- // // }
- // // }
- //
- // return type;
- // }
- // }
//debug
[JsonIgnore]
|