about summary refs log tree commit diff
path: root/LibMatrix
diff options
context:
space:
mode:
authorTheArcaneBrony <myrainbowdash949@gmail.com>2023-09-29 19:38:00 +0200
committerTheArcaneBrony <myrainbowdash949@gmail.com>2023-09-29 19:38:00 +0200
commit46df5b8e335754f1582fc4d41d9546808ed8ee66 (patch)
tree29fed832bed495f1bd233c37275cb560c19f34cf /LibMatrix
parentAdd more stuff, add unit tests (diff)
downloadLibMatrix-46df5b8e335754f1582fc4d41d9546808ed8ee66.tar.xz
Unit tests, small refactors
Diffstat (limited to 'LibMatrix')
-rw-r--r--LibMatrix/EventTypes/Spec/RoomMessageEventData.cs2
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs4
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs16
-rw-r--r--LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs2
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs16
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs38
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs3
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs2
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs49
-rw-r--r--LibMatrix/Responses/CreateRoomRequest.cs2
-rw-r--r--LibMatrix/Responses/LoginResponse.cs19
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs68
-rw-r--r--LibMatrix/RoomTypes/SpaceRoom.cs35
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs30
-rw-r--r--LibMatrix/StateEvent.cs32
15 files changed, 221 insertions, 97 deletions
diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs
index b76b176..f8ee58b 100644
--- a/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs
+++ b/LibMatrix/EventTypes/Spec/RoomMessageEventData.cs
@@ -28,4 +28,6 @@ public class RoomMessageEventContent : EventContent {
     /// </summary>
     [JsonPropertyName("url")]
     public string? Url { get; set; }
+
+    public string? FileName { get; set; }
 }
