diff --git a/config.json b/config.json index 75ad78b4..98468b6a 100644 --- a/config.json +++ b/config.json @@ -148,6 +148,18 @@ "tldr": "everyone" } }, + "help": { + "enabled": false + }, + "announce": { + "enabled": false + }, + "snippet": { + "enabled": false + }, + "poll": { + "enabled": false + }, "tldr": { "enabled": false, "defaultMessages": 50, diff --git a/src/api/utils/configAllowlist.js b/src/api/utils/configAllowlist.js index 69425325..c8de02b1 100644 --- a/src/api/utils/configAllowlist.js +++ b/src/api/utils/configAllowlist.js @@ -13,6 +13,11 @@ export const SAFE_CONFIG_KEYS = new Set([ 'starboard', 'permissions', 'memory', + 'help', + 'announce', + 'snippet', + 'poll', + 'tldr', ]); export const READABLE_CONFIG_KEYS = [...SAFE_CONFIG_KEYS, 'logging']; diff --git a/src/commands/announce.js b/src/commands/announce.js index 30fe3059..043f16a8 100644 --- a/src/commands/announce.js +++ b/src/commands/announce.js @@ -140,6 +140,14 @@ export function parseTime(timeStr) { export async function execute(interaction) { const config = getConfig(interaction.guildId); + if (!config.announce?.enabled) { + await safeReply(interaction, { + content: '❌ The /announce command is not enabled on this server.', + ephemeral: true, + }); + return; + } + if (!isModerator(interaction.member, config)) { await safeReply(interaction, { content: getPermissionError('announce', 'moderator'), diff --git a/src/commands/help.js b/src/commands/help.js index 9e74c252..1533fab8 100644 --- a/src/commands/help.js +++ b/src/commands/help.js @@ -11,7 +11,7 @@ import { getPool } from '../db.js'; import { info, error as logError } from '../logger.js'; import { getConfig } from '../modules/config.js'; import { isModerator } from '../utils/permissions.js'; -import { safeEditReply } from '../utils/safeSend.js'; +import { safeEditReply, safeReply } from '../utils/safeSend.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -325,6 +325,15 @@ async function handleList(interaction) { * @param {import('discord.js').ChatInputCommandInteraction} interaction */ export async function execute(interaction) { + const config = getConfig(interaction.guildId); + if (!config.help?.enabled) { + await safeReply(interaction, { + content: '❌ The /help command is not enabled on this server.', + ephemeral: true, + }); + return; + } + await interaction.deferReply({ ephemeral: true }); const subcommand = interaction.options.getSubcommand(); diff --git a/tests/commands/announce.test.js b/tests/commands/announce.test.js index d8045fae..7cbb5a87 100644 --- a/tests/commands/announce.test.js +++ b/tests/commands/announce.test.js @@ -13,6 +13,7 @@ vi.mock('../../src/logger.js', () => ({ vi.mock('../../src/modules/config.js', () => ({ getConfig: vi.fn().mockReturnValue({ + announce: { enabled: true }, permissions: { enabled: true, adminRoleId: null, usePermissions: true }, }), })); diff --git a/tests/commands/help.test.js b/tests/commands/help.test.js index 25558e7f..796df73c 100644 --- a/tests/commands/help.test.js +++ b/tests/commands/help.test.js @@ -10,7 +10,7 @@ vi.mock('../../src/db.js', () => ({ getPool: vi.fn(), })); vi.mock('../../src/modules/config.js', () => ({ - getConfig: vi.fn().mockReturnValue({}), + getConfig: vi.fn().mockReturnValue({ help: { enabled: true } }), })); vi.mock('../../src/utils/permissions.js', () => ({ isModerator: vi.fn().mockReturnValue(true), diff --git a/web/src/components/dashboard/config-editor.tsx b/web/src/components/dashboard/config-editor.tsx index db363700..a0c128a8 100644 --- a/web/src/components/dashboard/config-editor.tsx +++ b/web/src/components/dashboard/config-editor.tsx @@ -48,7 +48,7 @@ function parseNumberInput(raw: string, min?: number, max?: number): number | und function isGuildConfig(data: unknown): data is GuildConfig { if (typeof data !== "object" || data === null || Array.isArray(data)) return false; const obj = data as Record; - const knownSections = ["ai", "welcome", "spam", "moderation", "triage", "starboard", "permissions", "memory"] as const; + const knownSections = ["ai", "welcome", "spam", "moderation", "triage", "starboard", "permissions", "memory", "help", "announce", "snippet", "poll", "tldr"] as const; const hasKnownSection = knownSections.some((key) => key in obj); if (!hasKnownSection) return false; for (const key of knownSections) { @@ -1188,6 +1188,41 @@ export function ConfigEditor() { + {/* ═══ Community Feature Toggles ═══ */} + + +
+ Community Features +
+

Enable or disable community commands per guild.

+ {([ + { key: "help", label: "Help / FAQ", desc: "/help command for server knowledge base" }, + { key: "announce", label: "Announcements", desc: "/announce for scheduled messages" }, + { key: "snippet", label: "Code Snippets", desc: "/snippet for saving and sharing code" }, + { key: "poll", label: "Polls", desc: "/poll for community voting" }, + { key: "tldr", label: "TL;DR Summaries", desc: "/tldr for AI channel summaries" }, + ] as const).map(({ key, label, desc }) => ( +
+
+ {label} +

{desc}

+
+ { + setDraftConfig((prev) => ({ + ...prev, + [key]: { ...prev[key], enabled: v }, + })); + }} + disabled={saving} + label={label} + /> +
+ ))} +
+
+ {/* Diff view */} {hasChanges && savedConfig && (