about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs2
-rw-r--r--ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs2
-rw-r--r--ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs20
-rw-r--r--ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs3
-rw-r--r--ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs2
-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
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs73
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs76
-rw-r--r--Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs10
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthTests.cs19
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests.cs240
25 files changed, 563 insertions, 202 deletions
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs
index f04ec3a..0211f74 100644
--- a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs
+++ b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBot.cs
@@ -57,7 +57,7 @@ public class MRUBot : IHostedService {
         hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => {
             var inviteEvent =
                 args.Value.InviteState.Events.FirstOrDefault(x =>
-                    x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId);
+                    x.Type == "m.room.member" && x.StateKey == hs.UserId);
             _logger.LogInformation(
                 $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}");
             if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender == "@mxidupwitch:the-apothecary.club") {
diff --git a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs b/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs
index d633f89..fd6866c 100644
--- a/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs
+++ b/ExampleBots/MediaModeratorPoC/Bot/Commands/BanMediaCommand.cs
@@ -43,7 +43,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic
                         messageType: "m.text"));
 
                 //get replied message
-                var repliedMessage = await ctx.Room.GetEvent<StateEventResponse>(messageContent.RelatesTo!.InReplyTo!.EventId);
+                var repliedMessage = await ctx.Room.GetEventAsync<StateEventResponse>(messageContent.RelatesTo!.InReplyTo!.EventId);
 
                 //check if recommendation is in list
                 if (ctx.Args.Length < 2) {
diff --git a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs
index e6ba269..f9bbcf3 100644
--- a/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs
+++ b/ExampleBots/MediaModeratorPoC/Bot/MediaModBot.cs
@@ -109,17 +109,17 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot>
         hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => {
             var inviteEvent =
                 args.Value.InviteState.Events.FirstOrDefault(x =>
-                    x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId);
+                    x.Type == "m.room.member" && x.StateKey == hs.UserId);
             logger.LogInformation(
                 $"Got invite to {args.Key} by {inviteEvent.Sender} with reason: {(inviteEvent.TypedContent as RoomMemberEventContent).Reason}");
             if (inviteEvent.Sender.EndsWith(":rory.gay") || inviteEvent.Sender.EndsWith(":conduit.rory.gay")) {
                 try {
                     var senderProfile = await hs.GetProfileAsync(inviteEvent.Sender);
-                    await (hs.GetRoom(args.Key)).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!");
+                    await hs.GetRoom(args.Key).JoinAsync(reason: $"I was invited by {senderProfile.DisplayName ?? inviteEvent.Sender}!");
                 }
                 catch (Exception e) {
                     logger.LogError("{}", e.ToString());
-                    await (hs.GetRoom(args.Key)).LeaveAsync(reason: "I was unable to join the room: " + e);
+                    await hs.GetRoom(args.Key).LeaveAsync(reason: "I was unable to join the room: " + e);
                 }
             }
         });
