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

@@ -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;
}