Files
Nexumi/apps/bot/src/module-gates.ts
TheOnlyMace 8513848440 Implement command cooldown management and enhance command execution flow
- 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.
2026-07-25 15:37:56 +02:00

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