about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEmma [it/its]@Rory& <root@rory.gay>2023-12-01 12:16:00 +0100
committerEmma [it/its]@Rory& <root@rory.gay>2023-12-01 12:16:00 +0100
commit71d115dc8e915a620dd935955ba980fcbe421dad (patch)
treeb836f5b0e1b5955bbc08443f8df6d078bd0fa7ea
parentModeration bot work (diff)
downloadLibMatrix-71d115dc8e915a620dd935955ba980fcbe421dad.tar.xz
Cleanup, move ArcaneLibs to submodule instead of parent submodule
-rw-r--r--.gitmodules3
m---------ArcaneLibs0
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/Bot/FileStorageProvider.cs2
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/Bot/Interfaces/ICommand.cs6
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/Bot/MRUBotConfiguration.cs4
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj1
-rw-r--r--ExampleBots/LibMatrix.ExampleBot/Program.cs64
-rw-r--r--ExampleBots/ModerationBot/AccountData/BotData.cs2
-rw-r--r--ExampleBots/ModerationBot/Commands/BanMediaCommand.cs4
-rw-r--r--ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs8
-rw-r--r--ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs4
-rw-r--r--ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs6
-rw-r--r--ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs8
-rw-r--r--ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs6
-rw-r--r--ExampleBots/ModerationBot/FirstRunTasks.cs2
-rw-r--r--ExampleBots/ModerationBot/ModerationBot.cs257
-rw-r--r--ExampleBots/ModerationBot/ModerationBot.csproj1
-rw-r--r--ExampleBots/ModerationBot/PolicyEngine.cs34
-rw-r--r--ExampleBots/ModerationBot/Program.cs68
-rw-r--r--ExampleBots/ModerationBot/StateEventTypes/Policies/BasePolicy.cs4
-rw-r--r--ExampleBots/PluralContactBotPoC/PluralContactBotPoC.csproj1
-rw-r--r--ExampleBots/PluralContactBotPoC/Program.cs148
-rw-r--r--LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs4
-rw-r--r--LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs4
-rw-r--r--LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs10
-rw-r--r--LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs23
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs4
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs1
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs5
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs2
-rw-r--r--LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs28
-rw-r--r--LibMatrix/Helpers/MessageFormatter.cs6
-rw-r--r--LibMatrix/Helpers/SyncHelper.cs2
-rw-r--r--LibMatrix/Helpers/SyncStateResolver.cs4
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs14
-rw-r--r--LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs12
-rw-r--r--LibMatrix/Homeservers/RemoteHomeServer.cs12
-rw-r--r--LibMatrix/Interfaces/EventContent.cs2
-rw-r--r--LibMatrix/LibMatrix.csproj4
-rw-r--r--LibMatrix/MatrixException.cs2
-rw-r--r--LibMatrix/RoomTypes/GenericRoom.cs22
-rw-r--r--LibMatrix/Services/HomeserverProviderService.cs4
-rw-r--r--LibMatrix/Services/HomeserverResolverService.cs2
-rw-r--r--LibMatrix/StateEvent.cs5
-rw-r--r--LibMatrix/WhoAmIResponse.cs2
-rw-r--r--Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs2
-rw-r--r--Tests/LibMatrix.Tests/GlobalUsings.cs2
-rw-r--r--Tests/LibMatrix.Tests/Tests/RoomTests.cs6
-rw-r--r--Tests/LibMatrix.Tests/Tests/TestCleanup.cs2
-rw-r--r--Tests/TestDataGenerator/Program.cs62
-rw-r--r--Utilities/LibMatrix.DebugDataValidationApi/Program.cs3
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs2
-rw-r--r--Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs6
53 files changed, 461 insertions, 431 deletions
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..70d7a24
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ArcaneLibs"]
+	path = ArcaneLibs
+	url = git@github.com:TheArcaneBrony/ArcaneLibs.git
diff --git a/ArcaneLibs b/ArcaneLibs
new file mode 160000
+Subproject 3b815f2bffc65d7b42e8845464d6f07391d612e
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/FileStorageProvider.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/FileStorageProvider.cs
index 2dfcee5..935d53f 100644
--- a/ExampleBots/LibMatrix.ExampleBot/Bot/FileStorageProvider.cs
+++ b/ExampleBots/LibMatrix.ExampleBot/Bot/FileStorageProvider.cs
@@ -19,7 +19,7 @@ public class FileStorageProvider : IStorageProvider {
         new Logger<FileStorageProvider>(new LoggerFactory()).LogInformation("test");
         Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}");
         TargetPath = targetPath;
-        if(!Directory.Exists(targetPath)) {
+        if (!Directory.Exists(targetPath)) {
             Directory.CreateDirectory(targetPath);
         }
     }
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/Interfaces/ICommand.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/Interfaces/ICommand.cs
index 393ddbb..2ba5a27 100644
--- a/ExampleBots/LibMatrix.ExampleBot/Bot/Interfaces/ICommand.cs
+++ b/ExampleBots/LibMatrix.ExampleBot/Bot/Interfaces/ICommand.cs
@@ -1,4 +1,4 @@
-namespace LibMatrix.ExampleBot.Bot.Interfaces; 
+namespace LibMatrix.ExampleBot.Bot.Interfaces;
 
 public interface ICommand {
     public string Name { get; }
@@ -7,6 +7,6 @@ public interface ICommand {
     public Task<bool> CanInvoke(CommandContext ctx) {
         return Task.FromResult(true);
     }
-    
+
     public Task Invoke(CommandContext ctx);
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBotConfiguration.cs b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBotConfiguration.cs
index c7620df..dcdfc4c 100644
--- a/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBotConfiguration.cs
+++ b/ExampleBots/LibMatrix.ExampleBot/Bot/MRUBotConfiguration.cs
@@ -1,6 +1,6 @@
 using Microsoft.Extensions.Configuration;
 
-namespace LibMatrix.ExampleBot.Bot; 
+namespace LibMatrix.ExampleBot.Bot;
 
 public class MRUBotConfiguration {
     public MRUBotConfiguration(IConfiguration config) {
@@ -9,4 +9,4 @@ public class MRUBotConfiguration {
     public string Homeserver { get; set; } = "";
     public string AccessToken { get; set; } = "";
     public string Prefix { get; set; }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj b/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj
index 94c9045..a905524 100644
--- a/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj
+++ b/ExampleBots/LibMatrix.ExampleBot/LibMatrix.ExampleBot.csproj
@@ -17,7 +17,6 @@
   </PropertyGroup>

 

   <ItemGroup>

-      <ProjectReference Include="..\..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" />

       <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" />

   </ItemGroup>

 

diff --git a/ExampleBots/LibMatrix.ExampleBot/Program.cs b/ExampleBots/LibMatrix.ExampleBot/Program.cs
index ef40ecb..6d8775e 100644
--- a/ExampleBots/LibMatrix.ExampleBot/Program.cs
+++ b/ExampleBots/LibMatrix.ExampleBot/Program.cs
@@ -1,32 +1,32 @@
-// See https://aka.ms/new-console-template for more information

-

-using ArcaneLibs;

-using LibMatrix.ExampleBot.Bot;

-using LibMatrix.ExampleBot.Bot.Interfaces;

-using LibMatrix.ExampleBot.Bot.StartupTasks;

-using LibMatrix.Extensions;

-using LibMatrix.Services;

-using Microsoft.Extensions.DependencyInjection;

-using Microsoft.Extensions.Hosting;

-

-Console.WriteLine("Hello, World!");

-

-var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {

-    services.AddScoped<TieredStorageService>(x =>

-        new TieredStorageService(

-            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),

-            dataStorageProvider: new FileStorageProvider("bot_data/data/")

-        )

-    );

-    services.AddScoped<MRUBotConfiguration>();

-    services.AddRoryLibMatrixServices();

-    foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) {

-        Console.WriteLine($"Adding command {commandClass.Name}");

-        services.AddScoped(typeof(ICommand), commandClass);

-    }

-

-    // services.AddHostedService<ServerRoomSizeCalulator>();

-    services.AddHostedService<MRUBot>();

-}).UseConsoleLifetime().Build();

-

-await host.RunAsync();

+// See https://aka.ms/new-console-template for more information
+
+using ArcaneLibs;
+using LibMatrix.ExampleBot.Bot;
+using LibMatrix.ExampleBot.Bot.Interfaces;
+using LibMatrix.ExampleBot.Bot.StartupTasks;
+using LibMatrix.Extensions;
+using LibMatrix.Services;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+Console.WriteLine("Hello, World!");
+
+var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {
+    services.AddScoped<TieredStorageService>(x =>
+        new TieredStorageService(
+            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
+            dataStorageProvider: new FileStorageProvider("bot_data/data/")
+        )
+    );
+    services.AddScoped<MRUBotConfiguration>();
+    services.AddRoryLibMatrixServices();
+    foreach (var commandClass in new ClassCollector<ICommand>().ResolveFromAllAccessibleAssemblies()) {
+        Console.WriteLine($"Adding command {commandClass.Name}");
+        services.AddScoped(typeof(ICommand), commandClass);
+    }
+
+    // services.AddHostedService<ServerRoomSizeCalulator>();
+    services.AddHostedService<MRUBot>();
+}).UseConsoleLifetime().Build();
+
+await host.RunAsync();
diff --git a/ExampleBots/ModerationBot/AccountData/BotData.cs b/ExampleBots/ModerationBot/AccountData/BotData.cs
index df86589..ab680c2 100644
--- a/ExampleBots/ModerationBot/AccountData/BotData.cs
+++ b/ExampleBots/ModerationBot/AccountData/BotData.cs
@@ -11,4 +11,4 @@ public class BotData {
 
     [JsonPropertyName("default_policy_room")]
     public string? DefaultPolicyRoom { get; set; }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs b/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs
index 21e0a94..9e49b22 100644
--- a/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/BanMediaCommand.cs
@@ -20,7 +20,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -31,7 +31,7 @@ public class BanMediaCommand(IServiceProvider services, HomeserverProviderServic
     }
 
     public async Task Invoke(CommandContext ctx) {
-        
+
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var policyRoom = ctx.Homeserver.GetRoom(botData.DefaultPolicyRoom ?? botData.ControlRoom);
         var logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom);
diff --git a/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs b/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs
index 09d3caf..327a9a4 100644
--- a/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/DbgAllRoomsArePolicyListsCommand.cs
@@ -26,7 +26,7 @@ public class DbgAllRoomsArePolicyListsCommand
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -39,9 +39,9 @@ public class DbgAllRoomsArePolicyListsCommand
     public async Task Invoke(CommandContext ctx) {
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom);
-        
+
         var joinedRooms = await ctx.Homeserver.GetJoinedRooms();
-        
+
         await ctx.Homeserver.SetAccountDataAsync("gay.rory.moderation_bot.policy_lists", joinedRooms.ToDictionary(x => x.RoomId, x => new PolicyList() {
             Trusted = true
         }));
@@ -60,4 +60,4 @@ public class DbgAllRoomsArePolicyListsCommand
 
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs b/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs
index 395c87c..35c95f8 100644
--- a/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/DbgDumpActivePoliciesCommand.cs
@@ -26,7 +26,7 @@ public class DbgDumpActivePoliciesCommand
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -40,4 +40,4 @@ public class DbgDumpActivePoliciesCommand
         await ctx.Room.SendFileAsync("all.json", new MemoryStream(engine.ActivePolicies.ToJson().AsBytes().ToArray()), contentType: "application/json");
         await ctx.Room.SendFileAsync("by-type.json", new MemoryStream(engine.ActivePoliciesByType.ToJson().AsBytes().ToArray()), contentType: "application/json");
     }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs b/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs
index e9a645e..0013065 100644
--- a/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/DbgDumpAllStateTypesCommand.cs
@@ -26,7 +26,7 @@ public class DbgDumpAllStateTypesCommand
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -58,7 +58,7 @@ public class DbgDumpAllStateTypesCommand
 
         return (memberRoom, SummariseStateTypeCounts(states));
     }
