- 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.
89 lines
2.5 KiB
TypeScript
89 lines
2.5 KiB
TypeScript
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;
|
|
}
|