feat: add command blacklisting

This commit is contained in:
Petal Ladenson 2026-01-12 17:14:12 -07:00
parent 1cb554e9c5
commit a3aa02e779
12 changed files with 155 additions and 8 deletions

View file

@ -109,6 +109,9 @@ public partial class CommandTree
public static Command ProxyBlacklistShow = new Command("serverconfig proxy blacklist", "serverconfig proxy blacklist", "Displays the current list of channels where message proxying is disabled");
public static Command ProxyBlacklistAdd = new Command("serverconfig proxy blacklist add", "serverconfig proxy blacklist add all|<channel> [channel 2] [channel 3...]", "Disables message proxying in certain channels");
public static Command ProxyBlacklistRemove = new Command("serverconfig proxy blacklist remove", "serverconfig proxy blacklist remove all|<channel> [channel 2] [channel 3...]", "Enables message proxying in certain channels");
public static Command CommandBlacklistShow = new Command("serverconfig command blacklist", "serverconfig command blacklist", "Displays the current list of channels where running text commands is disabled");
public static Command CommandBlacklistAdd = new Command("serverconfig command blacklist add", "serverconfig command blacklist add all|<channel> [channel 2] [channel 3...]", "Disables running text commands in certain channels");
public static Command CommandBlacklistRemove = new Command("serverconfig command blacklist remove", "serverconfig command blacklist remove all|<channel> [channel 2] [channel 3...]", "Enables running text commands in certain channels");
public static Command ServerConfigLogClean = new Command("serverconfig log cleanup", "serverconfig log cleanup [on|off]", "Toggles whether to clean up other bots' log channels");
public static Command ServerConfigInvalidCommandResponse = new Command("serverconfig invalid command error", "serverconfig invalid command error [on|off]", "Sets whether to show an error message when an unknown command is sent");
public static Command ServerConfigRequireSystemTag = new Command("serverconfig require tag", "serverconfig require tag [on|off]", "Sets whether server users are required to have a system tag on proxied messages");
@ -160,7 +163,8 @@ public partial class CommandTree
ServerConfigLogClean, ServerConfigInvalidCommandResponse, ServerConfigRequireSystemTag,
ServerConfigSuppressNotifications,
LogChannel, LogChannelClear, LogShow, LogDisable, LogEnable,
ProxyBlacklistShow, ProxyBlacklistAdd, ProxyBlacklistRemove
ProxyBlacklistShow, ProxyBlacklistAdd, ProxyBlacklistRemove,
CommandBlacklistShow, CommandBlacklistAdd, CommandBlacklistRemove
};
public static Command[] AutoproxyCommands =

View file

