Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,18 @@
"tldr": "everyone"
}
},
"help": {
"enabled": false
},
"announce": {
"enabled": false
},
"snippet": {
"enabled": false
},
"poll": {
"enabled": false
},
"tldr": {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: New config sections not documented in README.md

Per AGENTS.md: "Document new config sections and keys in README.md's config reference when updating config.json."

The help, announce, snippet, poll, and tldr sections need entries in the README config reference. Each needs at minimum the enabled boolean documented, and tldr also needs defaultMessages, maxMessages, and cooldownSeconds.


🔵 Nitpick: Missing trailing newline — File doesn't end with a newline character.

"enabled": false,
"defaultMessages": 50,
Expand Down
5 changes: 5 additions & 0 deletions src/api/utils/configAllowlist.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand Down
8 changes: 8 additions & 0 deletions src/commands/announce.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
11 changes: 10 additions & 1 deletion src/commands/help.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down Expand Up @@ -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) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: autocomplete() (line 371) not gated behind config.help.enabled

When help.enabled is false, autocomplete() still queries the DB and returns topic suggestions. Users see autocomplete results, submit the command, and then get "not enabled" — confusing UX and a wasted DB query.

Add the same gate at the top of autocomplete():

export async function autocomplete(interaction) {
  const config = getConfig(interaction.guildId);
  if (!config.help?.enabled) {
    return await interaction.respond([]);
  }
  // ... existing logic
}

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();
Expand Down
1 change: 1 addition & 0 deletions tests/commands/announce.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ vi.mock('../../src/logger.js', () => ({

vi.mock('../../src/modules/config.js', () => ({
getConfig: vi.fn().mockReturnValue({
announce: { enabled: true },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: No test for the announce.enabled === false path

Same as help.test.js — the mock now returns enabled: true but there's no test verifying the gate rejects when announce.enabled is false.

Add a test like:

it('should reject when announce is disabled', async () => {
  const { getConfig } = await import('../../src/modules/config.js');
  getConfig.mockReturnValueOnce({
    announce: { enabled: false },
    permissions: { enabled: true, adminRoleId: null, usePermissions: true },
  });
  const interaction = createMockInteraction('list');
  await execute(interaction);
  expect(interaction.reply).toHaveBeenCalledWith(
    expect.objectContaining({
      content: expect.stringContaining('not enabled'),
      ephemeral: true,
    }),
  );
  expect(interaction.deferReply).not.toHaveBeenCalled();
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding test case for when announce.enabled is false to verify the gate check works

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/commands/announce.test.js
Line: 16

Comment:
consider adding test case for when `announce.enabled` is false to verify the gate check works

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

permissions: { enabled: true, adminRoleId: null, usePermissions: true },
}),
}));
Expand Down
2 changes: 1 addition & 1 deletion tests/commands/help.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } }),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Warning: No test for the help.enabled === false path

The mock was updated to return enabled: true so existing tests pass, but there's no test verifying that the command returns early with the "not enabled" message when help.enabled is false. This is the main feature of this PR — it should be tested.

Add a test like:

it('should reject when help is disabled', async () => {
  const { getConfig } = await import('../../src/modules/config.js');
  getConfig.mockReturnValueOnce({ help: { enabled: false } });
  const interaction = createInteraction('view');
  await execute(interaction);
  expect(interaction.reply).toHaveBeenCalledWith(
    expect.objectContaining({
      content: expect.stringContaining('not enabled'),
      ephemeral: true,
    }),
  );
  expect(interaction.deferReply).not.toHaveBeenCalled();
});

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding test case for when help.enabled is false to verify the gate check works

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/commands/help.test.js
Line: 13

Comment:
consider adding test case for when `help.enabled` is false to verify the gate check works

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

}));
vi.mock('../../src/utils/permissions.js', () => ({
isModerator: vi.fn().mockReturnValue(true),
Expand Down
37 changes: 36 additions & 1 deletion web/src/components/dashboard/config-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
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) {
Expand Down Expand Up @@ -1188,6 +1188,41 @@ export function ConfigEditor() {
</CardContent>
</Card>

{/* ═══ Community Feature Toggles ═══ */}
<Card>
<CardContent className="space-y-4 pt-6">
<div className="flex items-center justify-between">
<CardTitle className="text-base">Community Features</CardTitle>
</div>
<p className="text-xs text-muted-foreground">Enable or disable community commands per guild.</p>
{([
{ 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 }) => (
<div key={key} className="flex items-center justify-between">
<div>
<span className="text-sm font-medium">{label}</span>
<p className="text-xs text-muted-foreground">{desc}</p>
</div>
<ToggleSwitch
checked={draftConfig[key]?.enabled ?? false}
onChange={(v) => {
setDraftConfig((prev) => ({
...prev,
[key]: { ...prev[key], enabled: v },
}));
}}
disabled={saving}
label={label}
/>
</div>
))}
</CardContent>
</Card>

{/* Diff view */}
{hasChanges && savedConfig && (
<ConfigDiff original={savedConfig} modified={draftConfig} />
Expand Down
Loading