diff --git a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs
index e409f3a..c5bf14e 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomCreateEventData.cs
@@ -8,12 +8,16 @@ namespace LibMatrix.EventTypes.Spec.State;
 public class RoomCreateEventContent : EventContent {
     [JsonPropertyName("room_version")]
     public string? RoomVersion { get; set; }
+
     [JsonPropertyName("creator")]
     public string? Creator { get; set; }
+
     [JsonPropertyName("m.federate")]
     public bool? Federate { get; set; }
+
     [JsonPropertyName("predecessor")]
     public RoomCreatePredecessor? Predecessor { get; set; }
+
     [JsonPropertyName("type")]
     public string? Type { get; set; }
 
diff --git a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs b/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs
index 1a5d5f5..2ae9593 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomPowerLevelEventData.cs
@@ -7,34 +7,34 @@ namespace LibMatrix.EventTypes.Spec.State;
 [MatrixEvent(EventName = "m.room.power_levels")]
 public class RoomPowerLevelEventContent : EventContent {
     [JsonPropertyName("ban")]
-    public long? Ban { get; set; } // = 50;
+    public long? Ban { get; set; } = 50;
 
     [JsonPropertyName("events_default")]
-    public long EventsDefault { get; set; } // = 0;
+    public long EventsDefault { get; set; } = 0;
 
     [JsonPropertyName("events")]
     public Dictionary<string, long>? Events { get; set; } // = null!;
 
     [JsonPropertyName("invite")]
-    public long? Invite { get; set; } // = 50;
+    public long? Invite { get; set; } = 0;
 
     [JsonPropertyName("kick")]
-    public long? Kick { get; set; } // = 50;
+    public long? Kick { get; set; } = 50;
 
     [JsonPropertyName("notifications")]
     public NotificationsPL? NotificationsPl { get; set; } // = null!;
 
     [JsonPropertyName("redact")]
-    public long? Redact { get; set; } // = 50;
+    public long? Redact { get; set; } = 50;
 
     [JsonPropertyName("state_default")]
-    public long? StateDefault { get; set; } // = 50;
+    public long? StateDefault { get; set; } = 50;
 
     [JsonPropertyName("users")]
     public Dictionary<string, long>? Users { get; set; } // = null!;
 
     [JsonPropertyName("users_default")]
-    public long? UsersDefault { get; set; } // = 0;
+    public long? UsersDefault { get; set; } = 0;
 
     [Obsolete("Historical was a key related to MSC2716, a spec change on backfill that was dropped!", true)]
     [JsonIgnore]
@@ -47,7 +47,7 @@ public class RoomPowerLevelEventContent : EventContent {
     }
 
     public bool IsUserAdmin(string userId) {
-        return Users.TryGetValue(userId, out var level) && level >= Events.Max(x=>x.Value);
+        return Users.TryGetValue(userId, out var level) && level >= Events.Max(x => x.Value);
     }
 
     public bool UserHasPermission(string userId, string eventType) {
diff --git a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs b/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs
index a13ba2e..0a897dc 100644
--- a/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs
+++ b/LibMatrix/EventTypes/Spec/State/SpaceChildEventData.cs
@@ -9,7 +9,7 @@ public class SpaceChildEventContent : EventContent {
     [JsonPropertyName("auto_join")]
     public bool? AutoJoin { get; set; }
     [JsonPropertyName("via")]
-    public string[]? Via { get; set; }
+    public List<string>? Via { get; set; }
     [JsonPropertyName("suggested")]
     public bool? Suggested { get; set; }
 }
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 386fd4d..74972a1 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -16,10 +16,6 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) {
         string? setPresence = "online",
         SyncFilter? filter = null,
         CancellationToken? cancellationToken = null) {
-        // var outFileName = "sync-" +
-        //                   (await storageService.CacheStorageProvider.GetAllKeysAsync()).Count(
-        //                       x => x.StartsWith("sync")) +
-        //                   ".json";
         var url = $"/_matrix/client/v3/sync?timeout={timeout}&set_presence={setPresence}";
         if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}";
         if (filter is not null) url += $"&filter={filter.ToJson(ignoreNull: true, indent: false)}";
@@ -28,19 +24,17 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver) {
         try {
             var req = await homeserver._httpClient.GetAsync(url, cancellationToken: cancellationToken ?? CancellationToken.None);
 
-            // var res = await JsonSerializer.DeserializeAsync<SyncResult>(await req.Content.ReadAsStreamAsync());
-
 #if DEBUG && false
-            var jsonObj = await req.Content.ReadFromJsonAsync<JsonElement>();
             try {
-                await _homeServer._httpClient.PostAsJsonAsync(
-                    "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName, jsonObj);
+                await homeserver._httpClient.PostAsync(
+                    "http://localhost:5116/validate/" + typeof(SyncResult).AssemblyQualifiedName,
+                    new StreamContent(await req.Content.ReadAsStreamAsync()));
             }
+
             catch (Exception e) {
                 Console.WriteLine("[!!] Checking sync response failed: " + e);
             }
-
-            var res = jsonObj.Deserialize<SyncResult>();
+            var res = await req.Content.ReadFromJsonAsync<SyncResult>();
             return res;
 #else
             return await req.Content.ReadFromJsonAsync<SyncResult>();
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index b881e6c..f70dd39 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -1,3 +1,4 @@
+using System.Net.Http.Headers;
 using System.Net.Http.Json;
 using System.Text.Json;
 using System.Text.Json.Nodes;
@@ -12,18 +13,30 @@ using LibMatrix.Services;
 namespace LibMatrix.Homeservers;
 
 public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
-    public AuthenticatedHomeserverGeneric(string canonicalHomeServerDomain, string accessToken) : base(canonicalHomeServerDomain) {
+    public AuthenticatedHomeserverGeneric(string baseUrl, string accessToken) : base(baseUrl) {
         AccessToken = accessToken.Trim();
         SyncHelper = new SyncHelper(this);
+
+        _httpClient.Timeout = TimeSpan.FromMinutes(15);
+        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
     }
 
     public virtual SyncHelper SyncHelper { get; init; }
-    public virtual WhoAmIResponse WhoAmI { get; set; } = null!;
-    public virtual string UserId => WhoAmI.UserId;
+    private WhoAmIResponse? _whoAmI;
+
+    public WhoAmIResponse? WhoAmI => _whoAmI ??= _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami").Result;
+    public string UserId => WhoAmI.UserId;
+
+    // public virtual async Task<WhoAmIResponse> WhoAmI() {
+    // if (_whoAmI is not null) return _whoAmI;
+    // _whoAmI = await _httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami");
+    // return _whoAmI;
+    // }
+
     public virtual string AccessToken { get; set; }
 
     public virtual GenericRoom GetRoom(string roomId) {
-        if(roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId));
+        if (roomId is null || !roomId.StartsWith("!")) throw new ArgumentException("Room ID must start with !", nameof(roomId));
         return new GenericRoom(this, roomId);
     }
 
@@ -50,7 +63,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
     }
 
     public virtual async Task<GenericRoom> CreateRoom(CreateRoomRequest creationEvent) {
-        creationEvent.CreationContent["creator"] = UserId;
+        creationEvent.CreationContent["creator"] = WhoAmI.UserId;
         var res = await _httpClient.PostAsJsonAsync("/_matrix/client/v3/createRoom", creationEvent, new JsonSerializerOptions {
             DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
         });
@@ -61,9 +74,10 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
 
         var room = GetRoom((await res.Content.ReadFromJsonAsync<JsonObject>())!["room_id"]!.ToString());
 
-        foreach (var user in creationEvent.Invite) {
-            await room.InviteUser(user);
-        }
+        if (creationEvent.Invite is not null)
+            foreach (var user in creationEvent.Invite) {
+                await room.InviteUserAsync(user);
+            }
 
         return room;
     }
@@ -77,6 +91,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
     }
 
 #region Utility Functions
+
     public virtual async IAsyncEnumerable<GenericRoom> GetJoinedRoomsByType(string type) {
         var rooms = await GetJoinedRooms();
         var tasks = rooms.Select(async room => {
@@ -92,6 +107,7 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
             if (result is not null) yield return result;
         }
     }
+
 #endregion
 
 #region Account Data
@@ -104,11 +120,11 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
         // }
         //
         // return await res.Content.ReadFromJsonAsync<T>();
-        return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{UserId}/account_data/{key}");
+        return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/user/{WhoAmI.UserId}/account_data/{key}");
     }
 
     public virtual async Task SetAccountData(string key, object data) {
-        var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}", data);
+        var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{WhoAmI.UserId}/account_data/{key}", data);
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to set account data: {await res.Content.ReadAsStringAsync()}");
             throw new InvalidDataException($"Failed to set account data: {await res.Content.ReadAsStringAsync()}");
@@ -116,4 +132,6 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeServer {
     }
 
 #endregion
+
+
 }
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs
index 5319f46..a420d71 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverMxApiExtended.cs
@@ -4,5 +4,4 @@ using LibMatrix.Services;
 
 namespace LibMatrix.Homeservers;
 
-public class AuthenticatedHomeserverMxApiExtended(string canonicalHomeServerDomain, string accessToken) : AuthenticatedHomeserverGeneric(canonicalHomeServerDomain,
-    accessToken);
+public class AuthenticatedHomeserverMxApiExtended(string baseUrl, string accessToken) : AuthenticatedHomeserverGeneric(baseUrl, accessToken);
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
index ae26f69..e355d2d 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
@@ -102,7 +102,7 @@ public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric {
         }
     }
 