@ -642,6 +642,15 @@ public partial class CommandTree
else
return ctx.Execute<ServerConfig>(null, m => m.ShowProxyBlacklisted(ctx));
}
if (ctx.MatchMultiple(new[] { "command" }, new[] { "blacklist" }))
{
if (ctx.Match("enable", "on", "add", "deny"))
return ctx.Execute<ServerConfig>(null, m => m.SetCommandBlacklisted(ctx, true));
else if (ctx.Match("disable", "off", "remove", "allow"))
return ctx.Execute<ServerConfig>(null, m => m.SetCommandBlacklisted(ctx, false));
else
return ctx.Execute<ServerConfig>(null, m => m.ShowCommandBlacklisted(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 `{ctx.DefaultPrefix}commands serverconfig` for the list of possible config settings.");

View file

@ -77,6 +77,13 @@ public class ServerConfig
ChannelListMessage(0, "proxy blacklist")
));
items.Add(new(
"command blacklist",
"Channels where running text commands is disabled",
ChannelListMessage(ctx.GuildConfig!.CommandBlacklist.Length, "command blacklist"),
ChannelListMessage(0, "command blacklist")
));
await ctx.Paginate<PaginatedConfigItem>(
items.ToAsyncEnumerable(),
items.Count,
@ -210,7 +217,57 @@ public class ServerConfig
}
await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25,
$"Blacklisted channels for {ctx.Guild.Name}",
$"Blacklisted channels for proxying in {ctx.Guild.Name}",
null,
async (eb, l) =>
{
async Task<string> CategoryName(ulong? id) =>
id != null ? (await _cache.GetChannel(ctx.Guild.Id, id.Value)).Name : "(no category)";
ulong? lastCategory = null;
var fieldValue = new StringBuilder();
foreach (var channel in l)
{
if (lastCategory != channel!.ParentId && fieldValue.Length > 0)
{
eb.Field(new Embed.Field(await CategoryName(lastCategory), fieldValue.ToString()));
fieldValue.Clear();
}
else
{
fieldValue.Append("\n");
}
fieldValue.Append(channel.Mention());
lastCategory = channel.ParentId;
}
eb.Field(new Embed.Field(await CategoryName(lastCategory), fieldValue.ToString()));
});
}
public async Task ShowCommandBlacklisted(Context ctx)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var blacklist = await ctx.Repository.GetGuild(ctx.Guild.Id);
// Resolve all channels from the cache and order by position
var channels = (await Task.WhenAll(blacklist.CommandBlacklist
.Select(id => _cache.TryGetChannel(ctx.Guild.Id, id))))
.Where(c => c != null)
.OrderBy(c => c.Position)
.ToList();
if (channels.Count == 0)
{
await ctx.Reply("This server has no channels where running text commands is disabled.");
return;
}
await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25,
$"Blacklisted channels for running text commands in {ctx.Guild.Name}",
null,
async (eb, l) =>
{
@ -323,7 +380,40 @@ public class ServerConfig
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { ProxyBlacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} Channels {(shouldAdd ? "added to" : "removed from")} the proxy blacklist.");
$"{Emojis.Success} Channel(s) {(shouldAdd ? "added to" : "removed from")} the proxy blacklist.");
}
public async Task SetCommandBlacklisted(Context ctx, bool shouldAdd)
{
await ctx.CheckGuildContext().CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server");
var affectedChannels = new List<Channel>();
if (ctx.Match("all"))
affectedChannels = (await _cache.GetGuildChannels(ctx.Guild.Id))
// All the channel types you can proxy in
.Where(x => DiscordUtils.IsValidGuildChannel(x)).ToList();
else if (!ctx.HasNext()) throw new PKSyntaxError("You must pass one or more #channels.");
else
while (ctx.HasNext())
{
var channelString = ctx.PeekArgument();
var channel = await ctx.MatchChannel();
if (channel == null || channel.GuildId != ctx.Guild.Id) throw Errors.ChannelNotFound(channelString);
affectedChannels.Add(channel);
}
var guild = await ctx.Repository.GetGuild(ctx.Guild.Id);
var blacklist = guild.CommandBlacklist.ToHashSet();
if (shouldAdd)
blacklist.UnionWith(affectedChannels.Select(c => c.Id));
else
blacklist.ExceptWith(affectedChannels.Select(c => c.Id));
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { CommandBlacklist = blacklist.ToArray() });
await ctx.Reply(
$"{Emojis.Success} Channel(s) {(shouldAdd ? "added to" : "removed from")} the command blacklist.");
}
public async Task SetLogBlacklisted(Context ctx, bool shouldAdd)

View file

@ -140,7 +140,22 @@ public class MessageCreated: IEventHandler<MessageCreateEvent>
var config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
var guildConfig = guild != null ? await _repo.GetGuild(guild.Id) : null;
await _tree.ExecuteCommand(new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes));
var ctx = new Context(_services, shardId, guild, channel, evt, cmdStart, system, config, guildConfig, _config.Prefixes ?? BotConfig.DefaultPrefixes);
// If we're in a guild we need to check if this channel is on the command blacklist
if (guild is not null)
{
// If we're in a thread we want to check the root channel and the thread
var rootChannel = channel.IsThread() ? await _rest.GetChannelOrNull(channel.ParentId!.Value) : channel;
var msgCtx = await _repo.GetMessageContext(evt.Author.Id, guild.Id, rootChannel.Id, channel.Id != rootChannel.Id ? channel.Id : default);
// If the channel is in the command blacklist, then check if author has Manage Server
// If they do, we let the command run regardless
// If they don't a PK Error gets thrown here and caught in the catch
if (msgCtx.InCommandBlacklist)
await ctx.CheckAuthorPermission(PermissionSet.ManageGuild, "Manage Server"); ;
}
await _tree.ExecuteCommand(ctx);
}
catch (PKError)
{

View file

@ -33,6 +33,7 @@ public class MessageContext
public ulong? LogChannel { get; }
public bool InProxyBlacklist { get; }
public bool InCommandBlacklist { get; }
public bool InLogBlacklist { get; }
public bool LogCleanupEnabled { get; }
public bool RequireSystemTag { get; }

View file

@ -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 = 54;
private const int TargetSchemaVersion = 55;
private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger)

View file