-    
+
     private static (string Raw, string Html) SummariseStateTypeCounts(IList<StateEventResponse> states) {
         string raw = "Count | State type | Mapped type", html = "<table><tr><th>Count</th><th>State type</th><th>Mapped type</th></tr>";
         var groupedStates = states.GroupBy(x => x.Type).ToDictionary(x => x.Key, x => x.ToList()).OrderByDescending(x => x.Value.Count);
@@ -70,4 +70,4 @@ public class DbgDumpAllStateTypesCommand
         html += "</table>";
         return (raw, html);
     }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs b/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs
index 19a2c54..7496a07 100644
--- a/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/JoinRoomCommand.cs
@@ -19,7 +19,7 @@ public class JoinRoomCommand(IServiceProvider services, HomeserverProviderServic
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -30,16 +30,16 @@ public class JoinRoomCommand(IServiceProvider services, HomeserverProviderServic
     }
 
     public async Task Invoke(CommandContext ctx) {
-        
+
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var policyRoom = ctx.Homeserver.GetRoom(botData.DefaultPolicyRoom ?? botData.ControlRoom);
         var logRoom = ctx.Homeserver.GetRoom(botData.LogRoom ?? botData.ControlRoom);
 
         await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining room {ctx.Args[0]} with reason: {string.Join(' ', ctx.Args[1..])}"));
         var roomId = ctx.Args[0];
-        var servers = new List<string>() {ctx.Homeserver.ServerName};
+        var servers = new List<string>() { ctx.Homeserver.ServerName };
         if (roomId.StartsWith('[')) {
-            
+
         }
 
         if (roomId.StartsWith('#')) {
diff --git a/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs b/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs
index c3b7d12..6e64f6f 100644
--- a/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs
+++ b/ExampleBots/ModerationBot/Commands/JoinSpaceMembersCommand.cs
@@ -21,7 +21,7 @@ public class JoinSpaceMembersCommand(IServiceProvider services, HomeserverProvid
         //check if user is admin in control room
         var botData = await ctx.Homeserver.GetAccountDataAsync<BotData>("gay.rory.moderation_bot_data");
         var controlRoom = ctx.Homeserver.GetRoom(botData.ControlRoom);
-        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasPermission(ctx.MessageEvent.Sender, "m.room.ban");
+        var isAdmin = (await controlRoom.GetPowerLevelsAsync())!.UserHasStatePermission(ctx.MessageEvent.Sender, "m.room.ban");
         if (!isAdmin) {
             // await ctx.Reply("You do not have permission to use this command!");
             await ctx.Homeserver.GetRoom(botData.LogRoom!).SendMessageEventAsync(
@@ -37,9 +37,9 @@ public class JoinSpaceMembersCommand(IServiceProvider services, HomeserverProvid
 
         await logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccess($"Joining space children of {ctx.Args[0]} with reason: {string.Join(' ', ctx.Args[1..])}"));
         var roomId = ctx.Args[0];
-        var servers = new List<string>() {ctx.Homeserver.ServerName};
+        var servers = new List<string>() { ctx.Homeserver.ServerName };
         if (roomId.StartsWith('[')) {
-            
+
         }
 
         if (roomId.StartsWith('#')) {
diff --git a/ExampleBots/ModerationBot/FirstRunTasks.cs b/ExampleBots/ModerationBot/FirstRunTasks.cs
index ebbdc81..83356bf 100644
--- a/ExampleBots/ModerationBot/FirstRunTasks.cs
+++ b/ExampleBots/ModerationBot/FirstRunTasks.cs
@@ -81,4 +81,4 @@ public class FirstRunTasks {
 
         return botdata;
     }
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/ModerationBot.cs b/ExampleBots/ModerationBot/ModerationBot.cs
index 79b05bf..8a48b61 100644
--- a/ExampleBots/ModerationBot/ModerationBot.cs
+++ b/ExampleBots/ModerationBot/ModerationBot.cs
@@ -10,10 +10,10 @@ using LibMatrix.Responses;
 using LibMatrix.RoomTypes;
 using LibMatrix.Services;
 using LibMatrix.Utilities.Bot.Interfaces;
-using ModerationBot.AccountData;
-using ModerationBot.StateEventTypes;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
+using ModerationBot.AccountData;
+using ModerationBot.StateEventTypes;
 using ModerationBot.StateEventTypes.Policies;
 
 namespace ModerationBot;
@@ -74,9 +74,10 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
         Task.Run(async () => {
             while (!cancellationToken.IsCancellationRequested) {
                 var controlRoomMembers = _controlRoom.GetMembersAsync();
+                var pls = await _controlRoom.GetPowerLevelsAsync();
                 await foreach (var member in controlRoomMembers) {
                     if ((member.TypedContent as RoomMemberEventContent)?
-                        .Membership == "join") admins.Add(member.StateKey);
+                        .Membership == "join" && pls.UserHasTimelinePermission(member.Sender, RoomMessageEventContent.EventId)) admins.Add(member.StateKey);
                 }
 
                 await Task.Delay(TimeSpan.FromMinutes(5), cancellationToken);
@@ -104,7 +105,7 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
                 }
             }
         });
-        
+
         syncHelper.TimelineEventHandlers.Add(async @event => {
             var room = hs.GetRoom(@event.RoomId);
             try {
@@ -116,7 +117,7 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
                         || @event.GetType.IsAssignableTo(typeof(PolicyRuleEventContent))
                     ))
                     await engine.ReloadActivePolicyListById(@event.RoomId);
-                
+
                 var rules = await engine.GetMatchingPolicies(@event);
                 foreach (var matchedRule in rules) {
                     await _logRoom.SendMessageEventAsync(MessageFormatter.FormatSuccessJson(
@@ -125,131 +126,131 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
 
                 if (configuration.DemoMode) {
                     // foreach (var matchedRule in rules) {
-                        // await room.SendMessageEventAsync(MessageFormatter.FormatSuccessJson(
-                            // $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.EventId, roomId: matchedRule.RoomId, displayName: "rule")}", @matchedRule.RawContent));
+                    // await room.SendMessageEventAsync(MessageFormatter.FormatSuccessJson(
+                    // $"{MessageFormatter.HtmlFormatMessageLink(eventId: @event.EventId, roomId: room.RoomId, displayName: "Event")} matched {MessageFormatter.HtmlFormatMessageLink(eventId: @matchedRule.EventId, roomId: matchedRule.RoomId, displayName: "rule")}", @matchedRule.RawContent));
                     // }
                     return;
                 }
-//
-//                 if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) {
-//                     if (message is { MessageType: "m.image" }) {
-//                         //check media
-//                         // var matchedPolicy = await CheckMedia(@event);
-//                         var matchedPolicy = rules.FirstOrDefault();
-//                         if (matchedPolicy is null) return;
-//                         var matchedpolicyData = matchedPolicy.TypedContent as MediaPolicyEventContent;
-//                         await _logRoom.SendMessageEventAsync(
-//                             new RoomMessageEventContent(
-//                                 body:
-//                                 $"User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: {matchedPolicy.RawContent!.ToJson(ignoreNull: true)}",
-//                                 messageType: "m.text") {
-//                                 Format = "org.matrix.custom.html",
-//                                 FormattedBody =
-//                                     $"<font color=\"#FFFF00\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: <pre>{matchedPolicy.RawContent!.ToJson(ignoreNull: true)}</pre></font>"
-//                             });
-//                         switch (matchedpolicyData.Recommendation) {
-//                             case "warn_admins": {
-//                                 await _controlRoom.SendMessageEventAsync(
-//                                     new RoomMessageEventContent(
-//                                         body: $"{string.Join(' ', admins)}\nUser {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image {message.Url}",
-//                                         messageType: "m.text") {
-//                                         Format = "org.matrix.custom.html",
-//                                         FormattedBody = $"{string.Join(' ', admins.Select(u => MessageFormatter.HtmlFormatMention(u)))}\n" +
-//                                                         $"<font color=\"#FF0000\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image <a href=\"{message.Url}\">{message.Url}</a></font>"
-//                                     });
-//                                 break;
-//                             }
-//                             case "warn": {
-//                                 await room.SendMessageEventAsync(
-//                                     new RoomMessageEventContent(
-//                                         body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}",
-//                                         messageType: "m.text") {
-//                                         Format = "org.matrix.custom.html",
-//                                         FormattedBody =
-//                                             $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>"
-//                                     });
-//                                 break;
-//                             }
-//                             case "redact": {
-//                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified");
-//                                 break;
-//                             }
-//                             case "spoiler": {
-//                                 // <blockquote>
-//                                 //  <a href=\"https://matrix.to/#/@emma:rory.gay\">@emma:rory.gay</a><br>
-//                                 //  <a href=\"https://codeberg.org/crimsonfork/CN\"></a>
-//                                 //  <font color=\"#dc143c\" data-mx-color=\"#dc143c\">
-//                                 //      <b>CN</b>
-//                                 //  </font>:
-//                                 //  <a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">test</a><br>
-//                                 //  <span data-mx-spoiler=\"\"><a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">
-//                                 //      <img src=\"mxc://rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\" height=\"69\"></a>
-//                                 //  </span>
-//                                 // </blockquote>
-//                                 await room.SendMessageEventAsync(
-//                                     new RoomMessageEventContent(
-//                                         body:
-//                                         $"Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:",
-//                                         messageType: "m.text") {
-//                                         Format = "org.matrix.custom.html",
-//                                         FormattedBody =
-//                                             $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:</a></font>"
-//                                     });
-//                                 var imageUrl = message.Url;
-//                                 await room.SendMessageEventAsync(
-//                                     new RoomMessageEventContent(body: $"CN: {imageUrl}",
-//                                         messageType: "m.text") {
-//                                         Format = "org.matrix.custom.html",
-//                                         FormattedBody = $"""
-//                                                              <blockquote>
-//                                                                 <font color=\"#dc143c\" data-mx-color=\"#dc143c\">
-//                                                                     <b>CN</b>
-//                                                                 </font>:
-//                                                                 <a href=\"{imageUrl}\">{matchedpolicyData.Reason}</a><br>
-//                                                                 <span data-mx-spoiler=\"\">
-//                                                                     <a href=\"{imageUrl}\">
-//                                                                         <img src=\"{imageUrl}\" height=\"69\">
-//                                                                     </a>
-//                                                                 </span>
-//                                                              </blockquote>
-//                                                          """
-//                                     });
-//                                 await room.RedactEventAsync(@event.EventId, "Automatically spoilered: " + matchedpolicyData.Reason);
-//                                 break;
-//                             }
-//                             case "mute": {
-//                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
-//                                 //change powerlevel to -1
-//                                 var currentPls = await room.GetPowerLevelsAsync();
-//                                 if (currentPls is null) {
-//                                     logger.LogWarning("Unable to get power levels for {room}", room.RoomId);
-//                                     await _logRoom.SendMessageEventAsync(
-//                                         MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}"));
-//                                     return;
-//                                 }
-//
-//                                 currentPls.Users ??= new();
-//                                 currentPls.Users[@event.Sender] = -1;
-//                                 await room.SendStateEventAsync("m.room.power_levels", currentPls);
-//                                 break;
-//                             }
-//                             case "kick": {
-//                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
-//                                 await room.KickAsync(@event.Sender, matchedpolicyData.Reason);
-//                                 break;
-//                             }
-//                             case "ban": {
-//                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
-//                                 await room.BanAsync(@event.Sender, matchedpolicyData.Reason);
-//                                 break;
-//                             }
-//                             default: {
-//                                 throw new ArgumentOutOfRangeException("recommendation",
-//                                     $"Unknown response type {matchedpolicyData.Recommendation}!");
-//                             }
-//                         }
-//                     }
-//                 }
+                //
+                //                 if (@event is { Type: "m.room.message", TypedContent: RoomMessageEventContent message }) {
+                //                     if (message is { MessageType: "m.image" }) {
+                //                         //check media
+                //                         // var matchedPolicy = await CheckMedia(@event);
+                //                         var matchedPolicy = rules.FirstOrDefault();
+                //                         if (matchedPolicy is null) return;
+                //                         var matchedpolicyData = matchedPolicy.TypedContent as MediaPolicyEventContent;
+                //                         await _logRoom.SendMessageEventAsync(
+                //                             new RoomMessageEventContent(
+                //                                 body:
+                //                                 $"User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: {matchedPolicy.RawContent!.ToJson(ignoreNull: true)}",
+                //                                 messageType: "m.text") {
+                //                                 Format = "org.matrix.custom.html",
+                //                                 FormattedBody =
+                //                                     $"<font color=\"#FFFF00\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted an image in {MessageFormatter.HtmlFormatMention(room.RoomId)} that matched rule {matchedPolicy.StateKey}, applying action {matchedpolicyData.Recommendation}, as described in rule: <pre>{matchedPolicy.RawContent!.ToJson(ignoreNull: true)}</pre></font>"
+                //                             });
+                //                         switch (matchedpolicyData.Recommendation) {
+                //                             case "warn_admins": {
+                //                                 await _controlRoom.SendMessageEventAsync(
+                //                                     new RoomMessageEventContent(
+                //                                         body: $"{string.Join(' ', admins)}\nUser {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image {message.Url}",
+                //                                         messageType: "m.text") {
+                //                                         Format = "org.matrix.custom.html",
+                //                                         FormattedBody = $"{string.Join(' ', admins.Select(u => MessageFormatter.HtmlFormatMention(u)))}\n" +
+                //                                                         $"<font color=\"#FF0000\">User {MessageFormatter.HtmlFormatMention(@event.Sender)} posted a banned image <a href=\"{message.Url}\">{message.Url}</a></font>"
+                //                                     });
+                //                                 break;
+                //                             }
+                //                             case "warn": {
+                //                                 await room.SendMessageEventAsync(
+                //                                     new RoomMessageEventContent(
+                //                                         body: $"Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}",
+                //                                         messageType: "m.text") {
+                //                                         Format = "org.matrix.custom.html",
+                //                                         FormattedBody =
+                //                                             $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason ?? "No reason specified"}</a></font>"
+                //                                     });
+                //                                 break;
+                //                             }
+                //                             case "redact": {
+                //                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason ?? "No reason specified");
+                //                                 break;
+                //                             }
+                //                             case "spoiler": {
+                //                                 // <blockquote>
+                //                                 //  <a href=\"https://matrix.to/#/@emma:rory.gay\">@emma:rory.gay</a><br>
+                //                                 //  <a href=\"https://codeberg.org/crimsonfork/CN\"></a>
+                //                                 //  <font color=\"#dc143c\" data-mx-color=\"#dc143c\">
+                //                                 //      <b>CN</b>
+                //                                 //  </font>:
+                //                                 //  <a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">test</a><br>
+                //                                 //  <span data-mx-spoiler=\"\"><a href=\"https://the-apothecary.club/_matrix/media/v3/download/rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\">
+                //                                 //      <img src=\"mxc://rory.gay/sLkdxUhipiQaFwRkXcPSRwdg\" height=\"69\"></a>
+                //                                 //  </span>
+                //                                 // </blockquote>
+                //                                 await room.SendMessageEventAsync(
+                //                                     new RoomMessageEventContent(
+                //                                         body:
+                //                                         $"Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:",
+                //                                         messageType: "m.text") {
+                //                                         Format = "org.matrix.custom.html",
+                //                                         FormattedBody =
+                //                                             $"<font color=\"#FFFF00\">Please be careful when posting this image: {matchedpolicyData.Reason}, I have spoilered it for you:</a></font>"
+                //                                     });
+                //                                 var imageUrl = message.Url;
+                //                                 await room.SendMessageEventAsync(
+                //                                     new RoomMessageEventContent(body: $"CN: {imageUrl}",
+                //                                         messageType: "m.text") {
+                //                                         Format = "org.matrix.custom.html",
+                //                                         FormattedBody = $"""
+                //                                                              <blockquote>
+                //                                                                 <font color=\"#dc143c\" data-mx-color=\"#dc143c\">
+                //                                                                     <b>CN</b>
+                //                                                                 </font>:
+                //                                                                 <a href=\"{imageUrl}\">{matchedpolicyData.Reason}</a><br>
+                //                                                                 <span data-mx-spoiler=\"\">
+                //                                                                     <a href=\"{imageUrl}\">
+                //                                                                         <img src=\"{imageUrl}\" height=\"69\">
+                //                                                                     </a>
+                //                                                                 </span>
+                //                                                              </blockquote>
+                //                                                          """
+                //                                     });
+                //                                 await room.RedactEventAsync(@event.EventId, "Automatically spoilered: " + matchedpolicyData.Reason);
+                //                                 break;
+                //                             }
+                //                             case "mute": {
+                //                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
+                //                                 //change powerlevel to -1
+                //                                 var currentPls = await room.GetPowerLevelsAsync();
+                //                                 if (currentPls is null) {
+                //                                     logger.LogWarning("Unable to get power levels for {room}", room.RoomId);
+                //                                     await _logRoom.SendMessageEventAsync(
+                //                                         MessageFormatter.FormatError($"Unable to get power levels for {MessageFormatter.HtmlFormatMention(room.RoomId)}"));
+                //                                     return;
+                //                                 }
+                //
+                //                                 currentPls.Users ??= new();
+                //                                 currentPls.Users[@event.Sender] = -1;
+                //                                 await room.SendStateEventAsync("m.room.power_levels", currentPls);
+                //                                 break;
+                //                             }
+                //                             case "kick": {
+                //                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
+                //                                 await room.KickAsync(@event.Sender, matchedpolicyData.Reason);
+                //                                 break;
+                //                             }
+                //                             case "ban": {
+                //                                 await room.RedactEventAsync(@event.EventId, matchedpolicyData.Reason);
+                //                                 await room.BanAsync(@event.Sender, matchedpolicyData.Reason);
+                //                                 break;
+                //                             }
+                //                             default: {
+                //                                 throw new ArgumentOutOfRangeException("recommendation",
+                //                                     $"Unknown response type {matchedpolicyData.Recommendation}!");
+                //                             }
+                //                         }
+                //                     }
+                //                 }
             }
             catch (Exception e) {
                 logger.LogError("{}", e.ToString());
@@ -272,4 +273,4 @@ public class ModerationBot(AuthenticatedHomeserverGeneric hs, ILogger<Moderation
         logger.LogInformation("Shutting down bot!");
     }
 
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/ModerationBot.csproj b/ExampleBots/ModerationBot/ModerationBot.csproj
index 5c8f8ff..99eb0b9 100644
--- a/ExampleBots/ModerationBot/ModerationBot.csproj
+++ b/ExampleBots/ModerationBot/ModerationBot.csproj
@@ -17,7 +17,6 @@
   </PropertyGroup>

 

   <ItemGroup>

-<!--      <ProjectReference Include="..\..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" />-->

       <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" />

       <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" />

   </ItemGroup>

diff --git a/ExampleBots/ModerationBot/PolicyEngine.cs b/ExampleBots/ModerationBot/PolicyEngine.cs
index 5311637..8bfa448 100644
--- a/ExampleBots/ModerationBot/PolicyEngine.cs
+++ b/ExampleBots/ModerationBot/PolicyEngine.cs
@@ -11,9 +11,9 @@ using LibMatrix.Homeservers;
 using LibMatrix.Interfaces;
 using LibMatrix.RoomTypes;
 using LibMatrix.Services;
+using Microsoft.Extensions.Logging;
 using ModerationBot.AccountData;
 using ModerationBot.StateEventTypes;
-using Microsoft.Extensions.Logging;
 using ModerationBot.StateEventTypes.Policies;
 using ModerationBot.StateEventTypes.Policies.Implementations;
 
@@ -67,7 +67,7 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
                 var progressMsgContent = MessageFormatter.FormatSuccess($"{policyLists.Count}/{PolicyListAccountData.Count} policy lists loaded, " +
                                                                         $"{policyLists.Sum(x => x.Policies.Count)} policies total, {sw.Elapsed} elapsed.")
                     .SetReplaceRelation<RoomMessageEventContent>(progressMessage.EventId);
-                
+
                 _logRoom?.SendMessageEventAsync(progressMsgContent);
             }
         }
@@ -99,8 +99,8 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
 
         return policyList;
     }
-    
-    
+
+
     public async Task ReloadActivePolicyListById(string roomId) {
         if (!ActivePolicyLists.Any(x => x.Room.RoomId == roomId)) return;
         await LoadPolicyListAsync(hs.GetRoom(roomId), ActivePolicyLists.Single(x => x.Room.RoomId == roomId));
@@ -140,7 +140,7 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
     public async Task<List<BasePolicy>> GetMatchingPolicies(StateEventResponse @event) {
         List<BasePolicy> matchingPolicies = new();
         if (@event.Sender == @hs.UserId) return matchingPolicies; //ignore self at all costs
-        
+
         if (ActivePoliciesByType.TryGetValue(nameof(ServerPolicyRuleEventContent), out var serverPolicies)) {
             var userServer = @event.Sender.Split(':', 2)[1];
             matchingPolicies.AddRange(serverPolicies.Where(x => x.Entity == userServer));
@@ -160,19 +160,19 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
         return matchingPolicies;
     }
 
-#region Policy matching
+    #region Policy matching
 
     private async Task<List<BasePolicy>> CheckMessageContent(StateEventResponse @event) {
         var matchedRules = new List<BasePolicy>();
         var msgContent = @event.TypedContent as RoomMessageEventContent;
-        
+
         if (ActivePoliciesByType.TryGetValue(nameof(MessagePolicyContainsText), out var messageContainsPolicies))
             foreach (var policy in messageContainsPolicies) {
-                if((@msgContent?.Body?.ToLowerInvariant().Contains(policy.Entity.ToLowerInvariant()) ?? false) || (@msgContent?.FormattedBody?.ToLowerInvariant().Contains(policy.Entity.ToLowerInvariant()) ?? false))
+                if ((@msgContent?.Body?.ToLowerInvariant().Contains(policy.Entity.ToLowerInvariant()) ?? false) || (@msgContent?.FormattedBody?.ToLowerInvariant().Contains(policy.Entity.ToLowerInvariant()) ?? false))
                     matchedRules.Add(policy);
             }
-            
-        
+
+
         return matchedRules;
     }
 
@@ -238,17 +238,17 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
                 //check pixels every 10% of the way through the image using ImageSharp
                 // var image = Image.Load(await _hs._httpClient.GetStreamAsync(resolvedUri));
             }
-        else logger.LogInformation("No active media file policies");        
+        else logger.LogInformation("No active media file policies");
         // logger.LogInformation("{url} did not match any rules", @event.RawContent["url"]);
 
         return matchedRules;
     }
 
-#endregion
+    #endregion
 
-#region Internal code
+    #region Internal code
 
-#region Summarisation
+    #region Summarisation
 
     private static (string Raw, string Html) SummariseStateTypeCounts(IList<StateEventResponse> states) {
         string raw = "Count | State type | Mapped type", html = "<table><tr><th>Count</th><th>State type</th><th>Mapped type</th></tr>";
@@ -262,8 +262,8 @@ public class PolicyEngine(AuthenticatedHomeserverGeneric hs, ILogger<ModerationB
         return (raw, html);
     }
 
-#endregion
+    #endregion
 
-#endregion
+    #endregion
 
-}
\ No newline at end of file
+}
diff --git a/ExampleBots/ModerationBot/Program.cs b/ExampleBots/ModerationBot/Program.cs
index b41b0be..d125258 100644
--- a/ExampleBots/ModerationBot/Program.cs
+++ b/ExampleBots/ModerationBot/Program.cs
@@ -1,28 +1,40 @@
-// See https://aka.ms/new-console-template for more information

-

-using LibMatrix.Services;

-using LibMatrix.Utilities.Bot;

-using ModerationBot;

-using Microsoft.Extensions.DependencyInjection;

-using Microsoft.Extensions.Hosting;

-

-Console.WriteLine("Hello, World!");

-

-var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {

-    services.AddScoped<TieredStorageService>(x =>

-        new TieredStorageService(

-            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),

-            dataStorageProvider: new FileStorageProvider("bot_data/data/")

-        )

-    );

-    services.AddSingleton<ModerationBotConfiguration>();

-

-    services.AddRoryLibMatrixServices();

-    services.AddBot(withCommands: true);

-

-    services.AddSingleton<PolicyEngine>();

-    

-    services.AddHostedService<ModerationBot.ModerationBot>();

-}).UseConsoleLifetime().Build();

-

-await host.RunAsync();

+// See https://aka.ms/new-console-template for more information
+
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using ModerationBot;
+
+Console.WriteLine("Hello, World!");
+
+var builder = Host.CreateDefaultBuilder(args);
+
+builder.ConfigureHostOptions(host => {
+    host.ServicesStartConcurrently = true;
+    host.ServicesStopConcurrently = true;
+    host.ShutdownTimeout = TimeSpan.FromSeconds(5);
+});
+
+if (Environment.GetEnvironmentVariable("MODERATIONBOT_APPSETTINGS_PATH") is string path)
+    builder.ConfigureAppConfiguration(x => x.AddJsonFile(path));
+
+var host = builder.ConfigureServices((_, services) => {
+    services.AddScoped<TieredStorageService>(x =>
+        new TieredStorageService(
+            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
+            dataStorageProvider: new FileStorageProvider("bot_data/data/")
+        )
+    );
+    services.AddSingleton<ModerationBotConfiguration>();
+
+    services.AddRoryLibMatrixServices();
+    services.AddBot(withCommands: true);
+
+    services.AddSingleton<PolicyEngine>();
+
+    services.AddHostedService<ModerationBot.ModerationBot>();
+}).UseConsoleLifetime().Build();
+
+await host.RunAsync();
\ No newline at end of file
diff --git a/ExampleBots/ModerationBot/StateEventTypes/Policies/BasePolicy.cs b/ExampleBots/ModerationBot/StateEventTypes/Policies/BasePolicy.cs
index 94b2f63..21b44b2 100644
--- a/ExampleBots/ModerationBot/StateEventTypes/Policies/BasePolicy.cs
+++ b/ExampleBots/ModerationBot/StateEventTypes/Policies/BasePolicy.cs
@@ -41,12 +41,12 @@ public abstract class BasePolicy : EventContent {
         set => Expiry = value is null ? null : ((DateTimeOffset)value).ToUnixTimeMilliseconds();
     }
 
-#region Internal metadata
+    #region Internal metadata
 
     [JsonIgnore]
     public PolicyList PolicyList { get; set; }
 
     public StateEventResponse OriginalEvent { get; set; }
 
-#endregion
+    #endregion
 }
diff --git a/ExampleBots/PluralContactBotPoC/PluralContactBotPoC.csproj b/ExampleBots/PluralContactBotPoC/PluralContactBotPoC.csproj
index 664ff49..0cd647c 100644
--- a/ExampleBots/PluralContactBotPoC/PluralContactBotPoC.csproj
+++ b/ExampleBots/PluralContactBotPoC/PluralContactBotPoC.csproj
@@ -17,7 +17,6 @@
     </PropertyGroup>
 
     <ItemGroup>
-        <!--      <ProjectReference Include="..\..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj" />-->
         <ProjectReference Include="..\..\LibMatrix\LibMatrix.csproj" />
         <ProjectReference Include="..\..\Utilities\LibMatrix.Utilities.Bot\LibMatrix.Utilities.Bot.csproj" />
     </ItemGroup>
diff --git a/ExampleBots/PluralContactBotPoC/Program.cs b/ExampleBots/PluralContactBotPoC/Program.cs
index 49c6c68..f8d93c6 100644
--- a/ExampleBots/PluralContactBotPoC/Program.cs
+++ b/ExampleBots/PluralContactBotPoC/Program.cs
@@ -1,74 +1,74 @@
-// See https://aka.ms/new-console-template for more information

-

-using System.Text.Json;

-using System.Text.Json.Serialization;

-using ArcaneLibs.Extensions;

-using LibMatrix.Services;

-using LibMatrix.Utilities.Bot;

-using Microsoft.Extensions.DependencyInjection;

-using Microsoft.Extensions.Hosting;

-using PluralContactBotPoC;

-using PluralContactBotPoC.Bot;

-

-Console.WriteLine("Hello, World!");

-

-var randomBytes = new byte[32];

-Random.Shared.NextBytes(randomBytes);

-var ASToken = Convert.ToBase64String(randomBytes);

-Random.Shared.NextBytes(randomBytes);

-var HSToken = Convert.ToBase64String(randomBytes);

-

-var asConfig = new AppServiceConfiguration() {

-    Id = "plural_contact_bot",

-    Url = null,

-    SenderLocalpart = "plural_contact_bot",

-    AppserviceToken = ASToken,

-    HomeserverToken = HSToken,

-    Namespaces = new() {

-        Users = new() {

-            new() {

-                Exclusive = false,

-                Regex = "@.*"

-            }

-        },

-        Aliases = new() {

-            new() {

-                Exclusive = false,

-                Regex = "#.*"

-            }

-        },

-        Rooms = new() {

-            new() {

-                Exclusive = false,

-                Regex = "!.*"

-            }

-        }

-    },

-    RateLimited = false,

-    Protocols = new List<string>() { "matrix" }

-};

-

-if(File.Exists("appservice.json"))

-    asConfig = JsonSerializer.Deserialize<AppServiceConfiguration>(File.ReadAllText("appservice.json"))!;

-

-File.WriteAllText("appservice.yaml", asConfig.ToYaml());

-File.WriteAllText("appservice.json", asConfig.ToJson());

-Environment.Exit(0);

-

-var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {

-    services.AddScoped<TieredStorageService>(x =>

-        new TieredStorageService(

-            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),

-            dataStorageProvider: new FileStorageProvider("bot_data/data/")

-        )

-    );

-    services.AddSingleton<PluralContactBotConfiguration>();

-    services.AddSingleton<AppServiceConfiguration>();

-

-    services.AddRoryLibMatrixServices();

-    services.AddBot(withCommands: true);

-

-    services.AddHostedService<PluralContactBot>();

-}).UseConsoleLifetime().Build();

