From 0473bd8f01ebc0f4ffd0cbab0f9b54580e09edee Mon Sep 17 00:00:00 2001 From: Iris System Date: Sun, 10 Nov 2024 15:46:36 +1300 Subject: [PATCH] feat(bot): add new guild settings command --- PluralKit.Bot/CommandMeta/CommandHelp.cs | 9 +- PluralKit.Bot/CommandMeta/CommandTree.cs | 29 ++++- .../CommandSystem/Context/Context.cs | 5 +- PluralKit.Bot/Commands/ServerConfig.cs | 106 ++++++++++++++++-- PluralKit.Bot/Handlers/MessageCreated.cs | 4 +- PluralKit.Bot/Proxy/ProxyService.cs | 9 ++ .../Database/Functions/MessageContext.cs | 1 + .../Database/Functions/functions.sql | 2 + PluralKit.Core/Database/Migrations/48.sql | 9 ++ .../Database/Utils/DatabaseMigrator.cs | 2 +- PluralKit.Core/Models/GuildConfig.cs | 2 + PluralKit.Core/Models/Patch/GuildPatch.cs | 4 + docs/content/command-list.md | 4 +- docs/content/staff/compatibility.md | 2 +- docs/content/staff/moderation.md | 6 +- 15 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 PluralKit.Core/Database/Migrations/48.sql diff --git a/PluralKit.Bot/CommandMeta/CommandHelp.cs b/PluralKit.Bot/CommandMeta/CommandHelp.cs index 3f958725..62a5092b 100644 --- a/PluralKit.Bot/CommandMeta/CommandHelp.cs +++ b/PluralKit.Bot/CommandMeta/CommandHelp.cs @@ -105,10 +105,12 @@ public partial class CommandTree public static Command LogEnable = new Command("log enable", "log enable all| [channel 2] [channel 3...]", "Enables message logging in certain channels"); public static Command LogDisable = new Command("log disable", "log disable all| [channel 2] [channel 3...]", "Disables message logging in certain channels"); public static Command LogShow = new Command("log show", "log show", "Displays the current list of channels where logging is disabled"); - public static Command LogClean = new Command("logclean", "logclean [on|off]", "Toggles whether to clean up other bots' log channels"); public static Command BlacklistShow = new Command("blacklist show", "blacklist show", "Displays the current proxy blacklist"); public static Command BlacklistAdd = new Command("blacklist add", "blacklist add all| [channel 2] [channel 3...]", "Adds certain channels to the proxy blacklist"); public static Command BlacklistRemove = new Command("blacklist remove", "blacklist remove all| [channel 2] [channel 3...]", "Removes certain channels from the proxy blacklist"); + public static Command ServerConfigLogClean = new Command("serverconfig logclean", "serverconfig logclean [on|off]", "Toggles whether to clean up other bots' log channels"); + public static Command ServerConfigInvalidCommandResponse = new Command("serverconfig invalidcommanderror", "serverconfig invalidcommanderror [on|off]", "Sets whether to show an error message when an unknown command is sent"); + public static Command ServerConfigRequireSystemTag = new Command("serverconfig requiretag", "serverconfig requiretag [on|off]", "Sets whether server users are required to have a system tag on proxied messages"); public static Command Invite = new Command("invite", "invite", "Gets a link to invite PluralKit to other servers"); public static Command PermCheck = new Command("permcheck", "permcheck ", "Checks whether a server's permission setup is correct"); public static Command Admin = new Command("admin", "admin", "Super secret admin commands (sshhhh)"); @@ -151,6 +153,11 @@ public partial class CommandTree ConfigProxySwitch, ConfigNameFormat }; + public static Command[] ServerConfigCommands = + { + ServerConfigLogClean, ServerConfigInvalidCommandResponse, ServerConfigRequireSystemTag + }; + public static Command[] AutoproxyCommands = { AutoproxyOff, AutoproxyFront, AutoproxyLatch, AutoproxyMember diff --git a/PluralKit.Bot/CommandMeta/CommandTree.cs b/PluralKit.Bot/CommandMeta/CommandTree.cs index 9c1b3881..93223010 100644 --- a/PluralKit.Bot/CommandMeta/CommandTree.cs +++ b/PluralKit.Bot/CommandMeta/CommandTree.cs @@ -20,6 +20,8 @@ public partial class CommandTree return HandleAutoproxyCommand(ctx); if (ctx.Match("config", "cfg", "configure")) return HandleConfigCommand(ctx); + if (ctx.Match("serverconfig", "guildconfig", "scfg")) + return HandleServerConfigCommand(ctx); if (ctx.Match("list", "find", "members", "search", "query", "l", "f", "fd", "ls")) return ctx.Execute(SystemList, m => m.MemberList(ctx, ctx.System)); if (ctx.Match("link")) @@ -65,7 +67,7 @@ public partial class CommandTree return PrintCommandList(ctx, "message logging", LogCommands); else return PrintCommandExpectedError(ctx, LogCommands); if (ctx.Match("logclean")) - return ctx.Execute(LogClean, m => m.SetLogCleanup(ctx)); + return ctx.Execute(ServerConfigLogClean, m => m.SetLogCleanup(ctx), true); if (ctx.Match("blacklist", "bl")) if (ctx.Match("enable", "on", "add", "deny")) return ctx.Execute(BlacklistAdd, m => m.SetBlacklisted(ctx, true)); @@ -108,6 +110,10 @@ public partial class CommandTree if (ctx.Match("dashboard", "dash")) return ctx.Execute(Dashboard, m => m.Dashboard(ctx)); + // don't send an "invalid command" response if the guild has those turned off + if (ctx.GuildConfig != null && ctx.GuildConfig!.InvalidCommandResponseEnabled != true) + return Task.CompletedTask; + // remove compiler warning return ctx.Reply( $"{Emojis.Error} Unknown command {ctx.PeekArgument().AsCode()}. For a list of possible commands, see ."); @@ -534,6 +540,11 @@ public partial class CommandTree case "cfg": await PrintCommandList(ctx, "settings", ConfigCommands); break; + case "serverconfig": + case "guildconfig": + case "scfg": + await PrintCommandList(ctx, "server settings", ServerConfigCommands); + break; case "autoproxy": case "ap": await PrintCommandList(ctx, "autoproxy", AutoproxyCommands); @@ -604,4 +615,20 @@ public partial class CommandTree // todo: maybe add the list of configuration keys here? return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands config` for the list of possible config settings."); } + + private Task HandleServerConfigCommand(Context ctx) + { + if (!ctx.HasNext()) + return ctx.Execute(null, m => m.ShowConfig(ctx)); + + if (ctx.MatchMultiple(new[] { "log" }, new[] { "cleanup", "clean" }) || ctx.Match("logclean")) + return ctx.Execute(null, m => m.SetLogCleanup(ctx)); + if (ctx.MatchMultiple(new[] { "invalid", "unknown" }, new[] { "command" }, new[] { "error", "response" }) || ctx.Match("invalidcommanderror", "unknowncommanderror")) + return ctx.Execute(null, m => m.InvalidCommandResponse(ctx)); + if (ctx.MatchMultiple(new[] { "require", "enforce" }, new[] { "tag", "systemtag" }) || ctx.Match("requiretag", "enforcetag")) + return ctx.Execute(null, m => m.RequireSystemTag(ctx)); + + // todo: maybe add the list of configuration keys here? + return ctx.Reply($"{Emojis.Error} Could not find a setting with that name. Please see `pk;commands serverconfig` for the list of possible config settings."); + } } \ No newline at end of file diff --git a/PluralKit.Bot/CommandSystem/Context/Context.cs b/PluralKit.Bot/CommandSystem/Context/Context.cs index 4719e57a..80e7fe53 100644 --- a/PluralKit.Bot/CommandSystem/Context/Context.cs +++ b/PluralKit.Bot/CommandSystem/Context/Context.cs @@ -29,11 +29,13 @@ public class Context private Command? _currentCommand; public Context(ILifetimeScope provider, int shardId, Guild? guild, Channel channel, MessageCreateEvent message, - int commandParseOffset, PKSystem senderSystem, SystemConfig config) + int commandParseOffset, PKSystem senderSystem, SystemConfig config, + GuildConfig? guildConfig) { Message = (Message)message; ShardId = shardId; Guild = guild; + GuildConfig = guildConfig; Channel = channel; System = senderSystem; Config = config; @@ -59,6 +61,7 @@ public class Context public readonly Message Message; public readonly Guild Guild; + public readonly GuildConfig? GuildConfig; public readonly int ShardId; public readonly Cluster Cluster; diff --git a/PluralKit.Bot/Commands/ServerConfig.cs b/PluralKit.Bot/Commands/ServerConfig.cs index f411be77..da0a98f7 100644 --- a/PluralKit.Bot/Commands/ServerConfig.cs +++ b/PluralKit.Bot/Commands/ServerConfig.cs @@ -18,6 +18,70 @@ public class ServerConfig _cache = cache; } + private record PaginatedConfigItem(string Key, string Description, string? CurrentValue, string DefaultValue); + private string EnabledDisabled(bool value) => value ? "enabled" : "disabled"; + + public async Task ShowConfig(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + var items = new List(); + + // TODO: move log channel / blacklist into here + + items.Add(new( + "log cleanup", + "Whether to clean up other bots' log channels", + EnabledDisabled(ctx.GuildConfig!.LogCleanupEnabled), + "disabled" + )); + + items.Add(new( + "invalid command error", + "Whether to show an error message when an unknown command is sent", + EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled), + "enabled" + )); + + items.Add(new( + "require tag", + "Whether server users are required to have a system tag on proxied messages", + EnabledDisabled(ctx.GuildConfig!.RequireSystemTag), + "disabled" + )); + + await ctx.Paginate( + items.ToAsyncEnumerable(), + items.Count, + 10, + "Current settings for this server", + null, + (eb, l) => + { + var description = new StringBuilder(); + + foreach (var item in l) + { + description.Append(item.Key.AsCode()); + description.Append($" **({item.CurrentValue ?? item.DefaultValue})**"); + if (item.CurrentValue != null && item.CurrentValue != item.DefaultValue) + description.Append("\ud83d\udd39"); + + description.AppendLine(); + description.Append(item.Description); + description.AppendLine(); + description.AppendLine(); + } + + eb.Description(description.ToString()); + + // using *large* blue diamond here since it's easier to see in the small footer + eb.Footer(new("\U0001f537 means this setting was changed. Type `pk;serverconfig clear` to reset it to the default.")); + + return Task.CompletedTask; + } + ); + } + public async Task SetLogChannel(Context ctx) { await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); @@ -246,20 +310,16 @@ public class ServerConfig } await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); - - var guild = await ctx.Repository.GetGuild(ctx.Guild.Id); - bool? newValue = ctx.MatchToggleOrNull(); if (newValue == null) { - var guildCfg = await ctx.Repository.GetGuild(ctx.Guild.Id); - if (guildCfg.LogCleanupEnabled) + if (ctx.GuildConfig!.LogCleanupEnabled) eb.Description( - "Log cleanup is currently **on** for this server. To disable it, type `pk;logclean off`."); + "Log cleanup is currently **on** for this server. To disable it, type `pk;serverconfig logclean off`."); else eb.Description( - "Log cleanup is currently **off** for this server. To enable it, type `pk;logclean on`."); + "Log cleanup is currently **off** for this server. To enable it, type `pk;serverconfig logclean on`."); await ctx.Reply(embed: eb.Build()); return; } @@ -272,4 +332,36 @@ public class ServerConfig else await ctx.Reply($"{Emojis.Success} Log cleanup has been **disabled** for this server."); } + + public async Task InvalidCommandResponse(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + if (!ctx.HasNext()) + { + var msg = $"Error responses for unknown/invalid commands are currently **{EnabledDisabled(ctx.GuildConfig!.InvalidCommandResponseEnabled)}**."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { InvalidCommandResponseEnabled = newVal }); + await ctx.Reply($"Error responses for unknown/invalid commands are now {EnabledDisabled(newVal)}."); + } + + public async Task RequireSystemTag(Context ctx) + { + await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); + + if (!ctx.HasNext()) + { + var msg = $"System tags are currently **{(ctx.GuildConfig!.RequireSystemTag ? "required" : "not required")}** for PluralKit users in this server."; + await ctx.Reply(msg); + return; + } + + var newVal = ctx.MatchToggle(false); + await ctx.Repository.UpdateGuild(ctx.Guild.Id, new() { RequireSystemTag = newVal }); + await ctx.Reply($"System tags are now **{(newVal ? "required" : "not required")}** for PluralKit users in this server."); + } } \ No newline at end of file diff --git a/PluralKit.Bot/Handlers/MessageCreated.cs b/PluralKit.Bot/Handlers/MessageCreated.cs index 9a1ccde4..35edefa7 100644 --- a/PluralKit.Bot/Handlers/MessageCreated.cs +++ b/PluralKit.Bot/Handlers/MessageCreated.cs @@ -129,7 +129,9 @@ public class MessageCreated: IEventHandler { var system = await _repo.GetSystemByAccount(evt.Author.Id); var config = system != null ? await _repo.GetSystemConfig(system.Id) : null; - await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config)); + var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null; + + await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig)); } catch (PKError) { diff --git a/PluralKit.Bot/Proxy/ProxyService.cs b/PluralKit.Bot/Proxy/ProxyService.cs index 96a1ea93..f3635d5b 100644 --- a/PluralKit.Bot/Proxy/ProxyService.cs +++ b/PluralKit.Bot/Proxy/ProxyService.cs @@ -123,6 +123,15 @@ public class ProxyService return "PluralKit cannot proxy messages over 2000 characters in length."; } + if (ctx.RequireSystemTag) + { + var tag = ctx.SystemGuildTag ?? ctx.SystemTag; + if (tag == null) + return "This server requires PluralKit users to have a system tag, but you do not have one set."; + if (!ctx.TagEnabled) + return "This server requires PluralKit users to have a system tag, but your system tag is disabled in this server."; + } + var guild = await _cache.GetGuild(channel.GuildId.Value); var fileSizeLimit = guild.FileSizeLimit(); var bytesThreshold = fileSizeLimit * 1024 * 1024; diff --git a/PluralKit.Core/Database/Functions/MessageContext.cs b/PluralKit.Core/Database/Functions/MessageContext.cs index f3703bec..7f767b26 100644 --- a/PluralKit.Core/Database/Functions/MessageContext.cs +++ b/PluralKit.Core/Database/Functions/MessageContext.cs @@ -18,6 +18,7 @@ public class MessageContext public bool InBlacklist { get; } public bool InLogBlacklist { get; } public bool LogCleanupEnabled { get; } + public bool RequireSystemTag { get; } public bool ProxyEnabled { get; } public SwitchId? LastSwitch { get; } public MemberId[] LastSwitchMembers { get; } = new MemberId[0]; diff --git a/PluralKit.Core/Database/Functions/functions.sql b/PluralKit.Core/Database/Functions/functions.sql index f3da1c28..693dff5f 100644 --- a/PluralKit.Core/Database/Functions/functions.sql +++ b/PluralKit.Core/Database/Functions/functions.sql @@ -25,6 +25,7 @@ create function message_context(account_id bigint, guild_id bigint, channel_id b in_blacklist bool, in_log_blacklist bool, log_cleanup_enabled bool, + require_system_tag bool, deny_bot_usage bool ) @@ -63,6 +64,7 @@ as $$ ((channel_id = any (servers.log_blacklist)) or (thread_id = any (servers.log_blacklist))) as in_log_blacklist, coalesce(servers.log_cleanup_enabled, false) as log_cleanup_enabled, + coalesce(servers.require_system_tag, false) as require_system_tag, -- abuse_logs table coalesce(abuse_logs.deny_bot_usage, false) as deny_bot_usage diff --git a/PluralKit.Core/Database/Migrations/48.sql b/PluralKit.Core/Database/Migrations/48.sql new file mode 100644 index 00000000..088a53a4 --- /dev/null +++ b/PluralKit.Core/Database/Migrations/48.sql @@ -0,0 +1,9 @@ +-- database version 48 +-- +-- add guild settings for disabling "invalid command" responses & +-- enforcing the presence of system tags + +alter table servers add column invalid_command_response_enabled bool not null default true; +alter table servers add column require_system_tag bool not null default false; + +update info set schema_version = 48; \ No newline at end of file diff --git a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs index 85f9a293..0aea679a 100644 --- a/PluralKit.Core/Database/Utils/DatabaseMigrator.cs +++ b/PluralKit.Core/Database/Utils/DatabaseMigrator.cs @@ -9,7 +9,7 @@ namespace PluralKit.Core; internal class DatabaseMigrator { private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files - private const int TargetSchemaVersion = 47; + private const int TargetSchemaVersion = 48; private readonly ILogger _logger; public DatabaseMigrator(ILogger logger) diff --git a/PluralKit.Core/Models/GuildConfig.cs b/PluralKit.Core/Models/GuildConfig.cs index 20ed0639..7e6b7501 100644 --- a/PluralKit.Core/Models/GuildConfig.cs +++ b/PluralKit.Core/Models/GuildConfig.cs @@ -7,4 +7,6 @@ public class GuildConfig public ulong[] LogBlacklist { get; } public ulong[] Blacklist { get; } public bool LogCleanupEnabled { get; } + public bool InvalidCommandResponseEnabled { get; } + public bool RequireSystemTag { get; } } \ No newline at end of file diff --git a/PluralKit.Core/Models/Patch/GuildPatch.cs b/PluralKit.Core/Models/Patch/GuildPatch.cs index 7380ff5e..df146859 100644 --- a/PluralKit.Core/Models/Patch/GuildPatch.cs +++ b/PluralKit.Core/Models/Patch/GuildPatch.cs @@ -8,11 +8,15 @@ public class GuildPatch: PatchObject public Partial LogBlacklist { get; set; } public Partial Blacklist { get; set; } public Partial LogCleanupEnabled { get; set; } + public Partial InvalidCommandResponseEnabled { get; set; } + public Partial RequireSystemTag { get; set; } public override Query Apply(Query q) => q.ApplyPatch(wrapper => wrapper .With("log_channel", LogChannel) .With("log_blacklist", LogBlacklist) .With("blacklist", Blacklist) .With("log_cleanup_enabled", LogCleanupEnabled) + .With("invalid_command_response_enabled", InvalidCommandResponseEnabled) + .With("require_system_tag", RequireSystemTag) ); } \ No newline at end of file diff --git a/docs/content/command-list.md b/docs/content/command-list.md index afabba42..99b9b9b0 100644 --- a/docs/content/command-list.md +++ b/docs/content/command-list.md @@ -159,9 +159,11 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the - `pk;log disable <#channel> [#channel...]` - Disables logging messages posted in the given channel(s) (useful for staff channels and such). - `pk;log enable <#channel> [#channel...]` - Re-enables logging messages posted in the given channel(s). - `pk;log show` - Displays the current list of channels where logging is disabled. -- `pk;logclean ` - Enables or disables [log cleanup](/staff/compatibility/#log-cleanup). - `pk;blacklist add <#channel> [#channel...]` - Adds the given channel(s) to the proxy blacklist (proxying will be disabled here) - `pk;blacklist remove <#channel> [#channel...]` - Removes the given channel(s) from the proxy blacklist. +- `pk;serverconfig logclean [on|off]` - Enables or disables [log cleanup](/staff/compatibility/#log-cleanup). +- `pk;serverconfig invalidcommanderror [on|off]` - Sets whether to show an error message when an unknown command is sent +- `pk;serverconfig requiretag [on|off]` - Sets whether server users are required to have a system tag on proxied messages ## Utility - `pk;message ` - Looks up information about a proxied message by its message ID or link. diff --git a/docs/content/staff/compatibility.md b/docs/content/staff/compatibility.md index 8fd6f018..c40fb817 100644 --- a/docs/content/staff/compatibility.md +++ b/docs/content/staff/compatibility.md @@ -15,7 +15,7 @@ If your server uses an in-house bot for logging, you can use [the API](/api) to Another solution is for PluralKit to automatically delete log messages from other bots when they get posted. PluralKit supports this through the **log cleanup** feature. To enable it, use the following command: - pk;logclean on + pk;serverconfig logclean on This requires you to have the *Manage Server* permission on the server. diff --git a/docs/content/staff/moderation.md b/docs/content/staff/moderation.md index c4273bb2..835d70e3 100644 --- a/docs/content/staff/moderation.md +++ b/docs/content/staff/moderation.md @@ -28,7 +28,11 @@ You can also do the reverse operation by passing a Discord account ID (or a @men Both commands output a system card, which includes a linked account list. These commands also work in PluralKit's DMs. ### System tags -A common rule on servers with PluralKit is to enforce system tags. System tags are a little snippet of text, a symbol, an emoji, etc, that's added to the webhook name of every message proxied by a system. A system tag will allow you to identify members that share a system at a glance. Note that this isn't enforced by the bot; this is simply a suggestion for a helpful server policy :slightly_smiling_face: +A common rule on servers with PluralKit is to enforce system tags. System tags are a little snippet of text, a symbol, an emoji, etc, that's added to the webhook name of every message proxied by a system. A system tag will allow you to identify members that share a system at a glance. + +You can enforce system tags for all PluralKit users in your server with the following command: + + pk;serverconfig requiretag on ## Blocking users It's not possible to block specific PluralKit users. Discord webhooks don't count as 'real accounts', so there's no way to block them. PluralKit also can't control who gets to see a message, so there's also no way to implement user blocking on the bot's end. Sorry. :slightly_frowning_face: \ No newline at end of file