about summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
m---------ArcaneLibs0
-rw-r--r--LibMatrix.EventTypes/EventContent.cs16
-rw-r--r--LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs1
-rw-r--r--LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs5
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs4
-rw-r--r--LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs9
-rw-r--r--LibMatrix/Extensions/MatrixHttpClient.Single.cs19
-rw-r--r--LibMatrix/Helpers/MessageBuilder.cs12
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs8
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs107
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs1
-rw-r--r--LibMatrix/LibMatrix.csproj4
-rw-r--r--LibMatrix/Responses/SyncResponse.cs2
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs82
-rw-r--r--LibMatrix/Services/ServiceInstaller.cs10
-rw-r--r--LibMatrix/StateEvent.cs39
-rw-r--r--Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs16
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs56
-rw-r--r--Tests/LibMatrix.Tests/Tests/AuthTests.cs27
-rw-r--r--Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs14
-rw-r--r--Tests/LibMatrix.Tests/Tests/TestCleanup.cs69
-rw-r--r--Utilities/LibMatrix.TestDataGenerator/Program.cs1
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs (renamed from Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs)26
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs17
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs3
25 files changed, 381 insertions, 167 deletions
diff --git a/ArcaneLibs b/ArcaneLibs
-Subproject b7685c786b29e7f8ae2db6ff0f79a52efc57020
+Subproject 952f5ef673862fdb7ff6f51d032eca4577dab9c
diff --git a/LibMatrix.EventTypes/EventContent.cs b/LibMatrix.EventTypes/EventContent.cs
index c582cf2..a837252 100644
--- a/LibMatrix.EventTypes/EventContent.cs
+++ b/LibMatrix.EventTypes/EventContent.cs
@@ -1,10 +1,20 @@
+using System.Reflection;
 using System.Text.Json;
 using System.Text.Json.Nodes;
 using System.Text.Json.Serialization;
 
 namespace LibMatrix.EventTypes;
 
-public abstract class EventContent;
+public abstract class EventContent {
+    public static List<string> GetMatchingEventTypes<T>() where T : EventContent {
+        var type = typeof(T);
+        var eventTypes = new List<string>();
+        foreach (var attr in type.GetCustomAttributes<MatrixEventAttribute>(true)) {
+            eventTypes.Add(attr.EventName);
+        }
+        return eventTypes;
+    }
+}
 
 public class UnknownEventContent : TimelineEventContent;
 