@ -6,6 +6,7 @@ public class GuildConfig
public ulong? LogChannel { get; }
public ulong[] LogBlacklist { get; }
public ulong[] ProxyBlacklist { get; }
public ulong[] CommandBlacklist { get; }
public bool LogCleanupEnabled { get; }
public bool InvalidCommandResponseEnabled { get; }
public bool RequireSystemTag { get; }

View file

@ -7,6 +7,7 @@ public class GuildPatch: PatchObject
public Partial<ulong?> LogChannel { get; set; }
public Partial<ulong[]> LogBlacklist { get; set; }
public Partial<ulong[]> ProxyBlacklist { get; set; }
public Partial<ulong[]> CommandBlacklist { get; set; }
public Partial<bool> LogCleanupEnabled { get; set; }
public Partial<bool> InvalidCommandResponseEnabled { get; set; }
public Partial<bool> RequireSystemTag { get; set; }
@ -16,6 +17,7 @@ public class GuildPatch: PatchObject
.With("log_channel", LogChannel)
.With("log_blacklist", LogBlacklist)
.With("proxy_blacklist", ProxyBlacklist)
.With("command_blacklist", CommandBlacklist)
.With("log_cleanup_enabled", LogCleanupEnabled)
.With("invalid_command_response_enabled", InvalidCommandResponseEnabled)
.With("require_system_tag", RequireSystemTag)

View file

@ -24,6 +24,7 @@ create function message_context(account_id bigint, guild_id bigint, channel_id b
log_channel bigint,
in_proxy_blacklist bool,
in_command_blacklist bool,
in_log_blacklist bool,
log_cleanup_enabled bool,
require_system_tag bool,
@ -61,9 +62,11 @@ as $$
system_last_switch.timestamp as last_switch_timestamp,
-- servers table
servers.log_channel as log_channel,
servers.log_channel as log_channel,
((channel_id = any (servers.proxy_blacklist))
or (thread_id = any (servers.proxy_blacklist))) as in_proxy_blacklist,
((channel_id = any (servers.command_blacklist))
or (thread_id = any (servers.command_blacklist))) as in_command_blacklist,
((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,

View file

@ -0,0 +1,6 @@
-- database version 55
-- add command blacklist option for servers
alter table servers add column command_blacklist bigint[] not null default array[]::bigint[];
update info set schema_version = 55;

View file

@ -167,6 +167,9 @@ You can have a space after `pk;`, e.g. `pk;system` and `pk; system` will do the
- `pk;serverconfig proxy blacklist` - Displays the current proxy blacklist
- `pk;serverconfig proxy blacklist add all|<channel> [channel 2] [channel 3...]` - Adds certain channels to the proxy blacklist
- `pk;serverconfig proxy blacklist remove all|<channel> [channel 2] [channel 3...]` - Removes certain channels from the proxy blacklist
- `pk;serverconfig command blacklist` - Displays the current command blacklist
- `pk;serverconfig command blacklist add all|<channel> [channel 2] [channel 3...]` - Adds certain channels to the command blacklist
- `pk;serverconfig command blacklist remove all|<channel> [channel 2] [channel 3...]` - Removes certain channels from the command blacklist
## Utility
- `pk;message <message id|message link|reply>` - Looks up information about a proxied message by its message ID or link.

View file

@ -1,4 +1,7 @@
# Disabling proxying in a channel
# Disabling bot functionality
You can use the blacklist commands to disable proxying or text commands in some channels of your server. [You can also disable PluralKit by taking away its permissions.](/staff/permissions)
## Disabling proxying in a channel
It's possible to block a channel from being used for proxying. To do so, use the `pk;serverconfig proxy blacklist` command. For example:
pk;serverconfig proxy blacklist add #admin-channel #mod-channel #welcome
@ -7,3 +10,13 @@ It's possible to block a channel from being used for proxying. To do so, use the
pk;serverconfig proxy blacklist remove all
This requires you to have the *Manage Server* permission on the server.
## Disabling commands in a channel
It's possible to block a channel from being used for text commands. To do so, use the `pk;serverconfig command blacklist` command. For example:
pk;serverconfig command blacklist add #admin-channel #mod-channel #welcome
pk;serverconfig command blacklist add all
pk;serverconfig command blacklist remove #general-two
pk;serverconfig command blacklist remove all
This requires you to have the *Manage Server* permission on the server. If you have the *Manage Server* permission on the server you **will not be affected by the command blacklist** and will always be able to run commands.