Skip to content

feat: channel-level quiet mode via bot mention (#173)#213

Merged
BillChirico merged 7 commits intomainfrom
feat/issue-173
Mar 2, 2026
Merged

feat: channel-level quiet mode via bot mention (#173)#213
BillChirico merged 7 commits intomainfrom
feat/issue-173

Conversation

@BillChirico
Copy link
Collaborator

Summary

Implements channel-level quiet mode — moderators can silence the bot in a specific channel by mentioning it with @bot quiet.

Closes #173

Changes

Core Module (src/modules/quietMode.js)

  • handleQuietCommand(message, config) — parses quiet/unquiet/status commands from bot mentions
  • isQuietMode(guildId, channelId) — check if bot is currently silenced in a channel
  • Natural language duration parsing: 30m, 2h, 1 hour, for 30 minutes
  • Redis storage with TTL + in-memory Map fallback when Redis is unavailable
  • Permission gating via config.quietMode.allowedRoles (any / moderator / admin / specific role IDs)

Commands

Mention Action
@bot quiet Silence for default duration (30 min)
@bot quiet for 1 hour Silence for custom duration
@bot quiet 45m Silence for 45 minutes
@bot unquiet / resume Resume immediately
@bot status Show remaining quiet time

Events (src/modules/events.js)

  • On bot mention: check for quiet command first, then suppress AI if quiet mode active
  • On regular message accumulation: skip triage buffering if quiet mode active
  • Both paths have error isolation so quiet mode failures don't crash the event handler

Config (config.json)

{
  "quietMode": {
    "enabled": false,
    "allowedRoles": ["moderator"],
    "defaultDurationMinutes": 30,
    "maxDurationMinutes": 1440
  }
}

Dashboard (src/api/utils/configAllowlist.js)

Added quietMode to SAFE_CONFIG_KEYS so the web dashboard can edit it.

Files Changed

  • src/modules/quietMode.js — new module (345 lines)
  • src/modules/events.js — integrate quiet mode checks
  • config.json — add quietMode config section (disabled by default)
  • src/api/utils/configAllowlist.js — add quietMode to allowlist
  • tests/modules/quietMode.test.js — 41 tests

Testing

  • 41 tests, all passing
  • Covers: duration parsing, permission checks, Redis path, memory fallback, all commands
  • Verified pre-existing test failures (17 failures in ai-feedback, triage.coverage, redis) are on main before this branch

Notes

  • Quiet mode is disabled by default — opt-in via dashboard or config
  • Per-channel scope only — other channels unaffected
  • Bot still responds to quiet mode commands even when in quiet mode (so users can unquiet)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing
Copilot AI review requested due to automatic review settings March 2, 2026 04:32
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 2, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 38 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 44a852e and 4c3f2c7.

📒 Files selected for processing (7)
  • config.json
  • src/api/utils/configAllowlist.js
  • src/api/utils/configValidation.js
  • src/modules/events.js
  • src/modules/quietMode.js
  • tests/modules/quietMode.test.js
  • web/src/components/dashboard/ai-feedback-stats.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/issue-173

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Mar 2, 2026

Greptile Summary

This PR implements channel-level quiet mode, allowing moderators to temporarily silence the bot in specific channels via @bot quiet commands. The implementation is well-structured with proper error handling, comprehensive testing, and follows project conventions.

Key Changes:

  • New src/modules/quietMode.js module with Redis storage (memory fallback), natural language duration parsing, and permission checks
  • Integration in src/modules/events.js to suppress AI responses when quiet mode is active
  • Config schema and allowlist updates for dashboard editing
  • 41 comprehensive tests covering all scenarios

Code Quality:

  • ✅ Uses Winston logger (no console.log violations)
  • ✅ Proper error isolation with try-catch blocks
  • ✅ Feature gating (config.quietMode?.enabled)
  • ✅ TTL validation prevents invalid Redis operations
  • ✅ Disabled by default (opt-in)

Note: The missing defaultDurationMinutes property in configValidation.js schema was already acknowledged in previous review threads.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • Clean implementation following all project conventions (Winston logging, ESM, proper error handling), comprehensive test coverage (41 tests), safe defaults (disabled by default), and proper feature gating. The one noted issue (missing validation schema property) was acknowledged in previous review threads and doesn't affect functionality.
  • No files require special attention

Important Files Changed

Filename Overview
src/modules/quietMode.js New quiet mode module with Redis/memory fallback, natural language duration parsing, permission checks, and comprehensive error handling
src/modules/events.js Integrated quiet mode checks in message handler with proper error isolation and feature gating
src/api/utils/configValidation.js Added quietMode validation schema; missing defaultDurationMinutes property (acknowledged in previous review threads)
tests/modules/quietMode.test.js Comprehensive test suite with 41 tests covering duration parsing, permissions, storage (Redis + memory fallback), and all command scenarios

Sequence Diagram

sequenceDiagram
    participant User
    participant Discord
    participant EventHandler as events.js
    participant QuietMode as quietMode.js
    participant Storage as Redis/Memory
    participant AI as Triage/AI

    User->>Discord: @bot quiet 30m
    Discord->>EventHandler: MessageCreate event
    EventHandler->>QuietMode: handleQuietCommand(message, config)
    QuietMode->>QuietMode: Check permissions
    QuietMode->>QuietMode: Parse duration (30m → 1800s)
    QuietMode->>Storage: setQuiet(guildId, channelId, until, userId)
    Storage-->>QuietMode: OK
    QuietMode-->>EventHandler: return true (was quiet command)
    EventHandler->>User: "Going quiet for 30 minutes"
    
    Note over EventHandler,AI: Bot is now in quiet mode for this channel
    
    User->>Discord: Regular message
    Discord->>EventHandler: MessageCreate event
    EventHandler->>QuietMode: isQuietMode(guildId, channelId)
    QuietMode->>Storage: getQuiet(guildId, channelId)
    Storage-->>QuietMode: {until: timestamp, by: userId}
    QuietMode-->>EventHandler: true (quiet mode active)
    EventHandler->>EventHandler: return early (suppress AI)
    
    Note over EventHandler,AI: AI response suppressed
    
    User->>Discord: @bot unquiet
    Discord->>EventHandler: MessageCreate event
    EventHandler->>QuietMode: handleQuietCommand(message, config)
    QuietMode->>Storage: clearQuiet(guildId, channelId)
    Storage-->>QuietMode: OK
    QuietMode-->>EventHandler: return true
    EventHandler->>User: "Quiet mode lifted — I'm back!"
Loading

Last reviewed commit: 4c3f2c7

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a per-channel “quiet mode” feature that lets moderators (or other configured roles) temporarily suppress the bot’s AI behavior in a channel via bot-mention commands, with Redis-backed TTL storage and an in-memory fallback.

Changes:

  • Introduces src/modules/quietMode.js implementing quiet/unquiet/status commands, duration parsing, and Redis+memory storage.
  • Integrates quiet mode checks into the message event flow to suppress AI responses / triage accumulation while active.
  • Exposes quietMode to the dashboard config editor allowlist and adds a default quietMode section to config.json, plus comprehensive module tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/modules/quietMode.js New module for parsing, permission checks, and Redis/memory-backed quiet state management.
src/modules/events.js Hooks quiet mode command handling + quiet checks into mention/reply and accumulation paths.
src/api/utils/configAllowlist.js Adds quietMode to SAFE_CONFIG_KEYS so it can be edited via API/dashboard.
config.json Adds default quietMode config (disabled), but also includes a full-file reformat.
tests/modules/quietMode.test.js Adds Vitest coverage for parsing, permissions, storage, and command handling.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
…urationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

7 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

coderabbitai[bot]
coderabbitai bot previously approved these changes Mar 2, 2026
@BillChirico BillChirico merged commit 4c49a09 into main Mar 2, 2026
10 of 13 checks passed
@BillChirico BillChirico deleted the feat/issue-173 branch March 2, 2026 09:25
BillChirico added a commit that referenced this pull request Mar 2, 2026
* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx
BillChirico added a commit that referenced this pull request Mar 2, 2026
* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx
BillChirico added a commit that referenced this pull request Mar 2, 2026
* security: escape user content in triage prompt delimiters (#164)

Add escapePromptDelimiters() to HTML-encode < and > in user-supplied
message content before it is inserted between XML-style section tags
in the LLM prompt.

Without escaping, a crafted message containing the literal text
`</messages-to-evaluate>` could break out of the user-content section
and inject attacker-controlled instructions into the prompt structure.

Changes:
- Add escapePromptDelimiters(text) utility exported from triage-prompt.js
- Apply escape to m.content and m.replyTo.content in buildConversationText()
- Add 13 new tests covering the escape function and injection scenarios

Closes #164

* security: escape & chars and author fields in prompt delimiters

* fix(security): escape & in prompt delimiters and escape author fields

- Add & → &amp; escape first in escapePromptDelimiters() to prevent
  HTML entity bypass attacks (e.g. &lt;/messages-to-evaluate&gt;)
- Also escape m.author and m.replyTo.author since Discord display
  names are user-controlled and can contain < / > characters

Addresses review feedback on PR #204.

* fix: guard replyTo.content before .slice() to handle null/undefined

* perf: SQL-based conversation pagination + missing DB indexes (#221)

Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.

* feat: channel-level quiet mode via bot mention (#173) (#213)

* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx

* feat: audit log improvements — CSV/JSON export and real-time WebSocket stream (#215)

* feat: audit log improvements — CSV/JSON export, real-time WebSocket stream

- Add GET /:id/audit-log/export endpoint (CSV and JSON, up to 10k rows)
- Add /ws/audit-log WebSocket server for real-time audit entry broadcast
- Refactor buildFilters() shared helper to eliminate duplication
- Hook broadcastAuditEntry() into insertAuditEntry (RETURNING id+created_at)
- Wire setupAuditStream/stopAuditStream into startServer/stopServer lifecycle
- Add escapeCsvValue/rowsToCsv helpers with full test coverage
- 30 route tests + 17 WebSocket stream tests, all green

Closes #136

* fix: PR #215 review feedback - audit stream fixes

- ws.ping() crash: guard with readyState check + try/catch to avoid
  crashing heartbeat interval when socket not OPEN
- stopAuditStream race: make setupAuditStream async and await
  stopAuditStream() to prevent concurrent WebSocketServer creation
- Query param array coercion: add typeof === 'string' checks for
  startDate/endDate to handle Express string|string[]|undefined
- CSV CRLF quoting: add \r to RFC 4180 special-char check for proper
  Windows line ending handling
- Test timeouts: make AUTH_TIMEOUT_MS configurable via
  AUDIT_STREAM_AUTH_TIMEOUT_MS env var, use 100ms in tests

* feat: voice channel activity tracking — join/leave/move, leaderboard, export (#212)

* feat: add voice_sessions migration (#135)

* feat: add voice tracking module — join/leave/move/flush/leaderboard (#135)

* feat: wire voiceStateUpdate handler into event registration (#135)

* feat: add /voice command — leaderboard, stats, export subcommands (#135)

* feat: add voice config defaults to config.json (#135)

* feat: wire voice flush start/stop into bot lifecycle (#135)

* feat: add voice to config API allowlist (#135)

* fix: SQL UPDATE subquery for closeSession, fix import order (#135)

* fix(voice): resolve race conditions and missing config schema

- Fix openSession: update in-memory state only AFTER DB INSERT succeeds
- Fix closeSession: delete from in-memory state only AFTER DB UPDATE succeeds
- Fix: allow closeSession on leave/move even when feature is disabled
- Fix migration: add UNIQUE constraint to partial index to prevent duplicates
- Fix: move 'Voice join' log to after openSession succeeds
- Add voice config to CONFIG_SCHEMA for validation

---------

Co-authored-by: Bill <bill@example.com>

* feat(dashboard): auto-save config with 500ms debounce (#199)

* feat(dashboard): replace manual save with auto-save (500ms debounce)

- Remove 'Save Changes' button; saving now fires automatically 500ms
  after the last config change (no changes → no network call)
- Add saveStatus state ('idle' | 'saving' | 'saved' | 'error') with
  AutoSaveStatus component showing spinner, check, or error+retry
- Add isLoadingConfigRef guard so the initial fetchConfig load never
  triggers a spurious PATCH
- Ctrl+S still works: clears debounce timer and saves immediately
- Keep 'beforeunload' warning for validation errors that block save
- Replace yellow unsaved-changes banner with a destructive validation
  error banner (only shown when save is actually blocked)
- Error state shows 'Save failed' + 'Retry' button for user recovery

Closes #189

* test(dashboard): add auto-save tests for ConfigEditor

- No PATCH on initial config load
- Validation error banner suppresses auto-save
- 'Saving...' spinner visible while PATCH in-flight
- 'Save failed' + Retry button on PATCH error

* fix(dashboard): prevent fetchConfig from overwriting saveStatus after successful save

Add skipSaveStatusReset parameter to fetchConfig so that post-save reloads
preserve the 'saved' status indicator instead of immediately resetting to 'idle'.

* test(dashboard): use fake timers, restore vi.stubGlobal, fix assertions, add idle/saved coverage

- Replace real setTimeout delays with vi.useFakeTimers() + vi.advanceTimersByTimeAsync()
  for deterministic, fast debounce tests
- Add afterEach cleanup: vi.unstubAllGlobals() + vi.useRealTimers()
- Replace toBeTruthy() with toBeInTheDocument() for Testing Library queries
- Add idle state test (no status indicator shown after load)
- Add saved state test (shows 'Saved' after successful save)
- Update file-level comment to list all four states

---------

Co-authored-by: Bill Chirico <bill@volvox.gg>

* feat: Reaction role menus (#162) (#205)

* feat: reaction role menus - core module, command, event hooks, migration

Implements issue #162: reaction role menus.

- Add migration 004 creating reaction_role_menus and reaction_role_entries tables
- Add src/modules/reactionRoles.js with DB helpers, embed builder, event handlers
- Add src/commands/reactionrole.js with /reactionrole create|add|remove|delete|list
- Wire handleReactionRoleAdd/Remove into registerReactionHandlers in events.js

Roles are granted on reaction add and revoked on reaction remove.
All mappings persist in PostgreSQL across bot restarts.

* test: reaction role menus - 40 tests covering module and command

- tests/modules/reactionRoles.test.js: resolveEmojiString, buildReactionRoleEmbed,
  all DB helpers, handleReactionRoleAdd, handleReactionRoleRemove
- tests/commands/reactionrole.test.js: all 5 subcommands (create, add, remove,
  delete, list) including error paths and guild ownership checks
- Fix biome lint: import sort order + unused import removal

* fix: remove unused import in reactionrole command

---------

Co-authored-by: Bill Chirico <bill@volvox.gg>

* fix(security): validate GitHub owner/repo format before gh CLI call (#198)

* fix(security): validate GitHub owner/repo format before gh CLI call

Prevents API path traversal by validating owner/repo segments against
a strict allowlist regex before interpolating them into the gh CLI
invocation.

Adds:
- VALID_GH_NAME regex (/^[a-zA-Z0-9._-]+$/)
- isValidGhRepo() helper (exported for testing)
- Guard in fetchRepoEvents() — returns [] and warns on invalid input
- Strengthened guard in pollGuildFeed() split logic

Fixes #160

* test(security): add validation tests for GitHub owner/repo format

Covers isValidGhRepo(), VALID_GH_NAME regex, and fetchRepoEvents()
validation guard introduced in fix for #160.

19 new tests verify:
- Valid alphanumeric/dot/hyphen/underscore names pass
- Path traversal (../../etc/passwd) is rejected at both entry points
- Slashes, empty strings, non-strings, spaces all rejected
- Shell metacharacters (; && $()) blocked
- gh CLI is NOT invoked when validation fails
- warn() fires with the invalid values (observable audit trail)
- Valid owner/repo still reach gh CLI unchanged

* fix(security): reject pure-dot owner/repo names to prevent path traversal

* test(githubFeed): add tests for pure-dot path traversal bypass

---------

Co-authored-by: Bill Chirico <bill@volvox.gg>

---------

Co-authored-by: Bill <bill@example.com>
Co-authored-by: Bill Chirico <bill@volvox.gg>
BillChirico added a commit that referenced this pull request Mar 2, 2026
* feat: add role_menu_templates migration (#135)

* feat: add roleMenuTemplates module with built-ins, CRUD, and share (#135)

* feat: add /rolemenu command with template CRUD, apply, share (#135)

* feat: seed built-in role menu templates on startup (#135)

* test: add roleMenuTemplates tests — 36 passing (#135)

* test: add /rolemenu command tests — 19 passing (#135)

* fix: typo hasModeatorPerms → hasModeratorPerms

* perf: SQL-based conversation pagination + missing DB indexes (#221)

Fixes three performance bottlenecks identified in code review of
recently merged features (PR #121 conversations viewer, PR #190 AI feedback).

## Changes

### migrations/004_performance_indexes.cjs (new)
Four new indexes targeting hot query paths:

- idx_ai_feedback_guild_created (guild_id, created_at DESC)
  getFeedbackTrend() and getRecentFeedback() filtered by guild_id
  AND created_at but only had a single-column guild_id index, forcing
  a full guild scan + sort on every trend/recent call.

- idx_conversations_content_trgm (GIN, pg_trgm)
  content ILIKE '%...%' search was a sequential scan. GIN/trgm index
  reduces this from O(n) to O(log n * trigram matches).
  Requires pg_trgm extension (added idempotently).

- idx_conversations_guild_created (guild_id, created_at DESC)
  Default 30-day listing query filters guild_id + created_at. The
  existing 3-column (guild_id, channel_id, created_at) composite is
  suboptimal when channel_id is not in the predicate.

- idx_flagged_messages_guild_message (guild_id, message_id)
  Conversation detail + flag endpoints query flagged_messages by
  guild_id AND message_id = ANY(...). Existing index only covers
  (guild_id, status).

### src/api/routes/conversations.js
**GET / — Replace in-memory pagination with SQL CTE grouping**

Before: fetched up to 10,000 message rows into Node memory, grouped
them in JavaScript (O(n) time + memory), then sliced for pagination.
Every page request loaded the full 10k row dataset.

After: single SQL query using window functions (LAG + SUM OVER) to
identify conversation boundaries and aggregate summaries directly.
COUNT(*) OVER() provides total count without a second query.
Pagination happens at the DB with LIMIT/OFFSET on summary rows.
Memory overhead is now proportional to page size (default 25), not
total conversation volume.

Removed now-unused buildConversationSummary() helper (logic inlined
into the SQL-side aggregation).

**POST /:conversationId/flag — Parallel verification queries**

Before: msgCheck and anchorCheck ran sequentially (~2× RTT).
After: both run in parallel via Promise.all (1× RTT for verification).

### tests/api/routes/conversations.test.js
Updated 'should return paginated conversations' test to mock the new
SQL CTE response shape (pre-aggregated summary rows) instead of raw
message rows. All 41 conversation tests pass.

* feat: channel-level quiet mode via bot mention (#173) (#213)

* feat: quiet mode per-channel via bot mention (#173)

- Add quietMode.js module with Redis+memory storage
- Parse duration from natural language (30m, 1 hour, etc.)
- Permission gated via config.quietMode.allowedRoles
- Commands: quiet, unquiet, status
- Suppress AI responses during quiet mode in events.js
- Add quietMode section to config.json (disabled by default)
- Add quietMode to configAllowlist.js for dashboard editing

* test: add quiet mode tests (41 tests, all passing)

* style: fix biome formatting in quietMode.js, events.js, and test

* fix(web): fix ai-feedback-stats TypeScript and formatting errors

* fix: gate quiet mode checks on enabled flag, validate TTL, honor maxDurationMinutes config

- events.js: Wrap isQuietMode() calls in guildConfig.quietMode?.enabled check
  to avoid unnecessary Redis lookups and prevent stale records from suppressing
  AI responses when the feature is disabled (PRRT_kwDORICdSM5xdbmp, PRRT_kwDORICdSM5xdbmx)

- quietMode.js: Add TTL validation in setQuiet() to guard against 0, negative,
  or NaN values that would error in Redis (PRRT_kwDORICdSM5xdbm3)

- quietMode.js: Update parseDurationFromContent() to accept config parameter
  and honor guildConfig.quietMode.maxDurationMinutes. Also clamp defaultSeconds
  to the effective max (PRRT_kwDORICdSM5xdbm_)

- configValidation.js: Add quietMode schema entry with enabled, maxDurationMinutes,
  and allowedRoles properties (PRRT_kwDORICdSM5xdbnH)

* style: fix biome formatting in quietMode.js and ai-feedback-stats.tsx

* Fix: unterminated string in rolemenu.js

* Fix: lint issues and formatting

* fix: deterministic template lookup and correct roleId precedence

- Add ORDER BY to getTemplateByName for deterministic results
- Fix roleId precedence to preserve existing roleIds during merge
- Truncate Discord embed field values to 1024 chars

* fix: test assertion matches comment intent

The test expected template roleId to win, but the comment said existing
should take precedence. Fixed assertion to match documented behavior.

* fix: filter empty roleIds and only enable when valid options exist

- Filter out options with empty roleIds before saving
- Only enable role menu for non-built-in templates with valid options
- Add user-facing note when options are filtered

* chore: remove unused _MAX_DESCRIPTION_LEN constant

* fix: case-insensitive unique index for template names

Use LOWER(name) in unique index to match case-insensitive queries
and prevent duplicate templates differing only by case.

* fix(roleMenuTemplates): add type validation for roleId and description

- validateTemplateOptions now validates that optional roleId and
  description fields are strings when present
- Update JSDoc @see reference from issue #135 (voice tracking) to
  issue #216 (role menu templates)
- Update ON CONFLICT clause to use constraint name for consistency
  with the new LOWER(name) index

---------

Co-authored-by: Bill <bill@example.com>
Co-authored-by: Bill Chirico <bill@volvox.gg>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Channel-level quiet mode via bot mention

2 participants