Add global command channel rules and enhance command handling

- Introduced global command channel whitelist and blacklist in the GuildSettings model for improved command access control.
- Implemented command gate logic in the command routing to handle channel and role restrictions, providing user feedback for denied commands.
- Enhanced the CommandsManager component to support global rules and channel/role selection, improving the user interface for managing command permissions.
- Updated localization files to include new keys for global command rules and related messages, ensuring clarity in user interactions.
This commit is contained in:
TheOnlyMace
2026-07-22 20:26:40 +02:00
parent 0fb8195236
commit d31660f813
24 changed files with 963 additions and 185 deletions

View File

@@ -90,6 +90,22 @@ export const SETTINGS_SEARCH_FIELDS: SearchIndexEntry[] = [
hash: 'field-access-modules',
keywords: ['access']
},
{
id: 'setting-commands-global-allow',
kind: 'setting',
labelKey: 'modulePages.commands.globalAllowedChannels',
href: 'commands',
hash: 'field-commands-global-allow',
keywords: ['whitelist', 'channel', 'command']
},
{
id: 'setting-commands-global-deny',
kind: 'setting',
labelKey: 'modulePages.commands.globalDeniedChannels',
href: 'commands',
hash: 'field-commands-global-deny',
keywords: ['blacklist', 'channel', 'command']
},
// Moderation
{
id: 'setting-mod-enabled',

View File

@@ -0,0 +1,88 @@
import type { DiscordChannelOption, DiscordRoleOption } from '@nexumi/shared';
import { env } from './env';
import { redis } from './redis';
const DISCORD_API_BASE = 'https://discord.com/api/v10';
const CACHE_TTL_SECONDS = 60;
/** Discord channel types usable for slash commands. */
const COMMAND_CHANNEL_TYPES = new Set([
0, // GUILD_TEXT
5, // GUILD_ANNOUNCEMENT
10, // ANNOUNCEMENT_THREAD
11, // PUBLIC_THREAD
12, // PRIVATE_THREAD
15, // GUILD_FORUM
16 // GUILD_MEDIA
]);
async function discordBotFetch<T>(path: string): Promise<T> {
const response = await fetch(`${DISCORD_API_BASE}${path}`, {
headers: { Authorization: `Bot ${env.BOT_TOKEN}` },
cache: 'no-store'
});
if (!response.ok) {
throw new Error(`Discord API request to ${path} failed with status ${response.status}`);
}
return (await response.json()) as T;
}
interface DiscordChannelRest {
id: string;
name: string;
type: number;
parent_id?: string | null;
position?: number;
}
interface DiscordRoleRest {
id: string;
name: string;
color: number;
position: number;
managed?: boolean;
}
export async function listGuildChannelsForDashboard(guildId: string): Promise<DiscordChannelOption[]> {
const cacheKey = `dashboard:guild:${guildId}:channels`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached) as DiscordChannelOption[];
}
const channels = await discordBotFetch<DiscordChannelRest[]>(`/guilds/${guildId}/channels`);
const options = channels
.filter((channel) => COMMAND_CHANNEL_TYPES.has(channel.type))
.map((channel) => ({
id: channel.id,
name: channel.name,
type: channel.type,
parentId: channel.parent_id ?? null
}))
.sort((a, b) => a.name.localeCompare(b.name));
await redis.set(cacheKey, JSON.stringify(options), 'EX', CACHE_TTL_SECONDS);
return options;
}
export async function listGuildRolesForDashboard(guildId: string): Promise<DiscordRoleOption[]> {
const cacheKey = `dashboard:guild:${guildId}:roles`;
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached) as DiscordRoleOption[];
}
const roles = await discordBotFetch<DiscordRoleRest[]>(`/guilds/${guildId}/roles`);
const options = roles
.filter((role) => role.name !== '@everyone')
.map((role) => ({
id: role.id,
name: role.name,
color: role.color,
position: role.position
}))
.sort((a, b) => b.position - a.position);
await redis.set(cacheKey, JSON.stringify(options), 'EX', CACHE_TTL_SECONDS);
return options;
}

View File

@@ -0,0 +1,50 @@
import type { CommandGlobalRules, CommandGlobalRulesPatch } from '@nexumi/shared';
import { prisma } from '../prisma';
async function ensureGuildSettings(guildId: string) {
await prisma.guild.upsert({
where: { id: guildId },
update: {},
create: { id: guildId }
});
return prisma.guildSettings.upsert({
where: { guildId },
update: {},
create: { guildId }
});
}
function toRules(settings: {
commandAllowedChannelIds: string[];
commandDeniedChannelIds: string[];
}): CommandGlobalRules {
return {
allowedChannelIds: settings.commandAllowedChannelIds,
deniedChannelIds: settings.commandDeniedChannelIds
};
}
export async function getCommandGlobalRules(guildId: string): Promise<CommandGlobalRules> {
const settings = await ensureGuildSettings(guildId);
return toRules(settings);
}
export async function updateCommandGlobalRules(
guildId: string,
patch: CommandGlobalRulesPatch
): Promise<CommandGlobalRules> {
await ensureGuildSettings(guildId);
const updated = await prisma.guildSettings.update({
where: { guildId },
data: {
...(patch.allowedChannelIds !== undefined
? { commandAllowedChannelIds: patch.allowedChannelIds }
: {}),
...(patch.deniedChannelIds !== undefined
? { commandDeniedChannelIds: patch.deniedChannelIds }
: {})
}
});
return toRules(updated);
}