159 lines
5.5 KiB
TypeScript
159 lines
5.5 KiB
TypeScript
/**
|
|
* @fileoverview Bot settings endpoints for Aurora API.
|
|
* Provides endpoints for reading and updating bot configuration,
|
|
* as well as fetching Discord metadata.
|
|
*/
|
|
|
|
import type { RouteContext, RouteModule } from "./types";
|
|
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.
|
|
*
|
|
* Endpoints:
|
|
* - GET /api/settings - Get current bot configuration
|
|
* - POST /api/settings - Update bot configuration (partial merge)
|
|
* - GET /api/settings/meta - Get Discord metadata (roles, channels, commands)
|
|
*/
|
|
async function handler(ctx: RouteContext): Promise<Response | null> {
|
|
const { pathname, method, req } = ctx;
|
|
|
|
// Only handle requests to /api/settings*
|
|
if (!pathname.startsWith("/api/settings")) {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @route GET /api/settings
|
|
* @description Returns the current bot configuration.
|
|
* Configuration includes economy settings, leveling settings,
|
|
* command toggles, and other system settings.
|
|
* @response 200 - Full configuration object
|
|
* @response 500 - Error fetching settings
|
|
*
|
|
* @example
|
|
* // Response
|
|
* {
|
|
* "economy": { "dailyReward": 100, "streakBonus": 10 },
|
|
* "leveling": { "xpPerMessage": 15, "levelUpChannel": "123456789" },
|
|
* "commands": { "disabled": [], "channelLocks": {} }
|
|
* }
|
|
*/
|
|
if (pathname === "/api/settings" && method === "GET") {
|
|
return withErrorHandling(async () => {
|
|
const { config } = await import("@shared/lib/config");
|
|
return new Response(JSON.stringify(config, jsonReplacer), {
|
|
headers: { "Content-Type": "application/json" }
|
|
});
|
|
}, "fetch settings");
|
|
}
|
|
|
|
/**
|
|
* @route POST /api/settings
|
|
* @description Updates bot configuration with partial merge.
|
|
* Only the provided fields will be updated; other settings remain unchanged.
|
|
* After updating, commands are automatically reloaded.
|
|
*
|
|
* @body Partial configuration object
|
|
* @response 200 - `{ success: true }`
|
|
* @response 400 - Validation error
|
|
* @response 500 - Error saving settings
|
|
*
|
|
* @example
|
|
* // Request - Only update economy daily reward
|
|
* POST /api/settings
|
|
* { "economy": { "dailyReward": 150 } }
|
|
*/
|
|
if (pathname === "/api/settings" && method === "POST") {
|
|
try {
|
|
const partialConfig = await req.json();
|
|
const { saveConfig, config: currentConfig } = await import("@shared/lib/config");
|
|
const { deepMerge } = await import("@shared/lib/utils");
|
|
|
|
// Merge partial update into current config
|
|
const mergedConfig = deepMerge(currentConfig, partialConfig);
|
|
|
|
// saveConfig throws if validation fails
|
|
saveConfig(mergedConfig);
|
|
|
|
const { systemEvents, EVENTS } = await import("@shared/lib/events");
|
|
systemEvents.emit(EVENTS.ACTIONS.RELOAD_COMMANDS);
|
|
|
|
return jsonResponse({ success: true });
|
|
} catch (error) {
|
|
// Return 400 for validation errors
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
return errorResponse("Failed to save settings", 400, message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @route GET /api/settings/meta
|
|
* @description Returns Discord server metadata for settings UI.
|
|
* Provides lists of roles, channels, and registered commands.
|
|
*
|
|
* @response 200 - `{ roles: Role[], channels: Channel[], commands: Command[] }`
|
|
* @response 500 - Error fetching metadata
|
|
*
|
|
* @example
|
|
* // Response
|
|
* {
|
|
* "roles": [
|
|
* { "id": "123456789", "name": "Admin", "color": "#FF0000" }
|
|
* ],
|
|
* "channels": [
|
|
* { "id": "987654321", "name": "general", "type": 0 }
|
|
* ],
|
|
* "commands": [
|
|
* { "name": "daily", "category": "economy" }
|
|
* ]
|
|
* }
|
|
*/
|
|
if (pathname === "/api/settings/meta" && method === "GET") {
|
|
return withErrorHandling(async () => {
|
|
const { AuroraClient } = await import("../../../bot/lib/BotClient");
|
|
const { env } = await import("@shared/lib/env");
|
|
|
|
if (!env.DISCORD_GUILD_ID) {
|
|
return jsonResponse({ roles: [], channels: [], commands: [] });
|
|
}
|
|
|
|
const guild = AuroraClient.guilds.cache.get(env.DISCORD_GUILD_ID);
|
|
if (!guild) {
|
|
return jsonResponse({ roles: [], channels: [], commands: [] });
|
|
}
|
|
|
|
// Map roles and channels to a simplified format
|
|
const roles = guild.roles.cache
|
|
.sort((a, b) => b.position - a.position)
|
|
.map(r => ({ id: r.id, name: r.name, color: r.hexColor }));
|
|
|
|
const channels = guild.channels.cache
|
|
.map(c => ({ id: c.id, name: c.name, type: c.type }));
|
|
|
|
const commands = Array.from(AuroraClient.knownCommands.entries())
|
|
.map(([name, category]) => ({ name, category }))
|
|
.sort((a, b) => {
|
|
if (a.category !== b.category) return a.category.localeCompare(b.category);
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
|
|
return jsonResponse({ roles, channels, commands });
|
|
}, "fetch settings meta");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export const settingsRoutes: RouteModule = {
|
|
name: "settings",
|
|
handler
|
|
};
|