Skip to content

Commit 69ef675

Browse files
committed
feat: add enabled config gates to all community commands + web UI toggles
- /help and /announce now check config.help.enabled / config.announce.enabled - /snippet, /poll, /tldr already have enabled checks from their PRs - Added help, announce, snippet, poll, tldr to SAFE_CONFIG_KEYS (API writable) - Added Community Features card to web config editor with toggle for each - config.json includes all 5 new sections (disabled by default = opt-in)
1 parent 8ac54cf commit 69ef675

7 files changed

Lines changed: 73 additions & 3 deletions

File tree

config.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@
148148
"tldr": "everyone"
149149
}
150150
},
151+
"help": {
152+
"enabled": false
153+
},
154+
"announce": {
155+
"enabled": false
156+
},
157+
"snippet": {
158+
"enabled": false
159+
},
160+
"poll": {
161+
"enabled": false
162+
},
151163
"tldr": {
152164
"enabled": false,
153165
"defaultMessages": 50,

src/api/utils/configAllowlist.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export const SAFE_CONFIG_KEYS = new Set([
1313
'starboard',
1414
'permissions',
1515
'memory',
16+
'help',
17+
'announce',
18+
'snippet',
19+
'poll',
20+
'tldr',
1621
]);
1722

1823
export const READABLE_CONFIG_KEYS = [...SAFE_CONFIG_KEYS, 'logging'];

src/commands/announce.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,14 @@ export function parseTime(timeStr) {
140140
export async function execute(interaction) {
141141
const config = getConfig(interaction.guildId);
142142

143+
if (!config.announce?.enabled) {
144+
await safeReply(interaction, {
145+
content: '❌ The /announce command is not enabled on this server.',
146+
ephemeral: true,
147+
});
148+
return;
149+
}
150+
143151
if (!isModerator(interaction.member, config)) {
144152
await safeReply(interaction, {
145153
content: getPermissionError('announce', 'moderator'),

src/commands/help.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getPool } from '../db.js';
1111
import { info, error as logError } from '../logger.js';
1212
import { getConfig } from '../modules/config.js';
1313
import { isModerator } from '../utils/permissions.js';
14-
import { safeEditReply } from '../utils/safeSend.js';
14+
import { safeEditReply, safeReply } from '../utils/safeSend.js';
1515

1616
const __dirname = dirname(fileURLToPath(import.meta.url));
1717

@@ -325,6 +325,15 @@ async function handleList(interaction) {
325325
* @param {import('discord.js').ChatInputCommandInteraction} interaction
326326
*/
327327
export async function execute(interaction) {
328+
const config = getConfig(interaction.guildId);
329+
if (!config.help?.enabled) {
330+
await safeReply(interaction, {
331+
content: '❌ The /help command is not enabled on this server.',
332+
ephemeral: true,
333+
});
334+
return;
335+
}
336+
328337
await interaction.deferReply({ ephemeral: true });
329338

330339
const subcommand = interaction.options.getSubcommand();

tests/commands/announce.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ vi.mock('../../src/logger.js', () => ({
1313

1414
vi.mock('../../src/modules/config.js', () => ({
1515
getConfig: vi.fn().mockReturnValue({
16+
announce: { enabled: true },
1617
permissions: { enabled: true, adminRoleId: null, usePermissions: true },
1718
}),
1819
}));

tests/commands/help.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ vi.mock('../../src/db.js', () => ({
1010
getPool: vi.fn(),
1111
}));
1212
vi.mock('../../src/modules/config.js', () => ({
13-
getConfig: vi.fn().mockReturnValue({}),
13+
getConfig: vi.fn().mockReturnValue({ help: { enabled: true } }),
1414
}));
1515
vi.mock('../../src/utils/permissions.js', () => ({
1616
isModerator: vi.fn().mockReturnValue(true),

web/src/components/dashboard/config-editor.tsx

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function parseNumberInput(raw: string, min?: number, max?: number): number | und
4848
function isGuildConfig(data: unknown): data is GuildConfig {
4949
if (typeof data !== "object" || data === null || Array.isArray(data)) return false;
5050
const obj = data as Record<string, unknown>;
51-
const knownSections = ["ai", "welcome", "spam", "moderation", "triage", "starboard", "permissions", "memory"] as const;
51+
const knownSections = ["ai", "welcome", "spam", "moderation", "triage", "starboard", "permissions", "memory", "help", "announce", "snippet", "poll", "tldr"] as const;
5252
const hasKnownSection = knownSections.some((key) => key in obj);
5353
if (!hasKnownSection) return false;
5454
for (const key of knownSections) {
@@ -1188,6 +1188,41 @@ export function ConfigEditor() {
11881188
</CardContent>
11891189
</Card>
11901190

1191+
{/* ═══ Community Feature Toggles ═══ */}
1192+
<Card>
1193+
<CardContent className="space-y-4 pt-6">
1194+
<div className="flex items-center justify-between">
1195+
<CardTitle className="text-base">Community Features</CardTitle>
1196+
</div>
1197+
<p className="text-xs text-muted-foreground">Enable or disable community commands per guild.</p>
1198+
{([
1199+
{ key: "help", label: "Help / FAQ", desc: "/help command for server knowledge base" },
1200+
{ key: "announce", label: "Announcements", desc: "/announce for scheduled messages" },
1201+
{ key: "snippet", label: "Code Snippets", desc: "/snippet for saving and sharing code" },
1202+
{ key: "poll", label: "Polls", desc: "/poll for community voting" },
1203+
{ key: "tldr", label: "TL;DR Summaries", desc: "/tldr for AI channel summaries" },
1204+
] as const).map(({ key, label, desc }) => (
1205+
<div key={key} className="flex items-center justify-between">
1206+
<div>
1207+
<span className="text-sm font-medium">{label}</span>
1208+
<p className="text-xs text-muted-foreground">{desc}</p>
1209+
</div>
1210+
<ToggleSwitch
1211+
checked={draftConfig[key]?.enabled ?? false}
1212+
onChange={(v) => {
1213+
setDraftConfig((prev) => ({
1214+
...prev,
1215+
[key]: { ...prev[key], enabled: v },
1216+
}));
1217+
}}
1218+
disabled={saving}
1219+
label={label}
1220+
/>
1221+
</div>
1222+
))}
1223+
</CardContent>
1224+
</Card>
1225+
11911226
{/* Diff view */}
11921227
{hasChanges && savedConfig && (
11931228
<ConfigDiff original={savedConfig} modified={draftConfig} />

0 commit comments

Comments
 (0)