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 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 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 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 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 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"); 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, ServerConfigLogClean, ServerConfigInvalidCommandResponse, ServerConfigRequireSystemTag,
ServerConfigSuppressNotifications, ServerConfigSuppressNotifications,
LogChannel, LogChannelClear, LogShow, LogDisable, LogEnable, LogChannel, LogChannelClear, LogShow, LogDisable, LogEnable,
ProxyBlacklistShow, ProxyBlacklistAdd, ProxyBlacklistRemove ProxyBlacklistShow, ProxyBlacklistAdd, ProxyBlacklistRemove,
CommandBlacklistShow, CommandBlacklistAdd, CommandBlacklistRemove
}; };
public static Command[] AutoproxyCommands = public static Command[] AutoproxyCommands =

View file

@ -642,6 +642,15 @@ public partial class CommandTree
else else
return ctx.Execute<ServerConfig>(null, m => m.ShowProxyBlacklisted(ctx)); 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? // 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."); 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") 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>( await ctx.Paginate<PaginatedConfigItem>(
items.ToAsyncEnumerable(), items.ToAsyncEnumerable(),
items.Count, items.Count,
@ -210,7 +217,57 @@ public class ServerConfig
} }
await ctx.Paginate(channels.ToAsyncEnumerable(), channels.Count, 25, 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, null,
async (eb, l) => async (eb, l) =>
{ {
@ -323,7 +380,40 @@ public class ServerConfig
await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { ProxyBlacklist = blacklist.ToArray() }); await ctx.Repository.UpdateGuild(ctx.Guild.Id, new GuildPatch { ProxyBlacklist = blacklist.ToArray() });
await ctx.Reply( 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) 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 config = system != null ? await _repo.GetSystemConfig(system.Id) : null;
var guildConfig = guild != null ? await _repo.GetGuild(guild.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) catch (PKError)
{ {

View file

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

View file

@ -9,7 +9,7 @@ namespace PluralKit.Core;
internal class DatabaseMigrator internal class DatabaseMigrator
{ {
private const string RootPath = "PluralKit.Core.Database"; // "resource path" root for SQL files 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; private readonly ILogger _logger;
public DatabaseMigrator(ILogger logger) public DatabaseMigrator(ILogger logger)

View file

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

View file

@ -7,6 +7,7 @@ public class GuildPatch: PatchObject
public Partial<ulong?> LogChannel { get; set; } public Partial<ulong?> LogChannel { get; set; }
public Partial<ulong[]> LogBlacklist { get; set; } public Partial<ulong[]> LogBlacklist { get; set; }
public Partial<ulong[]> ProxyBlacklist { get; set; } public Partial<ulong[]> ProxyBlacklist { get; set; }
public Partial<ulong[]> CommandBlacklist { get; set; }
public Partial<bool> LogCleanupEnabled { get; set; } public Partial<bool> LogCleanupEnabled { get; set; }
public Partial<bool> InvalidCommandResponseEnabled { get; set; } public Partial<bool> InvalidCommandResponseEnabled { get; set; }
public Partial<bool> RequireSystemTag { get; set; } public Partial<bool> RequireSystemTag { get; set; }
@ -16,6 +17,7 @@ public class GuildPatch: PatchObject
.With("log_channel", LogChannel) .With("log_channel", LogChannel)
.With("log_blacklist", LogBlacklist) .With("log_blacklist", LogBlacklist)
.With("proxy_blacklist", ProxyBlacklist) .With("proxy_blacklist", ProxyBlacklist)
.With("command_blacklist", CommandBlacklist)
.With("log_cleanup_enabled", LogCleanupEnabled) .With("log_cleanup_enabled", LogCleanupEnabled)
.With("invalid_command_response_enabled", InvalidCommandResponseEnabled) .With("invalid_command_response_enabled", InvalidCommandResponseEnabled)
.With("require_system_tag", RequireSystemTag) .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, log_channel bigint,
in_proxy_blacklist bool, in_proxy_blacklist bool,
in_command_blacklist bool,
in_log_blacklist bool, in_log_blacklist bool,
log_cleanup_enabled bool, log_cleanup_enabled bool,
require_system_tag bool, require_system_tag bool,
@ -61,9 +62,11 @@ as $$
system_last_switch.timestamp as last_switch_timestamp, system_last_switch.timestamp as last_switch_timestamp,
-- servers table -- servers table
servers.log_channel as log_channel, servers.log_channel as log_channel,
((channel_id = any (servers.proxy_blacklist)) ((channel_id = any (servers.proxy_blacklist))
or (thread_id = any (servers.proxy_blacklist))) as in_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)) ((channel_id = any (servers.log_blacklist))
or (thread_id = any (servers.log_blacklist))) as in_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.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` - 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 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 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 ## Utility
- `pk;message <message id|message link|reply>` - Looks up information about a proxied message by its message ID or link. - `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: 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 pk;serverconfig proxy blacklist add #admin-channel #mod-channel #welcome
@ -6,4 +9,14 @@ It's possible to block a channel from being used for proxying. To do so, use the
pk;serverconfig proxy blacklist remove #general-two pk;serverconfig proxy blacklist remove #general-two
pk;serverconfig proxy blacklist remove all pk;serverconfig proxy blacklist remove all
This requires you to have the *Manage Server* permission on the server. 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.