@@ -37,6 +47,10 @@ public abstract class TimelineEventContent : EventContent {
         [JsonPropertyName("rel_type")]
         public string? RelationType { get; set; }
 
+        // used for reactions
+        [JsonPropertyName("key")]
+        public string? Key { get; set; }
+
         public class EventInReplyTo {
             [JsonPropertyName("event_id")]
             public string? EventId { get; set; }
diff --git a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
index ae893f8..9602bf3 100644
--- a/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/RoomMessageEventContent.cs
@@ -29,6 +29,7 @@ public class RoomMessageEventContent : TimelineEventContent {
     [JsonPropertyName("url")]
     public string? Url { get; set; }
 
+    [JsonPropertyName("filename")]
     public string? FileName { get; set; }
 
     [JsonPropertyName("info")]
diff --git a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 6006048..5bfd77b 100644
--- a/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -1,5 +1,7 @@
+using System.Security.Cryptography;
 using System.Text.Json.Serialization;
 using ArcaneLibs.Attributes;
+using ArcaneLibs.Extensions;
 
 namespace LibMatrix.EventTypes.Spec.State.Policy;
 
@@ -79,6 +81,7 @@ public abstract class PolicyRuleEventContent : EventContent {
     /// </summary>
     [JsonPropertyName("gay.rory.matrix_room_utils.readable_expiry_time_utc")]
     [FriendlyName(Name = "Expires at")]
+    [TableHide]
     public DateTime? ExpiryDateTime {
         get => Expiry == null ? null : DateTimeOffset.FromUnixTimeMilliseconds(Expiry.Value).DateTime;
         set {
@@ -86,6 +89,8 @@ public abstract class PolicyRuleEventContent : EventContent {
                 Expiry = ((DateTimeOffset)value).ToUnixTimeMilliseconds();
         }
     }
+
+    public string GetDraupnir2StateKey() => Convert.ToBase64String(SHA256.HashData($"{Entity}{Recommendation}".AsBytes().ToArray()));
 }
 
 public static class PolicyRecommendationTypes {
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
index c619d0e..f26b8e5 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomCreateEventContent.cs
@@ -15,8 +15,8 @@ public class RoomCreateEventContent : EventContent {
     [JsonPropertyName("m.federate")]
     public bool? Federate { get; set; }
 
-    [JsonPropertyName("predecessor")]
-    public RoomCreatePredecessor? Predecessor { get; set; }
+    // [JsonPropertyName("predecessor")]
+    // public RoomCreatePredecessor? Predecessor { get; set; }
 
     [JsonPropertyName("type")]
     public string? Type { get; set; }
diff --git a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index 49a1b62..eb156b3 100644
--- a/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
+++ b/LibMatrix.EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
@@ -57,12 +57,13 @@ public class RoomPowerLevelEventContent : EventContent {
         return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0);
     }
 
-    public bool UserHasStatePermission(string userId, string eventType) {
+    public bool UserHasStatePermission(string userId, string eventType, bool log = false) {
         ArgumentNullException.ThrowIfNull(userId);
         var userLevel = GetUserPowerLevel(userId);
         var eventLevel = GetStateEventPowerLevel(eventType);
-        
-        Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
+
+        if (log)
+            Console.WriteLine($"{userId}={userLevel} >= {eventType}={eventLevel} = {userLevel >= eventLevel}");
 
         return userLevel >= eventLevel;
     }
@@ -78,7 +79,7 @@ public class RoomPowerLevelEventContent : EventContent {
         if (Events is null) return StateDefault ?? 0;
         return Events.TryGetValue(eventType, out var level) ? level : StateDefault ?? 0;
     }
-    
+
     public long GetTimelineEventPowerLevel(string eventType) {
         ArgumentNullException.ThrowIfNull(eventType);
         if (Events is null) return EventsDefault ?? 0;
diff --git a/LibMatrix/Extensions/MatrixHttpClient.Single.cs b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
index c9cd260..4145a16 100644
--- a/LibMatrix/Extensions/MatrixHttpClient.Single.cs
+++ b/LibMatrix/Extensions/MatrixHttpClient.Single.cs
@@ -2,6 +2,7 @@
 // #define SYNC_HTTPCLIENT // Only allow one request as a time, for debugging
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
+using System.Net;
 using System.Net.Http.Headers;
 using System.Reflection;
 using System.Security.Cryptography.X509Certificates;
@@ -26,7 +27,8 @@ public class MatrixHttpClient {
                 EnableMultipleHttp2Connections = true
             };
             Client = new HttpClient(handler) {
-                DefaultRequestVersion = new Version(3, 0)
+                DefaultRequestVersion = new Version(3, 0),
+                Timeout = TimeSpan.FromDays(1)
             };
         }
         catch (PlatformNotSupportedException e) {
@@ -72,12 +74,15 @@ public class MatrixHttpClient {
         await _rateLimitSemaphore.WaitAsync(cancellationToken);
 #endif
 
-        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.Content?.Headers.ContentLength ?? 0)})");
+        Console.WriteLine($"Sending {request.Method} {BaseAddress}{request.RequestUri} ({Util.BytesToString(request.GetContentLength())})");
 
         if (request.RequestUri is null) throw new NullReferenceException("RequestUri is null");
         if (!request.RequestUri.IsAbsoluteUri) request.RequestUri = new Uri(BaseAddress, request.RequestUri);
         foreach (var (key, value) in AdditionalQueryParameters) request.RequestUri = request.RequestUri.AddQuery(key, value);
-        foreach (var (key, value) in DefaultRequestHeaders) request.Headers.Add(key, value);
+        foreach (var (key, value) in DefaultRequestHeaders) {
+            if (request.Headers.Contains(key)) continue;
+            request.Headers.Add(key, value);
+        }
 
         request.Options.Set(new HttpRequestOptionsKey<bool>("WebAssemblyEnableStreamingResponse"), true);
 
@@ -105,7 +110,13 @@ public class MatrixHttpClient {
     public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) {
         var responseMessage = await SendUnhandledAsync(request, cancellationToken);
         if (responseMessage.IsSuccessStatusCode) return responseMessage;
-
+        
+        //retry on gateway timeout
+        if (responseMessage.StatusCode == HttpStatusCode.GatewayTimeout) {
+            request.ResetSendStatus();
+            return await SendAsync(request, cancellationToken);
+        }
+        
         //error handling
         var content = await responseMessage.Content.ReadAsStringAsync(cancellationToken);
         if (content.Length == 0)
diff --git a/LibMatrix/Helpers/MessageBuilder.cs b/LibMatrix/Helpers/MessageBuilder.cs
index d897078..b639e1f 100644
--- a/LibMatrix/Helpers/MessageBuilder.cs
+++ b/LibMatrix/Helpers/MessageBuilder.cs
@@ -91,6 +91,18 @@ public class MessageBuilder(string msgType = "m.text", string format = "org.matr
         return this;
     }
 
+    public MessageBuilder WithMention(string id, string? displayName = null) {
+        Content.Body += $"@{displayName ?? id}";
+        Content.FormattedBody += $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
+        return this;
+    }
+
+    public MessageBuilder WithNewline() {
+        Content.Body += "\n";
+        Content.FormattedBody += "<br>";
+        return this;
+    }
+
     public MessageBuilder WithTable(Action<TableBuilder> tableBuilder) {
         var tb = new TableBuilder(this);
         this.WithHtmlTag("table", msb => tableBuilder(tb));
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index 1833bd0..c9ca85d 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -4,6 +4,7 @@ using ArcaneLibs.Extensions;
 using LibMatrix.Filters;
 using LibMatrix.Homeservers;
 using LibMatrix.Responses;
+using LibMatrix.Utilities;
 using Microsoft.Extensions.Logging;
 
 namespace LibMatrix.Helpers;
@@ -42,6 +43,7 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
             _filter = value;
             _filterIsDirty = true;
             _filterId = null;
+            _namedFilterName = null;
         }
     }
 
@@ -81,16 +83,16 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
         if (!string.IsNullOrWhiteSpace(Since)) url += $"&since={Since}";
         if (_filterId is not null) url += $"&filter={_filterId}";
 
-        logger?.LogInformation("SyncHelper: Calling: {}", url);
+        // logger?.LogInformation("SyncHelper: Calling: {}", url);
 
         try {
             var httpResp = await homeserver.ClientHttpClient.GetAsync(url, cancellationToken ?? CancellationToken.None);
             if (httpResp is null) throw new NullReferenceException("Failed to send HTTP request");
-            logger?.LogInformation("Got sync response: {} bytes, {} elapsed", httpResp.Content.Headers.ContentLength ?? -1, sw.Elapsed);
+            logger?.LogTrace("Got sync response: {} bytes, {} elapsed", httpResp.GetContentLength(), sw.Elapsed);
             var deserializeSw = Stopwatch.StartNew();
             var resp = await httpResp.Content.ReadFromJsonAsync<SyncResponse>(cancellationToken: cancellationToken ?? CancellationToken.None,
                 jsonTypeInfo: SyncResponseSerializerContext.Default.SyncResponse);
-            logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.Content.Headers.ContentLength ?? -1, deserializeSw.Elapsed, sw.Elapsed);
+            logger?.LogInformation("Deserialized sync response: {} bytes, {} elapsed, {} total", httpResp.GetContentLength(), deserializeSw.Elapsed, sw.Elapsed);
             var timeToWait = MinimumDelay.Subtract(sw.Elapsed);
             if (timeToWait.TotalMilliseconds > 0)
                 await Task.Delay(timeToWait);
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index c729a44..6be49b9 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -406,4 +406,111 @@ public class AuthenticatedHomeserverGeneric : RemoteHomeserver {
         public NamedFilterCache FilterCache { get; init; }
         public NamedFileCache FileCache { get; init; }
     }
+
+#region Authenticated Media
+
+    // TODO: implement /_matrix/client/v1/media/config when it's actually useful - https://spec.matrix.org/v1.11/client-server-api/#get_matrixclientv1mediaconfig
+
+    private (string ServerName, string MediaId) ParseMxcUri(string mxcUri) {
+        if (!mxcUri.StartsWith("mxc://")) throw new ArgumentException("Matrix Content URIs must start with 'mxc://'", nameof(mxcUri));
+        var parts = mxcUri[6..].Split('/');
+        if (parts.Length != 2) throw new ArgumentException($"Invalid Matrix Content URI '{mxcUri}' passed! Matrix Content URIs must exist of only 2 parts!", nameof(mxcUri));
+        return (parts[0], parts[1]);
+    }
+
+    public async Task<Stream> GetMediaStreamAsync(string mxcUri, string? filename = null, int? timeout = null) {
+        var (serverName, mediaId) = ParseMxcUri(mxcUri);
+        try {
+            var uri = $"/_matrix/client/v1/media/download/{serverName}/{mediaId}";
+            if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+            if (timeout is not null) uri += $"?timeout_ms={timeout}";
+            var res = await ClientHttpClient.GetAsync(uri);
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        //fallback to legacy media
+        try {
+            var uri = $"/_matrix/media/v1/download/{serverName}/{mediaId}";
+            if (!string.IsNullOrWhiteSpace(filename)) uri += $"/{HttpUtility.UrlEncode(filename)}";
+            if (timeout is not null) uri += $"?timeout_ms={timeout}";
+            var res = await ClientHttpClient.GetAsync(uri);
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download media"
+        };
+        // return default;
+    }
+
+    public async Task<Stream> GetThumbnailStreamAsync(string mxcUri, int width, int height, string? method = null, int? timeout = null) {
+        var (serverName, mediaId) = ParseMxcUri(mxcUri);
+        try {
+            var uri = new Uri($"/_matrix/client/v1/thumbnail/{serverName}/{mediaId}");
+            uri = uri.AddQuery("width", width.ToString());
+            uri = uri.AddQuery("height", height.ToString());
+            if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+            if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+            var res = await ClientHttpClient.GetAsync(uri.ToString());
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        //fallback to legacy media
+        try {
+            var uri = new Uri($"/_matrix/media/v1/thumbnail/{serverName}/{mediaId}");
+            uri = uri.AddQuery("width", width.ToString());
+            uri = uri.AddQuery("height", height.ToString());
+            if (!string.IsNullOrWhiteSpace(method)) uri = uri.AddQuery("method", method);
+            if (timeout is not null) uri = uri.AddQuery("timeout_ms", timeout.ToString());
+
+            var res = await ClientHttpClient.GetAsync(uri.ToString());
+            return await res.Content.ReadAsStreamAsync();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNKNOWN" }) throw;
+        }
+
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download media"
+        };
+        // return default;
+    }
+
+    public async Task<Dictionary<string, JsonValue>?> GetUrlPreviewAsync(string url) {
+        try {
+            var res = await ClientHttpClient.GetAsync($"/_matrix/client/v1/media/preview_url?url={HttpUtility.UrlEncode(url)}");
+            return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+        }
+        
+        //fallback to legacy media
+        try {
+            var res = await ClientHttpClient.GetAsync($"/_matrix/media/v1/preview_url?url={HttpUtility.UrlEncode(url)}");
+            return await res.Content.ReadFromJsonAsync<Dictionary<string, JsonValue>>();
+        }
+        catch (MatrixException e) {
+            if (e is not { ErrorCode: "M_UNRECOGNIZED" }) throw;
+        }
+        
+        throw new LibMatrixException() {
+            ErrorCode = LibMatrixException.ErrorCodes.M_UNSUPPORTED,
+            Error = "Failed to download URL preview"
+        };
+    }
+
+#endregion
 }
\ No newline at end of file
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index ecf3e3a..f9e3d04 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -107,6 +107,7 @@ public class RemoteHomeserver {
 
 #endregion
 
+    [Obsolete("This call uses the deprecated unauthenticated media endpoints, please switch to the relevant AuthenticatedHomeserver methods instead.", true)]
     public string? ResolveMediaUri(string? mxcUri) {
         if (mxcUri is null) return null;
         if (mxcUri.StartsWith("https://")) return mxcUri;
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index e037672..6158ff8 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -23,12 +23,14 @@
                 Using the NuGet version in development is annoying due to delays between pushing and being able to consume.
                 If you want to use a time-appropriate version of the library, recursively clone https://cgit.rory.gay/matrix/MatrixUtils.git
                 instead, since this will be locked by the MatrixUtils project, which contains both LibMatrix and ArcaneLibs as a submodule. -->
-        <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview*"/>
+        <PackageReference Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="ArcaneLibs" Version="*-preview.202*"/>
         <ProjectReference Include="..\LibMatrix.EventTypes\LibMatrix.EventTypes.csproj"/>
     </ItemGroup>
 
+    <!-- 
     <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild">
         <Warning Text="ArcaneLibs is being referenced from NuGet, which is dangerous. Please read the warning in LibMatrix.csproj!" Condition="!Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')"/>
     </Target>
+    -->
 
 </Project>
diff --git a/LibMatrix/Responses/SyncResponse.cs b/LibMatrix/Responses/SyncResponse.cs
index e4addb6..b2308c5 100644
--- a/LibMatrix/Responses/SyncResponse.cs
+++ b/LibMatrix/Responses/SyncResponse.cs
@@ -39,7 +39,7 @@ public class SyncResponse {
     // supporting classes
     public class PresenceDataStructure {
         [JsonPropertyName("events")]
-        public List<StateEventResponse> Events { get; set; } = new();
+        public List<StateEventResponse>? Events { get; set; }
     }
 
     public class RoomsDataStructure {
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index b906f08..8398ab9 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -106,7 +106,7 @@ public class GenericRoom {
                 Console.WriteLine("WARNING: Homeserver does not support getting event ID from state events, falling back to sync");
                 var sh = new SyncHelper(Homeserver);
                 var emptyFilter = new SyncFilter.EventFilter(types: [], limit: 1, senders: [], notTypes: ["*"]);
-                var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms:[]);
+                var emptyStateFilter = new SyncFilter.RoomFilter.StateFilter(types: [], limit: 1, senders: [], notTypes: ["*"], rooms: []);
                 sh.Filter = new() {
                     Presence = emptyFilter,
                     AccountData = emptyFilter,
@@ -121,10 +121,11 @@ public class GenericRoom {
                 var sync = await sh.SyncAsync();
                 var state = sync.Rooms.Join[RoomId].State.Events;
                 var stateEvent = state.FirstOrDefault(x => x.Type == type && x.StateKey == stateKey);
-                if (stateEvent is null) throw new LibMatrixException() {
-                    ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND,
-                    Error = "State event not found in sync response"
-                };
+                if (stateEvent is null)
+                    throw new LibMatrixException() {
+                        ErrorCode = LibMatrixException.ErrorCodes.M_NOT_FOUND,
+                        Error = "State event not found in sync response"
+                    };
                 return stateEvent.EventId;
             }
 
@@ -210,10 +211,11 @@ public class GenericRoom {
     public async Task<RoomIdResponse> JoinAsync(string[]? homeservers = null, string? reason = null, bool checkIfAlreadyMember = true) {
         if (checkIfAlreadyMember)
             try {
-                _ = await GetCreateEventAsync();
-                return new RoomIdResponse {
-                    RoomId = RoomId
-                };
+                var ser = await GetStateEventOrNullAsync(RoomMemberEventContent.EventId, Homeserver.UserId);
+                if (ser?.TypedContent is RoomMemberEventContent { Membership: "join" })
+                    return new RoomIdResponse {
+                        RoomId = RoomId
+                    };
             }
             catch { } //ignore
 
@@ -231,7 +233,7 @@ public class GenericRoom {
         // var sw = Stopwatch.StartNew();
         var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members");
         // if (sw.ElapsedMilliseconds > 1000)
-            // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         // var resText = await res.Content.ReadAsStringAsync();
         // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}");
@@ -239,7 +241,7 @@ public class GenericRoom {
             TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default
         });
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         foreach (var resp in result.Chunk) {
             if (resp?.Type != "m.room.member") continue;
@@ -248,14 +250,14 @@ public class GenericRoom {
         }
 
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
     }
 
     public async Task<FrozenSet<StateEventResponse>> GetMembersListAsync(bool joinedOnly = true) {
         // var sw = Stopwatch.StartNew();
         var res = await Homeserver.ClientHttpClient.GetAsync($"/_matrix/client/v3/rooms/{RoomId}/members");
         // if (sw.ElapsedMilliseconds > 1000)
-            // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call responded in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         // var resText = await res.Content.ReadAsStringAsync();
         // Console.WriteLine($"Members call response read in {sw.GetElapsedAndRestart()}");
@@ -263,7 +265,7 @@ public class GenericRoom {
             TypeInfoResolver = ChunkedStateEventResponseSerializerContext.Default
         });
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call deserialised in {sw.GetElapsedAndRestart()}");
         // else sw.Restart();
         var members = new List<StateEventResponse>();
         foreach (var resp in result.Chunk) {
@@ -273,7 +275,7 @@ public class GenericRoom {
         }
 
         // if (sw.ElapsedMilliseconds > 100)
-            // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
+        // Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
         return members.ToFrozenSet();
     }
 
@@ -316,9 +318,12 @@ public class GenericRoom {
     public Task<RoomPowerLevelEventContent?> GetPowerLevelsAsync() =>
         GetStateAsync<RoomPowerLevelEventContent>("m.room.power_levels");
 
+    [Obsolete("This method will be merged into GetNameAsync() in the future.")]
     public async Task<string> GetNameOrFallbackAsync(int maxMemberNames = 2) {
         try {
-            return await GetNameAsync();
+            var name = await GetNameAsync();
+            if (!string.IsNullOrEmpty(name)) return name;
+            throw new();
         }
         catch {
             try {
@@ -352,22 +357,6 @@ public class GenericRoom {
         return Task.WhenAll(tasks);
     }
 
-    public async Task<string?> GetResolvedRoomAvatarUrlAsync(bool useOriginHomeserver = false) {
-        var avatar = await GetAvatarUrlAsync();
-        if (avatar?.Url is null) return null;
-        if (!avatar.Url.StartsWith("mxc://")) return avatar.Url;
-        if (useOriginHomeserver)
-            try {
-                var hs = avatar.Url.Split('/', 3)[1];
-                return await new HomeserverResolverService(NullLogger<HomeserverResolverService>.Instance).ResolveMediaUri(hs, avatar.Url);
-            }
-            catch (Exception e) {
-                Console.WriteLine(e);
-            }
-
-        return Homeserver.ResolveMediaUri(avatar.Url);
-    }
-
 #endregion
 
 #region Simple calls
@@ -390,7 +379,7 @@ public class GenericRoom {
 
     public async Task UnbanAsync(string userId, string? reason = null) =>
         await Homeserver.ClientHttpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/unban",
-            new UserIdAndReason { UserId = userId, Reason = reason});
+            new UserIdAndReason { UserId = userId, Reason = reason });
 
     public async Task InviteUserAsync(string userId, string? reason = null, bool skipExisting = true) {
         if (skipExisting && await GetStateOrNullAsync<RoomMemberEventContent>("m.room.member", userId) is not null)
@@ -407,7 +396,7 @@ public class GenericRoom {
             .Content.ReadFromJsonAsync<EventIdResponse>();
 
     public async Task<EventIdResponse?> SendStateEventAsync(string eventType, string stateKey, object content) =>
-        await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}/{stateKey}", content))
+        await (await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType.UrlEncode()}/{stateKey.UrlEncode()}", content))
             .Content.ReadFromJsonAsync<EventIdResponse>();
 
     public async Task<EventIdResponse> SendTimelineEventAsync(string eventType, TimelineEventContent content) {
@@ -442,6 +431,16 @@ public class GenericRoom {
 
         return await res.Content.ReadFromJsonAsync<T>();
     }
+    
+    public async Task<T?> GetRoomAccountDataOrNullAsync<T>(string key) {
+        try {
+            return await GetRoomAccountDataAsync<T>(key);
+        }
+        catch (MatrixException e) {
+            if (e.ErrorCode == "M_NOT_FOUND") return default;
+            throw;
+        }
+    }
 
     public async Task SetRoomAccountDataAsync(string key, object data) {
         var res = await Homeserver.ClientHttpClient.PutAsJsonAsync($"/_matrix/client/v3/user/{Homeserver.UserId}/rooms/{RoomId}/account_data/{key}", data);
@@ -454,10 +453,17 @@ public class GenericRoom {
     public Task<StateEventResponse> GetEventAsync(string eventId) =>
         Homeserver.ClientHttpClient.GetFromJsonAsync<StateEventResponse>($"/_matrix/client/v3/rooms/{RoomId}/event/{eventId}");
 
-    public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string reason) {
+    public async Task<EventIdResponse> RedactEventAsync(string eventToRedact, string? reason = null) {
         var data = new { reason };
-        return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(
-            $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
+        var url = $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid().ToString()}";
+        while (true) {
+            try {
+                return (await (await Homeserver.ClientHttpClient.PutAsJsonAsync(url, data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
+            } catch (MatrixException e) {
+                if (e is { ErrorCode: MatrixException.ErrorCodes.M_FORBIDDEN }) throw;
+                throw;
+            }
+        }
     }
 
 #endregion
@@ -524,7 +530,7 @@ public class GenericRoom {
 
         var uri = new Uri(path, UriKind.Relative);
         if (dir == "b" || dir == "f") uri = uri.AddQuery("dir", dir);
-        else if(!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir));
+        else if (!string.IsNullOrWhiteSpace(dir)) throw new ArgumentException("Invalid direction", nameof(dir));
         if (!string.IsNullOrEmpty(from)) uri = uri.AddQuery("from", from);
         if (chunkLimit is not null) uri = uri.AddQuery("limit", chunkLimit.Value.ToString());
         if (recurse is not null) uri = uri.AddQuery("recurse", recurse.Value.ToString());
diff --git a/LibMatrix/Services/ServiceInstaller.cs b/LibMatrix/Services/ServiceInstaller.cs
index 06ea9de..8b7e54b 100644
--- a/LibMatrix/Services/ServiceInstaller.cs
+++ b/LibMatrix/Services/ServiceInstaller.cs
@@ -5,23 +5,13 @@ namespace LibMatrix.Services;
 
 public static class ServiceInstaller {
     public static IServiceCollection AddRoryLibMatrixServices(this IServiceCollection services, RoryLibMatrixConfiguration? config = null) {
-        //Check required services
-        // if (!services.Any(x => x.ServiceType == typeof(TieredStorageService)))
-        // throw new Exception("[RMUCore/DI] No TieredStorageService has been registered!");
         //Add config
         services.AddSingleton(config ?? new RoryLibMatrixConfiguration());
 
         //Add services
         services.AddSingleton<HomeserverResolverService>(sp => new HomeserverResolverService(sp.GetRequiredService<ILogger<HomeserverResolverService>>()));
-
-        // if (services.First(x => x.ServiceType == typeof(TieredStorageService)).Lifetime == ServiceLifetime.Singleton) {
         services.AddSingleton<HomeserverProviderService>();
-        // }
-        // else {
-        // services.AddScoped<HomeserverProviderService>();
-        // }
 
-        // services.AddScoped<MatrixHttpClient>();
         return services;
     }
 }
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 073d26d..cc870e4 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -44,6 +44,7 @@ public class StateEvent {
     public string FriendlyTypeNamePlural => MappedType.GetFriendlyNamePluralOrNull() ?? Type;
 
     private static readonly JsonSerializerOptions TypedContentSerializerOptions = new() {
+        // We need these, NumberHandling covers other number types that we don't want to convert
         Converters = {
             new JsonFloatStringConverter(),
             new JsonDoubleStringConverter(),
@@ -55,9 +56,6 @@ public class StateEvent {
     [SuppressMessage("ReSharper", "PropertyCanBeMadeInitOnly.Global")]
     public EventContent? TypedContent {
         get {
-            // if (Type == "m.receipt") {
-            // return null;
-            // }
             try {
                 var mappedType = GetStateEventType(Type);
                 if (mappedType == typeof(UnknownEventContent))
@@ -81,6 +79,18 @@ public class StateEvent {
         }
     }
 
+    public T? ContentAs<T>() {
+        try {
+            return RawContent.Deserialize<T>(TypedContentSerializerOptions)!;
+        }
+        catch (JsonException e) {
+            Console.WriteLine(e);
+            Console.WriteLine("Content:\n" + (RawContent?.ToJson() ?? "null"));
+        }
+
+        return default;
+    }
+
     [JsonPropertyName("state_key")]
     public string? StateKey { get; set; }
 
@@ -156,7 +166,7 @@ public class StateEventResponse : StateEvent {
     public string? Sender { get; set; }
 
     [JsonPropertyName("unsigned")]
-    public UnsignedData? Unsigned { get; set; }
+    public JsonObject? Unsigned { get; set; }
 
     [JsonPropertyName("event_id")]
     public string? EventId { get; set; }
@@ -254,4 +264,23 @@ public class StateEventContentPolymorphicTypeInfoResolver : DefaultJsonTypeInfoR
 }
 */
 
-#endregion
\ No newline at end of file
+#endregion
+
+/*
+public class ForgivingObjectConverter<T> : JsonConverter<T> where T : new() {
+    public override T? Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options) {
+        try {
+            var text = JsonDocument.ParseValue(ref reader).RootElement.GetRawText();
+            return JsonSerializer.Deserialize<T>(text, options);
+        }
+        catch (JsonException ex) {
+            Console.WriteLine(ex);
+            return null;
+        }
+    }
+
+    public override bool CanConvert(Type typeToConvert) => true;
+
+    public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options)
+        => JsonSerializer.Serialize<T>(writer, value, options);
+}*/
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
index 401223c..2819f80 100644
--- a/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
+++ b/Tests/LibMatrix.Tests/Abstractions/HomeserverAbstraction.cs
@@ -78,19 +78,7 @@ public class HomeserverAbstraction(HomeserverProviderService _hsProvider, Config
         
         var username = _config.TestUsername;
         var password = _config.TestPassword;
-        
-        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 log in", e);
-        }
-        
-        return (username, password, reg.AccessToken);
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        return ("", "", "");
     }
 }
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
new file mode 100644
index 0000000..712e45a
--- /dev/null
+++ b/Tests/LibMatrix.Tests/Tests/AuthMediaTests.cs
@@ -0,0 +1,56 @@
+using ArcaneLibs.Extensions;
+using ArcaneLibs.Extensions.Streams;
+using LibMatrix.Homeservers;
+using LibMatrix.Services;
+using LibMatrix.Tests.Abstractions;
+using LibMatrix.Tests.Fixtures;
+using Xunit.Abstractions;
+using Xunit.Microsoft.DependencyInjection.Abstracts;
+
+namespace LibMatrix.Tests.Tests;
+
+public class AuthMediaTests : TestBed<TestFixture> {
+    private readonly TestFixture _fixture;
+    private readonly HomeserverResolverService _resolver;
+    private readonly Config _config;
+    private readonly HomeserverProviderService _provider;
+    private readonly HomeserverAbstraction _hsAbstraction;
+
+    public AuthMediaTests(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
+        _fixture = fixture;
+        _resolver = _fixture.GetService<HomeserverResolverService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverResolverService)}");
+        _config = _fixture.GetService<Config>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(Config)}");
+        _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
+        _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
+    }
+
+    [Fact]
+    public async Task UploadFileAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+        var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+        Assert.NotNull(mxcUri);
+    }
+    
+    [Fact]
+    public async Task DownloadFileAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+
+        var mxcUri = await hs.UploadFile("test", "LibMatrix test file".AsBytes());
+        Assert.NotNull(mxcUri);
+        
+        var file = await hs.GetMediaStreamAsync(mxcUri);
+        Assert.NotNull(file);
+        
+        var data = file!.ReadToEnd().AsString();
+        Assert.Equal("LibMatrix test file", data);
+    }
+    
+    [SkippableFact(typeof(LibMatrixException))] // This test will fail if the homeserver does not support URL previews
+    public async Task GetUrlPreviewAsync() {
+        var hs = await _hsAbstraction.GetConfiguredHomeserver();
+        var preview = await hs.GetUrlPreviewAsync("https://matrix.org");
+        
+        Assert.NotNull(preview);
+    }
+}
\ No newline at end of file
diff --git a/Tests/LibMatrix.Tests/Tests/AuthTests.cs b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
index 633c842..69e6231 100644
--- a/Tests/LibMatrix.Tests/Tests/AuthTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/AuthTests.cs
@@ -20,17 +20,36 @@ public class AuthTests : TestBed<TestFixture> {
     
     [Fact]
     public async Task LoginWithPassword() {
-        var credentials = await _hsAbstraction.GetKnownCredentials();
+        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 rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+        var username = Guid.NewGuid().ToString();
+        var password = Guid.NewGuid().ToString();
         
-        var login = await _provider.Login(_config.TestHomeserver!, credentials.username, credentials.password);
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        
+        var login = await _provider.Login(_config.TestHomeserver!, username, password);
         Assert.NotNull(login);
         Assert.NotNull(login.AccessToken);
     }
 
     [Fact]
     public async Task LoginWithToken() {
-        var credentials = await _hsAbstraction.GetKnownCredentials();
-        var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, credentials.token);
+        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 rhs = await _provider.GetRemoteHomeserver(_config.TestHomeserver);
+        var username = Guid.NewGuid().ToString();
+        var password = Guid.NewGuid().ToString();
+        
+        var reg = await rhs.RegisterAsync(username, password, "Unit tests!");
+        
+        var hs = await _provider.GetAuthenticatedWithToken(_config.TestHomeserver!, reg.AccessToken);
         Assert.NotNull(hs);
         Assert.NotNull(hs.WhoAmI);
         hs.WhoAmI.VerifyRequiredFields();
diff --git a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
index 03f3c24..20f975e 100644
--- a/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/RemoteHomeserverTests.cs
@@ -18,13 +18,13 @@ public class RemoteHomeserverTests : TestBed<TestFixture> {
         _provider = _fixture.GetService<HomeserverProviderService>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverProviderService)}");
     }
 
-    [Fact]
-    public async Task ResolveMedia() {
-        var hs = await _provider.GetRemoteHomeserver("matrix.org");
-        var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
-        
-        Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
-    }
+    // [Fact]
+    // public async Task ResolveMedia() {
+    //     var hs = await _provider.GetRemoteHomeserver("matrix.org");
+    //     var media = hs.ResolveMediaUri("mxc://matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo");
+    //     
+    //     Assert.Equal("https://matrix-client.matrix.org/_matrix/media/v3/download/matrix.org/eqwrRZRoPpNbcMeUwyXAuVRo", media);
+    // }
 
     [Fact]
     public async Task ResolveRoomAliasAsync() {
diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
deleted file mode 100644
index 1c5747c..0000000
--- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-// using System.Diagnostics;
-// using LibMatrix.Helpers;
-// using LibMatrix.Services;
-// using LibMatrix.Tests.Abstractions;
-// using LibMatrix.Tests.Fixtures;
-// using Microsoft.Extensions.Logging;
-// using Xunit.Abstractions;
-// using Xunit.Microsoft.DependencyInjection.Abstracts;
-//
-// namespace LibMatrix.Tests.Tests;
-//
-// public class TestCleanup : TestBed<TestFixture> {
-//     private readonly HomeserverAbstraction _hsAbstraction;
-//     private readonly ILogger<TestCleanup> _logger;
-//
-//     public TestCleanup(ITestOutputHelper testOutputHelper, TestFixture fixture) : base(testOutputHelper, fixture) {
-//         // _fixture = fixture;
-//         _logger = _fixture.GetService<ILogger<TestCleanup>>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(ILogger<TestCleanup>)}");
-//         _hsAbstraction = _fixture.GetService<HomeserverAbstraction>(_testOutputHelper) ?? throw new InvalidOperationException($"Failed to get {nameof(HomeserverAbstraction)}");
-//     }
-//
-//     [SkippableFact(typeof(MatrixException))]
-//     public async Task Cleanup() {
-//         // 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 hs = await _hsAbstraction.GetConfiguredHomeserver();
-//         Assert.NotNull(hs);
-//
-//         var syncHelper = new SyncHelper(hs, _logger) {
-//             Timeout = 3000
-//         };
-//         _testOutputHelper.WriteLine("Starting sync loop");
-//         var cancellationTokenSource = new CancellationTokenSource();
-//         var sw = Stopwatch.StartNew();
-//         syncHelper.SyncReceivedHandlers.Add(async response => {
-//             // if (sw.ElapsedMilliseconds >= 3000) {
-//                 // _testOutputHelper.WriteLine("Cancelling sync loop");
-//
-//                 var tasks = (await hs.GetJoinedRooms()).Select(async room => {
-//                     _logger.LogInformation("Leaving room: {}", room.RoomId);
-//                     await room.LeaveAsync();
-//                     await room.ForgetAsync();
-//                     return room;
-//                 }).ToList();
-//                 await Task.WhenAll(tasks);
-//
-//                 // cancellationTokenSource.Cancel();
-//             // }
-//
-//             sw.Restart();
-//             if (response.Rooms?.Leave is { Count: > 0 }) {
-//                 // foreach (var room in response.Rooms.Leave) {
-//                 // await hs.GetRoom(room.Key).ForgetAsync();
-//                 // }
-//                 var tasks2 = response.Rooms.Leave.Select(async room => {
-//                     await hs.GetRoom(room.Key).ForgetAsync();
-//                     return room;
-//                 }).ToList();
-//                 await Task.WhenAll(tasks2);
-//             }
-//         });
-//         await syncHelper.RunSyncLoopAsync(cancellationToken: cancellationTokenSource.Token);
-//
-//         Assert.NotNull(hs);
-//         await hs.Logout();
-//     }
-// }
\ No newline at end of file
diff --git a/Utilities/LibMatrix.TestDataGenerator/Program.cs b/Utilities/LibMatrix.TestDataGenerator/Program.cs
index 2583817..f3750a8 100644
--- a/Utilities/LibMatrix.TestDataGenerator/Program.cs
+++ b/Utilities/LibMatrix.TestDataGenerator/Program.cs
@@ -2,6 +2,7 @@
 
 using LibMatrix.Services;
 using LibMatrix.Utilities.Bot;
+using LibMatrix.Utilities.Bot.AppServices;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using TestDataGenerator.Bot;
diff --git a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs
index afda89e..2cfcf32 100644
--- a/Utilities/LibMatrix.Utilities.Bot/AppServiceConfiguration.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/AppServices/AppServiceConfiguration.cs
@@ -1,23 +1,47 @@
-namespace LibMatrix.Utilities.Bot;
+using System.Text.Json.Serialization;
+
+namespace LibMatrix.Utilities.Bot.AppServices;
 
 public class AppServiceConfiguration {
+    [JsonPropertyName("id")]
     public string Id { get; set; } = null!;
+
+    [JsonPropertyName("url")]
     public string? Url { get; set; } = null!;
+
+    [JsonPropertyName("sender_localpart")]
     public string SenderLocalpart { get; set; } = null!;
+
+    [JsonPropertyName("as_token")]
     public string AppserviceToken { get; set; } = null!;
+
+    [JsonPropertyName("hs_token")]
     public string HomeserverToken { get; set; } = null!;
+
+    [JsonPropertyName("protocols")]
     public List<string>? Protocols { get; set; } = null!;
+
+    [JsonPropertyName("rate_limited")]
     public bool? RateLimited { get; set; } = null!;
 
+    [JsonPropertyName("namespaces")]
     public AppserviceNamespaces Namespaces { get; set; } = null!;
 
     public class AppserviceNamespaces {
+        [JsonPropertyName("users")]
         public List<AppserviceNamespace>? Users { get; set; } = null;
+
+        [JsonPropertyName("aliases")]
         public List<AppserviceNamespace>? Aliases { get; set; } = null;
+
+        [JsonPropertyName("rooms")]
         public List<AppserviceNamespace>? Rooms { get; set; } = null;
 
         public class AppserviceNamespace {
+            [JsonPropertyName("exclusive")]
             public bool Exclusive { get; set; }
+
+            [JsonPropertyName("regex")]
             public string Regex { get; set; } = null!;
         }
     }
diff --git a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs
index 621c1ee..ca6a4d8 100644
--- a/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/BotCommandInstaller.cs
@@ -1,8 +1,7 @@
 using ArcaneLibs;
-using LibMatrix.EventTypes.Spec.State;
 using LibMatrix.Homeservers;
-using LibMatrix.Responses;
 using LibMatrix.Services;
+using LibMatrix.Utilities.Bot.AppServices;
 using LibMatrix.Utilities.Bot.Interfaces;
 using LibMatrix.Utilities.Bot.Services;
 using Microsoft.Extensions.DependencyInjection;
@@ -22,6 +21,20 @@ public class BotInstaller(IServiceCollection services) {
         services.AddScoped<AuthenticatedHomeserverGeneric>(x => {
             var config = x.GetService<LibMatrixBotConfiguration>() ?? throw new Exception("No configuration found!");
             var hsProvider = x.GetService<HomeserverProviderService>() ?? throw new Exception("No homeserver provider found!");
+            
+            if (x.GetService<AppServiceConfiguration>() is AppServiceConfiguration appsvcConfig)
+                config.AccessToken = appsvcConfig.AppserviceToken;
+            else if (Environment.GetEnvironmentVariable("LIBMATRIX_ACCESS_TOKEN_PATH") is string path)
+                config.AccessTokenPath = path;
+            
+            if(string.IsNullOrWhiteSpace(config.AccessToken) && string.IsNullOrWhiteSpace(config.AccessTokenPath))
+                throw new Exception("Unable to add bot service without an access token or access token path!");
+            
+            if(!string.IsNullOrWhiteSpace(config.AccessTokenPath)) {
+                var token = File.ReadAllText(config.AccessTokenPath);
+                config.AccessToken = token;
+            }
+            
             var hs = hsProvider.GetAuthenticatedWithToken(config.Homeserver, config.AccessToken).Result;
 
             return hs;
diff --git a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs
index 245442f..728b169 100644
--- a/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/LibMatrixBotConfiguration.cs
@@ -6,7 +6,8 @@ namespace LibMatrix.Utilities.Bot;
 public class LibMatrixBotConfiguration {
     public LibMatrixBotConfiguration(IConfiguration config) => config.GetRequiredSection("LibMatrixBot").Bind(this);
     public string Homeserver { get; set; }
-    public string AccessToken { get; set; }
+    public string? AccessToken { get; set; }
+    public string? AccessTokenPath { get; set; }
     public List<string> Prefixes { get; set; }
     public bool MentionPrefix { get; set; }
     public string? LogRoom { get; set; }