@@ -161,16 +161,16 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot>
                             case "warn": {
                                 await room.SendMessageEventAsync(
                                     new RoomMessageEventContent(
-                                        body: $"Please be careful when posting this image: {matchedpolicyData.Reason}",
+                                        body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}",
                                         messageType: "m.text") {
                                         Format = "org.matrix.custom.html",
                                         FormattedBody =
-                                            $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}</a></font>"
+                                            $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>"
                                     });
                                 break;
                             }
                             case "redact": {
-                                await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
+                                await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified");
                                 break;
                             }
                             case "spoiler": {
@@ -220,9 +220,15 @@ public class MediaModBot(AuthenticatedHomeserverGeneric hs, ILogger<MediaModBot>
                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
                                 //change powerlevel to -1
                                 var currentPls = await room.GetPowerLevelsAsync();
+                                if(currentPls is null) {
+                                    logger.LogWarning("Unable to get power levels for {room}", room.RoomId);
+                                    await _logRoom.SendMessageEventAsync(
+                                        MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}"));
+                                    return;
+                                }
+                                currentPls.Users ??= new();
                                 currentPls.Users[@event.Sender] = -1;
                                 await room.SendStateEventAsync("m.room.power_levels", currentPls);
-
                                 break;
                             }
                             case "kick": {
diff --git a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs
index 6686a37..0096c78 100644
--- a/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs
+++ b/ExampleBots/MediaModeratorPoC/Bot/StateEventTypes/MediaPolicyStateEventData.cs
@@ -4,7 +4,8 @@ using LibMatrix.Interfaces;
 
 namespace MediaModeratorPoC.Bot.StateEventTypes;
 
-[MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")]
+[
+    MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.homeserver")]
 [MatrixEvent(EventName = "gay.rory.media_moderator_poc.rule.media")]
 public class MediaPolicyEventContent : EventContent {
     /// <summary>
diff --git a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs
index c3cebe2..0bd2bbf 100644
--- a/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs
+++ b/ExampleBots/PluralContactBotPoC/Bot/PluralContactBot.cs
@@ -41,7 +41,7 @@ public class PluralContactBot(AuthenticatedHomeserverGeneric hs, ILogger<PluralC
         hs.SyncHelper.InviteReceivedHandlers.Add(async Task (args) => {
             var inviteEvent =
                 args.Value.InviteState.Events.FirstOrDefault(x =>
-                    x.Type == "m.room.member" && x.StateKey == hs.WhoAmI.UserId);
+                    x.Type == "m.room.member" && x.StateKey == hs.UserId);
             logger.LogInformation("Got invite to {} by {} with reason: {}", args.Key, inviteEvent.Sender, (inviteEvent.TypedContent as RoomMemberEventContent).Reason);
 
             try {
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;
+    }
+}
+*/
diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
new file mode 100644
index 0000000..abd3e99
--- /dev/null
+++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
@@ -0,0 +1,73 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+
+namespace LibMatrix.Tests.Abstractions;
+
+public static class HomeserverAbstraction {
+    public static async Task<AuthenticatedHomeserverGeneric> GetHomeserver() {
+        var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay");
+        // string username = Guid.NewGuid().ToString();
+        // string password = Guid.NewGuid().ToString();
+        string username = "@f1a2d2d6-1924-421b-91d0-893b347b2a49:matrixunittests.rory.gay";
+        string password = "d6d782d6-8bc9-4fac-9cd8-78e101b4298b";
+        LoginResponse reg;
+        try {
+            reg = await rhs.LoginAsync(username, password);
+        }
+        catch (MatrixException e) {
+            if (e.ErrorCode == "M_FORBIDDEN") {
+                await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!");
+                reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+            }
+            else throw new Exception("Failed to register", e);
+        }
+
+        var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay");
+
+        var rooms = await hs.GetJoinedRooms();
+
+        var disbandRoomTasks = rooms.Select(async room => {
+            // await room.DisbandRoomAsync();
+            await room.LeaveAsync();
+            await room.ForgetAsync();
+            return room;
+        }).ToList();
+        await Task.WhenAll(disbandRoomTasks);
+
+        // foreach (var room in rooms) {
+        //     // await room.DisbandRoomAsync();
+        //     await room.LeaveAsync();
+        //     await room.ForgetAsync();
+        // }
+
+        return hs;
+    }
+
+    public static async Task<AuthenticatedHomeserverGeneric> GetRandomHomeserver() {
+        var rhs = new RemoteHomeServer("https://matrixunittests.rory.gay");
+        LoginResponse reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!");
+        var hs = await reg.GetAuthenticatedHomeserver("https://matrixunittests.rory.gay");
+
+        var rooms = await hs.GetJoinedRooms();
+
+        var disbandRoomTasks = rooms.Select(async room => {
+            // await room.DisbandRoomAsync();
+            await room.LeaveAsync();
+            await room.ForgetAsync();
+            return room;
+        }).ToList();
+        await Task.WhenAll(disbandRoomTasks);
+
+        return hs;
+    }
+
+    public static async IAsyncEnumerable<AuthenticatedHomeserverGeneric> GetRandomHomeservers(int count = 1) {
+        var createSpaceTasks = Enumerable
+            .Range(0, count)
+            .Select(_ => GetRandomHomeserver()).ToAsyncEnumerable();
+        await foreach (var hs in createSpaceTasks) {
+            yield return hs;
+        }
+    }
+}
diff --git a/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs
new file mode 100644
index 0000000..44c35da
--- /dev/null
+++ b/Tests/LibMatrix.Tests/Abstractions/RoomAbstraction.cs
@@ -0,0 +1,76 @@
+using ArcaneLibs.Extensions;
+using LibMatrix.EventTypes.Spec.State;
+using LibMatrix.Homeservers;
+using LibMatrix.Responses;
+using LibMatrix.RoomTypes;
+
+namespace LibMatrix.Tests.Abstractions;
+
+public static class RoomAbstraction {
+    public static async Task<GenericRoom> GetTestRoom(AuthenticatedHomeserverGeneric hs) {
+        var testRoom = await hs.CreateRoom(new CreateRoomRequest() {
+            Name = "LibMatrix Test Room",
+            // Visibility = CreateRoomVisibility.Public,
+            RoomAliasName = Guid.NewGuid().ToString()
+        });
+
+        await testRoom.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new());
+
+        return testRoom;
+    }
+
+    private static SemaphoreSlim _spaceSemaphore = null!;
+
+    public static async Task<SpaceRoom> GetTestSpace(AuthenticatedHomeserverGeneric hs, int roomCount = 100, bool addSpaces = false, int spaceSizeReduction = 10) {
+        _spaceSemaphore ??= new(roomCount / spaceSizeReduction, roomCount / spaceSizeReduction);
+        var crq = new CreateRoomRequest() {
+            Name = $"LibMatrix Test Space ({roomCount} children)",
+            // Visibility = CreateRoomVisibility.Public,
+            RoomAliasName = Guid.NewGuid().ToString(),
+            InitialState = new()
+        };
+        crq._creationContentBaseType.Type = "m.space";
+
+
+        var createRoomTasks = Enumerable.Range(0, roomCount)
+            .Select(_ => hs.CreateRoom(new CreateRoomRequest() {
+                Name = $"LibMatrix Test Room {Guid.NewGuid()}",
+                // Visibility = CreateRoomVisibility.Public,
+                RoomAliasName = Guid.NewGuid().ToString()
+            })).ToAsyncEnumerable();
+
+        await foreach (var room in createRoomTasks) {
+            crq.InitialState.Add(new() {
+                Type = "m.space.child",
+                StateKey = room.RoomId,
+                TypedContent = new SpaceChildEventContent() {
+                    Via = new() {
+                        room.RoomId.Split(":")[1]
+                    }
+                }
+            });
+        }
+
+        if (addSpaces) {
+            for (int i = 0; i < roomCount; i++) {
+                var space = await GetTestSpace(hs, roomCount - spaceSizeReduction, true, spaceSizeReduction);
+                crq.InitialState.Add(new() {
+                    Type = "m.space.child",
+                    StateKey = space.RoomId,
+                    TypedContent = new SpaceChildEventContent() {
+                        Via = new() {
+                            space.RoomId.Split(":")[1]
+                        }
+                    }
+                });
+            }
+        }
+
+        var testSpace = (await hs.CreateRoom(crq)).AsSpace;
+
+        await testSpace.SendStateEventAsync("gay.rory.libmatrix.unit_test_room", new());
+
+        // _spaceSemaphore.Release();
+        return testSpace;
+    }
+}
diff --git a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
new file mode 100644
index 0000000..f737363
--- /dev/null
+++ b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
@@ -0,0 +1,10 @@
+namespace LibMatrix.Tests.DataTests;
+
+public static class WhoAmITests {
+    public static void VerifyRequiredFields(this WhoAmIResponse obj, bool isAppservice = false) {
+        Assert.NotNull(obj);
+        Assert.NotNull(obj.UserId);
+        if(!isAppservice)
+            Assert.NotNull(obj.DeviceId);
+    }
+}
diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
index 72a509d..5476b84 100644
--- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
@@ -1,4 +1,5 @@
 using LibMatrix.Services;
+using LibMatrix.Tests.DataTests;
 using LibMatrix.Tests.Fixtures;
 using Xunit.Abstractions;
 using Xunit.Microsoft.DependencyInjection.Abstracts;
@@ -45,6 +46,24 @@ public class AuthTests : TestBed<TestFixture> {
         var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken);
         Assert.NotNull(hs);
         Assert.NotNull(hs.WhoAmI);
+        hs.WhoAmI.VerifyRequiredFields();
+        Assert.NotNull(hs.UserId);
+        Assert.NotNull(hs.AccessToken);
+        await hs.Logout();
+    }
+
+    [Fact]
+    public async Task RegisterAsync() {
+        var rhs = await _provider.GetRemoteHomeserver("matrixunittests.rory.gay");
+        var reg = await rhs.RegisterAsync(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), "Unit tests!");
+        Assert.NotNull(reg);
+        Assert.NotNull(reg.AccessToken);
+        Assert.NotNull(reg.DeviceId);
+        Assert.NotNull(reg.UserId);
+        var hs = await reg.GetAuthenticatedHomeserver();
+        Assert.NotNull(hs);
+        Assert.NotNull(hs.WhoAmI);
+        hs.WhoAmI.VerifyRequiredFields();
         Assert.NotNull(hs.UserId);
         Assert.NotNull(hs.AccessToken);
         await hs.Logout();
diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs
index ef63ec9..17c219d 100644
--- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/RoomTests.cs
@@ -1,7 +1,9 @@
+using System.Text;
 using ArcaneLibs.Extensions;
 using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Homeservers;
 using LibMatrix.Services;
+using LibMatrix.Tests.Abstractions;
 using LibMatrix.Tests.Fixtures;
 using Xunit.Abstractions;
 using Xunit.Microsoft.DependencyInjection.Abstracts;
@@ -22,34 +24,29 @@ public class RoomTests : TestBed<TestFixture> {
     }
 
     private async Task<AuthenticatedHomeserverGeneric> GetHomeserver() {
-        Assert.False(string.IsNullOrWhiteSpace(_config.TestHomeserver), $"{nameof(_config.TestHomeserver)} must be set in appsettings!");
-        Assert.False(string.IsNullOrWhiteSpace(_config.TestUsername), $"{nameof(_config.TestUsername)} must be set in appsettings!");
-        Assert.False(string.IsNullOrWhiteSpace(_config.TestPassword), $"{nameof(_config.TestPassword)} must be set in appsettings!");
-
-        // var server = await _resolver.ResolveHomeserverFromWellKnown(_config.TestHomeserver!);
-        var login = await _provider.Login(_config.TestHomeserver!, _config.TestUsername!, _config.TestPassword!);
-        Assert.NotNull(login);
-
-        var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, login.AccessToken);
-        return hs;
+        return await HomeserverAbstraction.GetHomeserver();
     }
 
     [Fact]
     public async Task GetJoinedRoomsAsync() {
-        var hs = await GetHomeserver();
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        //make 100 rooms
+        var createRoomTasks = Enumerable.Range(0, 100).Select(_ => RoomAbstraction.GetTestRoom(hs)).ToList();
+        await Task.WhenAll(createRoomTasks);
 
         var rooms = await hs.GetJoinedRooms();
         Assert.NotNull(rooms);
         Assert.NotEmpty(rooms);
         Assert.All(rooms, Assert.NotNull);
+        Assert.Equal(100, rooms.Count);
 
         await hs.Logout();
     }
 
     [Fact]
     public async Task GetNameAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var name = await room.GetNameAsync();
         Assert.NotNull(name);
@@ -58,8 +55,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [SkippableFact(typeof(MatrixException))]
     public async Task GetTopicAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var topic = await room.GetTopicAsync();
         Assert.NotNull(topic);
@@ -72,8 +69,8 @@ public class RoomTests : TestBed<TestFixture> {
         Assert.True(StateEvent.KnownStateEventTypes is { Count: > 0 }, "StateEvent.KnownStateEventTypes is empty!");
         Assert.True(StateEvent.KnownStateEventTypesByName is { Count: > 0 }, "StateEvent.KnownStateEventTypesByName is empty!");
 
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var members = room.GetMembersAsync();
         Assert.NotNull(members);
@@ -98,41 +95,10 @@ public class RoomTests : TestBed<TestFixture> {
         Assert.True(hitMembers, "No members were found in the room");
     }
 
-    /*
-     tests remaining:
-     GetStateAsync(string,string) 0% 8/8
-       GetMessagesAsync(string,int,string,string) 0% 7/7
-       JoinAsync(string[],string) 0% 8/8
-       SendMessageEventAsync(RoomMessageEventContent) 0% 1/1
-       GetAliasesAsync() 0% 4/4
-       GetCanonicalAliasAsync() 0% 1/1
-       GetAvatarUrlAsync() 0% 1/1
-       GetJoinRuleAsync() 0% 1/1
-       GetHistoryVisibilityAsync() 0% 1/1
-       GetGuestAccessAsync() 0% 1/1
-       GetCreateEventAsync() 0% 1/1
-       GetRoomType() 0% 4/4
-       GetPowerLevelsAsync() 0% 1/1
-       ForgetAsync() 0% 1/1
-       LeaveAsync(string) 0% 1/1
-       KickAsync(string,string) 0% 1/1
-       BanAsync(string,string) 0% 1/1
-       UnbanAsync(string) 0% 1/1
-       SendStateEventAsync(string,object) 0% 1/1
-       SendStateEventAsync(string,string,object) 0% 1/1
-       SendTimelineEventAsync(string,EventContent) 0% 5/5
-       SendFileAsync(string,string,Stream) 0% 6/6
-       GetRoomAccountData<T>(string) 0% 8/8
-       SetRoomAccountData(string,object) 0% 7/7
-       GetEvent<T>(string) 0% 3/3
-       RedactEventAsync(string,string) 0% 4/4
-       InviteUser(string,string) 0% 3/3
-     */
-
     [Fact]
     public async Task JoinAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var id = await room.JoinAsync();
         Assert.NotNull(id);
@@ -142,8 +108,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [SkippableFact(typeof(MatrixException))]
     public async Task GetAliasesAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var aliases = await room.GetAliasesAsync();
         Assert.NotNull(aliases);
@@ -153,8 +119,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [SkippableFact(typeof(MatrixException))]
     public async Task GetCanonicalAliasAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var alias = await room.GetCanonicalAliasAsync();
         Assert.NotNull(alias);
@@ -164,8 +130,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [SkippableFact(typeof(MatrixException))]
     public async Task GetAvatarUrlAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var url = await room.GetAvatarUrlAsync();
         Assert.NotNull(url);
@@ -175,8 +141,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [Fact]
     public async Task GetJoinRuleAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var rule = await room.GetJoinRuleAsync();
         Assert.NotNull(rule);
@@ -186,8 +152,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [Fact]
     public async Task GetHistoryVisibilityAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var visibility = await room.GetHistoryVisibilityAsync();
         Assert.NotNull(visibility);
@@ -197,8 +163,8 @@ public class RoomTests : TestBed<TestFixture> {
 
     [Fact]
     public async Task GetGuestAccessAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         try {
             var access = await room.GetGuestAccessAsync();
@@ -207,15 +173,15 @@ public class RoomTests : TestBed<TestFixture> {
             Assert.NotEmpty(access.GuestAccess);
         }
         catch (Exception e) {
-            if(e is not MatrixException exception) throw;
+            if (e is not MatrixException exception) throw;
             Assert.Equal("M_NOT_FOUND", exception.ErrorCode);
         }
     }
 
     [Fact]
     public async Task GetCreateEventAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var create = await room.GetCreateEventAsync();
         Assert.NotNull(create);
@@ -225,16 +191,16 @@ public class RoomTests : TestBed<TestFixture> {
 
     [Fact]
     public async Task GetRoomType() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         await room.GetRoomType();
     }
 
     [Fact]
     public async Task GetPowerLevelsAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         var power = await room.GetPowerLevelsAsync();
         Assert.NotNull(power);
@@ -246,54 +212,73 @@ public class RoomTests : TestBed<TestFixture> {
         Assert.NotNull(power.EventsDefault);
         Assert.NotNull(power.UsersDefault);
         Assert.NotNull(power.Users);
-        Assert.NotNull(power.Events);
+        // Assert.NotNull(power.Events);
     }
 
-    [Fact(Skip = "This test is destructive!")]
+    [Fact]
     public async Task ForgetAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         await room.ForgetAsync();
     }
 
-    [Fact(Skip = "This test is destructive!")]
+    [Fact]
     public async Task LeaveAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
         await room.LeaveAsync();
     }
 
-    [Fact(Skip = "This test is destructive!")]
+    [Fact]
     public async Task KickAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var hs2 = await HomeserverAbstraction.GetRandomHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
-        // await room.KickAsync(_config.TestUserId, "test");
+        await room.InviteUserAsync(hs2.UserId,"Unit test!");
+        await hs2.GetRoom(room.RoomId).JoinAsync();
+        await room.KickAsync(hs2.UserId, "test");
+        var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId);
+        Assert.NotNull(banState);
+        Assert.Equal("leave", banState.Membership);
     }
 
-    [Fact(Skip = "This test is destructive!")]
+    [Fact]
     public async Task BanAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var hs2 = await HomeserverAbstraction.GetRandomHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
-        // await room.BanAsync(_config.TestUserId, "test");
+        await room.BanAsync(hs2.UserId, "test");
+        var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId);
+        Assert.NotNull(banState);
+        Assert.Equal("ban", banState.Membership);
     }
 
