Enhance bot functionality with new features and improvements
- Updated package dependencies to include `@sentry/node` for error tracking. - Refactored command routing to incorporate user and guild blacklisting checks, improving command execution safety. - Enhanced health server metrics to include queue waiting times, providing better insights into system performance. - Introduced confirmation handling for guild backup commands, streamlining user interactions. - Improved moderation commands with better error handling and case management, ensuring robust moderation capabilities. - Added new duration parsing functions to handle timeout durations effectively, enhancing moderation features. - Updated localization files to reflect new command structures and user guidance improvements.
This commit is contained in:
275
apps/bot/src/module-gates.ts
Normal file
275
apps/bot/src/module-gates.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user