-    public AuthenticatedHomeserverSynapse(string canonicalHomeServerDomain, string accessToken) : base(canonicalHomeServerDomain, accessToken) {
+    public AuthenticatedHomeserverSynapse(string baseUrl, string accessToken) : base(baseUrl, accessToken) {
         Admin = new(this);
     }
 }
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index ab3ab51..d10c837 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -1,19 +1,22 @@
 using System.Net.Http.Json;
+using System.Text.Json;
 using System.Text.Json.Serialization;
 using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Extensions;
 using LibMatrix.Responses;
+using LibMatrix.Services;
 
 namespace LibMatrix.Homeservers;
 
-public class RemoteHomeServer(string canonicalHomeServerDomain) {
-    // _httpClient.Timeout = TimeSpan.FromSeconds(5);
+public class RemoteHomeServer(string baseUrl) {
 
     private Dictionary<string, object> _profileCache { get; set; } = new();
-    public string HomeServerDomain { get; } = canonicalHomeServerDomain.Trim();
-    public string FullHomeServerDomain { get; set; }
-    public MatrixHttpClient _httpClient { get; set; } = new();
+    public string BaseUrl { get; } = baseUrl.Trim();
+    public MatrixHttpClient _httpClient { get; set; } = new() {
+        BaseAddress = new Uri(new HomeserverResolverService().ResolveHomeserverFromWellKnown(baseUrl).Result ?? throw new InvalidOperationException("Failed to resolve homeserver")),
+        Timeout = TimeSpan.FromSeconds(120)
+    };
 
     public async Task<ProfileResponseEventContent> GetProfileAsync(string mxid) {
         if (mxid is null) throw new ArgumentNullException(nameof(mxid));
@@ -46,6 +49,42 @@ public class RemoteHomeServer(string canonicalHomeServerDomain) {
         if (!resp.IsSuccessStatusCode) Console.WriteLine("ResolveAlias: " + data.ToJson());
         return data;
     }
+
+#region Authentication
+
+    public async Task<LoginResponse> LoginAsync(string username, string password, string? deviceName = null) {
+        var resp = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/login", new {
+            type = "m.login.password",
+            identifier = new {
+                type = "m.id.user",
+                user = username
+            },
+            password = password,
+            initial_device_display_name = deviceName
+        });
+        var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
+        if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data.ToJson());
+        return data;
+    }
+
+    public async Task<LoginResponse> RegisterAsync(string username, string password, string? deviceName = null) {
+        var resp = await _httpClient.PostAsJsonAsync("/_matrix/client/r0/register", new {
+            kind = "user",
+            auth = new {
+                type = "m.login.dummy"
+            },
+            username,
+            password,
+            initial_device_display_name = deviceName ?? "LibMatrix"
+        }, new JsonSerializerOptions() {
+            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+        });
+        var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
+        if (!resp.IsSuccessStatusCode) Console.WriteLine("Register: " + data.ToJson());
+        return data;
+    }
+
+#endregion
 }
 
 public class AliasResult {
diff --git a/LibMatrix/Responses/CreateRoomRequest.cs b/LibMatrix/Responses/CreateRoomRequest.cs
index 381271b..511b3da 100644
--- a/LibMatrix/Responses/CreateRoomRequest.cs
+++ b/LibMatrix/Responses/CreateRoomRequest.cs
@@ -40,7 +40,7 @@ public class CreateRoomRequest {
     public JsonObject CreationContent { get; set; } = new();
 
     [JsonPropertyName("invite")]
-    public List<string> Invite { get; set; }
+    public List<string>? Invite { get; set; }
 
     /// <summary>
     ///     For use only when you can't use the CreationContent property
diff --git a/LibMatrix/Responses/LoginResponse.cs b/LibMatrix/Responses/LoginResponse.cs
index 175f337..eb53c0a 100644
--- a/LibMatrix/Responses/LoginResponse.cs
+++ b/LibMatrix/Responses/LoginResponse.cs
@@ -1,19 +1,30 @@
 using System.Text.Json.Serialization;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
 
 namespace LibMatrix.Responses;
 
 public class LoginResponse {
     [JsonPropertyName("access_token")]
-    public string AccessToken { get; set; }
+    public string AccessToken { get; set; } = null!;
 
     [JsonPropertyName("device_id")]
-    public string DeviceId { get; set; }
+    public string DeviceId { get; set; } = null!;
+
+    private string? _homeserver;
 
     [JsonPropertyName("home_server")]
-    public string Homeserver { get; set; }
+    public string Homeserver {
+        get => _homeserver ?? UserId.Split(':', 2).Last();
+        protected init => _homeserver = value;
+    }
 
     [JsonPropertyName("user_id")]
-    public string UserId { get; set; }
+    public string UserId { get; set; } = null!;
+
+    public async Task<AuthenticatedHomeserverGeneric> GetAuthenticatedHomeserver(string? proxy = null) {
+        return new AuthenticatedHomeserverGeneric(proxy ?? await new HomeserverResolverService().ResolveHomeserverFromWellKnown(Homeserver), AccessToken);
+    }
 }
 public class LoginRequest {
     [JsonPropertyName("type")]
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index ab748fe..78a0873 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -28,14 +28,6 @@ public class GenericRoom {
 
     public string RoomId { get; set; }
 
-    [Obsolete("", true)]
-    public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "") {
-        var url = $"/_matrix/client/v3/rooms/{RoomId}/state";
-        if (!string.IsNullOrEmpty(type)) url += $"/{type}";
-        if (!string.IsNullOrEmpty(stateKey)) url += $"/{stateKey}";
-        return await _httpClient.GetFromJsonAsync<JsonElement>(url);
-    }
-
     public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() {
         var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/state");
         var result =
@@ -68,7 +60,7 @@ public class GenericRoom {
         }
         catch (MatrixException e) {
             // if (e is not { ErrorCodode: "M_NOT_FOUND" }) {
-                throw;
+            throw;
             // }
 
             // Console.WriteLine(e);
@@ -202,21 +194,18 @@ public class GenericRoom {
         return resu;
     }
 
-    public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) {
-        var content = new MultipartFormDataContent();
-        content.Add(new StreamContent(fileStream), "file", fileName);
-        var res = await
-            (
-                await _httpClient.PutAsync(
-                    $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(),
-                    content
-                )
-            )
-            .Content.ReadFromJsonAsync<EventIdResponse>();
-        return res;
+    public async Task<EventIdResponse> SendFileAsync(string fileName, Stream fileStream, string messageType = "m.file") {
+        var url = await Homeserver.UploadFile(fileName, fileStream);
+        var content = new RoomMessageEventContent() {
+            MessageType = messageType,
+            Url = url,
+            Body = fileName,
+            FileName = fileName,
+        };
+        return await SendTimelineEventAsync("m.room.message", content);
     }
 
-    public async Task<T> GetRoomAccountData<T>(string key) {
+    public async Task<T> GetRoomAccountDataAsync<T>(string key) {
         var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}");
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to get room account data: {await res.Content.ReadAsStringAsync()}");
@@ -226,7 +215,7 @@ public class GenericRoom {
         return await res.Content.ReadFromJsonAsync<T>();
     }
 
-    public async Task SetRoomAccountData(string key, object data) {
+    public async Task SetRoomAccountDataAsync(string key, object data) {
         var res = await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data);
         if (!res.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to set room account data: {await res.Content.ReadAsStringAsync()}");
@@ -236,7 +225,7 @@ public class GenericRoom {
 
     public readonly SpaceRoom AsSpace;
 
-    public async Task<T> GetEvent<T>(string eventId) {
+    public async Task<T> GetEventAsync<T>(string eventId) {
         return await _httpClient.GetFromJsonAsync<T>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}");
     }
 
@@ -246,9 +235,38 @@ public class GenericRoom {
             $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
     }
 
-    public async Task InviteUser(string userId, string? reason = null) {
+    public async Task InviteUserAsync(string userId, string? reason = null) {
         await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
     }
+
+#region Disband room
+
+    public async Task DisbandRoomAsync() {
+        var states = GetFullStateAsync();
+        List<string> stateTypeIgnore = new() {
+            "m.room.create",
+            "m.room.power_levels",
+            "m.room.join_rules",
+            "m.room.history_visibility",
+            "m.room.guest_access",
+            "m.room.member",
+        };
+        await foreach (var state in states) {
+            if (state is null || state.RawContent is not { Count: > 0 }) continue;
+            if (state.Type == "m.room.member" && state.StateKey != Homeserver.UserId)
+                try {
+                    await BanAsync(state.StateKey, "Disbanding room");
+                }
+                catch (MatrixException e) {
+                    if (e.ErrorCode != "M_FORBIDDEN") throw;
+                }
+
+            if (stateTypeIgnore.Contains(state.Type)) continue;
+            await SendStateEventAsync(state.Type, state.StateKey, new());
+        }
+    }
+
+#endregion
 }
 
 public class RoomIdResponse {
diff --git a/LibMatrix/RoomTypes/SpaceRoom.cs b/LibMatrix/RoomTypes/SpaceRoom.cs
index a43ae82..4a8e247 100644
--- a/LibMatrix/RoomTypes/SpaceRoom.cs
+++ b/LibMatrix/RoomTypes/SpaceRoom.cs
@@ -1,26 +1,37 @@
 using ArcaneLibs.Extensions;
 using LibMatrix.Homeservers;
+using Microsoft.Extensions.Logging;
 
 namespace LibMatrix.RoomTypes;
 
-public class SpaceRoom : GenericRoom {
-    private new readonly AuthenticatedHomeserverGeneric _homeserver;
+public class SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : GenericRoom(homeserver, roomId) {
     private readonly GenericRoom _room;
 
-    public SpaceRoom(AuthenticatedHomeserverGeneric homeserver, string roomId) : base(homeserver, roomId) {
-        _homeserver = homeserver;
-    }
-
-    private static SemaphoreSlim _semaphore = new(1, 1);
     public async IAsyncEnumerable<GenericRoom> GetChildrenAsync(bool includeRemoved = false) {
-        // await _semaphore.WaitAsync();
         var rooms = new List<GenericRoom>();
         var state = GetFullStateAsync();
         await foreach (var stateEvent in state) {
-            if (stateEvent.Type != "m.space.child") continue;
-            if (stateEvent.RawContent.ToJson() != "{}" || includeRemoved)
-                yield return _homeserver.GetRoom(stateEvent.StateKey);
+            if (stateEvent!.Type != "m.space.child") continue;
+            if (stateEvent.RawContent!.ToJson() != "{}" || includeRemoved)
+                yield return Homeserver.GetRoom(stateEvent.StateKey);
+        }
+    }
+
+    public async Task<EventIdResponse> AddChildAsync(GenericRoom room) {
+        var members = room.GetMembersAsync(true);
+        Dictionary<string, int> memberCountByHs = new();
+        await foreach (var member in members) {
+            var server = member.StateKey.Split(':')[1];
+            if (memberCountByHs.ContainsKey(server)) memberCountByHs[server]++;
+            else memberCountByHs[server] = 1;
         }
-        // _semaphore.Release();
+
+        var resp = await SendStateEventAsync("m.space.child", room.RoomId, new {
+            via = memberCountByHs
+                .OrderByDescending(x => x.Value)
+                .Select(x => x.Key)
+                .Take(10)
+        });
+        return resp;
     }
 }
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 49167fa..666d2a2 100644
--- a/LibMatrix/Services/HomeserverProviderService.cs
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -9,29 +9,29 @@ using Microsoft.Extensions.Logging;
 namespace LibMatrix.Services;
 
 public class HomeserverProviderService {
-    private readonly TieredStorageService _tieredStorageService;
     private readonly ILogger<HomeserverProviderService> _logger;
     private readonly HomeserverResolverService _homeserverResolverService;
 
-    public HomeserverProviderService(TieredStorageService tieredStorageService,
-        ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
-        _tieredStorageService = tieredStorageService;
+    public HomeserverProviderService(ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
         _logger = logger;
         _homeserverResolverService = homeserverResolverService;
-        logger.LogDebug("New HomeserverProviderService created with TieredStorageService<{}>!",
-            string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name)));
     }
 
     private static Dictionary<string, SemaphoreSlim> _authenticatedHomeserverSemaphore = new();
     private static Dictionary<string, AuthenticatedHomeserverGeneric> _authenticatedHomeServerCache = new();
 
+    private static Dictionary<string, SemaphoreSlim> _remoteHomeserverSemaphore = new();
+    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));
         await sem.WaitAsync();
-        if (_authenticatedHomeServerCache.ContainsKey(homeserver + accessToken)) {
-            sem.Release();
-            return _authenticatedHomeServerCache[homeserver + accessToken];
+        lock (_authenticatedHomeServerCache) {
+            if (_authenticatedHomeServerCache.ContainsKey(homeserver + accessToken)) {
+                sem.Release();
+                return _authenticatedHomeServerCache[homeserver + accessToken];
+            }
         }
 
         var domain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
@@ -45,12 +45,11 @@ public class HomeserverProviderService {
             hs = new AuthenticatedHomeserverGeneric(homeserver, accessToken);
         }
 
-        hs.FullHomeServerDomain = domain;
         hs._httpClient = hc;
         hs._httpClient.Timeout = TimeSpan.FromMinutes(15);
         hs._httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
 
-        hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
+        // (() => hs.WhoAmI) = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
 
         lock(_authenticatedHomeServerCache)
             _authenticatedHomeServerCache[homeserver + accessToken] = hs;
@@ -60,11 +59,10 @@ public class HomeserverProviderService {
     }
 
     public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? proxy = null) {
-        var hs = new RemoteHomeServer(homeserver);
-        hs.FullHomeServerDomain = proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
-        hs._httpClient.Dispose();
-        hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
-        hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
+        var hs = new RemoteHomeServer(proxy ?? await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver));
+        // hs._httpClient.Dispose();
+        // hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
+        // hs._httpClient.Timeout = TimeSpan.FromSeconds(120);
         return hs;
     }
 
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 97348a5..b42bd64 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -2,6 +2,7 @@ using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
+using System.Text.Json.Serialization.Metadata;
 using ArcaneLibs;
 using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes;
@@ -48,7 +49,7 @@ public class StateEvent {
 
             return null;
         }
-        set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value));
+        set => RawContent = JsonSerializer.Deserialize<JsonObject>(JsonSerializer.Serialize(value, value.GetType()));
     }
 
     [JsonPropertyName("state_key")]
@@ -120,3 +121,32 @@ public class StateEvent {
     [JsonIgnore]
     public string cdtype => TypedContent.GetType().Name;
 }
+
+/*
+public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoResolver
+{
+    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
+    {
+        JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
+
+        Type baseType = typeof(EventContent);
+        if (jsonTypeInfo.Type == baseType) {
+            jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions {
+                TypeDiscriminatorPropertyName = "type",
+                IgnoreUnrecognizedTypeDiscriminators = true,
+                UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType,
+
+                DerivedTypes = StateEvent.KnownStateEventTypesByName.Select(x => new JsonDerivedType(x.Value, x.Key)).ToList()
+
+                // DerivedTypes = new ClassCollector<EventContent>()
+                // .ResolveFromAllAccessibleAssemblies()
+                // .SelectMany(t => t.GetCustomAttributes<MatrixEventAttribute>()
+                // .Select(a => new JsonDerivedType(t, attr.EventName));
+
+            };
+        }
+
+        return jsonTypeInfo;
+    }
+}
+*/