-    [Fact(Skip = "This test is destructive!")]
+    [Fact]
     public async Task UnbanAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var hs2 = await HomeserverAbstraction.GetRandomHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
-        // await room.UnbanAsync(_config.TestUserId);
+        await room.BanAsync(hs2.UserId, "test");
+        var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId);
+        Assert.NotNull(banState);
+        Assert.Equal("ban", banState.Membership);
+        await room.UnbanAsync(hs2.UserId);
+        var unbanState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId);
+        Assert.NotNull(unbanState);
+        Assert.Equal("leave", unbanState.Membership);
     }
 
     [SkippableFact(typeof(MatrixException))]
     public async Task SendStateEventAsync() {
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
+
         await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new ProfileResponseEventContent() {
             DisplayName = "wee_woo",
             AvatarUrl = "no"
@@ -305,12 +290,21 @@ public class RoomTests : TestBed<TestFixture> {
     }
 
     [SkippableFact(typeof(MatrixException))]
-    public async Task GetStateEventAsync() {
+    public async Task SendAndGetStateEventAsync() {
         await SendStateEventAsync();
-
-        var hs = await GetHomeserver();
-        var room = hs.GetRoom(_config.TestRoomId);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
+
+        await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", new ProfileResponseEventContent() {
+            DisplayName = "wee_woo",
+            AvatarUrl = "no"
+        });
+        await room.SendStateEventAsync("gay.rory.libmatrix.unit_tests", "state_key_maybe", new ProfileResponseEventContent() {
+            DisplayName = "wee_woo",
+            AvatarUrl = "yes"
+        });
+
         var state1 = await room.GetStateAsync<ProfileResponseEventContent>("gay.rory.libmatrix.unit_tests");
         Assert.NotNull(state1);
         Assert.NotNull(state1.DisplayName);
@@ -329,4 +323,62 @@ public class RoomTests : TestBed<TestFixture> {
         Assert.Equal("wee_woo", state2.DisplayName);
         Assert.Equal("yes", state2.AvatarUrl);
     }
+
+    [Fact]
+    public async Task DisbandAsync() {
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
+        Assert.NotNull(room);
+
+        await room.DisbandRoomAsync();
+    }
+
+    [Fact]
+    public async Task SendFileAsync() {
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
+        Assert.NotNull(room);
+
+        var res = await room.SendFileAsync("test.txt", new MemoryStream(Encoding.UTF8.GetBytes("This test was written by Emma [it/its], member of the Rory& system." +
+                                                                                                            "\nIf you are reading this on matrix, it means the unit test for uploading a file works!")));
+        Assert.NotNull(res);
+        Assert.NotNull(res.EventId);
+    }
+
+    [Fact]
+    public async Task GetSpaceChildrenAsync() {
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var space = await RoomAbstraction.GetTestSpace(hs, 2, false, 1);
+        Assert.NotNull(space);
+        var children = space.GetChildrenAsync();
+        Assert.NotNull(children);
+        int found = 0;
+        await foreach (var room in children) {
+            found++;
+        }
+        Assert.Equal(2, found);
+    }
+
+    [Fact]
+    public async Task InviteAndJoinAsync() {
+        var otherUsers = HomeserverAbstraction.GetRandomHomeservers(7);
+        var hs = await HomeserverAbstraction.GetHomeserver();
+        var room = await RoomAbstraction.GetTestRoom(hs);
+        Assert.NotNull(room);
+
+        // var expectedCount = 1;
+
+        await foreach(var otherUser in otherUsers) {
+            await room.InviteUserAsync(otherUser.UserId);
+            await otherUser.GetRoom(room.RoomId).JoinAsync();
+        }
+
+        var states = room.GetMembersAsync(false);
+        var count = 0;
+        await foreach(var state in states) {
+            count++;
+        }
+        // Assert.Equal(++expectedCount, count);
+        Assert.Equal(8, count);
+    }
 }