about summary refs log tree commit diff
path: root/MatrixRoomUtils.Core
diff options
context:
space:
mode:
Diffstat (limited to 'MatrixRoomUtils.Core')
-rw-r--r--MatrixRoomUtils.Core/Attributes/TraceAttribute.cs10
-rw-r--r--MatrixRoomUtils.Core/AuthenticatedHomeServer.cs8
-rw-r--r--MatrixRoomUtils.Core/Authentication/MatrixAuth.cs35
-rw-r--r--MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs21
-rw-r--r--MatrixRoomUtils.Core/Helpers/MediaResolver.cs6
-rw-r--r--MatrixRoomUtils.Core/Helpers/SyncHelper.cs4
-rw-r--r--MatrixRoomUtils.Core/Interfaces/IHomeServer.cs42
-rw-r--r--MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs28
-rw-r--r--MatrixRoomUtils.Core/RemoteHomeServer.cs1
-rw-r--r--MatrixRoomUtils.Core/Responses/LoginResponse.cs2
-rw-r--r--MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs226
-rw-r--r--MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs14
-rw-r--r--MatrixRoomUtils.Core/Services/HomeserverProviderService.cs57
-rw-r--r--MatrixRoomUtils.Core/Services/HomeserverResolverService.cs14
-rw-r--r--MatrixRoomUtils.Core/Services/ServiceInstaller.cs4
-rw-r--r--MatrixRoomUtils.Core/StateEvent.cs4
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs11
-rw-r--r--MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs11
18 files changed, 235 insertions, 263 deletions
diff --git a/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs b/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs
deleted file mode 100644
index 34a0b67..0000000
--- a/MatrixRoomUtils.Core/Attributes/TraceAttribute.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using System.Runtime.CompilerServices;
-
-namespace MatrixRoomUtils.Core.Attributes; 
-
-[AttributeUsage(AttributeTargets.All)]
-public class TraceAttribute : Attribute {
-    public TraceAttribute([CallerMemberName] string callerName = "") {
-        Console.WriteLine($"{callerName} called!");
-    }
-}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
index 10bbb36..09da766 100644
--- a/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
+++ b/MatrixRoomUtils.Core/AuthenticatedHomeServer.cs
@@ -28,7 +28,7 @@ public class AuthenticatedHomeServer : IHomeServer {
     }
 
     public WhoAmIResponse WhoAmI { get; set; } = null!;
-    public string UserId { get; }
+    public string UserId => WhoAmI.UserId;
     public string AccessToken { get; set; }
 
 
