about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--LibMatrix/Extensions/HttpClientExtensions.cs16
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs9
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs46
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs10
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs36
-rw-r--r--LibMatrix/Interfaces/EventContent.cs2
-rw-r--r--LibMatrix/LibMatrix.csproj3
-rw-r--r--LibMatrix/MatrixException.cs3
-rw-r--r--LibMatrix/Responses/LoginResponse.cs4
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs76
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs44
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs31
-rw-r--r--LibMatrix/Services/TieredStorageService.cs11
-rw-r--r--LibMatrix/StateEvent.cs12
-rw-r--r--Tests/LibMatrix.Tests/Tests/ResolverTest.cs2
-rw-r--r--Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs8
16 files changed, 184 insertions, 129 deletions
diff --git a/LibMatrix/Extensions/HttpClientExtensions.cs b/LibMatrix/Extensions/HttpClientExtensions.cs
index 5bb0dc2..913864e 100644
--- a/LibMatrix/Extensions/HttpClientExtensions.cs
+++ b/LibMatrix/Extensions/HttpClientExtensions.cs
@@ -53,11 +53,21 @@ public class MatrixHttpClient : HttpClient {
             Console.WriteLine(e);
         }
 
-        var a = await base.SendAsync(request, cancellationToken);
-        if (a.IsSuccessStatusCode) return a;
+        HttpResponseMessage responseMessage;
+        try {
+            responseMessage = await base.SendAsync(request, cancellationToken);
+        }
+        catch (Exception e) {
+            typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance)
+                ?.SetValue(request, 0);
+            await Task.Delay(2500);
+            return await SendAsync(request, cancellationToken);
+        }
+
+        if (responseMessage.IsSuccessStatusCode) return responseMessage;
 
         //error handling
-        var content = await a.Content.ReadAsStringAsync(cancellationToken);
+        var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
         if (content.Length == 0)
             throw new MatrixException() {
                 ErrorCode = "M_UNKNOWN",
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index fb7fad2..a63b8bb 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -15,12 +15,13 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
     public bool FullState { get; set; } = false;
 
     public bool IsInitialSync { get; set; } = true;
-    
+
     public async Task<SyncResponse?> SyncAsync(CancellationToken? cancellationToken = null) {
         if (homeserver is null) {
             Console.WriteLine("Null passed as homeserver for SyncHelper!");
             throw new ArgumentNullException("Null passed as homeserver for SyncHelper!");
         }
+
         var url = $"/_matrix/client/v3/sync?timeout={Timeout}&set_presence={SetPresence}&full_state={(FullState ? "true" : "false")}";
         if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}";
         if (Filter is not null) url += $"&filter={Filter.ToJson(ignoreNull: true, indent: false)}";
@@ -45,7 +46,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
         while (!cancellationToken?.IsCancellationRequested ?? true) {
             var sync = await SyncAsync(cancellationToken);
             if (sync is null) continue;
-            Since = string.IsNullOrWhiteSpace(sync?.NextBatch) ? Since : sync.NextBatch;
+            if (!string.IsNullOrWhiteSpace(sync?.NextBatch)) Since = sync.NextBatch;
             yield return sync;
         }
     }
@@ -73,7 +74,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
         var tasks = SyncReceivedHandlers.Select(x => x(syncResponse)).ToList();
         await Task.WhenAll(tasks);
 
