forked from syntaxbullet/aurorabot
Add function to fetch guild-specific config from database with: - 60-second cache TTL - Fallback to file-based config for migration period - Cache invalidation helper
312 lines
8.9 KiB
TypeScript
312 lines
8.9 KiB
TypeScript
import { jsonReplacer } from './utils';
|
|
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { z } from 'zod';
|
|
|
|
const configPath = join(import.meta.dir, '..', 'config', 'config.json');
|
|
|
|
export interface GuildConfig {
|
|
studentRole?: string;
|
|
visitorRole?: string;
|
|
colorRoles: string[];
|
|
welcomeChannelId?: string;
|
|
welcomeMessage?: string;
|
|
feedbackChannelId?: string;
|
|
terminal?: {
|
|
channelId: string;
|
|
messageId: string;
|
|
};
|
|
moderation: {
|
|
cases: {
|
|
dmOnWarn: boolean;
|
|
logChannelId?: string;
|
|
autoTimeoutThreshold?: number;
|
|
};
|
|
};
|
|
}
|
|
|
|
const guildConfigCache = new Map<string, { config: GuildConfig; timestamp: number }>();
|
|
const CACHE_TTL_MS = 60000;
|
|
|
|
export async function getGuildConfig(guildId: string): Promise<GuildConfig> {
|
|
const cached = guildConfigCache.get(guildId);
|
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
|
|
return cached.config;
|
|
}
|
|
|
|
try {
|
|
const { guildSettingsService } = await import('@shared/modules/guild-settings/guild-settings.service');
|
|
const dbSettings = await guildSettingsService.getSettings(guildId);
|
|
|
|
if (dbSettings) {
|
|
const config: GuildConfig = {
|
|
studentRole: dbSettings.studentRoleId,
|
|
visitorRole: dbSettings.visitorRoleId,
|
|
colorRoles: dbSettings.colorRoleIds ?? [],
|
|
welcomeChannelId: dbSettings.welcomeChannelId,
|
|
welcomeMessage: dbSettings.welcomeMessage,
|
|
feedbackChannelId: dbSettings.feedbackChannelId,
|
|
terminal: dbSettings.terminalChannelId ? {
|
|
channelId: dbSettings.terminalChannelId,
|
|
messageId: dbSettings.terminalMessageId ?? "",
|
|
} : undefined,
|
|
moderation: {
|
|
cases: {
|
|
dmOnWarn: dbSettings.moderationDmOnWarn ?? true,
|
|
logChannelId: dbSettings.moderationLogChannelId,
|
|
autoTimeoutThreshold: dbSettings.moderationAutoTimeoutThreshold,
|
|
},
|
|
},
|
|
};
|
|
|
|
guildConfigCache.set(guildId, { config, timestamp: Date.now() });
|
|
return config;
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load guild config from database:", error);
|
|
}
|
|
|
|
const fileConfig: GuildConfig = {
|
|
studentRole: config.studentRole,
|
|
visitorRole: config.visitorRole,
|
|
colorRoles: config.colorRoles,
|
|
welcomeChannelId: config.welcomeChannelId,
|
|
welcomeMessage: config.welcomeMessage,
|
|
feedbackChannelId: config.feedbackChannelId,
|
|
terminal: config.terminal,
|
|
moderation: config.moderation,
|
|
};
|
|
|
|
return fileConfig;
|
|
}
|
|
|
|
export function invalidateGuildConfigCache(guildId: string) {
|
|
guildConfigCache.delete(guildId);
|
|
}
|
|
|
|
export interface GameConfigType {
|
|
leveling: {
|
|
base: number;
|
|
exponent: number;
|
|
chat: {
|
|
cooldownMs: number;
|
|
minXp: number;
|
|
maxXp: number;
|
|
}
|
|
},
|
|
economy: {
|
|
daily: {
|
|
amount: bigint;
|
|
streakBonus: bigint;
|
|
weeklyBonus: bigint;
|
|
cooldownMs: number;
|
|
},
|
|
transfers: {
|
|
allowSelfTransfer: boolean;
|
|
minAmount: bigint;
|
|
},
|
|
exam: {
|
|
multMin: number;
|
|
multMax: number;
|
|
}
|
|
},
|
|
inventory: {
|
|
maxStackSize: bigint;
|
|
maxSlots: number;
|
|
},
|
|
commands: Record<string, boolean>;
|
|
lootdrop: {
|
|
activityWindowMs: number;
|
|
minMessages: number;
|
|
spawnChance: number;
|
|
cooldownMs: number;
|
|
reward: {
|
|
min: number;
|
|
max: number;
|
|
currency: string;
|
|
}
|
|
};
|
|
studentRole: string;
|
|
visitorRole: string;
|
|
colorRoles: string[];
|
|
welcomeChannelId?: string;
|
|
welcomeMessage?: string;
|
|
feedbackChannelId?: string;
|
|
terminal?: {
|
|
channelId: string;
|
|
messageId: string;
|
|
};
|
|
moderation: {
|
|
prune: {
|
|
maxAmount: number;
|
|
confirmThreshold: number;
|
|
batchSize: number;
|
|
batchDelayMs: number;
|
|
};
|
|
cases: {
|
|
dmOnWarn: boolean;
|
|
logChannelId?: string;
|
|
autoTimeoutThreshold?: number;
|
|
};
|
|
};
|
|
trivia: {
|
|
entryFee: bigint;
|
|
rewardMultiplier: number;
|
|
timeoutSeconds: number;
|
|
cooldownMs: number;
|
|
categories: number[];
|
|
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
|
};
|
|
system: Record<string, any>;
|
|
}
|
|
|
|
// Initial default config state
|
|
export const config: GameConfigType = {} as GameConfigType;
|
|
|
|
const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
|
.refine((val) => {
|
|
try {
|
|
BigInt(val);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}, { message: "Must be a valid integer" })
|
|
.transform((val) => BigInt(val));
|
|
|
|
const configSchema = z.object({
|
|
leveling: z.object({
|
|
base: z.number(),
|
|
exponent: z.number(),
|
|
chat: z.object({
|
|
cooldownMs: z.number(),
|
|
minXp: z.number(),
|
|
maxXp: z.number(),
|
|
})
|
|
}),
|
|
economy: z.object({
|
|
daily: z.object({
|
|
amount: bigIntSchema,
|
|
streakBonus: bigIntSchema,
|
|
weeklyBonus: bigIntSchema.default(50n),
|
|
cooldownMs: z.number(),
|
|
}),
|
|
transfers: z.object({
|
|
allowSelfTransfer: z.boolean(),
|
|
minAmount: bigIntSchema,
|
|
}),
|
|
exam: z.object({
|
|
multMin: z.number(),
|
|
multMax: z.number(),
|
|
})
|
|
}),
|
|
inventory: z.object({
|
|
maxStackSize: bigIntSchema,
|
|
maxSlots: z.number(),
|
|
}),
|
|
commands: z.record(z.string(), z.boolean()),
|
|
lootdrop: z.object({
|
|
activityWindowMs: z.number(),
|
|
minMessages: z.number(),
|
|
spawnChance: z.number(),
|
|
cooldownMs: z.number(),
|
|
reward: z.object({
|
|
min: z.number(),
|
|
max: z.number(),
|
|
currency: z.string(),
|
|
})
|
|
|
|
}),
|
|
studentRole: z.string(),
|
|
visitorRole: z.string(),
|
|
colorRoles: z.array(z.string()).default([]),
|
|
welcomeChannelId: z.string().optional(),
|
|
welcomeMessage: z.string().optional(),
|
|
feedbackChannelId: z.string().optional(),
|
|
terminal: z.object({
|
|
channelId: z.string(),
|
|
messageId: z.string()
|
|
}).optional(),
|
|
moderation: z.object({
|
|
prune: z.object({
|
|
maxAmount: z.number().default(100),
|
|
confirmThreshold: z.number().default(50),
|
|
batchSize: z.number().default(100),
|
|
batchDelayMs: z.number().default(1000)
|
|
}),
|
|
cases: z.object({
|
|
dmOnWarn: z.boolean().default(true),
|
|
logChannelId: z.string().optional(),
|
|
autoTimeoutThreshold: z.number().optional()
|
|
})
|
|
}).default({
|
|
prune: {
|
|
maxAmount: 100,
|
|
confirmThreshold: 50,
|
|
batchSize: 100,
|
|
batchDelayMs: 1000
|
|
},
|
|
cases: {
|
|
dmOnWarn: true
|
|
}
|
|
}),
|
|
trivia: z.object({
|
|
entryFee: bigIntSchema,
|
|
rewardMultiplier: z.number().min(0).max(10),
|
|
timeoutSeconds: z.number().min(5).max(300),
|
|
cooldownMs: z.number().min(0),
|
|
categories: z.array(z.number()).default([]),
|
|
difficulty: z.enum(['easy', 'medium', 'hard', 'random']).default('random'),
|
|
}).default({
|
|
entryFee: 50n,
|
|
rewardMultiplier: 1.8,
|
|
timeoutSeconds: 30,
|
|
cooldownMs: 60000,
|
|
categories: [],
|
|
difficulty: 'random'
|
|
}),
|
|
system: z.record(z.string(), z.any()).default({}),
|
|
});
|
|
|
|
export function reloadConfig() {
|
|
if (!existsSync(configPath)) {
|
|
throw new Error(`Config file not found at ${configPath}`);
|
|
}
|
|
|
|
const raw = readFileSync(configPath, 'utf-8');
|
|
const rawConfig = JSON.parse(raw);
|
|
|
|
// Update config object in place
|
|
// We use Object.assign to keep the reference to the exported 'config' object same
|
|
const validatedConfig = configSchema.parse(rawConfig);
|
|
Object.assign(config, validatedConfig);
|
|
|
|
console.log("🔄 Config reloaded from disk.");
|
|
}
|
|
|
|
// Initial load
|
|
reloadConfig();
|
|
|
|
// Backwards compatibility alias
|
|
export const GameConfig = config;
|
|
|
|
export function saveConfig(newConfig: unknown) {
|
|
// Validate and transform input
|
|
const validatedConfig = configSchema.parse(newConfig);
|
|
|
|
const jsonString = JSON.stringify(validatedConfig, jsonReplacer, 4);
|
|
writeFileSync(configPath, jsonString, 'utf-8');
|
|
reloadConfig();
|
|
}
|
|
|
|
export function toggleCommand(commandName: string, enabled: boolean) {
|
|
const newConfig = {
|
|
...config,
|
|
commands: {
|
|
...config.commands,
|
|
[commandName]: enabled
|
|
}
|
|
};
|
|
saveConfig(newConfig);
|
|
}
|