forked from syntaxbullet/aurorabot
refactor: consolidate config types and remove file-based config
Tickets: #2, #3 - Remove duplicate type definitions from shared/lib/config.ts - Import types from schema files (game-settings.ts, guild-settings.ts) - Add GuildConfig interface to guild-settings.ts schema - Rename ModerationConfig to ModerationCaseConfig in moderation.service.ts - Delete shared/config/config.json and shared/scripts/migrate-config-to-db.ts - Update settings API to use gameSettingsService exclusively - Return DB format (strings) from API instead of runtime BigInts - Fix moderation service tests to pass config as parameter Breaking Changes: - Removes legacy file-based configuration system - API now returns database format with string values for BigInt fields
This commit is contained in:
@@ -10,6 +10,26 @@ import { relations, type InferSelectModel, type InferInsertModel } from 'drizzle
|
|||||||
export type GuildSettings = InferSelectModel<typeof guildSettings>;
|
export type GuildSettings = InferSelectModel<typeof guildSettings>;
|
||||||
export type GuildSettingsInsert = InferInsertModel<typeof guildSettings>;
|
export type GuildSettingsInsert = InferInsertModel<typeof guildSettings>;
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const guildSettings = pgTable('guild_settings', {
|
export const guildSettings = pgTable('guild_settings', {
|
||||||
guildId: bigint('guild_id', { mode: 'bigint' }).primaryKey(),
|
guildId: bigint('guild_id', { mode: 'bigint' }).primaryKey(),
|
||||||
studentRoleId: bigint('student_role_id', { mode: 'bigint' }),
|
studentRoleId: bigint('student_role_id', { mode: 'bigint' }),
|
||||||
|
|||||||
@@ -1,29 +1,12 @@
|
|||||||
import { jsonReplacer } from './utils';
|
import type {
|
||||||
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
|
LevelingConfig,
|
||||||
import { join } from 'node:path';
|
EconomyConfig as EconomyConfigDB,
|
||||||
import { z } from 'zod';
|
InventoryConfig as InventoryConfigDB,
|
||||||
|
LootdropConfig,
|
||||||
const configPath = join(import.meta.dir, '..', 'config', 'config.json');
|
TriviaConfig as TriviaConfigDB,
|
||||||
|
ModerationConfig
|
||||||
export interface GuildConfig {
|
} from "@db/schema/game-settings";
|
||||||
studentRole?: string;
|
import type { GuildConfig } from "@db/schema/guild-settings";
|
||||||
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 guildConfigCache = new Map<string, { config: GuildConfig; timestamp: number }>();
|
||||||
const CACHE_TTL_MS = 60000;
|
const CACHE_TTL_MS = 60000;
|
||||||
@@ -82,16 +65,10 @@ export function invalidateGuildConfigCache(guildId: string) {
|
|||||||
guildConfigCache.delete(guildId);
|
guildConfigCache.delete(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LevelingConfig {
|
// Re-export DB types
|
||||||
base: number;
|
export type { LevelingConfig, LootdropConfig, ModerationConfig };
|
||||||
exponent: number;
|
|
||||||
chat: {
|
|
||||||
cooldownMs: number;
|
|
||||||
minXp: number;
|
|
||||||
maxXp: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Runtime config types with BigInt for numeric fields
|
||||||
export interface EconomyConfig {
|
export interface EconomyConfig {
|
||||||
daily: {
|
daily: {
|
||||||
amount: bigint;
|
amount: bigint;
|
||||||
@@ -114,18 +91,6 @@ export interface InventoryConfig {
|
|||||||
maxSlots: number;
|
maxSlots: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LootdropConfig {
|
|
||||||
activityWindowMs: number;
|
|
||||||
minMessages: number;
|
|
||||||
spawnChance: number;
|
|
||||||
cooldownMs: number;
|
|
||||||
reward: {
|
|
||||||
min: number;
|
|
||||||
max: number;
|
|
||||||
currency: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TriviaConfig {
|
export interface TriviaConfig {
|
||||||
entryFee: bigint;
|
entryFee: bigint;
|
||||||
rewardMultiplier: number;
|
rewardMultiplier: number;
|
||||||
@@ -135,20 +100,6 @@ export interface TriviaConfig {
|
|||||||
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
difficulty: 'easy' | 'medium' | 'hard' | 'random';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModerationConfig {
|
|
||||||
prune: {
|
|
||||||
maxAmount: number;
|
|
||||||
confirmThreshold: number;
|
|
||||||
batchSize: number;
|
|
||||||
batchDelayMs: number;
|
|
||||||
};
|
|
||||||
cases: {
|
|
||||||
dmOnWarn: boolean;
|
|
||||||
logChannelId?: string;
|
|
||||||
autoTimeoutThreshold?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GameConfigType {
|
export interface GameConfigType {
|
||||||
leveling: LevelingConfig;
|
leveling: LevelingConfig;
|
||||||
economy: EconomyConfig;
|
economy: EconomyConfig;
|
||||||
@@ -158,160 +109,11 @@ export interface GameConfigType {
|
|||||||
trivia: TriviaConfig;
|
trivia: TriviaConfig;
|
||||||
moderation: ModerationConfig;
|
moderation: ModerationConfig;
|
||||||
system: Record<string, unknown>;
|
system: Record<string, unknown>;
|
||||||
studentRole: string;
|
|
||||||
visitorRole: string;
|
|
||||||
colorRoles: string[];
|
|
||||||
welcomeChannelId?: string;
|
|
||||||
welcomeMessage?: string;
|
|
||||||
feedbackChannelId?: string;
|
|
||||||
terminal?: {
|
|
||||||
channelId: string;
|
|
||||||
messageId: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config: GameConfigType = {} as GameConfigType;
|
export const config: GameConfigType = {} as GameConfigType;
|
||||||
|
|
||||||
const bigIntSchema = z.union([z.string(), z.number(), z.bigint()])
|
export const GameConfig = config;
|
||||||
.refine((val) => {
|
|
||||||
try {
|
|
||||||
BigInt(val);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}, { message: "Must be a valid integer" })
|
|
||||||
.transform((val) => BigInt(val));
|
|
||||||
|
|
||||||
const fileConfigSchema = 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({}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type FileConfig = z.infer<typeof fileConfigSchema>;
|
|
||||||
|
|
||||||
function loadFromFile(): FileConfig | null {
|
|
||||||
if (!existsSync(configPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const raw = readFileSync(configPath, 'utf-8');
|
|
||||||
const rawConfig = JSON.parse(raw);
|
|
||||||
return fileConfigSchema.parse(rawConfig);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Failed to load config from file:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyFileConfig(fileConfig: FileConfig) {
|
|
||||||
Object.assign(config, {
|
|
||||||
leveling: fileConfig.leveling,
|
|
||||||
economy: fileConfig.economy,
|
|
||||||
inventory: fileConfig.inventory,
|
|
||||||
commands: fileConfig.commands,
|
|
||||||
lootdrop: fileConfig.lootdrop,
|
|
||||||
trivia: fileConfig.trivia,
|
|
||||||
moderation: fileConfig.moderation,
|
|
||||||
system: fileConfig.system,
|
|
||||||
studentRole: fileConfig.studentRole,
|
|
||||||
visitorRole: fileConfig.visitorRole,
|
|
||||||
colorRoles: fileConfig.colorRoles,
|
|
||||||
welcomeChannelId: fileConfig.welcomeChannelId,
|
|
||||||
welcomeMessage: fileConfig.welcomeMessage,
|
|
||||||
feedbackChannelId: fileConfig.feedbackChannelId,
|
|
||||||
terminal: fileConfig.terminal,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadFromDatabase(): Promise<boolean> {
|
async function loadFromDatabase(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
@@ -358,88 +160,48 @@ async function loadFromDatabase(): Promise<boolean> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadDefaults(): Promise<void> {
|
||||||
|
console.warn("⚠️ No game config found in database. Using defaults.");
|
||||||
|
const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service');
|
||||||
|
const defaults = gameSettingsService.getDefaults();
|
||||||
|
Object.assign(config, {
|
||||||
|
leveling: defaults.leveling,
|
||||||
|
economy: {
|
||||||
|
...defaults.economy,
|
||||||
|
daily: {
|
||||||
|
...defaults.economy.daily,
|
||||||
|
amount: BigInt(defaults.economy.daily.amount),
|
||||||
|
streakBonus: BigInt(defaults.economy.daily.streakBonus),
|
||||||
|
weeklyBonus: BigInt(defaults.economy.daily.weeklyBonus),
|
||||||
|
},
|
||||||
|
transfers: {
|
||||||
|
...defaults.economy.transfers,
|
||||||
|
minAmount: BigInt(defaults.economy.transfers.minAmount),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
inventory: {
|
||||||
|
...defaults.inventory,
|
||||||
|
maxStackSize: BigInt(defaults.inventory.maxStackSize),
|
||||||
|
},
|
||||||
|
commands: defaults.commands,
|
||||||
|
lootdrop: defaults.lootdrop,
|
||||||
|
trivia: {
|
||||||
|
...defaults.trivia,
|
||||||
|
entryFee: BigInt(defaults.trivia.entryFee),
|
||||||
|
},
|
||||||
|
moderation: defaults.moderation,
|
||||||
|
system: defaults.system,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function reloadConfig(): Promise<void> {
|
export async function reloadConfig(): Promise<void> {
|
||||||
const dbLoaded = await loadFromDatabase();
|
const dbLoaded = await loadFromDatabase();
|
||||||
|
|
||||||
if (!dbLoaded) {
|
if (!dbLoaded) {
|
||||||
const fileConfig = loadFromFile();
|
await loadDefaults();
|
||||||
if (fileConfig) {
|
|
||||||
applyFileConfig(fileConfig);
|
|
||||||
console.log("📄 Game config loaded from file (database not available).");
|
|
||||||
} else {
|
|
||||||
console.warn("⚠️ No game config found in database or file. Using defaults.");
|
|
||||||
const { gameSettingsService } = await import('@shared/modules/game-settings/game-settings.service');
|
|
||||||
const defaults = gameSettingsService.getDefaults();
|
|
||||||
Object.assign(config, {
|
|
||||||
leveling: defaults.leveling,
|
|
||||||
economy: {
|
|
||||||
...defaults.economy,
|
|
||||||
daily: {
|
|
||||||
...defaults.economy.daily,
|
|
||||||
amount: BigInt(defaults.economy.daily.amount),
|
|
||||||
streakBonus: BigInt(defaults.economy.daily.streakBonus),
|
|
||||||
weeklyBonus: BigInt(defaults.economy.daily.weeklyBonus),
|
|
||||||
},
|
|
||||||
transfers: {
|
|
||||||
...defaults.economy.transfers,
|
|
||||||
minAmount: BigInt(defaults.economy.transfers.minAmount),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
inventory: {
|
|
||||||
...defaults.inventory,
|
|
||||||
maxStackSize: BigInt(defaults.inventory.maxStackSize),
|
|
||||||
},
|
|
||||||
commands: defaults.commands,
|
|
||||||
lootdrop: defaults.lootdrop,
|
|
||||||
trivia: {
|
|
||||||
...defaults.trivia,
|
|
||||||
entryFee: BigInt(defaults.trivia.entryFee),
|
|
||||||
},
|
|
||||||
moderation: defaults.moderation,
|
|
||||||
system: defaults.system,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadFileSync(): void {
|
|
||||||
const fileConfig = loadFromFile();
|
|
||||||
if (fileConfig) {
|
|
||||||
applyFileConfig(fileConfig);
|
|
||||||
console.log("📄 Game config loaded from file (sync).");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GameConfig = config;
|
|
||||||
|
|
||||||
export function saveConfig(newConfig: unknown) {
|
|
||||||
const validatedConfig = fileConfigSchema.parse(newConfig);
|
|
||||||
const jsonString = JSON.stringify(validatedConfig, jsonReplacer, 4);
|
|
||||||
writeFileSync(configPath, jsonString, 'utf-8');
|
|
||||||
applyFileConfig(validatedConfig);
|
|
||||||
console.log("🔄 Config saved to file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleCommand(commandName: string, enabled: boolean) {
|
|
||||||
const fileConfig = loadFromFile();
|
|
||||||
if (!fileConfig) {
|
|
||||||
console.error("Cannot toggle command: no file config available");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newConfig = {
|
|
||||||
...fileConfig,
|
|
||||||
commands: {
|
|
||||||
...fileConfig.commands,
|
|
||||||
[commandName]: enabled
|
|
||||||
}
|
|
||||||
};
|
|
||||||
saveConfig(newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initializeConfig(): Promise<void> {
|
export async function initializeConfig(): Promise<void> {
|
||||||
loadFileSync();
|
|
||||||
await reloadConfig();
|
await reloadConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
loadFileSync();
|
|
||||||
|
|||||||
@@ -14,19 +14,7 @@ const mockReturning = mock();
|
|||||||
const mockSet = mock();
|
const mockSet = mock();
|
||||||
const mockWhere = mock();
|
const mockWhere = mock();
|
||||||
|
|
||||||
// Mock Config
|
|
||||||
const mockConfig = {
|
|
||||||
moderation: {
|
|
||||||
cases: {
|
|
||||||
dmOnWarn: true,
|
|
||||||
autoTimeoutThreshold: 3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
mock.module("@shared/lib/config", () => ({
|
|
||||||
config: mockConfig
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Mock View
|
// Mock View
|
||||||
const mockGetUserWarningEmbed = mock(() => ({}));
|
const mockGetUserWarningEmbed = mock(() => ({}));
|
||||||
@@ -66,9 +54,6 @@ describe("ModerationService", () => {
|
|||||||
mockSet.mockClear();
|
mockSet.mockClear();
|
||||||
mockWhere.mockClear();
|
mockWhere.mockClear();
|
||||||
mockGetUserWarningEmbed.mockClear();
|
mockGetUserWarningEmbed.mockClear();
|
||||||
// Reset config to defaults
|
|
||||||
mockConfig.moderation.cases.dmOnWarn = true;
|
|
||||||
mockConfig.moderation.cases.autoTimeoutThreshold = 3;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("issueWarning", () => {
|
describe("issueWarning", () => {
|
||||||
@@ -100,7 +85,6 @@ describe("ModerationService", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should not DM if dmOnWarn is false", async () => {
|
it("should not DM if dmOnWarn is false", async () => {
|
||||||
mockConfig.moderation.cases.dmOnWarn = false;
|
|
||||||
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
|
mockFindFirst.mockResolvedValue({ caseId: "CASE-0001" });
|
||||||
mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]);
|
mockReturning.mockResolvedValue([{ caseId: "CASE-0002" }]);
|
||||||
mockFindMany.mockResolvedValue([]);
|
mockFindMany.mockResolvedValue([]);
|
||||||
@@ -109,7 +93,8 @@ describe("ModerationService", () => {
|
|||||||
|
|
||||||
await ModerationService.issueWarning({
|
await ModerationService.issueWarning({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
dmTarget: mockDmTarget
|
dmTarget: mockDmTarget,
|
||||||
|
config: { dmOnWarn: false }
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(mockDmTarget.send).not.toHaveBeenCalled();
|
expect(mockDmTarget.send).not.toHaveBeenCalled();
|
||||||
@@ -125,7 +110,8 @@ describe("ModerationService", () => {
|
|||||||
|
|
||||||
const result = await ModerationService.issueWarning({
|
const result = await ModerationService.issueWarning({
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
timeoutTarget: mockTimeoutTarget
|
timeoutTarget: mockTimeoutTarget,
|
||||||
|
config: { autoTimeoutThreshold: 3 }
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.autoTimeoutIssued).toBe(true);
|
expect(result.autoTimeoutIssued).toBe(true);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { CreateCaseOptions, ClearCaseOptions, SearchCasesFilter } from "@/m
|
|||||||
import { getUserWarningEmbed } from "@/modules/moderation/moderation.view";
|
import { getUserWarningEmbed } from "@/modules/moderation/moderation.view";
|
||||||
import { CaseType } from "@shared/lib/constants";
|
import { CaseType } from "@shared/lib/constants";
|
||||||
|
|
||||||
export interface ModerationConfig {
|
export interface ModerationCaseConfig {
|
||||||
dmOnWarn?: boolean;
|
dmOnWarn?: boolean;
|
||||||
autoTimeoutThreshold?: number;
|
autoTimeoutThreshold?: number;
|
||||||
}
|
}
|
||||||
@@ -66,7 +66,7 @@ export class ModerationService {
|
|||||||
guildName?: string;
|
guildName?: string;
|
||||||
dmTarget?: { send: (options: any) => Promise<any> };
|
dmTarget?: { send: (options: any) => Promise<any> };
|
||||||
timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> };
|
timeoutTarget?: { timeout: (duration: number, reason: string) => Promise<any> };
|
||||||
config?: ModerationConfig;
|
config?: ModerationCaseConfig;
|
||||||
}) {
|
}) {
|
||||||
const moderationCase = await this.createCase({
|
const moderationCase = await this.createCase({
|
||||||
type: CaseType.WARN,
|
type: CaseType.WARN,
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { guildSettingsService } from "@shared/modules/guild-settings/guild-settings.service";
|
|
||||||
import { config } from "@shared/lib/config";
|
|
||||||
import { env } from "@shared/lib/env";
|
|
||||||
|
|
||||||
async function migrateConfigToDatabase() {
|
|
||||||
const guildId = env.DISCORD_GUILD_ID;
|
|
||||||
|
|
||||||
if (!guildId) {
|
|
||||||
console.error("DISCORD_GUILD_ID not set. Cannot migrate config.");
|
|
||||||
console.log("Set DISCORD_GUILD_ID in your environment to migrate config.");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Migrating config for guild ${guildId}...`);
|
|
||||||
|
|
||||||
const existing = await guildSettingsService.getSettings(guildId);
|
|
||||||
if (existing) {
|
|
||||||
console.log("Guild settings already exist in database:");
|
|
||||||
console.log(JSON.stringify(existing, null, 2));
|
|
||||||
console.log("\nSkipping migration. Delete existing settings first if you want to re-migrate.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await guildSettingsService.upsertSettings({
|
|
||||||
guildId,
|
|
||||||
studentRoleId: config.studentRole ?? undefined,
|
|
||||||
visitorRoleId: config.visitorRole ?? undefined,
|
|
||||||
colorRoleIds: config.colorRoles ?? [],
|
|
||||||
welcomeChannelId: config.welcomeChannelId ?? undefined,
|
|
||||||
welcomeMessage: config.welcomeMessage ?? undefined,
|
|
||||||
feedbackChannelId: config.feedbackChannelId ?? undefined,
|
|
||||||
terminalChannelId: config.terminal?.channelId ?? undefined,
|
|
||||||
terminalMessageId: config.terminal?.messageId ?? undefined,
|
|
||||||
moderationLogChannelId: config.moderation?.cases?.logChannelId ?? undefined,
|
|
||||||
moderationDmOnWarn: config.moderation?.cases?.dmOnWarn ?? true,
|
|
||||||
moderationAutoTimeoutThreshold: config.moderation?.cases?.autoTimeoutThreshold ?? undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("✅ Migration complete!");
|
|
||||||
console.log("\nGuild settings are now stored in the database.");
|
|
||||||
console.log("You can manage them via:");
|
|
||||||
console.log(" - /settings command in Discord");
|
|
||||||
console.log(" - API endpoints at /api/guilds/:guildId/settings");
|
|
||||||
}
|
|
||||||
|
|
||||||
migrateConfigToDatabase()
|
|
||||||
.then(() => process.exit(0))
|
|
||||||
.catch((error) => {
|
|
||||||
console.error("Migration failed:", error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -7,13 +7,6 @@
|
|||||||
import type { RouteContext, RouteModule } from "./types";
|
import type { RouteContext, RouteModule } from "./types";
|
||||||
import { jsonResponse, errorResponse, withErrorHandling } from "./utils";
|
import { jsonResponse, errorResponse, withErrorHandling } from "./utils";
|
||||||
|
|
||||||
/**
|
|
||||||
* JSON replacer for BigInt serialization.
|
|
||||||
*/
|
|
||||||
function jsonReplacer(_key: string, value: unknown): unknown {
|
|
||||||
return typeof value === "bigint" ? value.toString() : value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Settings routes handler.
|
* Settings routes handler.
|
||||||
*
|
*
|
||||||
@@ -32,26 +25,31 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @route GET /api/settings
|
* @route GET /api/settings
|
||||||
* @description Returns the current bot configuration.
|
* @description Returns the current bot configuration from database.
|
||||||
* Configuration includes economy settings, leveling settings,
|
* Configuration includes economy settings, leveling settings,
|
||||||
* command toggles, and other system settings.
|
* command toggles, and other system settings.
|
||||||
* @response 200 - Full configuration object
|
* @response 200 - Full configuration object (DB format with strings for BigInts)
|
||||||
* @response 500 - Error fetching settings
|
* @response 500 - Error fetching settings
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* // Response
|
* // Response
|
||||||
* {
|
* {
|
||||||
* "economy": { "dailyReward": 100, "streakBonus": 10 },
|
* "economy": { "daily": { "amount": "100", "streakBonus": "10" } },
|
||||||
* "leveling": { "xpPerMessage": 15, "levelUpChannel": "123456789" },
|
* "leveling": { "base": 100, "exponent": 1.5 },
|
||||||
* "commands": { "disabled": [], "channelLocks": {} }
|
* "commands": { "disabled": [], "channelLocks": {} }
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
if (pathname === "/api/settings" && method === "GET") {
|
if (pathname === "/api/settings" && method === "GET") {
|
||||||
return withErrorHandling(async () => {
|
return withErrorHandling(async () => {
|
||||||
const { config } = await import("@shared/lib/config");
|
const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service");
|
||||||
return new Response(JSON.stringify(config, jsonReplacer), {
|
const settings = await gameSettingsService.getSettings();
|
||||||
headers: { "Content-Type": "application/json" }
|
|
||||||
});
|
if (!settings) {
|
||||||
|
// Return defaults if no settings in DB yet
|
||||||
|
return jsonResponse(gameSettingsService.getDefaults());
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse(settings);
|
||||||
}, "fetch settings");
|
}, "fetch settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +59,7 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
|||||||
* Only the provided fields will be updated; other settings remain unchanged.
|
* Only the provided fields will be updated; other settings remain unchanged.
|
||||||
* After updating, commands are automatically reloaded.
|
* After updating, commands are automatically reloaded.
|
||||||
*
|
*
|
||||||
* @body Partial configuration object
|
* @body Partial configuration object (DB format with strings for BigInts)
|
||||||
* @response 200 - `{ success: true }`
|
* @response 200 - `{ success: true }`
|
||||||
* @response 400 - Validation error
|
* @response 400 - Validation error
|
||||||
* @response 500 - Error saving settings
|
* @response 500 - Error saving settings
|
||||||
@@ -69,19 +67,15 @@ async function handler(ctx: RouteContext): Promise<Response | null> {
|
|||||||
* @example
|
* @example
|
||||||
* // Request - Only update economy daily reward
|
* // Request - Only update economy daily reward
|
||||||
* POST /api/settings
|
* POST /api/settings
|
||||||
* { "economy": { "dailyReward": 150 } }
|
* { "economy": { "daily": { "amount": "150" } } }
|
||||||
*/
|
*/
|
||||||
if (pathname === "/api/settings" && method === "POST") {
|
if (pathname === "/api/settings" && method === "POST") {
|
||||||
try {
|
try {
|
||||||
const partialConfig = await req.json();
|
const partialConfig = await req.json() as Record<string, unknown>;
|
||||||
const { saveConfig, config: currentConfig } = await import("@shared/lib/config");
|
const { gameSettingsService } = await import("@shared/modules/game-settings/game-settings.service");
|
||||||
const { deepMerge } = await import("@shared/lib/utils");
|
|
||||||
|
// Use upsertSettings to merge partial update
|
||||||
// Merge partial update into current config
|
await gameSettingsService.upsertSettings(partialConfig as Record<string, unknown>);
|
||||||
const mergedConfig = deepMerge(currentConfig, partialConfig);
|
|
||||||
|
|
||||||
// saveConfig throws if validation fails
|
|
||||||
saveConfig(mergedConfig);
|
|
||||||
|
|
||||||
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
||||||
systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS);
|
systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS);
|
||||||
|
|||||||
Reference in New Issue
Block a user