about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--AccountData/BotData.cs2
-rw-r--r--Commands/BanMediaCommand.cs4
-rw-r--r--Commands/DbgAllRoomsArePolicyListsCommand.cs8
-rw-r--r--Commands/DbgDumpActivePoliciesCommand.cs4
-rw-r--r--Commands/DbgDumpAllStateTypesCommand.cs6
-rw-r--r--Commands/JoinRoomCommand.cs8
-rw-r--r--Commands/JoinSpaceMembersCommand.cs6
-rw-r--r--FirstRunTasks.cs2
-rw-r--r--ModerationBot.cs257
-rw-r--r--ModerationBot.csproj1
-rw-r--r--PolicyEngine.cs34
-rw-r--r--Program.cs68
-rw-r--r--StateEventTypes/Policies/BasePolicy.cs4
13 files changed, 208 insertions, 196 deletions
diff --git a/AccountData/BotData.cs b/AccountData/BotData.cs
index df86589..ab680c2 100644
--- a/AccountData/BotData.cs
+++ b/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/Commands/BanMediaCommand.cs b/Commands/BanMediaCommand.cs
index 21e0a94..9e49b22 100644
--- a/Commands/BanMediaCommand.cs
+++ b/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/Commands/DbgAllRoomsArePolicyListsCommand.cs b/Commands/DbgAllRoomsArePolicyListsCommand.cs
index 09d3caf..327a9a4 100644
--- a/Commands/DbgAllRoomsArePolicyListsCommand.cs
+++ b/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/Commands/DbgDumpActivePoliciesCommand.cs b/Commands/DbgDumpActivePoliciesCommand.cs
index 395c87c..35c95f8 100644
--- a/Commands/DbgDumpActivePoliciesCommand.cs
+++ b/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/Commands/DbgDumpAllStateTypesCommand.cs b/Commands/DbgDumpAllStateTypesCommand.cs
index e9a645e..0013065 100644
--- a/Commands/DbgDumpAllStateTypesCommand.cs
+++ b/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/Commands/JoinRoomCommand.cs b/Commands/JoinRoomCommand.cs
index 19a2c54..7496a07 100644
--- a/Commands/JoinRoomCommand.cs
+++ b/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/Commands/JoinSpaceMembersCommand.cs b/Commands/JoinSpaceMembersCommand.cs
index c3b7d12..6e64f6f 100644
--- a/Commands/JoinSpaceMembersCommand.cs
+++ b/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/FirstRunTasks.cs b/FirstRunTasks.cs
index ebbdc81..83356bf 100644
--- a/FirstRunTasks.cs
+++ b/FirstRunTasks.cs
@@ -81,4 +81,4 @@ public class FirstRunTasks {
 
         return botdata;
     }
-}
\ No newline at end of file
+}
diff --git a/ModerationBot.cs b/ModerationBot.cs
index 79b05bf..8a48b61 100644
--- a/ModerationBot.cs
+++ b/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/ModerationBot.csproj b/ModerationBot.csproj
index 5c8f8ff..99eb0b9 100644
--- a/ModerationBot.csproj
+++ b/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/PolicyEngine.cs b/PolicyEngine.cs
index 5311637..8bfa448 100644
--- a/PolicyEngine.cs
+++ b/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/Program.cs b/Program.cs
index b41b0be..d125258 100644
--- a/Program.cs
+++ b/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/StateEventTypes/Policies/BasePolicy.cs b/StateEventTypes/Policies/BasePolicy.cs
index 94b2f63..21b44b2 100644
--- a/StateEventTypes/Policies/BasePolicy.cs
+++ b/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
 }