- Introduced `applyCommandCooldown` function to manage per-command cooldowns after execution, improving command rate limiting. - Updated command execution flow to include cooldown application, ensuring users are informed of cooldown status. - Enhanced error handling in interaction replies, allowing for more graceful error management and user feedback. - Improved the handling of interaction responses across various commands, ensuring consistent user experience. - Added permission checks for new commands, ensuring only authorized users can execute specific actions.
311 lines
7.8 KiB
TypeScript
311 lines
7.8 KiB
TypeScript
import type { DashboardModuleId } from '@nexumi/shared';
|
|
import type { PrismaClient } from '@prisma/client';
|
|
import type { Redis } from 'ioredis';
|
|
import { createHash } from 'node:crypto';
|
|
|
|
const REDIS_ONLY_MODULES = new Set<DashboardModuleId>([
|
|
'giveaways',
|
|
'tags',
|
|
'selfroles',
|
|
'feeds',
|
|
'scheduler',
|
|
'guildbackup',
|
|
'messages'
|
|
]);
|
|
|
|
/** Slash command name → dashboard module. Core/GDPR/utility (except gated) omit. */
|
|
const COMMAND_MODULE_MAP: Record<string, DashboardModuleId> = {
|
|
ban: 'moderation',
|
|
unban: 'moderation',
|
|
kick: 'moderation',
|
|
timeout: 'moderation',
|
|
untimeout: 'moderation',
|
|
warn: 'moderation',
|
|
purge: 'moderation',
|
|
slowmode: 'moderation',
|
|
lock: 'moderation',
|
|
unlock: 'moderation',
|
|
nick: 'moderation',
|
|
case: 'moderation',
|
|
modnote: 'moderation',
|
|
automod: 'automod',
|
|
welcome: 'welcome',
|
|
verify: 'verification',
|
|
rank: 'leveling',
|
|
leaderboard: 'leveling',
|
|
xp: 'leveling',
|
|
balance: 'economy',
|
|
daily: 'economy',
|
|
weekly: 'economy',
|
|
work: 'economy',
|
|
pay: 'economy',
|
|
gamble: 'economy',
|
|
slots: 'economy',
|
|
blackjack: 'economy',
|
|
coinflip: 'economy',
|
|
shop: 'economy',
|
|
inventory: 'economy',
|
|
eco: 'economy',
|
|
'8ball': 'fun',
|
|
dice: 'fun',
|
|
rps: 'fun',
|
|
choose: 'fun',
|
|
trivia: 'fun',
|
|
tictactoe: 'fun',
|
|
connect4: 'fun',
|
|
hangman: 'fun',
|
|
meme: 'fun',
|
|
cat: 'fun',
|
|
dog: 'fun',
|
|
giveaway: 'giveaways',
|
|
ticket: 'tickets',
|
|
selfroles: 'selfroles',
|
|
tag: 'tags',
|
|
starboard: 'starboard',
|
|
suggest: 'suggestions',
|
|
suggestion: 'suggestions',
|
|
birthday: 'birthdays',
|
|
voice: 'tempvoice',
|
|
invites: 'stats',
|
|
stats: 'stats',
|
|
feeds: 'feeds',
|
|
schedule: 'scheduler',
|
|
announce: 'scheduler',
|
|
backup: 'guildbackup',
|
|
embed: 'messages'
|
|
};
|
|
|
|
function redisModulesKey(guildId: string): string {
|
|
return `dashboard:modules:${guildId}`;
|
|
}
|
|
|
|
function funConfigKey(guildId: string): string {
|
|
return `fun:config:${guildId}`;
|
|
}
|
|
|
|
function hashPercent(input: string): number {
|
|
const digest = createHash('sha256').update(input).digest();
|
|
return digest.readUInt32BE(0) % 100;
|
|
}
|
|
|
|
export function moduleForCommand(commandName: string): DashboardModuleId | null {
|
|
return COMMAND_MODULE_MAP[commandName] ?? null;
|
|
}
|
|
|
|
/** Component customId → dashboard module (null = no module gate). */
|
|
export function moduleForComponent(customId: string): DashboardModuleId | null {
|
|
if (customId.startsWith('mod:confirm:') || customId.startsWith('mod:cancel:')) {
|
|
return 'moderation';
|
|
}
|
|
if (customId.startsWith('verify:')) {
|
|
return 'verification';
|
|
}
|
|
if (customId.startsWith('eco:bj:')) {
|
|
return 'economy';
|
|
}
|
|
if (customId.startsWith('fun:')) {
|
|
return 'fun';
|
|
}
|
|
if (customId.startsWith('gw:')) {
|
|
return 'giveaways';
|
|
}
|
|
if (customId.startsWith('ticket:') || customId === 'ticket:select') {
|
|
return 'tickets';
|
|
}
|
|
if (customId.startsWith('sr:')) {
|
|
return 'selfroles';
|
|
}
|
|
if (customId.startsWith('sug:')) {
|
|
return 'suggestions';
|
|
}
|
|
if (customId.startsWith('tv:')) {
|
|
return 'tempvoice';
|
|
}
|
|
if (customId.startsWith('gbackup:')) {
|
|
return 'guildbackup';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export async function isFeatureFlagEnabled(
|
|
prisma: PrismaClient,
|
|
key: string,
|
|
guildId: string | null
|
|
): Promise<boolean> {
|
|
const flag = await prisma.featureFlag.findUnique({ where: { key } });
|
|
if (!flag) {
|
|
return true;
|
|
}
|
|
if (guildId && flag.whitelistGuildIds.includes(guildId)) {
|
|
return true;
|
|
}
|
|
if (!flag.enabled) {
|
|
return false;
|
|
}
|
|
if (flag.rolloutPercent >= 100) {
|
|
return true;
|
|
}
|
|
if (flag.rolloutPercent <= 0 || !guildId) {
|
|
return false;
|
|
}
|
|
return hashPercent(`${flag.key}:${guildId}`) < flag.rolloutPercent;
|
|
}
|
|
|
|
async function isRedisModuleEnabled(
|
|
redis: Redis,
|
|
guildId: string,
|
|
moduleId: DashboardModuleId
|
|
): Promise<boolean> {
|
|
const value = await redis.hget(redisModulesKey(guildId), moduleId);
|
|
if (value === null) {
|
|
return true;
|
|
}
|
|
return value === 'true';
|
|
}
|
|
|
|
async function isFunEnabled(redis: Redis, guildId: string): Promise<boolean> {
|
|
const raw = await redis.get(funConfigKey(guildId));
|
|
if (!raw) {
|
|
return true;
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(raw) as { enabled?: boolean };
|
|
return parsed.enabled ?? true;
|
|
} catch {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export async function isModuleEnabled(
|
|
prisma: PrismaClient,
|
|
redis: Redis,
|
|
guildId: string,
|
|
moduleId: DashboardModuleId
|
|
): Promise<boolean> {
|
|
const flagOk = await isFeatureFlagEnabled(prisma, moduleId, guildId);
|
|
if (!flagOk) {
|
|
return false;
|
|
}
|
|
|
|
if (REDIS_ONLY_MODULES.has(moduleId)) {
|
|
return isRedisModuleEnabled(redis, guildId, moduleId);
|
|
}
|
|
|
|
switch (moduleId) {
|
|
case 'moderation': {
|
|
const settings = await prisma.guildSettings.findUnique({
|
|
where: { guildId },
|
|
select: { moderationEnabled: true }
|
|
});
|
|
return settings?.moderationEnabled ?? true;
|
|
}
|
|
case 'automod': {
|
|
const config = await prisma.autoModConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'logging': {
|
|
const config = await prisma.loggingConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'welcome': {
|
|
const config = await prisma.welcomeConfig.findUnique({
|
|
where: { guildId },
|
|
select: { welcomeEnabled: true, leaveEnabled: true }
|
|
});
|
|
if (!config) {
|
|
return true;
|
|
}
|
|
return Boolean(config.welcomeEnabled || config.leaveEnabled);
|
|
}
|
|
case 'verification': {
|
|
const config = await prisma.verificationConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? false;
|
|
}
|
|
case 'leveling': {
|
|
const config = await prisma.levelingConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'economy': {
|
|
const config = await prisma.economyConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'fun':
|
|
return isFunEnabled(redis, guildId);
|
|
case 'tickets': {
|
|
const config = await prisma.ticketConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'starboard': {
|
|
const config = await prisma.starboardConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? false;
|
|
}
|
|
case 'suggestions': {
|
|
const config = await prisma.suggestionConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'birthdays': {
|
|
const config = await prisma.birthdayConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? true;
|
|
}
|
|
case 'tempvoice': {
|
|
const config = await prisma.tempVoiceConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? false;
|
|
}
|
|
case 'stats': {
|
|
const config = await prisma.statsConfig.findUnique({
|
|
where: { guildId },
|
|
select: { enabled: true }
|
|
});
|
|
return config?.enabled ?? false;
|
|
}
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export async function isUserBlacklisted(prisma: PrismaClient, userId: string): Promise<boolean> {
|
|
const row = await prisma.globalUserBlacklist.findUnique({
|
|
where: { userId },
|
|
select: { id: true }
|
|
});
|
|
return Boolean(row);
|
|
}
|
|
|
|
export async function isGuildBlacklisted(prisma: PrismaClient, guildId: string): Promise<boolean> {
|
|
const row = await prisma.guildBlacklist.findUnique({
|
|
where: { guildId },
|
|
select: { id: true }
|
|
});
|
|
return Boolean(row);
|
|
}
|