@@ -80,7 +80,7 @@ public class AuthenticatedHomeServer : IHomeServer {
                 var url = $"/_synapse/admin/v1/rooms?limit={Math.Min(limit, 100)}&dir={dir}&order_by={orderBy}";
                 if (!string.IsNullOrEmpty(searchTerm)) url += $"&search_term={searchTerm}";
 
-                if (res?.NextBatch != null) url += $"&from={res.NextBatch}";
+                if (res?.NextBatch is not null) url += $"&from={res.NextBatch}";
 
                 Console.WriteLine($"--- ADMIN Querying Room List with URL: {url} - Already have {i} items... ---");
 
@@ -88,7 +88,7 @@ public class AuthenticatedHomeServer : IHomeServer {
                 totalRooms ??= res?.TotalRooms;
                 Console.WriteLine(res.ToJson(false));
                 foreach (var room in res.Rooms) {
-                    if (localFilter != null) {
+                    if (localFilter is not null) {
                         if (!room.RoomId.Contains(localFilter.RoomIdContains)) {
                             totalRooms--;
                             continue;
@@ -144,7 +144,7 @@ public class AuthenticatedHomeServer : IHomeServer {
                             continue;
                         }
                     }
-                    // if (contentSearch != null && !string.IsNullOrEmpty(contentSearch) &&
+                    // if (contentSearch is not null && !string.IsNullOrEmpty(contentSearch) &&
                     //     !(
                     //         room.Name?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true ||
                     //         room.CanonicalAlias?.Contains(contentSearch, StringComparison.InvariantCultureIgnoreCase) == true ||
diff --git a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs b/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
deleted file mode 100644
index 28e0c49..0000000
--- a/MatrixRoomUtils.Core/Authentication/MatrixAuth.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using System.Net.Http.Json;
-using System.Text.Json;
-using MatrixRoomUtils.Core.Extensions;
-using MatrixRoomUtils.Core.Responses;
-using MatrixRoomUtils.Core.StateEventTypes;
-
-namespace MatrixRoomUtils.Core.Authentication;
-
-public class MatrixAuth {
-    [Obsolete("This is possibly broken and should not be used.", true)]
-    public static async Task<LoginResponse> Login(string homeserver, string username, string password) {
-        Console.WriteLine($"Logging in to {homeserver} as {username}...");
-        homeserver = (new RemoteHomeServer(homeserver)).FullHomeServerDomain;
-        var hc = new MatrixHttpClient();
-        var payload = new {
-            type = "m.login.password",
-            identifier = new {
-                type = "m.id.user",
-                user = username
-            },
-            password,
-            initial_device_display_name = "Rory&::MatrixRoomUtils"
-        };
-        Console.WriteLine($"Sending login request to {homeserver}...");
-        var resp = await hc.PostAsJsonAsync($"{homeserver}/_matrix/client/v3/login", payload);
-        Console.WriteLine($"Login: {resp.StatusCode}");
-        var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
-        if (!resp.IsSuccessStatusCode) Console.WriteLine("Login: " + data);
-
-        Console.WriteLine($"Login: {data.ToJson()}");
-        return data.Deserialize<LoginResponse>();
-        //var token = data.GetProperty("access_token").GetString();
-        //return token;
-    }
-}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs
index 47b3121..852e1d8 100644
--- a/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs
+++ b/MatrixRoomUtils.Core/Extensions/HttpClientExtensions.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
 using System.Text.Json;
 
 namespace MatrixRoomUtils.Core.Extensions;
@@ -18,23 +19,35 @@ public static class HttpClientExtensions {
 
 public class MatrixHttpClient : HttpClient {
     public override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+        try
+        {
+            HttpRequestOptionsKey<bool> WebAssemblyEnableStreamingResponseKey = new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse");
+            request.Options.Set(WebAssemblyEnableStreamingResponseKey, true);
+            // var asm = Assembly.Load("Microsoft.AspNetCore.Components.WebAssembly");
+            // var browserHttpHandlerType = asm.GetType("Microsoft.AspNetCore.Components.WebAssembly.Http.WebAssemblyHttpRequestMessageExtensions", true);
+            // var browserHttpHandlerMethod = browserHttpHandlerType.GetMethod("SetBrowserResponseStreamingEnabled", BindingFlags.Public | BindingFlags.Static);
+            // browserHttpHandlerMethod?.Invoke(null, new object[] {request, true});
+        }
+        catch (Exception e)
+        {
+            Console.WriteLine("Failed to set browser response streaming:");
+            Console.WriteLine(e);
+        }
         var a = await base.SendAsync(request, cancellationToken);
         if (!a.IsSuccessStatusCode) {
             Console.WriteLine($"Failed to send request: {a.StatusCode}");
             var content = await a.Content.ReadAsStringAsync(cancellationToken);
             if (content.StartsWith('{')) {
                 var ex = JsonSerializer.Deserialize<MatrixException>(content);
-                if (ex?.RetryAfterMs != null) {
+                if (ex?.RetryAfterMs is not null) {
                     await Task.Delay(ex.RetryAfterMs.Value, cancellationToken);
+                    typeof(HttpRequestMessage).GetField("_sendStatus", BindingFlags.NonPublic | BindingFlags.Instance)?.SetValue(request, 0);
                     return await SendAsync(request, cancellationToken);
                 }
-
                 throw ex!;
             }
-
             throw new InvalidDataException("Encountered invalid data:\n" + content);
         }
-
         return a;
     }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Helpers/MediaResolver.cs b/MatrixRoomUtils.Core/Helpers/MediaResolver.cs
new file mode 100644
index 0000000..0869135
--- /dev/null
+++ b/MatrixRoomUtils.Core/Helpers/MediaResolver.cs
@@ -0,0 +1,6 @@
+namespace MatrixRoomUtils.Core.Helpers; 
+
+public class MediaResolver {
+    public static string ResolveMediaUri(string homeserver, string mxc) => 
+        mxc.Replace("mxc://", $"{homeserver}/_matrix/media/v3/download/");
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
index 1b9c598..eff412b 100644
--- a/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
+++ b/MatrixRoomUtils.Core/Helpers/SyncHelper.cs
@@ -20,7 +20,7 @@ public class SyncHelper {
 
     public async Task<SyncResult?> Sync(string? since = null, CancellationToken? cancellationToken = null) {
         var outFileName = "sync-" +
-                          (await _storageService.CacheStorageProvider.GetAllKeys()).Count(x => x.StartsWith("sync")) +
+                          (await _storageService.CacheStorageProvider.GetAllKeysAsync()).Count(x => x.StartsWith("sync")) +
                           ".json";
         var url = "/_matrix/client/v3/sync?timeout=30000&set_presence=online";
         if (!string.IsNullOrWhiteSpace(since)) url += $"&since={since}";
@@ -29,7 +29,7 @@ public class SyncHelper {
         try {
             var res = await _homeServer._httpClient.GetFromJsonAsync<SyncResult>(url,
                 cancellationToken: cancellationToken ?? CancellationToken.None);
-            await _storageService.CacheStorageProvider.SaveObject(outFileName, res);
+            await _storageService.CacheStorageProvider.SaveObjectAsync(outFileName, res);
             Console.WriteLine($"Wrote file: {outFileName}");
             return res;
         }
diff --git a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
index 4fa6c1b..4ee2a3e 100644
--- a/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
+++ b/MatrixRoomUtils.Core/Interfaces/IHomeServer.cs
@@ -6,34 +6,36 @@ using MatrixRoomUtils.Core.StateEventTypes;
 namespace MatrixRoomUtils.Core.Interfaces;
 
 public class IHomeServer {
-    private readonly Dictionary<string, ProfileResponse?> _profileCache = new();
+    private readonly Dictionary<string, object> _profileCache = new();
     public string HomeServerDomain { get; set; }
     public string FullHomeServerDomain { get; set; }
 
     protected internal MatrixHttpClient _httpClient { get; set; } = new();
 
-
     public async Task<ProfileResponse> GetProfile(string mxid, bool debounce = false, bool cache = true) {
-        if (cache) {
-            if (debounce) await Task.Delay(Random.Shared.Next(100, 500));
-            if (_profileCache.ContainsKey(mxid)) {
-                while (_profileCache[mxid] == null) {
-                    Console.WriteLine($"Waiting for profile cache for {mxid}, currently {_profileCache[mxid]?.ToJson() ?? "null"} within {_profileCache.Count} profiles...");
-                    await Task.Delay(Random.Shared.Next(50, 500));
-                }
-
-                return _profileCache[mxid];
-            }
+        // if (cache) {
+        //     if (debounce) await Task.Delay(Random.Shared.Next(100, 500));
+        //     if (_profileCache.ContainsKey(mxid)) {
+        //         while (_profileCache[mxid] == null) {
+        //             Console.WriteLine($"Waiting for profile cache for {mxid}, currently {_profileCache[mxid]?.ToJson() ?? "null"} within {_profileCache.Count} profiles...");
+        //             await Task.Delay(Random.Shared.Next(50, 500));
+        //         }
+        //
+        //         return _profileCache[mxid];
+        //     }
+        // }
+        if(mxid is null) throw new ArgumentNullException(nameof(mxid));
+        if (_profileCache.ContainsKey(mxid)) {
+            if (_profileCache[mxid] is SemaphoreSlim s) await s.WaitAsync();
+            if (_profileCache[mxid] is ProfileResponse p) return p;
         }
-
-        _profileCache.Add(mxid, null);
+        _profileCache[mxid] = new SemaphoreSlim(1);
+        
         var resp = await _httpClient.GetAsync($"/_matrix/client/v3/profile/{mxid}");
-        var data = await resp.Content.ReadFromJsonAsync<JsonElement>();
+        var data = await resp.Content.ReadFromJsonAsync<ProfileResponse>();
         if (!resp.IsSuccessStatusCode) Console.WriteLine("Profile: " + data);
-        var profile = data.Deserialize<ProfileResponse>();
-        _profileCache[mxid] = profile;
-        return profile;
+        _profileCache[mxid] = data;
+        
+        return data;
     }
-
-    public string? ResolveMediaUri(string mxc) => mxc.Replace("mxc://", $"{FullHomeServerDomain}/_matrix/media/v3/download/");
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs b/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs
index 01314da..eefb79c 100644
--- a/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs
+++ b/MatrixRoomUtils.Core/Interfaces/Services/IStorageProvider.cs
@@ -2,45 +2,45 @@ namespace MatrixRoomUtils.Core.Interfaces.Services;
 
 public interface IStorageProvider {
     // save all children of a type with reflection
-    public Task SaveAllChildren<T>(string key, T value) {
+    public Task SaveAllChildrenAsync<T>(string key, T value) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveAllChildren<T>(key, value)!");
-        return Task.CompletedTask;
+        throw new NotImplementedException();
     }
     
     // load all children of a type with reflection
-    public Task<T?> LoadAllChildren<T>(string key) {
+    public Task<T?> LoadAllChildrenAsync<T>(string key) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadAllChildren<T>(key)!");
-        return Task.FromResult(default(T));
+        throw new NotImplementedException();
     }
 
 
-    public Task SaveObject<T>(string key, T value) {
+    public Task SaveObjectAsync<T>(string key, T value) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement SaveObject<T>(key, value)!");
-        return Task.CompletedTask;
+        throw new NotImplementedException();
     }
     
     // load
-    public Task<T?> LoadObject<T>(string key) {
+    public Task<T?> LoadObjectAsync<T>(string key) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement LoadObject<T>(key)!");
-        return Task.FromResult(default(T));
+        throw new NotImplementedException();
     }
     
     // check if exists
-    public Task<bool> ObjectExists(string key) {
+    public Task<bool> ObjectExistsAsync(string key) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement ObjectExists(key)!");
-        return Task.FromResult(false);
+        throw new NotImplementedException();
     }
     
     // get all keys
-    public Task<List<string>> GetAllKeys() {
+    public Task<List<string>> GetAllKeysAsync() {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement GetAllKeys()!");
-        return Task.FromResult(new List<string>());
+        throw new NotImplementedException();
     }
     
 
     // delete
-    public Task DeleteObject(string key) {
+    public Task DeleteObjectAsync(string key) {
         Console.WriteLine($"StorageProvider<{GetType().Name}> does not implement DeleteObject(key)!");
-        return Task.CompletedTask;
+        throw new NotImplementedException();
     }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/RemoteHomeServer.cs b/MatrixRoomUtils.Core/RemoteHomeServer.cs
index 9c5f2a3..e6c28c3 100644
--- a/MatrixRoomUtils.Core/RemoteHomeServer.cs
+++ b/MatrixRoomUtils.Core/RemoteHomeServer.cs
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
 using System.Text.Json;
 using MatrixRoomUtils.Core.Extensions;
 using MatrixRoomUtils.Core.Interfaces;
+using MatrixRoomUtils.Core.Services;
 
 namespace MatrixRoomUtils.Core;
 
diff --git a/MatrixRoomUtils.Core/Responses/LoginResponse.cs b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
index b248739..239ea03 100644
--- a/MatrixRoomUtils.Core/Responses/LoginResponse.cs
+++ b/MatrixRoomUtils.Core/Responses/LoginResponse.cs
@@ -10,7 +10,7 @@ public class LoginResponse {
     public string DeviceId { get; set; }
 
     [JsonPropertyName("home_server")]
-    public string HomeServer { get; set; }
+    public string Homeserver { get; set; }
 
     [JsonPropertyName("user_id")]
     public string UserId { get; set; }
diff --git a/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs
index 8dc30d1..f57c855 100644
--- a/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs
+++ b/MatrixRoomUtils.Core/RoomTypes/GenericRoom.cs
@@ -1,15 +1,17 @@
 using System.Net.Http.Json;
-using System.Text;
 using System.Text.Json;
 using System.Web;
 using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Responses;
 using MatrixRoomUtils.Core.RoomTypes;
+using MatrixRoomUtils.Core.StateEventTypes;
+using Microsoft.Extensions.Logging;
 
 namespace MatrixRoomUtils.Core;
 
 public class GenericRoom {
     internal readonly AuthenticatedHomeServer _homeServer;
-    internal readonly HttpClient _httpClient;
+    internal readonly MatrixHttpClient _httpClient;
 
     public GenericRoom(AuthenticatedHomeServer homeServer, string roomId) {
         _homeServer = homeServer;
@@ -21,51 +23,41 @@ public class GenericRoom {
 
     public string RoomId { get; set; }
 
-    public async Task<JsonElement?> GetStateAsync(string type, string stateKey = "", bool logOnFailure = true) {
+    [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);
+    }
 
-        var res = await _httpClient.GetAsync(url);
-        if (!res.IsSuccessStatusCode) {
-            if (logOnFailure) Console.WriteLine($"{RoomId}/{stateKey}/{type} - got status: {res.StatusCode}");
-            return null;
+    public async IAsyncEnumerable<StateEventResponse?> GetFullStateAsync() {
+        var res = await _httpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/state");
+        var result =
+            JsonSerializer.DeserializeAsyncEnumerable<StateEventResponse>(await res.Content.ReadAsStreamAsync());
+        await foreach (var resp in result) {
+            yield return resp;
         }
-
-        var result = await res.Content.ReadFromJsonAsync<JsonElement>();
-        return result;
     }
 
-    public async Task<T?> GetStateAsync<T>(string type, string stateKey = "", bool logOnFailure = false) {
-        var res = await GetStateAsync(type, stateKey, logOnFailure);
-        if (res == null) return default;
-        return res.Value.Deserialize<T>();
+    public async Task<T?> GetStateAsync<T>(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<T>(url);
     }
 
     public async Task<MessagesResponse> GetMessagesAsync(string from = "", int limit = 10, string dir = "b",
         string filter = "") {
         var url = $"/_matrix/client/v3/rooms/{RoomId}/messages?from={from}&limit={limit}&dir={dir}";
         if (!string.IsNullOrEmpty(filter)) url += $"&filter={filter}";
-        var res = await _httpClient.GetAsync(url);
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to get messages for {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to get messages for {RoomId} - got status: {res.StatusCode}");
-        }
-
-        var result = await res.Content.ReadFromJsonAsync<MessagesResponse>();
-        return result ?? new MessagesResponse();
+        var res = await _httpClient.GetFromJsonAsync<MessagesResponse>(url);
+        return res ?? new MessagesResponse();
     }
 
     public async Task<string> GetNameAsync() {
-        var res = await GetStateAsync("m.room.name");
-        if (!res.HasValue) {
-            Console.WriteLine($"Room {RoomId} has no name!");
-            return RoomId;
-        }
-
-        var resn = res?.TryGetProperty("name", out var name) ?? false ? name.GetString() ?? RoomId : RoomId;
-        //Console.WriteLine($"Got name: {resn}");
-        return resn;
+        var res = await GetStateAsync<RoomNameEventData>("m.room.name");
+        return res.Name ?? RoomId;
     }
 
     public async Task JoinAsync(string[]? homeservers = null, string? reason = null) {
@@ -78,165 +70,89 @@ public class GenericRoom {
         });
     }
 
-    public async Task<List<string>> GetMembersAsync(bool joinedOnly = true) {
-        var res = await GetStateAsync("");
-        if (!res.HasValue) return new List<string>();
-        var members = new List<string>();
-        foreach (var member in res.Value.EnumerateArray()) {
-            if (member.GetProperty("type").GetString() != "m.room.member") continue;
-            if (joinedOnly && member.GetProperty("content").GetProperty("membership").GetString() != "join") continue;
-            var memberId = member.GetProperty("state_key").GetString();
-            members.Add(
-                memberId ?? throw new InvalidOperationException("Event type was member but state key was null!"));
+    public async IAsyncEnumerable<StateEventResponse> GetMembersAsync(bool joinedOnly = true) {
+        var res = GetFullStateAsync();
+        await foreach (var member in res) {
+            if (member.Type != "m.room.member") continue;
+            if (joinedOnly && (member.TypedContent as RoomMemberEventData).Membership is not "join") continue;
+            yield return member;
         }
-
-        return members;
     }
 
     public async Task<List<string>> GetAliasesAsync() {
-        var res = await GetStateAsync("m.room.aliases");
-        if (!res.HasValue) return new List<string>();
-        var aliases = new List<string>();
-        foreach (var alias in res.Value.GetProperty("aliases").EnumerateArray()) aliases.Add(alias.GetString() ?? "");
-
-        return aliases;
+        var res = await GetStateAsync<RoomAliasEventData>("m.room.aliases");
+        return res.Aliases;
     }
 
-    public async Task<string> GetCanonicalAliasAsync() {
-        var res = await GetStateAsync("m.room.canonical_alias");
-        if (!res.HasValue) return "";
-        return res.Value.GetProperty("alias").GetString() ?? "";
-    }
-
-    public async Task<string> GetTopicAsync() {
-        var res = await GetStateAsync("m.room.topic");
-        if (!res.HasValue) return "";
-        return res.Value.GetProperty("topic").GetString() ?? "";
-    }
-
-    public async Task<string> GetAvatarUrlAsync() {
-        var res = await GetStateAsync("m.room.avatar");
-        if (!res.HasValue) return "";
-        return res.Value.GetProperty("url").GetString() ?? "";
-    }
+    public async Task<CanonicalAliasEventData?> GetCanonicalAliasAsync() =>
+        await GetStateAsync<CanonicalAliasEventData>("m.room.canonical_alias");
 
-    public async Task<JoinRulesEventData> GetJoinRuleAsync() {
-        var res = await GetStateAsync("m.room.join_rules");
-        if (!res.HasValue) return new JoinRulesEventData();
-        return res.Value.Deserialize<JoinRulesEventData>() ?? new JoinRulesEventData();
-    }
+    public async Task<RoomTopicEventData?> GetTopicAsync() =>
+        await GetStateAsync<RoomTopicEventData>("m.room.topic");
 
-    public async Task<string> GetHistoryVisibilityAsync() {
-        var res = await GetStateAsync("m.room.history_visibility");
-        if (!res.HasValue) return "";
-        return res.Value.GetProperty("history_visibility").GetString() ?? "";
-    }
+    public async Task<RoomAvatarEventData?> GetAvatarUrlAsync() =>
+        await GetStateAsync<RoomAvatarEventData>("m.room.avatar");
 
-    public async Task<string> GetGuestAccessAsync() {
-        var res = await GetStateAsync("m.room.guest_access");
-        if (!res.HasValue) return "";
-        return res.Value.GetProperty("guest_access").GetString() ?? "";
-    }
+    public async Task<JoinRulesEventData> GetJoinRuleAsync() =>
+        await GetStateAsync<JoinRulesEventData>("m.room.join_rules");
 
-    public async Task<CreateEvent> GetCreateEventAsync() {
-        var res = await GetStateAsync("m.room.create");
-        if (!res.HasValue) return new CreateEvent();
+    public async Task<HistoryVisibilityData?> GetHistoryVisibilityAsync() =>
+        await GetStateAsync<HistoryVisibilityData>("m.room.history_visibility");
 
-        res.FindExtraJsonElementFields(typeof(CreateEvent));
+    public async Task<GuestAccessData?> GetGuestAccessAsync() =>
+        await GetStateAsync<GuestAccessData>("m.room.guest_access");
 
-        return res.Value.Deserialize<CreateEvent>() ?? new CreateEvent();
-    }
+    public async Task<CreateEvent> GetCreateEventAsync() =>
+        await GetStateAsync<CreateEvent>("m.room.create");
 
     public async Task<string?> GetRoomType() {
-        var res = await GetStateAsync("m.room.create");
-        if (!res.HasValue) return null;
-        if (res.Value.TryGetProperty("type", out var type)) return type.GetString();
-        return null;
+        var res = await GetStateAsync<RoomCreateEventData>("m.room.create");
+        return res.Type;
     }
 
-    public async Task ForgetAsync() {
-        var res = await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to forget room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to forget room {RoomId} - got status: {res.StatusCode}");
-        }
-    }
+    public async Task ForgetAsync() =>
+        await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
 
-    public async Task LeaveAsync(string? reason = null) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new {
+    public async Task LeaveAsync(string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/leave", new {
             reason
         });
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to leave room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to leave room {RoomId} - got status: {res.StatusCode}");
-        }
-    }
 
-    public async Task KickAsync(string userId, string? reason = null) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick",
+    public async Task KickAsync(string userId, string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/kick",
             new UserIdAndReason() { UserId = userId, Reason = reason });
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to kick {userId} from room {RoomId} - got status: {res.StatusCode}");
-        }
-    }
 
-    public async Task BanAsync(string userId, string? reason = null) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban",
+    public async Task BanAsync(string userId, string? reason = null) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/ban",
             new UserIdAndReason() { UserId = userId, Reason = reason });
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to ban {userId} from room {RoomId} - got status: {res.StatusCode}");
-        }
-    }
 
-    public async Task UnbanAsync(string userId) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
+    public async Task UnbanAsync(string userId) =>
+        await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
             new UserIdAndReason() { UserId = userId });
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to unban {userId} from room {RoomId} - got status: {res.StatusCode}");
-        }
-    }
-
-    public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) {
-        var res = await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content);
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine(
-                $"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception(
-                $"Failed to send state event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-        }
 
-        return await res.Content.ReadFromJsonAsync<EventIdResponse>();
-    }
+    public async Task<EventIdResponse> SendStateEventAsync(string eventType, object content) =>
+        await (await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content))
+            .Content.ReadFromJsonAsync<EventIdResponse>();
 
     public async Task<EventIdResponse> SendMessageEventAsync(string eventType, MessageEventData content) {
-        var url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid();
-        var res = await _httpClient.PutAsJsonAsync(url, content);
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-        }
-
+        var res = await _httpClient.PutAsJsonAsync(
+            $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(), content);
         var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
-
         return resu;
     }
 
     public async Task<EventIdResponse> SendFileAsync(string eventType, string fileName, Stream fileStream) {
-        var url = $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid();
         var content = new MultipartFormDataContent();
         content.Add(new StreamContent(fileStream), "file", fileName);
-        var res = await _httpClient.PutAsync(url, content);
-        if (!res.IsSuccessStatusCode) {
-            Console.WriteLine($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-            throw new Exception($"Failed to send event {eventType} to room {RoomId} - got status: {res.StatusCode}");
-        }
-
-        var resu = await res.Content.ReadFromJsonAsync<EventIdResponse>();
-
-        return resu;
+        var res = await
+            (
+                await _httpClient.PutAsync(
+                    $"/_matrix/client/v3/rooms/{RoomId}/send/{eventType}/" + Guid.NewGuid(),
+                    content
+                )
+            )
+            .Content.ReadFromJsonAsync<EventIdResponse>();
+        return res;
     }
 
     public readonly SpaceRoom AsSpace;
diff --git a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
index 6b586c7..3be3130 100644
--- a/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
+++ b/MatrixRoomUtils.Core/RoomTypes/SpaceRoom.cs
@@ -8,20 +8,18 @@ namespace MatrixRoomUtils.Core.RoomTypes;
 public class SpaceRoom : GenericRoom {
     private readonly AuthenticatedHomeServer _homeServer;
     private readonly GenericRoom _room;
+
     public SpaceRoom(AuthenticatedHomeServer homeServer, string roomId) : base(homeServer, roomId) {
         _homeServer = homeServer;
     }
 
     public async Task<List<GenericRoom>> GetRoomsAsync(bool includeRemoved = false) {
         var rooms = new List<GenericRoom>();
-        var state = await GetStateAsync("");
-        if (state != null) {
-            var states = state.Value.Deserialize<StateEventResponse[]>()!;
-            foreach (var stateEvent in states.Where(x => x.Type == "m.space.child")) {
-                var roomId = stateEvent.StateKey;
-                if(stateEvent.TypedContent.ToJson() != "{}" || includeRemoved)
-                    rooms.Add(await _homeServer.GetRoom(roomId));
-            }
+        var state = GetFullStateAsync().ToBlockingEnumerable().ToList();
+        var childStates = state.Where(x => x.Type == "m.space.child");
+        foreach (var stateEvent in childStates) {
+            if (stateEvent.TypedContent.ToJson() != "{}" || includeRemoved)
+                rooms.Add(await _homeServer.GetRoom(stateEvent.StateKey));
         }
 
         return rooms;
diff --git a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
index 8a22d33..870e0d4 100644
--- a/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
+++ b/MatrixRoomUtils.Core/Services/HomeserverProviderService.cs
@@ -1,7 +1,11 @@
 using System.Net.Http.Headers;
 using System.Net.Http.Json;
+using System.Text.Json;
+using System.Text.Json.Serialization;
 using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Responses;
 using Microsoft.Extensions.Logging;
+
 namespace MatrixRoomUtils.Core.Services;
 
 public class HomeserverProviderService {
@@ -9,7 +13,8 @@ public class HomeserverProviderService {
     private readonly ILogger<HomeserverProviderService> _logger;
     private readonly HomeserverResolverService _homeserverResolverService;
 
-    public HomeserverProviderService(TieredStorageService tieredStorageService, ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
+    public HomeserverProviderService(TieredStorageService tieredStorageService,
+        ILogger<HomeserverProviderService> logger, HomeserverResolverService homeserverResolverService) {
         Console.WriteLine("Homeserver provider service instantiated!");
         _tieredStorageService = tieredStorageService;
         _logger = logger;
@@ -18,9 +23,11 @@ public class HomeserverProviderService {
             $"New HomeserverProviderService created with TieredStorageService<{string.Join(", ", tieredStorageService.GetType().GetProperties().Select(x => x.Name))}>!");
     }
 
-    public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken) {
+    public async Task<AuthenticatedHomeServer> GetAuthenticatedWithToken(string homeserver, string accessToken,
+        string? overrideFullDomain = null) {
         var hs = new AuthenticatedHomeServer(_tieredStorageService, homeserver, accessToken);
-        hs.FullHomeServerDomain = await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
+        hs.FullHomeServerDomain = overrideFullDomain ??
+                                  await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
         hs._httpClient.Dispose();
         hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
         hs._httpClient.Timeout = TimeSpan.FromSeconds(5);
@@ -29,4 +36,48 @@ public class HomeserverProviderService {
         hs.WhoAmI = (await hs._httpClient.GetFromJsonAsync<WhoAmIResponse>("/_matrix/client/v3/account/whoami"))!;
         return hs;
     }
+
+    public async Task<RemoteHomeServer> GetRemoteHomeserver(string homeserver, string? overrideFullDomain = null) {
+        var hs = new RemoteHomeServer(homeserver);
+        hs.FullHomeServerDomain = overrideFullDomain ??
+                                  await _homeserverResolverService.ResolveHomeserverFromWellKnown(homeserver);
+        hs._httpClient.Dispose();
+        hs._httpClient = new MatrixHttpClient { BaseAddress = new Uri(hs.FullHomeServerDomain) };
+        hs._httpClient.Timeout = TimeSpan.FromSeconds(5);
+        return hs;
+    }
+
+    public async Task<LoginResponse> Login(string homeserver, string user, string password,
+        string? overrideFullDomain = null) {
+        var hs = await GetRemoteHomeserver(homeserver, overrideFullDomain);
+        var payload = new LoginRequest {
+            Identifier = new() { User = user },
+            Password = password
+        };
+        var resp = await hs._httpClient.PostAsJsonAsync("/_matrix/client/v3/login", payload);
+        var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
+        return data!;
+    }
+
+    private class LoginRequest {
+        [JsonPropertyName("type")]
+        public string Type { get; set; } = "m.login.password";
+
+        [JsonPropertyName("identifier")]
+        public LoginIdentifier Identifier { get; set; } = new();
+
+        [JsonPropertyName("password")]
+        public string Password { get; set; } = "";
+
+        [JsonPropertyName("initial_device_display_name")]
+        public string InitialDeviceDisplayName { get; set; } = "Rory&::LibMatrix";
+
+        public class LoginIdentifier {
+            [JsonPropertyName("type")]
+            public string Type { get; set; } = "m.id.user";
+
+            [JsonPropertyName("user")]
+            public string User { get; set; } = "";
+        }
+    }
 }
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs b/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs
index f6363ab..526a261 100644
--- a/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs
+++ b/MatrixRoomUtils.Core/Services/HomeserverResolverService.cs
@@ -6,11 +6,12 @@ using Microsoft.Extensions.Logging;
 namespace MatrixRoomUtils.Core.Services; 
 
 public class HomeserverResolverService {
-    private readonly HttpClient _httpClient;
+    private readonly MatrixHttpClient _httpClient = new MatrixHttpClient();
     private readonly ILogger<HomeserverResolverService> _logger;
 
-    public HomeserverResolverService(HttpClient httpClient, ILogger<HomeserverResolverService> logger) {
-        _httpClient = httpClient;
+    private static Dictionary<string, object> _wellKnownCache = new();
+    
+    public HomeserverResolverService(ILogger<HomeserverResolverService> logger) {
         _logger = logger;
     }
 
@@ -22,6 +23,12 @@ public class HomeserverResolverService {
     }
 
     private async Task<string> _resolveHomeserverFromWellKnown(string homeserver) {
+        if(homeserver is null) throw new ArgumentNullException(nameof(homeserver));
+        if (_wellKnownCache.ContainsKey(homeserver)) {
+            if (_wellKnownCache[homeserver] is SemaphoreSlim s) await s.WaitAsync();
+            if (_wellKnownCache[homeserver] is string p) return p;
+        }
+        _wellKnownCache[homeserver] = new SemaphoreSlim(1);
         string? result = null;
         _logger.LogInformation($"Attempting to resolve homeserver: {homeserver}");
         if (!homeserver.StartsWith("http")) homeserver = "https://" + homeserver;
@@ -34,6 +41,7 @@ public class HomeserverResolverService {
 
         if(result is not null) {
             _logger.LogInformation($"Resolved homeserver: {homeserver} -> {result}");
+            _wellKnownCache.TryAdd(homeserver, result);
             return result;
         }
         
diff --git a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
index 4a831c1..ef9238d 100644
--- a/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
+++ b/MatrixRoomUtils.Core/Services/ServiceInstaller.cs
@@ -9,14 +9,14 @@ public static class ServiceInstaller {
         if (!services.Any(x => x.ServiceType == typeof(TieredStorageService)))
             throw new Exception("[MRUCore/DI] No TieredStorageService has been registered!");
         //Add config
-        if(config != null)
+        if(config is not null)
             services.AddSingleton(config);
         else {
             services.AddSingleton(new RoryLibMatrixConfiguration());
         }
         //Add services
         services.AddScoped<HomeserverProviderService>();
-        services.AddScoped<HomeserverResolverService>();
+        services.AddSingleton<HomeserverResolverService>();
         services.AddScoped<HttpClient>();
         return services;
     }
diff --git a/MatrixRoomUtils.Core/StateEvent.cs b/MatrixRoomUtils.Core/StateEvent.cs
index f2c8701..18b4632 100644
--- a/MatrixRoomUtils.Core/StateEvent.cs
+++ b/MatrixRoomUtils.Core/StateEvent.cs
@@ -29,7 +29,7 @@ public class StateEvent {
         get => _type;
         set {
             _type = value;
-            if (RawContent != null && this is StateEventResponse stateEventResponse) {
+            if (RawContent is not null && this is StateEventResponse stateEventResponse) {
                 if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
                 var x = GetType.Name;
             }
@@ -46,7 +46,7 @@ public class StateEvent {
         get => _rawContent;
         set {
             _rawContent = value;
-            if (Type != null && this is StateEventResponse stateEventResponse) {
+            if (Type is not null && this is StateEventResponse stateEventResponse) {
                 if (File.Exists($"unknown_state_events/{Type}/{stateEventResponse.EventId}.json")) return;
                 var x = GetType.Name;
             }
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs
new file mode 100644
index 0000000..efc946d
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Common/MjolnirShortcodeEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core.StateEventTypes;
+
+[MatrixEvent(EventName = "org.matrix.mjolnir.shortcode")]
+public class MjolnirShortcodeEventData : IStateEventType {
+    [JsonPropertyName("shortcode")]
+    public string? Shortcode { get; set; }
+}
\ No newline at end of file
diff --git a/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs
new file mode 100644
index 0000000..5141ed2
--- /dev/null
+++ b/MatrixRoomUtils.Core/StateEventTypes/Spec/RoomAliasEventData.cs
@@ -0,0 +1,11 @@
+using System.Text.Json.Serialization;
+using MatrixRoomUtils.Core.Extensions;
+using MatrixRoomUtils.Core.Interfaces;
+
+namespace MatrixRoomUtils.Core;
+
+[MatrixEvent(EventName = "m.room.alias")]
+public class RoomAliasEventData : IStateEventType {
+    [JsonPropertyName("aliases")]
+    public List<string>? Aliases { get; set; }
+}
\ No newline at end of file