-

-await host.RunAsync();

+// See https://aka.ms/new-console-template for more information
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using PluralContactBotPoC;
+using PluralContactBotPoC.Bot;
+
+Console.WriteLine("Hello, World!");
+
+var randomBytes = new byte[32];
+Random.Shared.NextBytes(randomBytes);
+var ASToken = Convert.ToBase64String(randomBytes);
+Random.Shared.NextBytes(randomBytes);
+var HSToken = Convert.ToBase64String(randomBytes);
+
+var asConfig = new AppServiceConfiguration() {
+    Id = "plural_contact_bot",
+    Url = null,
+    SenderLocalpart = "plural_contact_bot",
+    AppserviceToken = ASToken,
+    HomeserverToken = HSToken,
+    Namespaces = new() {
+        Users = new() {
+            new() {
+                Exclusive = false,
+                Regex = "@.*"
+            }
+        },
+        Aliases = new() {
+            new() {
+                Exclusive = false,
+                Regex = "#.*"
+            }
+        },
+        Rooms = new() {
+            new() {
+                Exclusive = false,
+                Regex = "!.*"
+            }
+        }
+    },
+    RateLimited = false,
+    Protocols = new List<string>() { "matrix" }
+};
+
+if (File.Exists("appservice.json"))
+    asConfig = JsonSerializer.Deserialize<AppServiceConfiguration>(File.ReadAllText("appservice.json"))!;
+
+File.WriteAllText("appservice.yaml", asConfig.ToYaml());
+File.WriteAllText("appservice.json", asConfig.ToJson());
+Environment.Exit(0);
+
+var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {
+    services.AddScoped<TieredStorageService>(x =>
+        new TieredStorageService(
+            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
+            dataStorageProvider: new FileStorageProvider("bot_data/data/")
+        )
+    );
+    services.AddSingleton<PluralContactBotConfiguration>();
+    services.AddSingleton<AppServiceConfiguration>();
+
+    services.AddRoryLibMatrixServices();
+    services.AddBot(withCommands: true);
+
+    services.AddHostedService<PluralContactBot>();
+}).UseConsoleLifetime().Build();
+
+await host.RunAsync();
diff --git a/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs
index 558e4fc..8ffbca5 100644
--- a/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/Ephemeral/PresenceStateEventContent.cs
@@ -3,8 +3,10 @@ using LibMatrix.Interfaces;
 
 namespace LibMatrix.EventTypes.Spec.State;
 
