From 4ac8b4759e65895d2be8fcd2c02b08c7d95c3fdb Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 18 Dec 2025 16:25:54 +0100 Subject: [PATCH] feat: Add `/config` admin command to dynamically edit and save bot configuration. --- .gitignore | 2 +- src/commands/admin/config.ts | 68 ++++++++++++++++++++++++++++++++++++ src/index.ts | 2 +- src/lib/config.ts | 19 ++++++++-- 4 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 src/commands/admin/config.ts diff --git a/.gitignore b/.gitignore index 69f5d10..27defaa 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ db-data node_modules -src/config +config/ # output out diff --git a/src/commands/admin/config.ts b/src/commands/admin/config.ts new file mode 100644 index 0000000..cabc311 --- /dev/null +++ b/src/commands/admin/config.ts @@ -0,0 +1,68 @@ +import { createCommand } from "@lib/utils"; +import { SlashCommandBuilder, PermissionFlagsBits, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, ModalSubmitInteraction } from "discord.js"; +import { config, saveConfig } from "@lib/config"; +import type { GameConfigType } from "@lib/config"; +import { createSuccessEmbed, createErrorEmbed } from "@lib/embeds"; + +export const configCommand = createCommand({ + data: new SlashCommandBuilder() + .setName("config") + .setDescription("Edit the bot configuration") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + console.log(`Config command executed by ${interaction.user.tag}`); + const replacer = (key: string, value: any) => { + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + }; + + const currentConfigJson = JSON.stringify(config, replacer, 4); + + const modal = new ModalBuilder() + .setCustomId("config-modal") + .setTitle("Edit Configuration"); + + const jsonInput = new TextInputBuilder() + .setCustomId("json-input") + .setLabel("Configuration JSON") + .setStyle(TextInputStyle.Paragraph) + .setValue(currentConfigJson) + .setRequired(true); + + const actionRow = new ActionRowBuilder().addComponents(jsonInput); + modal.addComponents(actionRow); + + await interaction.showModal(modal); + + try { + const submitted = await interaction.awaitModalSubmit({ + time: 300000, // 5 minutes + filter: (i) => i.customId === "config-modal" && i.user.id === interaction.user.id + }); + + const jsonString = submitted.fields.getTextInputValue("json-input"); + + try { + const newConfig = JSON.parse(jsonString); + saveConfig(newConfig as GameConfigType); + + await submitted.reply({ + embeds: [createSuccessEmbed("Configuration updated successfully.", "Config Saved")] + }); + } catch (parseError) { + await submitted.reply({ + embeds: [createErrorEmbed(`Invalid JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, "Config Update Failed")], + ephemeral: true + }); + } + + } catch (error) { + // Timeout or other error handling if needed, usually just ignore timeouts for modals + if (error instanceof Error && error.message.includes('time')) { + // specific timeout handling if desired + } + } + } +}); diff --git a/src/index.ts b/src/index.ts index ddaf0c9..17da523 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,4 +11,4 @@ await KyokoClient.deployCommands(); if (!env.DISCORD_BOT_TOKEN) { throw new Error("❌ DISCORD_BOT_TOKEN is not set in environment variables."); } -KyokoClient.login(env.DISCORD_BOT_TOKEN); \ No newline at end of file +KyokoClient.login(env.DISCORD_BOT_TOKEN); \ No newline at end of file diff --git a/src/lib/config.ts b/src/lib/config.ts index 0e6b068..32478c3 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,7 +1,7 @@ -import { readFileSync, existsSync } from 'fs'; -import { join } from 'path'; +import { readFileSync, existsSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; -const configPath = join(process.cwd(), 'src', 'config', 'config.json'); +const configPath = join(import.meta.dir, '..', '..', 'config', 'config.json'); export interface GameConfigType { leveling: { @@ -81,3 +81,16 @@ reloadConfig(); // Backwards compatibility alias export const GameConfig = config; + +export function saveConfig(newConfig: GameConfigType) { + const replacer = (key: string, value: any) => { + if (typeof value === 'bigint') { + return value.toString(); + } + return value; + }; + + const jsonString = JSON.stringify(newConfig, replacer, 4); + writeFileSync(configPath, jsonString, 'utf-8'); + reloadConfig(); +}