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([ 'giveaways', 'tags', 'selfroles', 'feeds', 'scheduler', 'guildbackup', 'messages' ]); /** Slash command name → dashboard module. Core/GDPR/utility (except gated) omit. */ const COMMAND_MODULE_MAP: Record = { 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 { 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 { const value = await redis.hget(redisModulesKey(guildId), moduleId); if (value === null) { return true; } return value === 'true'; } async function isFunEnabled(redis: Redis, guildId: string): Promise { 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 { 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 { const row = await prisma.globalUserBlacklist.findUnique({ where: { userId }, select: { id: true } }); return Boolean(row); } export async function isGuildBlacklisted(prisma: PrismaClient, guildId: string): Promise { const row = await prisma.guildBlacklist.findUnique({ where: { guildId }, select: { id: true } }); return Boolean(row); }