-[MatrixEvent(EventName = "m.presence")]
+[MatrixEvent(EventName = EventId)]
 public class PresenceEventContent : EventContent {
+    public const string EventId = "m.presence";
+
     [JsonPropertyName("presence"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
     public string? Presence { get; set; }
     [JsonPropertyName("last_active_ago")]
diff --git a/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
index 661cf63..b947096 100644
--- a/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/Ephemeral/RoomTypingEventContent.cs
@@ -3,8 +3,10 @@ using LibMatrix.Interfaces;
 
 namespace LibMatrix.EventTypes.Spec.State;
 
-[MatrixEvent(EventName = "m.typing")]
+[MatrixEvent(EventName = EventId)]
 public class RoomTypingEventContent : TimelineEventContent {
+    public const string EventId = "m.typing";
+
     [JsonPropertyName("user_ids")]
     public string[]? UserIds { get; set; }
 }
diff --git a/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs
index 8a22489..944ed99 100644
--- a/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/RoomMessageEventContent.cs
@@ -3,8 +3,10 @@ using LibMatrix.Interfaces;
 
 namespace LibMatrix.EventTypes.Spec;
 
-[MatrixEvent(EventName = "m.room.message")]
+[MatrixEvent(EventName = EventId)]
 public class RoomMessageEventContent : TimelineEventContent {
+    public const string EventId = "m.room.message";
+
     public RoomMessageEventContent(string? messageType = "m.notice", string? body = null) {
         MessageType = messageType;
         Body = body;
@@ -27,9 +29,9 @@ public class RoomMessageEventContent : TimelineEventContent {
     /// </summary>
     [JsonPropertyName("url")]
     public string? Url { get; set; }
-    
+
     public string? FileName { get; set; }
-    
+
     [JsonPropertyName("info")]
     public FileInfoStruct? FileInfo { get; set; }
 
@@ -41,5 +43,5 @@ public class RoomMessageEventContent : TimelineEventContent {
         [JsonPropertyName("thumbnail_url")]
         public string? ThumbnailUrl { get; set; }
     }
-    
+
 }
diff --git a/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
index 757a9e9..80d87d6 100644
--- a/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/Policy/PolicyRuleStateEventContent.cs
@@ -4,27 +4,34 @@ using LibMatrix.Interfaces;
 namespace LibMatrix.EventTypes.Spec.State;
 
 //spec
-[MatrixEvent(EventName = "m.policy.rule.server")] //spec
+[MatrixEvent(EventName = EventId)] //spec
 [MatrixEvent(EventName = "m.room.rule.server")] //???
 [MatrixEvent(EventName = "org.matrix.mjolnir.rule.server")] //legacy
-public class ServerPolicyRuleEventContent : PolicyRuleEventContent { }
+public class ServerPolicyRuleEventContent : PolicyRuleEventContent {
+    public const string EventId = "m.policy.rule.server";
+}
 
-[MatrixEvent(EventName = "m.policy.rule.user")] //spec
+[MatrixEvent(EventName = EventId)] //spec
 [MatrixEvent(EventName = "m.room.rule.user")] //???
 [MatrixEvent(EventName = "org.matrix.mjolnir.rule.user")] //legacy
-public class UserPolicyRuleEventContent : PolicyRuleEventContent { }
+public class UserPolicyRuleEventContent : PolicyRuleEventContent {
+    public const string EventId = "m.policy.rule.user";
+}
 
-[MatrixEvent(EventName = "m.policy.rule.room")] //spec
+[MatrixEvent(EventName = EventId)] //spec
 [MatrixEvent(EventName = "m.room.rule.room")] //???
 [MatrixEvent(EventName = "org.matrix.mjolnir.rule.room")] //legacy
-public class RoomPolicyRuleEventContent : PolicyRuleEventContent { }
+public class RoomPolicyRuleEventContent : PolicyRuleEventContent {
+    public const string EventId = "m.policy.rule.room";
+}
 
 public abstract class PolicyRuleEventContent : EventContent {
     /// <summary>
     ///     Entity this ban applies to, can use * and ? as globs.
+    ///     Policy is invalid if entity is null
     /// </summary>
     [JsonPropertyName("entity")]
-    public string Entity { get; set; }
+    public string? Entity { get; set; }
 
     /// <summary>
     ///     Reason this user is banned
@@ -65,4 +72,4 @@ public static class PolicyRecommendationTypes {
     ///     Mute this user
     /// </summary>
     public static string Mute = "support.feline.policy.recommendation_mute"; //stable prefix: m.mute, msc pending
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs
index 28d525c..830386d 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAliasEventContent.cs
@@ -3,8 +3,10 @@ using LibMatrix.Interfaces;
 
 namespace LibMatrix.EventTypes.Spec.State;
 
-[MatrixEvent(EventName = "m.room.alias")]
+[MatrixEvent(EventName = EventId)]
 public class RoomAliasEventContent : TimelineEventContent {
+    public const string EventId = "m.room.alias";
+
     [JsonPropertyName("aliases")]
     public List<string>? Aliases { get; set; }
 }
diff --git a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs
index fb05b2a..9c208ba 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomAvatarEventContent.cs
@@ -6,6 +6,7 @@ namespace LibMatrix.EventTypes.Spec.State;
 [MatrixEvent(EventName = EventId)]
 public class RoomAvatarEventContent : TimelineEventContent {
     public const string EventId = "m.room.avatar";
+
     [JsonPropertyName("url")]
     public string? Url { get; set; }
 
diff --git a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
index a5dec35..5ba253c 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomCanonicalAliasEventContent.cs
@@ -3,10 +3,13 @@ using LibMatrix.Interfaces;
 
 namespace LibMatrix.EventTypes.Spec.State;
 
-[MatrixEvent(EventName = "m.room.canonical_alias")]
+[MatrixEvent(EventName = EventId)]
 public class RoomCanonicalAliasEventContent : TimelineEventContent {
+    public const string EventId = "m.room.canonical_alias";
+
     [JsonPropertyName("alias")]
     public string? Alias { get; set; }
+
     [JsonPropertyName("alt_aliases")]
     public string[]? AltAliases { get; set; }
 }
diff --git a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
index 3eacd44..9ad67eb 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomNameEventContent.cs
@@ -9,4 +9,4 @@ public class RoomNameEventContent : TimelineEventContent {
 
     [JsonPropertyName("name")]
     public string? Name { get; set; }
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
index 6d01b8c..08f8ad5 100644
--- a/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
+++ b/LibMatrix/EventTypes/Spec/State/RoomInfo/RoomPowerLevelEventContent.cs
@@ -13,9 +13,6 @@ public class RoomPowerLevelEventContent : TimelineEventContent {
     [JsonPropertyName("events_default")]
     public long? EventsDefault { get; set; } = 0;
 
-    [JsonPropertyName("events")]
-    public Dictionary<string, long>? Events { get; set; } // = null!;
-
     [JsonPropertyName("invite")]
     public long? Invite { get; set; } = 0;
 
@@ -31,6 +28,9 @@ public class RoomPowerLevelEventContent : TimelineEventContent {
     [JsonPropertyName("state_default")]
     public long? StateDefault { get; set; } = 50;
 
+    [JsonPropertyName("events")]
+    public Dictionary<string, long>? Events { get; set; } // = null!;
+
     [JsonPropertyName("users")]
     public Dictionary<string, long>? Users { get; set; } // = null!;
 
@@ -48,17 +48,22 @@ public class RoomPowerLevelEventContent : TimelineEventContent {
     }
 
     public bool IsUserAdmin(string userId) {
-        if(userId is null) throw new ArgumentNullException(nameof(userId));
+        if (userId is null) throw new ArgumentNullException(nameof(userId));
         return Users.TryGetValue(userId, out var level) && level >= Events.Max(x => x.Value);
     }
 
-    public bool UserHasPermission(string userId, string eventType) {
-        if(userId is null) throw new ArgumentNullException(nameof(userId));
+    public bool UserHasTimelinePermission(string userId, string eventType) {
+        if (userId is null) throw new ArgumentNullException(nameof(userId));
         return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, EventsDefault ?? 0);
     }
 
+    public bool UserHasStatePermission(string userId, string eventType) {
+        if (userId is null) throw new ArgumentNullException(nameof(userId));
+        return Users.TryGetValue(userId, out var level) && level >= Events.GetValueOrDefault(eventType, StateDefault ?? 50);
+    }
+
     public long GetUserPowerLevel(string userId) {
-        if(userId is null) throw new ArgumentNullException(nameof(userId));
+        if (userId is null) throw new ArgumentNullException(nameof(userId));
         return Users.TryGetValue(userId, out var level) ? level : UsersDefault ?? UsersDefault ?? 0;
     }
 
@@ -67,13 +72,8 @@ public class RoomPowerLevelEventContent : TimelineEventContent {
     }
 
     public void SetUserPowerLevel(string userId, long powerLevel) {
-        if(userId is null) throw new ArgumentNullException(nameof(userId));
+        if (userId is null) throw new ArgumentNullException(nameof(userId));
         Users ??= new();
-        if (Users.TryGetValue(userId, out var level)) {
-            Users[userId] = powerLevel;
-        }
-        else {
-            Users.Add(userId, powerLevel);
-        }
+        Users[userId] = powerLevel;
     }
 }
diff --git a/LibMatrix/Helpers/MessageFormatter.cs b/LibMatrix/Helpers/MessageFormatter.cs
index 3ebc9a2..03efeec 100644
--- a/LibMatrix/Helpers/MessageFormatter.cs
+++ b/LibMatrix/Helpers/MessageFormatter.cs
@@ -35,15 +35,15 @@ public static class MessageFormatter {
     public static string HtmlFormatMention(string id, string? displayName = null) {
         return $"<a href=\"https://matrix.to/#/{id}\">{displayName ?? id}</a>";
     }
-    
+
     public static string HtmlFormatMessageLink(string roomId, string eventId, string[]? servers = null, string? displayName = null) {
         if (servers is not { Length: > 0 }) servers = new[] { roomId.Split(':', 2)[1] };
         return $"<a href=\"https://matrix.to/#/{roomId}/{eventId}?via={string.Join("&via=", servers)}\">{displayName ?? eventId}</a>";
     }
 
-#region Extension functions
+    #region Extension functions
 
     public static RoomMessageEventContent ToMatrixMessage(this Exception e, string error) => FormatException(error, e);
 
-#endregion
+    #endregion
 }
diff --git a/LibMatrix/Helpers/SyncHelper.cs b/LibMatrix/Helpers/SyncHelper.cs
index a63b8bb..a05b915 100644
--- a/LibMatrix/Helpers/SyncHelper.cs
+++ b/LibMatrix/Helpers/SyncHelper.cs
@@ -125,4 +125,4 @@ public class SyncHelper(AuthenticatedHomeserverGeneric homeserver, ILogger? logg
     /// Event fired when an account data event is received
     /// </summary>
     public List<Func<StateEventResponse, Task>> AccountDataReceivedHandlers { get; } = new();
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/Helpers/SyncStateResolver.cs b/LibMatrix/Helpers/SyncStateResolver.cs
index 0070d60..3482be3 100644
--- a/LibMatrix/Helpers/SyncStateResolver.cs
+++ b/LibMatrix/Helpers/SyncStateResolver.cs
@@ -74,7 +74,7 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge
         return oldState;
     }
 
-#region Merge rooms
+    #region Merge rooms
 
     private SyncResponse.RoomsDataStructure MergeRoomsDataStructure(SyncResponse.RoomsDataStructure oldState, SyncResponse.RoomsDataStructure newState) {
         oldState.Join ??= new();
@@ -170,5 +170,5 @@ public class SyncStateResolver(AuthenticatedHomeserverGeneric homeserver, ILogge
         return oldData;
     }
 
-#endregion
+    #endregion
 }
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
index 288608d..e85ecd2 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverGeneric.cs
@@ -136,7 +136,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
         }
     }
 
-#region Utility Functions
+    #region Utility Functions
 
     public virtual async IAsyncEnumerable<GenericRoom> GetJoinedRoomsByType(string type) {
         var rooms = await GetJoinedRooms();
@@ -154,9 +154,9 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
         }
     }
 
-#endregion
+    #endregion
 
-#region Account Data
+    #region Account Data
 
     public virtual async Task<T> GetAccountDataAsync<T>(string key) {
         // var res = await _httpClient.GetAsync($"/_matrix/client/v3/user/{UserId}/account_data/{key}");
@@ -177,7 +177,7 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
         }
     }
 
-#endregion
+    #endregion
 
     public async Task UpdateProfileAsync(UserProfileResponse? newProfile, bool preserveCustomRoomProfile = true) {
         if (newProfile is null) return;
@@ -299,11 +299,11 @@ public class AuthenticatedHomeserverGeneric(string serverName, string accessToke
         return await res.Content.ReadFromJsonAsync<RoomIdResponse>() ?? throw new Exception("Failed to join room?");
     }
 
-#region Room Profile Utility
+    #region Room Profile Utility
 
     private async Task<KeyValuePair<string, RoomMemberEventContent>> GetOwnRoomProfileWithIdAsync(GenericRoom room) {
         return new KeyValuePair<string, RoomMemberEventContent>(room.RoomId, await room.GetStateAsync<RoomMemberEventContent>("m.room.member", WhoAmI.UserId!));
     }
 
-#endregion
-}
\ No newline at end of file
+    #endregion
+}
diff --git a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
index 15e5b65..28ff775 100644
--- a/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
+++ b/LibMatrix/Homeservers/AuthenticatedHomeserverSynapse.cs
@@ -52,29 +52,29 @@ public class AuthenticatedHomeserverSynapse : AuthenticatedHomeserverGeneric {
                             totalRooms--;
                             continue;
                         }
-                        if(!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) {
+                        if (!room.GuestAccess?.Contains(localFilter.GuestAccessContains) == true) {
                             totalRooms--;
                             continue;
                         }
-                        if(!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) {
+                        if (!room.HistoryVisibility?.Contains(localFilter.HistoryVisibilityContains) == true) {
                             totalRooms--;
                             continue;
                         }
 
-                        if(localFilter.CheckFederation && room.Federatable != localFilter.Federatable) {
+                        if (localFilter.CheckFederation && room.Federatable != localFilter.Federatable) {
                             totalRooms--;
                             continue;
                         }
-                        if(localFilter.CheckPublic && room.Public != localFilter.Public) {
+                        if (localFilter.CheckPublic && room.Public != localFilter.Public) {
                             totalRooms--;
                             continue;
                         }
 
-                        if(room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) {
+                        if (room.JoinedMembers < localFilter.JoinedMembersGreaterThan || room.JoinedMembers > localFilter.JoinedMembersLessThan) {
                             totalRooms--;
                             continue;
                         }
-                        if(room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) {
+                        if (room.JoinedLocalMembers < localFilter.JoinedLocalMembersGreaterThan || room.JoinedLocalMembers > localFilter.JoinedLocalMembersLessThan) {
                             totalRooms--;
                             continue;
                         }
diff --git a/LibMatrix/Homeservers/RemoteHomeServer.cs b/LibMatrix/Homeservers/RemoteHomeServer.cs
index 55a3a02..f8d61fd 100644
--- a/LibMatrix/Homeservers/RemoteHomeServer.cs
+++ b/LibMatrix/Homeservers/RemoteHomeServer.cs
@@ -68,7 +68,7 @@ public class RemoteHomeserver(string baseUrl) {
         return data;
     }
 
-#region Authentication
+    #region Authentication
 
     public async Task<LoginResponse> LoginAsync(string username, string password, string? deviceName = null) {
         var resp = await ClientHttpClient.PostAsJsonAsync("/_matrix/client/r0/login", new {
@@ -102,13 +102,13 @@ public class RemoteHomeserver(string baseUrl) {
         return data;
     }
 
-#endregion
+    #endregion
 
     public async Task<ServerVersionResponse> GetServerVersionAsync() {
         return await ServerHttpClient.GetFromJsonAsync<ServerVersionResponse>("/_matrix/federation/v1/version");
     }
-    
-    
+
+
     public string? ResolveMediaUri(string? mxcUri) {
         if (mxcUri is null) return null;
         if (mxcUri.StartsWith("https://")) return mxcUri;
@@ -120,11 +120,11 @@ public class ServerVersionResponse {
 
     [JsonPropertyName("server")]
     public ServerInfo Server { get; set; }
-    
+
     public class ServerInfo {
         [JsonPropertyName("name")]
         public string Name { get; set; }
-        
+
         [JsonPropertyName("version")]
         public string Version { get; set; }
     }
diff --git a/LibMatrix/Interfaces/EventContent.cs b/LibMatrix/Interfaces/EventContent.cs
index 1fb6974..76419a6 100644
--- a/LibMatrix/Interfaces/EventContent.cs
+++ b/LibMatrix/Interfaces/EventContent.cs
@@ -45,4 +45,4 @@ public abstract class TimelineEventContent : EventContent {
             public string RelType { get; set; }
         }
     }
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/LibMatrix.csproj b/LibMatrix/LibMatrix.csproj
index 690556f..afe06d7 100644
--- a/LibMatrix/LibMatrix.csproj
+++ b/LibMatrix/LibMatrix.csproj
@@ -16,13 +16,13 @@
     </ItemGroup>
 
     <ItemGroup>
-        <ProjectReference Condition="Exists('..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="..\..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/>
+        <ProjectReference Condition="Exists('..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj')" Include="..\ArcaneLibs\ArcaneLibs\ArcaneLibs.csproj"/>
         <!-- This is dangerous, but eases development since locking the version will drift out of sync without noticing,
                 which causes build errors due to missing functions.
                 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://git.rory.gay/matrix/MatrixRoomUtils.git
                 instead, since this will be locked by the MatrixRoomUtils 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*"/>
     </ItemGroup>
     
     <Target Name="ArcaneLibsNugetWarning" AfterTargets="AfterBuild">
diff --git a/LibMatrix/MatrixException.cs b/LibMatrix/MatrixException.cs
index f127abf..863c6d4 100644
--- a/LibMatrix/MatrixException.cs
+++ b/LibMatrix/MatrixException.cs
@@ -17,7 +17,7 @@ public class MatrixException : Exception {
     public int? RetryAfterMs { get; set; }
 
     public string RawContent { get; set; }
-    
+
     public string? GetAsJson() => new { ErrorCode, Error, SoftLogout, RetryAfterMs }.ToJson(ignoreNull: true);
 
 
diff --git a/LibMatrix/RoomTypes/GenericRoom.cs b/LibMatrix/RoomTypes/GenericRoom.cs
index d26b1f8..a64e167 100644
--- a/LibMatrix/RoomTypes/GenericRoom.cs
+++ b/LibMatrix/RoomTypes/GenericRoom.cs
@@ -185,7 +185,7 @@ public class GenericRoom {
         Console.WriteLine($"Members call iterated in {sw.GetElapsedAndRestart()}");
     }
 
-#region Utility shortcuts
+    #region Utility shortcuts
 
     public async Task<EventIdResponse?> SendMessageEventAsync(RoomMessageEventContent content) =>
         await SendTimelineEventAsync("m.room.message", content);
@@ -253,9 +253,9 @@ public class GenericRoom {
         await Task.WhenAll(tasks);
     }
 
-#endregion
+    #endregion
 
-#region Simple calls
+    #region Simple calls
 
     public async Task ForgetAsync() =>
         await _httpClient.PostAsync($"/_matrix/client/v3/rooms/{RoomId}/forget", null);
@@ -283,9 +283,9 @@ public class GenericRoom {
         await _httpClient.PostAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/invite", new UserIdAndReason(userId, reason));
     }
 
-#endregion
+    #endregion
 
-#region Events
+    #region Events
 
     public async Task<EventIdResponse?> SendStateEventAsync(string eventType, object content) =>
         await (await _httpClient.PutAsJsonAsync($"/_matrix/client/v3/rooms/{RoomId}/state/{eventType}", content))
@@ -346,9 +346,9 @@ public class GenericRoom {
             $"/_matrix/client/v3/rooms/{RoomId}/redact/{eventToRedact}/{Guid.NewGuid()}", data)).Content.ReadFromJsonAsync<EventIdResponse>())!;
     }
 
-#endregion
+    #endregion
 
-#region Utilities
+    #region Utilities
 
     public async Task<Dictionary<string, List<string>>> GetMembersByHomeserverAsync(bool joinedOnly = true) {
         if (Homeserver is AuthenticatedHomeserverMxApiExtended mxaeHomeserver)
@@ -366,11 +366,11 @@ public class GenericRoom {
         return roomHomeservers;
     }
 
-#endregion
+    #endregion
 
     public readonly SpaceRoom AsSpace;
 
-#region Disband room
+    #region Disband room
 
     public async Task DisbandRoomAsync() {
         var states = GetFullStateAsync();
@@ -397,10 +397,10 @@ public class GenericRoom {
         }
     }
 
-#endregion
+    #endregion
 }
 
 public class RoomIdResponse {
     [JsonPropertyName("room_id")]
     public string RoomId { get; set; } = null!;
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/Services/HomeserverProviderService.cs b/LibMatrix/Services/HomeserverProviderService.cs
index 5c8827c..a42077a 100644
--- a/LibMatrix/Services/HomeserverProviderService.cs
+++ b/LibMatrix/Services/HomeserverProviderService.cs
@@ -29,7 +29,7 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger
 
         var rhs = await RemoteHomeserver.Create(homeserver, proxy);
         var clientVersions = await rhs.GetClientVersionsAsync();
-        if(proxy is not null)
+        if (proxy is not null)
             Console.WriteLine($"Homeserver {homeserver} proxied via {proxy}...");
         Console.WriteLine($"{homeserver}: " + clientVersions.ToJson());
 
@@ -68,4 +68,4 @@ public class HomeserverProviderService(ILogger<HomeserverProviderService> logger
         var data = await resp.Content.ReadFromJsonAsync<LoginResponse>();
         return data!;
     }
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/Services/HomeserverResolverService.cs b/LibMatrix/Services/HomeserverResolverService.cs
index 556bf86..f9f92d6 100644
--- a/LibMatrix/Services/HomeserverResolverService.cs
+++ b/LibMatrix/Services/HomeserverResolverService.cs
@@ -78,4 +78,4 @@ public class HomeserverResolverService(ILogger<HomeserverResolverService>? logge
         public string? Client { get; set; }
         public string? Server { get; set; }
     }
-}
\ No newline at end of file
+}
diff --git a/LibMatrix/StateEvent.cs b/LibMatrix/StateEvent.cs
index 3e8c4b5..6d69820 100644
--- a/LibMatrix/StateEvent.cs
+++ b/LibMatrix/StateEvent.cs
@@ -39,7 +39,7 @@ public class StateEvent {
     [JsonIgnore]
     public EventContent TypedContent {
         get {
-            if(Type == "m.receipt") {
+            if (Type == "m.receipt") {
                 return null!;
             }
             try {
@@ -181,8 +181,7 @@ public class StateEventResponse : StateEvent {
 
 [JsonSourceGenerationOptions(WriteIndented = true)]
 [JsonSerializable(typeof(ChunkedStateEventResponse))]
-internal partial class ChunkedStateEventResponseSerializerContext : JsonSerializerContext
-{
+internal partial class ChunkedStateEventResponseSerializerContext : JsonSerializerContext {
 }
 
 public class EventList {
diff --git a/LibMatrix/WhoAmIResponse.cs b/LibMatrix/WhoAmIResponse.cs
index 3884b1d..4eb5f2e 100644
--- a/LibMatrix/WhoAmIResponse.cs
+++ b/LibMatrix/WhoAmIResponse.cs
@@ -10,4 +10,4 @@ public class WhoAmIResponse {
     public string? DeviceId { get; set; }
     [JsonPropertyName("is_guest")]
     public bool? IsGuest { get; set; }
-}
\ No newline at end of file
+}
diff --git a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
index f737363..056cd3c 100644
--- a/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
+++ b/Tests/LibMatrix.Tests/DataTests/WhoAmITests.cs
@@ -4,7 +4,7 @@ public static class WhoAmITests {
     public static void VerifyRequiredFields(this WhoAmIResponse obj, bool isAppservice = false) {
         Assert.NotNull(obj);
         Assert.NotNull(obj.UserId);
-        if(!isAppservice)
+        if (!isAppservice)
             Assert.NotNull(obj.DeviceId);
     }
 }
diff --git a/Tests/LibMatrix.Tests/GlobalUsings.cs b/Tests/LibMatrix.Tests/GlobalUsings.cs
index 8c927eb..c802f44 100644
--- a/Tests/LibMatrix.Tests/GlobalUsings.cs
+++ b/Tests/LibMatrix.Tests/GlobalUsings.cs
@@ -1 +1 @@
-global using Xunit;
\ No newline at end of file
+global using Xunit;
diff --git a/Tests/LibMatrix.Tests/Tests/RoomTests.cs b/Tests/LibMatrix.Tests/Tests/RoomTests.cs
index 65f1cca..3a11de9 100644
--- a/Tests/LibMatrix.Tests/Tests/RoomTests.cs
+++ b/Tests/LibMatrix.Tests/Tests/RoomTests.cs
@@ -109,7 +109,7 @@ public class RoomTests : TestBed<TestFixture> {
         var hs2 = await HomeserverAbstraction.GetRandomHomeserver();
         var room = await RoomAbstraction.GetTestRoom(hs);
         Assert.NotNull(room);
-        await room.InviteUserAsync(hs2.UserId,"Unit test!");
+        await room.InviteUserAsync(hs2.UserId, "Unit test!");
         await hs2.GetRoom(room.RoomId).JoinAsync();
         await room.KickAsync(hs2.UserId, "test");
         var banState = await room.GetStateAsync<RoomMemberEventContent>("m.room.member", hs2.UserId);
@@ -241,7 +241,7 @@ public class RoomTests : TestBed<TestFixture> {
         // var expectedCount = 1;
 
         var tasks = new List<Task>();
-        await foreach(var otherUser in otherUsers) {
+        await foreach (var otherUser in otherUsers) {
             tasks.Add(Task.Run(async () => {
                 await room.InviteUserAsync(otherUser.UserId);
                 await otherUser.GetRoom(room.RoomId).JoinAsync();
@@ -251,7 +251,7 @@ public class RoomTests : TestBed<TestFixture> {
 
         var states = room.GetMembersAsync(false);
         var count = 0;
-        await foreach(var state in states) {
+        await foreach (var state in states) {
             count++;
         }
         // Assert.Equal(++expectedCount, count);
diff --git a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
index e93de3d..8fb7443 100644
--- a/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
+++ b/Tests/LibMatrix.Tests/Tests/TestCleanup.cs
@@ -59,7 +59,7 @@ public class TestCleanup : TestBed<TestFixture> {
             sw.Restart();
             if (response.Rooms?.Leave is { Count: > 0 }) {
                 // foreach (var room in response.Rooms.Leave) {
-                    // await hs.GetRoom(room.Key).ForgetAsync();
+                // await hs.GetRoom(room.Key).ForgetAsync();
                 // }
                 var tasks = response.Rooms.Leave.Select(async room => {
                     await hs.GetRoom(room.Key).ForgetAsync();
diff --git a/Tests/TestDataGenerator/Program.cs b/Tests/TestDataGenerator/Program.cs
index 9bd091b..18ba61e 100644
--- a/Tests/TestDataGenerator/Program.cs
+++ b/Tests/TestDataGenerator/Program.cs
@@ -1,31 +1,31 @@
-// See https://aka.ms/new-console-template for more information

-

-using System.Text.Json;

-using System.Text.Json.Serialization;

-using ArcaneLibs.Extensions;

-using LibMatrix.Services;

-using LibMatrix.Utilities.Bot;

-using Microsoft.Extensions.DependencyInjection;

-using Microsoft.Extensions.Hosting;

-using PluralContactBotPoC;

-using PluralContactBotPoC.Bot;

-

-Console.WriteLine("Hello, World!");

-

-var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {

-    services.AddScoped<TieredStorageService>(x =>

-        new TieredStorageService(

-            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),

-            dataStorageProvider: new FileStorageProvider("bot_data/data/")

-        )

-    );

-    // services.AddSingleton<DataFetcherConfiguration>();

-    services.AddSingleton<AppServiceConfiguration>();

-

-    services.AddRoryLibMatrixServices();

-    services.AddBot(withCommands: false);

-

-    services.AddHostedService<DataFetcher>();

-}).UseConsoleLifetime().Build();

-

-await host.RunAsync();

+// See https://aka.ms/new-console-template for more information
+
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using ArcaneLibs.Extensions;
+using LibMatrix.Services;
+using LibMatrix.Utilities.Bot;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using PluralContactBotPoC;
+using PluralContactBotPoC.Bot;
+
+Console.WriteLine("Hello, World!");
+
+var host = Host.CreateDefaultBuilder(args).ConfigureServices((_, services) => {
+    services.AddScoped<TieredStorageService>(x =>
+        new TieredStorageService(
+            cacheStorageProvider: new FileStorageProvider("bot_data/cache/"),
+            dataStorageProvider: new FileStorageProvider("bot_data/data/")
+        )
+    );
+    // services.AddSingleton<DataFetcherConfiguration>();
+    services.AddSingleton<AppServiceConfiguration>();
+
+    services.AddRoryLibMatrixServices();
+    services.AddBot(withCommands: false);
+
+    services.AddHostedService<DataFetcher>();
+}).UseConsoleLifetime().Build();
+
+await host.RunAsync();
diff --git a/Utilities/LibMatrix.DebugDataValidationApi/Program.cs b/Utilities/LibMatrix.DebugDataValidationApi/Program.cs
index cf9dc55..2c324d8 100644
--- a/Utilities/LibMatrix.DebugDataValidationApi/Program.cs
+++ b/Utilities/LibMatrix.DebugDataValidationApi/Program.cs
@@ -6,8 +6,7 @@ builder.Services.AddControllers();
 // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
 builder.Services.AddEndpointsApiExplorer();
 builder.Services.AddSwaggerGen();
-builder.Services.AddCors(options =>
-{
+builder.Services.AddCors(options => {
     options.AddPolicy(
         "Open",
         policy => policy.AllowAnyOrigin().AllowAnyHeader());
diff --git a/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs
index 39b66e3..46032c7 100644
--- a/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/FileStorageProvider.cs
@@ -18,7 +18,7 @@ public class FileStorageProvider : IStorageProvider {
         new Logger<FileStorageProvider>(new LoggerFactory()).LogInformation("test");
         Console.WriteLine($"Initialised FileStorageProvider with path {targetPath}");
         TargetPath = targetPath;
-        if(!Directory.Exists(targetPath)) {
+        if (!Directory.Exists(targetPath)) {
             Directory.CreateDirectory(targetPath);
         }
     }
diff --git a/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs
index 7065683..82439e8 100644
--- a/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs
+++ b/Utilities/LibMatrix.Utilities.Bot/Interfaces/ICommand.cs
@@ -1,4 +1,4 @@
-namespace LibMatrix.Utilities.Bot.Interfaces; 
+namespace LibMatrix.Utilities.Bot.Interfaces;
 
 public interface ICommand {
     public string Name { get; }
@@ -7,6 +7,6 @@ public interface ICommand {
     public Task<bool> CanInvoke(CommandContext ctx) {
         return Task.FromResult(true);
     }
-    
+
     public Task Invoke(CommandContext ctx);
-}
\ No newline at end of file
+}