-        if (syncResponse.AccountData is { Events: { Count: > 0 } }) {
+        if (syncResponse.AccountData is { Events.Count: > 0 }) {
             foreach (var accountDataEvent in syncResponse.AccountData.Events) {
                 tasks = AccountDataReceivedHandlers.Select(x => x(accountDataEvent)).ToList();
                 await Task.WhenAll(tasks);
@@ -124,4 +125,4 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
     /// Event fired when an account data event is received
     /// </summary>
     public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new();
-}
+}
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index c3684a1..37696eb 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -13,43 +13,41 @@ using LibMatrix.Services;
 
 namespace LibMatrix.Homeservers;
 
-public class AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : RemoteHomeserver(baseUrl) {
-    public static async Task<T> Create<T>(string baseUrl, string accessToken) where T : AuthenticatedHomeserverGeneric {
-        var instance = Activator.CreateInstance(typeof(T), baseUrl, accessToken) as T
+public class AuthenticatedHomeserverGeneric(string serverName, string accessToken) : RemoteHomeserver(serverName) {
+    public static async Task<T> Create<T>(string serverName, string accessToken, string? proxy = null) where T : AuthenticatedHomeserverGeneric {
+        var instance = Activator.CreateInstance(typeof(T), serverName, accessToken) as T
                        ?? throw new InvalidOperationException($"Failed to create instance of {typeof(T).Name}");
-        var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl);
-        
+        HomeserverResolverService.WellKnownUris? urls = null;
+        if(proxy is null)
+            urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(serverName);
+
         instance.ClientHttpClient = new() {
-            BaseAddress = new Uri(urls.client
-                                  ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+            BaseAddress = new Uri(proxy ?? urls?.Client
+                ?? throw new InvalidOperationException("Failed to resolve homeserver")),
             Timeout = TimeSpan.FromMinutes(15),
             DefaultRequestHeaders = {
                 Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
             }
         };
         instance.ServerHttpClient = new() {
-            BaseAddress = new Uri(urls.server
-                                  ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+            BaseAddress = new Uri(proxy ?? urls?.Server
+                ?? throw new InvalidOperationException("Failed to resolve homeserver")),
             Timeout = TimeSpan.FromMinutes(15),
             DefaultRequestHeaders = {
                 Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
             }
         };
+
         instance.WhoAmI = await instance.ClientHttpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami");
+
+        if (proxy is not null) {
+            instance.ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", serverName);
+            instance.ServerHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", serverName);
+        }
+
         return instance;
     }
 
-    // Activator.CreateInstance(baseUrl, accessToken) {
-    //     _httpClient = new() {
-    //         BaseAddress = new Uri(await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl)
-    //                               ?? throw new InvalidOperationException("Failed to resolve homeserver")),
-    //         Timeout = TimeSpan.FromMinutes(15),
-    //         DefaultRequestHeaders = {
-    //             Authorization = new AuthenticationHeaderValue("Bearer", accessToken)
-    //         }
-    //     }
-    // };
-
     public WhoAmIResponse? WhoAmI { get; set; }
     public string? UserId => WhoAmI?.UserId;
     public string? UserLocalpart => UserId?.Split(":")[0][1..];
@@ -176,12 +174,6 @@ public class AuthenticatedHomeserverGeneric(string baseUrl, string accessToken)
 
 #endregion
 
-    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 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})");
@@ -247,7 +239,7 @@ public class AuthenticatedHomeserverGeneric(string baseUrl, string accessToken)
             if (sync.Rooms is null) break;
             List<Task> tasks = new();
             foreach (var (roomId, roomData) in sync.Rooms.Join) {
-                if (roomData.State is { Events: { Count: > 0 } }) {
+                if (roomData.State is { Events.Count: > 0 }) {
                     var incommingRoomProfile =
                         roomData.State?.Events?.FirstOrDefault(x => x.Type == "m.room.member" && x.StateKey == WhoAmI.UserId)?.TypedContent as RoomMemberEventContent;
                     if (incommingRoomProfile is null) continue;
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
index 0910cbe..15e5b65 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
@@ -6,11 +6,7 @@ namespace LibMatrix.Homeservers;
 
 public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric {
     public readonly SynapseAdminApi Admin;
-    public class SynapseAdminApi {
-        private readonly AuthenticatedHomeserverGeneric _authenticatedHomeserver;
-
-        public SynapseAdminApi(AuthenticatedHomeserverGeneric authenticatedHomeserver) => _authenticatedHomeserver = authenticatedHomeserver;
-
+    public class SynapseAdminApi(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;
             var i = 0;
@@ -23,7 +19,7 @@ public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric {
 
                 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<AdminRoomListingResult>(url);
                 totalRooms ??= res?.TotalRooms;
                 Console.WriteLine(res.ToJson(false));
                 foreach (var room in res.Rooms) {
@@ -101,7 +97,7 @@ public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric {
         }
     }
 
-    public AuthenticatedHomeserverSynapse(string baseUrl, string accessToken) : base(baseUrl, accessToken) {
+    public AuthenticatedHomeserverSynapse(string serverName, string accessToken) : base(serverName, accessToken) {
         Admin = new(this);
     }
 }
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index 0757f6e..55a3a02 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -10,24 +10,31 @@ using LibMatrix.Services;
 namespace LibMatrix.Homeservers;
 
 public class RemoteHomeserver(string baseUrl) {
-    public static async Task<RemoteHomeserver> Create(string baseUrl) {
-        var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl);
-        return new RemoteHomeserver(baseUrl) {
-            ClientHttpClient = new() {
-                BaseAddress = new Uri(urls.client ?? throw new InvalidOperationException("Failed to resolve homeserver")),
-                Timeout = TimeSpan.FromSeconds(120)
-            },
-            ServerHttpClient = new() {
-                BaseAddress = new Uri(urls.server ?? throw new InvalidOperationException("Failed to resolve homeserver")),
-                Timeout = TimeSpan.FromSeconds(120)
-            }
+    public static async Task<RemoteHomeserver> Create(string baseUrl, string? proxy = null) {
+        var homeserver = new RemoteHomeserver(baseUrl);
+        homeserver.WellKnownUris = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl);
+        homeserver.ClientHttpClient = new() {
+            BaseAddress = new Uri(proxy ?? homeserver.WellKnownUris.Client ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+            Timeout = TimeSpan.FromSeconds(120)
+        };
+        homeserver.ServerHttpClient = new() {
+            BaseAddress = new Uri(proxy ?? homeserver.WellKnownUris.Server ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+            Timeout = TimeSpan.FromSeconds(120)
         };
+
+        if (proxy is not null) {
+            homeserver.ClientHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl);
+            homeserver.ServerHttpClient.DefaultRequestHeaders.Add("MXAE_UPSTREAM", baseUrl);
+        }
+
+        return homeserver;
     }
 
     private Dictionary<string, object> _profileCache { get; set; } = new();
     public string BaseUrl { get; } = baseUrl;
     public MatrixHttpClient ClientHttpClient { get; set; }
     public MatrixHttpClient ServerHttpClient { get; set; }
+    public HomeserverResolverService.WellKnownUris WellKnownUris { get; set; }
 
     public async Task<UserProfileResponse> GetProfileAsync(string mxid) {
         if (mxid is null) throw new ArgumentNullException(nameof(mxid));
@@ -100,6 +107,13 @@ public class RemoteHomeserver(string baseUrl) {
     public async Task<ServerVersionResponse> GetServerVersionAsync() {
         return await ServerHttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version");
     }
+    
+    
+    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 class ServerVersionResponse {
diff --git a/LibMatrix/Interfaces/EventContent.cs b/LibMatrix/Interfaces/EventContent.cs
index 5cf0503..51671dd 100644
--- a/LibMatrix/Interfaces/EventContent.cs
+++ b/LibMatrix/Interfaces/EventContent.cs
@@ -13,7 +13,7 @@ public abstract class EventContent {
         [JsonPropertyName("m.in_reply_to")]
         public EventInReplyTo? InReplyTo { get; set; }
 
-        public abstract class EventInReplyTo {
+        public class EventInReplyTo {
             [JsonPropertyName("event_id")]
             public string EventId { get; set; }
 
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index 805695b..e2e1433 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -5,6 +5,9 @@
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
         <LangVersion>preview</LangVersion>
+
+        <Optimize>true</Optimize>
+        <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
     </PropertyGroup>
 
     <ItemGroup>
diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs
index 3aaad19..f127abf 100644
--- a/LibMatrix/MatrixException.cs
+++ b/LibMatrix/MatrixException.cs
@@ -17,6 +17,9 @@ public class MatrixException : Exception {
     public int? RetryAfterMs { get; set; }
 
     public string RawContent { get; set; }
+    
+    public string? GetAsJson() => new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true);
+
 
     public override string Message =>
         $"{ErrorCode}: {ErrorCode switch {
diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
index a9ef3be..82004fc 100644
--- a/LibMatrix/Responses/LoginResponse.cs
+++ b/LibMatrix/Responses/LoginResponse.cs
@@ -24,8 +24,8 @@ public class LoginResponse {
     public string UserId { get; set; } = null!;
 
     public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) {
-        var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver);
-        return await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(proxy ?? urls.client, AccessToken);
+        // var urls = await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver);
+        return await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(Homeserver, AccessToken, proxy);
     }
 }
 public class LoginRequest {
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index 96bcefd..700e530 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -1,7 +1,9 @@
+using System.Diagnostics;
 using System.Net.Http.Json;
 using System.Text.Json;
 using System.Text.Json.Serialization;
 using System.Web;
+using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes.Spec;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Extensions;
@@ -96,22 +98,23 @@ public class GenericRoom {
         return await res.Content.ReadFromJsonAsync<RoomIdResponse>() ?? throw new Exception("Failed to join room?");
     }
 
-    
     public async IAsyncEnumerable<StateEventResponse> GetMembersAsync(bool joinedOnly = true) {
-        // var res = GetFullStateAsync();
-        // await foreach (var member in res) {
-        //     if (member?.Type != "m.room.member") continue;
-        //     if (joinedOnly && (member.TypedContent as RoomMemberEventContent)?.Membership is not "join") continue;
-        //     yield return member;
-        // }
+        var sw = Stopwatch.StartNew();
         var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members");
+        Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
         var resText = await res.Content.ReadAsStringAsync();
-        var result = await JsonSerializer.DeserializeAsync<ChunkedStateEventResponse>(await res.Content.ReadAsStreamAsync());
+        Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}");
+        var result = await JsonSerializer.DeserializeAsync<ChunkedStateEventResponse>(await res.Content.ReadAsStreamAsync(), new JsonSerializerOptions() {
+            TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default
+        });
+        Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
         foreach (var resp in result.Chunk) {
             if (resp?.Type != "m.room.member") continue;
             if (joinedOnly && (resp.TypedContent as RoomMemberEventContent)?.Membership is not "join") continue;
             yield return resp;
         }
+
+        Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
     }
 
 #region Utility shortcuts
@@ -153,7 +156,7 @@ public class GenericRoom {
     public async Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() =>
         await GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels");
 
-    public async Task<string> GetNameOrFallbackAsync() {
+    public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) {
         try {
             return await GetNameAsync();
         }
@@ -166,8 +169,9 @@ public class GenericRoom {
                     memberList.Add((member.TypedContent is RoomMemberEventContent memberEvent ? memberEvent.DisplayName : "") ?? "");
                 memberCount = memberList.Count;
                 memberList.RemoveAll(string.IsNullOrWhiteSpace);
-                if (memberList.Count >= 3)
-                    return string.Join(", ", memberList.Take(2)) + " and " + (memberCount - 2) + " others.";
+                memberList = memberList.OrderBy(x => x).ToList();
+                if (memberList.Count > maxMemberNames)
+                    return string.Join(", ", memberList.Take(maxMemberNames)) + " and " + (memberCount - maxMemberNames) + " others.";
                 return string.Join(", ", memberList);
             }
             catch {
@@ -176,8 +180,15 @@ public class GenericRoom {
         }
     }
 
+    public async Task InviteUsersAsync(IEnumerable<string> users, string? reason = null, bool skipExisting = true) {
+        var tasks = users.Select(x => InviteUserAsync(x, reason, skipExisting)).ToList();
+        await Task.WhenAll(tasks);
+    }
+
 #endregion
 
+#region Simple calls
+
     public async Task ForgetAsync() =>
         await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
 
@@ -198,6 +209,16 @@ public class GenericRoom {
         await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
             new UserIdAndReason { UserId = userId });
 
+    public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) {
+        if (skipExisting && await GetStateAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
+            return;
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
+    }
+
+#endregion
+
+#region Events
+
     public async Task<EventIdResponse?> SendStateEventAsync(string eventType, object content) =>
         await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content))
             .Content.ReadFromJsonAsync<EventIdResponse>();
@@ -243,8 +264,6 @@ public class GenericRoom {
         }
     }
 
-    public readonly SpaceRoom AsSpace;
-
     public async Task<T> GetEventAsync<T>(string eventId) {
         return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}");
     }
@@ -255,12 +274,30 @@ public class GenericRoom {
             $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
     }
 
-    public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) {
-        if (skipExisting && await GetStateAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
-            return;
-        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
+#endregion
+
+#region Utilities
+
+    public async Task<Dictionary<string, List<string>>> GetMembersByHomeserverAsync(bool joinedOnly = true) {
+        if (Homeserver is AuthenticatedHomeserverMxApiExtended mxaeHomeserver)
+            return await Homeserver.ClientHttpClient.GetFromJsonAsync<Dictionary<string, List<string>>>(
+                $"/_matrix/client/v3/rooms/{RoomId}/members_by_homeserver?joined_only={joinedOnly}");
+        Dictionary<string, List<string>> roomHomeservers = new();
+        var members = GetMembersAsync();
+        await foreach (var member in members) {
+            string memberHs = member.StateKey.Split(':', 2)[1];
+            roomHomeservers.TryAdd(memberHs, new());
+            roomHomeservers[memberHs].Add(member.StateKey);
+        }
+
+        Console.WriteLine($"Finished processing {RoomId}");
+        return roomHomeservers;
     }
 
+#endregion
+
+    public readonly SpaceRoom AsSpace;
+
 #region Disband room
 
     public async Task DisbandRoomAsync() {
@@ -289,11 +326,6 @@ public class GenericRoom {
     }
 
 #endregion
-
-    public async Task InviteUsersAsync(IEnumerable<string> users, string? reason = null, bool skipExisting = true) {
-        var tasks = users.Select(x => InviteUserAsync(x, reason, skipExisting)).ToList();
-        await Task.WhenAll(tasks);
-    }
 }
 
 public class RoomIdResponse {
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index a43f518..5c8827c 100644
--- a/LibMatrix/Services/HomeserverProviderService.cs
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -6,15 +6,7 @@ using Microsoft.Extensions.Logging;
 
 namespace LibMatrix.Services;
 
-public class HomeserverProviderService {
-    private readonly ILogger<HomeserverProviderService> _logger;
-    private readonly HomeserverResolverService _homeserverResolverService;
-
-    public HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
-        _logger = logger;
-        _homeserverResolverService = homeserverResolverService;
-    }
-
+public class HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
     private static Dictionary<string, SemaphoreSlim> _authenticatedHomeserverSemaphore = new();
     private static Dictionary<string, AuthenticatedHomeserverGeneric> _authenticatedHomeserverCache = new();
 
@@ -22,40 +14,44 @@ public class HomeserverProviderService {
     private static Dictionary<string, RemoteHomeserver> _remoteHomeserverCache = new();
 
     public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedWithToken(string homeserver, string accessToken, string? proxy = null) {
-        var sem = _authenticatedHomeserverSemaphore.GetOrCreate(homeserver + accessToken, _ => new SemaphoreSlim(1, 1));
+        var cacheKey = homeserver + accessToken + proxy;
+        var sem = _authenticatedHomeserverSemaphore.GetOrCreate(cacheKey, _ => new SemaphoreSlim(1, 1));
         await sem.WaitAsync();
+        AuthenticatedHomeserverGeneric? hs;
         lock (_authenticatedHomeserverCache) {
-            if (_authenticatedHomeserverCache.ContainsKey(homeserver + accessToken)) {
+            if (_authenticatedHomeserverCache.TryGetValue(cacheKey, out hs)) {
                 sem.Release();
-                return _authenticatedHomeserverCache[homeserver + accessToken];
+                return hs;
             }
         }
 
         // var domain = proxy ?? (await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver)).client;
 
-        var rhs = await RemoteHomeserver.Create(homeserver);
-        var serverVersion = await rhs.GetServerVersionAsync();
-        
+        var rhs = await RemoteHomeserver.Create(homeserver, proxy);
+        var clientVersions = await rhs.GetClientVersionsAsync();
+        if(proxy is not null)
+            Console.WriteLine($"Homeserver {homeserver} proxied via {proxy}...");
+        Console.WriteLine($"{homeserver}: " + clientVersions.ToJson());
 
-        AuthenticatedHomeserverGeneric hs;
-        if (true) {
-            hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverMxApiExtended>(homeserver, accessToken);
-        }
+        if (clientVersions.UnstableFeatures.TryGetValue("gay.rory.mxapiextensions.v0", out bool a) && a)
+            hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverMxApiExtended>(homeserver, accessToken, proxy);
         else {
-            hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverSynapse>(homeserver, accessToken);
+            var serverVersion = await rhs.GetServerVersionAsync();
+            if (serverVersion is { Server.Name: "Synapse" })
+                hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverSynapse>(homeserver, accessToken, proxy);
+            else
+                hs = await AuthenticatedHomeserverGeneric.Create<AuthenticatedHomeserverGeneric>(homeserver, accessToken, proxy);
         }
 
-        // (() => hs.WhoAmI) = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
-
         lock (_authenticatedHomeserverCache)
-            _authenticatedHomeserverCache[homeserver + accessToken] = hs;
+            _authenticatedHomeserverCache[cacheKey] = hs;
         sem.Release();
 
         return hs;
     }
 
     public async Task<RemoteHomeserver> GetRemoteHomeserver(string homeserver, string? proxy = null) {
-        var hs = await RemoteHomeserver.Create(proxy ?? homeserver);
+        var hs = await RemoteHomeserver.Create(homeserver, proxy);
         // hs._httpClient.Dispose();
         // hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.ServerName) };
         // hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index 06771b0..c8b6bb7 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -1,3 +1,4 @@
+using System.Collections.Concurrent;
 using System.Text.Json;
 using ArcaneLibs.Extensions;
 using LibMatrix.Extensions;
@@ -8,13 +9,11 @@ namespace LibMatrix.Services;
 public class HomeserverResolverService(ILogger<HomeserverResolverService>? logger = null) {
     private readonly MatrixHttpClient _httpClient = new();
 
-    private static readonly Dictionary<string, (string, string)> _wellKnownCache = new();
-    private static readonly Dictionary<string, SemaphoreSlim> _wellKnownSemaphores = new();
+    private static readonly ConcurrentDictionary<string, WellKnownUris> _wellKnownCache = new();
+    private static readonly ConcurrentDictionary<string, SemaphoreSlim> _wellKnownSemaphores = new();
 
-    public async Task<(string client, string server)> ResolveHomeserverFromWellKnown(string homeserver) {
+    public async Task<WellKnownUris> ResolveHomeserverFromWellKnown(string homeserver) {
         if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
-        // if(!_wellKnownSemaphores.ContainsKey(homeserver))
-            // _wellKnownSemaphores[homeserver] = new(1, 1);
         _wellKnownSemaphores.TryAdd(homeserver, new(1, 1));
         await _wellKnownSemaphores[homeserver].WaitAsync();
         if (_wellKnownCache.TryGetValue(homeserver, out var known)) {
@@ -23,11 +22,11 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
         }
         
         logger?.LogInformation("Resolving homeserver: {}", homeserver);
-        var res = (
-            await _tryResolveFromClientWellknown(homeserver),
-            await _tryResolveFromServerWellknown(homeserver)
-        );
-        _wellKnownCache.Add(homeserver, res!);
+        var res = new WellKnownUris {
+            Client = await _tryResolveFromClientWellknown(homeserver),
+            Server = await _tryResolveFromServerWellknown(homeserver)
+        };
+        _wellKnownCache.TryAdd(homeserver, res);
         _wellKnownSemaphores[homeserver].Release();
         return res;
     }
@@ -54,6 +53,11 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
             return hs;
         }
 
+        // fallback: most servers host these on the same location
+        var clientUrl = await _tryResolveFromClientWellknown(homeserver);
+        if (clientUrl is not null && await _httpClient.CheckSuccessStatus($"{clientUrl}/_matrix/federation/v1/version"))
+            return clientUrl;
+
         logger?.LogInformation("No server well-known...");
         return null;
     }
@@ -62,7 +66,12 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
         if (homeserver is null) throw new ArgumentNullException(nameof(homeserver));
         if (mxc is null) throw new ArgumentNullException(nameof(mxc));
         if (!mxc.StartsWith("mxc://")) throw new InvalidDataException("mxc must start with mxc://");
-        homeserver = (await ResolveHomeserverFromWellKnown(homeserver)).client;
+        homeserver = (await ResolveHomeserverFromWellKnown(homeserver)).Client;
         return mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/");
     }
+
+    public class WellKnownUris {
+        public string? Client { get; set; }
+        public string? Server { get; set; }
+    }
 }
diff --git a/LibMatrix/Services/TieredStorageService.cs b/LibMatrix/Services/TieredStorageService.cs
index f242785..280340e 100644
--- a/LibMatrix/Services/TieredStorageService.cs
+++ b/LibMatrix/Services/TieredStorageService.cs
@@ -2,12 +2,7 @@ using LibMatrix.Interfaces.Services;
 
 namespace LibMatrix.Services;
 
-public class TieredStorageService {
-    public IStorageProvider? CacheStorageProvider { get; }
-    public IStorageProvider? DataStorageProvider { get; }
-
-    public TieredStorageService(IStorageProvider? cacheStorageProvider, IStorageProvider? dataStorageProvider) {
-        CacheStorageProvider = cacheStorageProvider;
-        DataStorageProvider = dataStorageProvider;
-    }
+public class TieredStorageService(IStorageProvider? cacheStorageProvider, IStorageProvider? dataStorageProvider) {
+    public IStorageProvider? CacheStorageProvider { get; } = cacheStorageProvider;
+    public IStorageProvider? DataStorageProvider { get; } = dataStorageProvider;
 }
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index dbb3401..3e8c4b5 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -36,6 +36,7 @@ public class StateEvent {
         return eventType ?? typeof(UnknownEventContent);
     }
 
+    [JsonIgnore]
     public EventContent TypedContent {
         get {
             if(Type == "m.receipt") {
@@ -134,6 +135,7 @@ public class StateEvent {
     public string cdtype => TypedContent.GetType().Name;
 }
 
+
 public class StateEventResponse : StateEvent {
     [JsonPropertyName("origin_server_ts")]
     public ulong OriginServerTs { get; set; }
@@ -150,8 +152,8 @@ public class StateEventResponse : StateEvent {
     [JsonPropertyName("event_id")]
     public string EventId { get; set; }
 
-    [JsonPropertyName("user_id")]
-    public string UserId { get; set; }
+    // [JsonPropertyName("user_id")]
+    // public string UserId { get; set; }
 
     [JsonPropertyName("replaces_state")]
     public new string ReplacesState { get; set; }
@@ -177,6 +179,12 @@ public class StateEventResponse : StateEvent {
     }
 }
 
+[JsonSourceGenerationOptions(WriteIndented = true)]
+[JsonSerializable(typeof(ChunkedStateEventResponse))]
+internal partial class ChunkedStateEventResponseSerializerContext : JsonSerializerContext
+{
+}
+
 public class EventList {
     [JsonPropertyName("events")]
     public List<StateEventResponse>? Events { get; set; } = new();
diff --git a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs b/Tests/LibMatrix.Tests/Tests/ResolverTest.cs
index cece41b..804ad6c 100644
--- a/Tests/LibMatrix.Tests/Tests/ResolverTest.cs
+++ b/Tests/LibMatrix.Tests/Tests/ResolverTest.cs
@@ -21,7 +21,7 @@ public class ResolverTest : TestBed<TestFixture> {
     public async Task ResolveServer() {
         foreach (var (domain, expected) in _config.ExpectedHomeserverMappings) {
             var server = await _resolver.ResolveHomeserverFromWellKnown(domain);
-            Assert.Equal(expected, server.client);
+            Assert.Equal(expected, server.Client);
         }
     }
 
diff --git a/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs b/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs
index 4dbee54..81753d1 100644
--- a/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs
+++ b/Utilities/LibMatrix.DebugDataValidationApi/Controllers/ValidationController.cs
@@ -6,12 +6,8 @@ namespace LibMatrix.DebugDataValidationApi.Controllers;
 
 [ApiController]
 [Route("/")]
-public class ValidationController : ControllerBase {
-    private readonly ILogger<ValidationController> _logger;
-
-    public ValidationController(ILogger<ValidationController> logger) {
-        _logger = logger;
-    }
+public class ValidationController(ILogger<ValidationController> logger) : ControllerBase {
+    private readonly ILogger<ValidationController> _logger = logger;
 
     [HttpPost("/validate/{type}")]
     public Task<bool> Get([FromRoute] string type, [FromBody] JsonElement content) {