Skip to content

feat: /poll voting system with buttons (#47)#104

Merged
BillChirico merged 2 commits intomainfrom
feat/poll-system
Feb 27, 2026
Merged

feat: /poll voting system with buttons (#47)#104
BillChirico merged 2 commits intomainfrom
feat/poll-system

Conversation

@BillChirico
Copy link
Collaborator

Summary

Adds a rich poll system with Discord button-based voting and auto-close.

What's New

Migration

  • migrations/003_polls.cjs: Creates polls table with JSONB options/votes, indexes for guild and open polls

Command: /poll

  • /poll create — Create a poll with 2-10 comma-separated options, optional duration, multi-vote, and anonymous mode
  • /poll close — Close a poll early (author or moderator only)
  • /poll list — List active polls in the server

Voting

  • Button-based voting with poll_vote_<pollId>_<optionIndex> custom IDs
  • Single-vote mode: clicking a new option replaces previous vote, clicking same removes it
  • Multi-vote mode: each click toggles that option independently
  • Ephemeral confirmations: ✅ Voted for X / ❌ Vote removed for X
  • Live embed updates with vote bars (▓▓▓▓▓░░░░░ 50% (5 votes))

Auto-Close

  • Integrated into existing scheduler — expired polls close on each tick (60s)
  • Closed polls show final results with disabled buttons

Tests

  • 27 new tests covering create (valid/invalid options), voting (single/multi mode), close (author/mod/permission), list, embed building, and button construction

Files Changed

  • migrations/003_polls.cjs — new
  • src/commands/poll.js — new
  • src/modules/pollHandler.js — new (embed builder, vote handler, close logic)
  • src/modules/events.js — added poll button handler registration
  • src/modules/scheduler.js — added expired poll close check
  • tests/commands/poll.test.js — new (27 tests)

All 1672 tests pass. Biome clean on all changed files.

Closes #47

- Add polls migration (003_polls.cjs) with JSONB options/votes
- Add /poll create, close, list subcommands
- Add button interaction handler for poll_vote_<id>_<idx> buttons
- Support single-vote and multi-vote modes
- Auto-close expired polls via scheduler integration
- Build poll embeds with vote bars and live vote counts
- 27 new tests covering create, vote, close, list, embed, and buttons

Closes #47
Copilot AI review requested due to automatic review settings February 27, 2026 02:32
@claude
Copy link

claude bot commented Feb 27, 2026

Claude finished @BillChirico's task in 3m 17s —— View job


Code Review: PR #104 — Poll Voting System

  • Read AGENTS.md for project conventions
  • Read all changed files
  • Analyze code quality, architecture, security, and testing
  • Post inline comments for specific issues
  • Post top-level summary with verdict

Verdict: Changes Requested — 8 issues found (2 critical, 5 warnings, 1 nitpick).

The second commit (77e0f09) fixed several critical issues from earlier reviews (race condition via FOR UPDATE, button label overflow, embed title truncation, guild scoping on votes, closes_at expiry check). The core poll logic is solid and test coverage is good (27 tests).

Remaining issues:

# Severity File Issue
1 🔴 Critical src/commands/poll.js:64-68 getPool() throws — null-check is dead code
2 🔴 Critical config.json poll missing from allowedCommands — defaults to admin-only
3 🟡 Warning src/commands/poll.js:70-79 No top-level try/catch for subcommand handlers
4 🟡 Warning src/commands/poll.js:140-143 Uses channel.send() instead of safeSend()
5 🟡 Warning src/modules/pollHandler.js:216-218 message.edit() without allowedMentions
6 🟡 Warning src/modules/pollHandler.js:266 closePoll() edit without allowedMentions
7 🟡 Warning src/modules/scheduler.js:183 Poll expiry coupled to scheduled-message success
8 🔵 Nitpick src/commands/poll.js:24 No .setMaxLength(256) on question option

See inline comments for suggested fixes. Documentation updates also needed (AGENTS.md, README.md, config.json) per project conventions.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Warning

Rate limit exceeded

@BillChirico has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 6 minutes and 3 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 b8d9bc2 and 77e0f09.

📒 Files selected for processing (3)
  • src/commands/help.js
  • src/modules/pollHandler.js
  • tests/commands/poll.test.js
📝 Walkthrough

Walkthrough

Introduces a complete poll system with database migration, slash command handlers, voting mechanics, embed builders, and scheduler integration. Includes poll creation with configurable options, manual/auto-closure, button-based voting with single/multi-vote support, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Database Schema
migrations/003_polls.cjs
Creates polls table with fields for guild/channel/message/author IDs, question, JSON options and votes, multi-vote and anonymous flags, duration and closure tracking. Includes indexes on guild_id and open polls.
Poll Command
src/commands/poll.js
Implements /poll slash command with create (question, options 2-10, duration, multi, anonymous), close (ID-based, author/moderator only), and list (active polls) subcommands. Handles poll creation with database insertion, embed/button generation, and message posting.
Poll System Core
src/modules/pollHandler.js
Provides poll utilities: buildPollEmbed (vote tallies, bars, footer), buildPollButtons (paginated 5-per-row), handlePollVote (single/multi-vote toggling), closePoll (mark closed, disable buttons), and closeExpiredPolls (queries and closes expired entries).
Event Handling
src/modules/events.js
Registers poll vote button interaction handler via registerPollButtonHandler function, filtering for poll_vote_ customIds and delegating to handlePollVote with error logging.
Scheduler Integration
src/modules/scheduler.js
Adds closeExpiredPolls call in scheduled message processing to clean up expired polls after each batch.
Test Coverage
tests/commands/poll.test.js
Comprehensive tests covering command metadata, create/close/list subcommands, voting (single/multi modes, toggling), embed verification, button distribution, permission checks, and edge cases.

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the main feature: a poll voting system with button-based voting, directly matching the primary changeset focus.
Description check ✅ Passed The PR description is well-organized and directly relates to the changeset, covering all major components including migrations, commands, voting mechanics, auto-close, and tests.
Linked Issues check ✅ Passed The PR implements all core requirements from issue #47: /poll create with 2-10 options [#47], button-based voting [#47], poll options (duration, multi, anonymous) [#47], auto-close functionality [#47], /poll close for manual close [#47], database persistence [#47], and prevents double-voting in single-vote mode [#47].
Out of Scope Changes check ✅ Passed All changes are directly scoped to the poll system implementation: database migration, poll command, poll handler module, event handler registration, scheduler integration, and comprehensive tests. No unrelated changes detected.
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/poll-system

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.

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary

🔴 Critical (1)

  1. Race condition on vote writes (src/modules/pollHandler.js:146-172) — Read-modify-write on the JSONB votes column without any locking. Concurrent voters can silently lose votes. Needs SELECT ... FOR UPDATE in a transaction or atomic JSONB update.

🟡 Warning (3)

  1. Vote lookup not scoped to guild_id (src/modules/pollHandler.js:119) — Query fetches poll by id only; should include guild_id for defense-in-depth (consistent with /poll close).
  2. Button label exceeds 80-char Discord limit (src/modules/pollHandler.js:90-92) — Option text truncated to 80 chars, then "N. " prefix prepended → up to 84 chars. Discord will reject labels > 80 chars.
  3. poll missing from config.json allowedCommands — Without an entry, the permission system defaults to admin-only (permissions.js:98). Add "poll": "everyone" (or "moderator") to allowedCommands.

🔵 Nitpick (3)

  1. No getPool() null check in handlePollVote, closePoll, closeExpiredPolls — would throw TypeError if DB is down.
  2. question length not validated (src/commands/poll.js:84-95) — Embed titles cap at 256 chars; slash command strings allow 6000. Long questions will fail.
  3. Documentation not updated — Per AGENTS.md, new commands/modules require updates to: AGENTS.md Key Files table + Database Tables section, README.md command list, and config.json docs.

See inline comments for suggested fixes.

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Greptile Summary

Adds a complete button-based poll system with robust transaction handling and auto-expiration.

Key Changes:

  • Database: New polls table with JSONB options/votes, partial index for open polls
  • Command: /poll create/close/list with 2-10 option validation, duration support, multi-vote mode
  • Voting: Button interactions with FOR UPDATE row locking to prevent race conditions
  • Auto-close: Integrated into existing 60s scheduler, closes expired polls and disables buttons
  • UI: Vote bars with percentages, proper truncation for Discord limits (256 char titles, 80 char buttons)
  • Tests: 27 tests covering all subcommands, voting modes, edge cases, and UI builders

Code Quality:

  • Follows project conventions: Winston logging, parameterized queries, safeSend wrappers
  • Race condition from previous review resolved with transaction + FOR UPDATE locking
  • Comprehensive error handling with isolated failures (individual poll close errors don't crash scheduler)
  • All 1672 tests passing, Biome clean

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The implementation is production-ready: the race condition from previous review has been resolved with proper transaction + FOR UPDATE locking, all code follows project conventions (Winston logging, parameterized queries, safeSend wrappers), comprehensive 27-test suite with all 1672 tests passing, proper Discord limit handling (embed titles, button labels), isolated error handling prevents cascading failures, and Biome clean
  • No files require special attention

Important Files Changed

Filename Overview
migrations/003_polls.cjs Creates polls table with proper JSONB columns, indexes for guild filtering and open polls query optimization
src/commands/poll.js Implements /poll create/close/list subcommands with proper validation (2-10 options), moderator checks, and Winston logging
src/modules/pollHandler.js Handles button voting with transaction + FOR UPDATE locking (addresses race condition), correct embed/button truncation logic
src/modules/events.js Registers poll button handler with proper error handling and ephemeral error messages
src/modules/scheduler.js Integrates expired poll closing into existing 60s scheduler loop with isolated error handling
tests/commands/poll.test.js Comprehensive 27-test suite covering create/close/list commands, single/multi-vote modes, edge cases, and UI builders

Sequence Diagram

sequenceDiagram
    participant U as User
    participant D as Discord
    participant C as poll.js
    participant H as pollHandler.js
    participant DB as PostgreSQL
    participant S as scheduler.js

    Note over U,S: Poll Creation Flow
    U->>D: /poll create
    D->>C: execute(interaction)
    C->>C: validate options (2-10)
    C->>DB: INSERT poll
    DB-->>C: poll row with id
    C->>H: buildPollEmbed(poll)
    C->>H: buildPollButtons(pollId)
    C->>D: send poll message
    D-->>C: message with id
    C->>DB: UPDATE message_id
    C-->>U: ✅ Poll created

    Note over U,S: Button Voting Flow
    U->>D: Click button
    D->>H: handlePollVote(interaction)
    H->>DB: BEGIN + SELECT FOR UPDATE
    DB-->>H: locked poll row
    H->>H: toggle vote logic
    H->>DB: UPDATE votes + COMMIT
    H->>H: buildPollEmbed(updated)
    H->>D: edit message embed
    H-->>U: ✅ Voted (ephemeral)

    Note over U,S: Auto-Close Flow
    loop Every 60 seconds
        S->>DB: SELECT expired polls
        DB-->>S: poll ids
        S->>H: closePoll(pollId, client)
        H->>DB: UPDATE closed = true
        H->>D: edit message (disabled buttons)
    end
Loading

Last reviewed commit: 77e0f09

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.

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link

greptile-apps bot commented Feb 27, 2026

Additional Comments (1)

config.json
poll command missing from allowedCommands — defaults to admin-only

    "allowedCommands": {
      "ping": "everyone",
      "memory": "everyone",
      "poll": "everyone",
      "config": "admin",
Prompt To Fix With AI
This is a comment left during a code review.
Path: config.json
Line: 136-156

Comment:
`poll` command missing from `allowedCommands` — defaults to admin-only

```suggestion
    "allowedCommands": {
      "ping": "everyone",
      "memory": "everyone",
      "poll": "everyone",
      "config": "admin",
```

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@migrations/003_polls.cjs`:
- Around line 28-29: Add a partial index to support the auto-close scheduler by
indexing the closes_at column for open polls: create an index on
polls(closes_at) with a WHERE closed = false (e.g., name it
idx_polls_closes_at_open) so the expiry closer can seek by closes_at without
scanning the whole polls table; add this via pgm.sql in the same migration
alongside the existing idx_polls_guild and idx_polls_open entries.
- Around line 10-25: Add DB-level CHECK constraints on the polls table to
guarantee JSON shape: update the CREATE TABLE for polls to include a CHECK on
options such as jsonb_typeof(options) = 'array' AND jsonb_array_length(options)
BETWEEN 2 AND 10, and a CHECK on votes such as jsonb_typeof(votes) = 'object'
(so the default '{}' remains valid). Place these checks alongside the column
definitions for options and votes so malformed JSONB rows are rejected at
insert/update.

In `@src/commands/poll.js`:
- Around line 64-67: getPool() can throw when the DB isn't initialized so the
current null-check never executes; wrap the getPool() call in a try/catch around
where it's used in the command (the code that currently calls const pool =
getPool()) and handle the thrown error by calling await
safeEditReply(interaction, { content: '❌ Database is not available.' }) (or
similar user-facing message) and returning; ensure you reference the same
interaction and safeEditReply call so the command always responds instead of
crashing.
- Around line 117-147: The code inserts the poll into the DB before sending and
uses interaction.channel.send directly, which can leave a DB row without a
message_id if send fails and bypasses the project's safeSend wrapper; change the
flow to use the safeSend(messageOptions) utility (instead of
interaction.channel.send) when sending the embed built by buildPollEmbed and
components from buildPollButtons, and ensure you only INSERT or persist the poll
row after a successful send (or if you must INSERT first, catch send failures
and DELETE or roll back the DB row using pool.query so no stale poll remains),
then update the poll row with message_id only after safeSend succeeds.
- Around line 85-95: Validate the parsed question and options in handleCreate()
before building the embed/components and saving to the DB: ensure
question.length <= 256 (Embed title limit), ensure the combined options text
used in the embed description is <= 4096, and ensure each button label generated
from the options array (refer to the options variable created from optionsRaw)
is truncated so the final label including any prefix like "N. " is <= 80
characters (fix the current slice logic that slices to 77 then prefixes, instead
compute available space = 80 - prefix.length and slice to that). If any limit is
exceeded, reject the creation with a clear validation error instead of
proceeding to build/send the message.

In `@src/modules/events.js`:
- Around line 295-301: The poll button handler currently accepts all
interactions with customId starting with 'poll_vote_' and calls handlePollVote
without checking guild-level feature flags; update registerPollButtonHandler to
first ensure the interaction comes from a guild (use interaction.guildId or
interaction.guild?.id) and then check the guild's config for the polls module
(e.g. call your existing helper like isModuleEnabled(guildId, 'polls') or
getGuildConfig(guildId).polls.enabled) and return early if the module is
disabled before invoking handlePollVote. Ensure the check runs before any async
work and preserve existing behavior for non-guild interactions and non-poll
buttons.

In `@src/modules/pollHandler.js`:
- Around line 130-136: The interaction vote path currently only checks
poll.closed; update the handler in pollHandler.js (the interaction vote branch
that calls safeReply) to also reject votes whose poll.closes_at is in the past:
compute current time (e.g. Date.now()/new Date()) and if poll.closes_at && new
Date(poll.closes_at) <= now, respond via safeReply with the same
closed/ephemeral message and return; ensure this check runs before any
state-changing vote logic so votes after the deadline are not accepted.
- Around line 146-173: The vote update currently does a read-modify-write on
poll.votes (using poll, pollId, votes, optionIndex and interaction.user.id)
which can race; wrap the read and UPDATE in a DB transaction with a row lock:
get a client from pool (pool.connect()), BEGIN, SELECT votes FROM polls WHERE id
= $1 FOR UPDATE to load the latest votes, apply the same toggle/replace logic to
that locked row's votes, then UPDATE polls SET votes = $1 WHERE id = $2 and
COMMIT; ensure you RELEASE the client on error/finish and roll back on failure
so concurrent clicks serialize and no votes are lost.
- Around line 90-95: The button label truncation must account for the numeric
prefix length; in the poll creation code (see variables options, i, pollId and
the ButtonBuilder call inside currentRow.addComponents) compute the prefix like
`${i + 1}. `, then compute maxTextLen = Math.max(0, 80 - prefix.length), use
that to decide if and how to slice options[i] (e.g., slice to maxTextLen and
append "..." only when truncated), and finally setLabel to
`${prefix}${truncatedOrFullText}` so the final label never exceeds Discord's
80-character limit.

In `@src/modules/scheduler.js`:
- Around line 182-183: The call to closeExpiredPolls(client) is currently inside
the same outer try as the scheduled-message polling so if scheduled-message
processing throws, poll closure is skipped; move closeExpiredPolls(client) into
its own independent try/catch block (or finally-level block) so it always runs
regardless of scheduled-message errors, catching and logging any errors from
closeExpiredPolls separately to avoid bubbling and interfering with the
scheduled-message flow.

In `@tests/commands/poll.test.js`:
- Around line 561-786: Add two tests to tests/commands/poll.test.js: (1) "should
reject vote if poll already closed by closes_at" — reuse
createMockButtonInteraction and import handlePollVote, mock the SELECT to return
a poll object whose closes_at is a Date/time in the past (and closed: false) and
assert interaction.reply is called with an ephemeral message containing "poll is
closed" (or similar closed wording used by handlePollVote); (2) "should handle
getPool() throwing during poll handling" — simulate getPool throwing by making
getPool.mockImplementationOnce(() => { throw new Error('boom') }) before
importing/ calling handlePollVote, then call handlePollVote with a mock
interaction and assert interaction.reply is called with an ephemeral error
message (or that the error path used by handlePollVote is invoked); reference
the existing helpers createMockButtonInteraction, mockPool/mockReturnValue or
getPool mock and the handlePollVote function when adding these tests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a3d0639 and b8d9bc2.

📒 Files selected for processing (6)
  • migrations/003_polls.cjs
  • src/commands/poll.js
  • src/modules/events.js
  • src/modules/pollHandler.js
  • src/modules/scheduler.js
  • tests/commands/poll.test.js
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Agent
  • GitHub Check: Greptile Review
  • GitHub Check: claude-review
🧰 Additional context used
📓 Path-based instructions (6)
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

**/*.{js,ts,jsx,tsx}: Use ESM modules with import/export syntax; never use require()
Always use node: protocol for Node.js builtins (e.g., import { readFileSync } from 'node:fs')
Always use semicolons in code
Use single quotes for strings (enforced by Biome)
Use 2-space indentation (enforced by Biome)

Files:

  • src/modules/events.js
  • tests/commands/poll.test.js
  • src/modules/pollHandler.js
  • src/commands/poll.js
  • src/modules/scheduler.js
src/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{js,ts,jsx,tsx}: Always use Winston for logging via import { info, warn, error } from '../logger.js'; never use console.log, console.warn, console.error, or any console.* method in src/ files
Pass structured metadata to Winston logging calls (e.g., info('Message processed', { userId, channelId }))
Use custom error classes from src/utils/errors.js for error handling
Always log errors with context before re-throwing
Use getConfig(guildId?) from src/modules/config.js to read configuration values
Use setConfigValue(path, value, guildId?) from src/modules/config.js to update configuration at runtime
Use safeSend() utility for all outgoing Discord messages to enforce allowedMentions and prevent mention spam
Use sanitizeMentions() to strip @everyone/@here from outgoing text via zero-width space insertion before sending
Use splitMessage() utility to handle Discord's 2000-character message limit
onConfigChange callbacks receive (newValue, oldValue, fullPath, guildId) as parameters

Files:

  • src/modules/events.js
  • src/modules/pollHandler.js
  • src/commands/poll.js
  • src/modules/scheduler.js
src/modules/events.js

📄 CodeRabbit inference engine (AGENTS.md)

Module handler functions must be registered in src/modules/events.js via client.on() event listeners

Files:

  • src/modules/events.js
src/modules/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Check config.yourModule.enabled before processing in module handlers

Files:

  • src/modules/events.js
  • src/modules/pollHandler.js
  • src/modules/scheduler.js
tests/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

tests/**/*.{js,ts,jsx,tsx}: All new code must include tests; test coverage must maintain 80% threshold on statements, branches, functions, and lines
Use Vitest for testing; run pnpm test before every commit and pnpm test:coverage to verify 80% coverage threshold

Files:

  • tests/commands/poll.test.js
src/commands/**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/commands/**/*.{js,ts,jsx,tsx}: Slash commands must export a data property with a SlashCommandBuilder and an async execute(interaction) function
Export adminOnly = true for moderator-only slash commands
Duration-based commands (timeout, tempban, slowmode) must use parseDuration() from src/utils/duration.js for parsing duration arguments
Always call checkHierarchy(moderator, target) before executing moderation actions to prevent moderating users with equal or higher roles

Files:

  • src/commands/poll.js
🧠 Learnings (7)
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to src/modules/events.js : Module handler functions must be registered in `src/modules/events.js` via `client.on()` event listeners

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to src/**/*.{js,ts,jsx,tsx} : Use `safeSend()` utility for all outgoing Discord messages to enforce allowedMentions and prevent mention spam

Applied to files:

  • src/modules/events.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to src/commands/**/*{ban,kick,warn,timeout,mute}*.{js,ts,jsx,tsx} : Moderation commands must follow the pattern: deferReply() → validate → sendDmNotification() → execute Discord action → createCase() → sendModLogEmbed() → checkEscalation()

Applied to files:

  • src/modules/events.js
  • src/commands/poll.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to tests/**/*.{js,ts,jsx,tsx} : Use Vitest for testing; run `pnpm test` before every commit and `pnpm test:coverage` to verify 80% coverage threshold

Applied to files:

  • tests/commands/poll.test.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to tests/**/*.{js,ts,jsx,tsx} : All new code must include tests; test coverage must maintain 80% threshold on statements, branches, functions, and lines

Applied to files:

  • tests/commands/poll.test.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to src/commands/**/*.{js,ts,jsx,tsx} : Slash commands must export a `data` property with a SlashCommandBuilder and an async `execute(interaction)` function

Applied to files:

  • src/commands/poll.js
📚 Learning: 2026-02-26T22:59:10.394Z
Learnt from: CR
Repo: VolvoxLLC/volvox-bot PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-02-26T22:59:10.394Z
Learning: Applies to src/index.js : Tempban scheduler runs on a 60-second interval; started in `index.js` startup and stopped in graceful shutdown

Applied to files:

  • src/modules/scheduler.js
🧬 Code graph analysis (5)
src/modules/events.js (2)
src/modules/pollHandler.js (1)
  • handlePollVote (109-202)
src/utils/safeSend.js (1)
  • safeReply (138-145)
tests/commands/poll.test.js (4)
src/commands/poll.js (6)
  • subcommand (70-70)
  • options (92-95)
  • poll (135-135)
  • poll (182-182)
  • execute (61-79)
  • embed (136-136)
src/modules/pollHandler.js (12)
  • options (22-22)
  • poll (128-128)
  • poll (221-221)
  • i (84-84)
  • userId (146-146)
  • handlePollVote (109-202)
  • embed (59-63)
  • embed (179-179)
  • embed (228-228)
  • buildPollEmbed (21-70)
  • rows (81-81)
  • buildPollButtons (80-102)
src/db.js (1)
  • getPool (142-147)
src/utils/permissions.js (1)
  • isModerator (143-173)
src/modules/pollHandler.js (4)
src/commands/poll.js (8)
  • poll (135-135)
  • poll (182-182)
  • options (92-95)
  • lines (237-237)
  • pool (64-64)
  • pool (118-133)
  • pool (170-173)
  • pool (221-227)
src/db.js (1)
  • getPool (142-147)
src/utils/safeSend.js (1)
  • safeReply (138-145)
src/logger.js (1)
  • info (230-232)
src/commands/poll.js (5)
src/modules/pollHandler.js (20)
  • pool (116-116)
  • pool (119-119)
  • pool (212-212)
  • pool (214-217)
  • pool (248-248)
  • pool (249-251)
  • options (22-22)
  • poll (128-128)
  • poll (221-221)
  • rows (81-81)
  • embed (59-63)
  • embed (179-179)
  • embed (228-228)
  • buildPollEmbed (21-70)
  • buttons (229-229)
  • buildPollButtons (80-102)
  • pollId (113-113)
  • closePoll (211-239)
  • lines (38-44)
  • voterCount (36-36)
src/db.js (1)
  • getPool (142-147)
src/utils/safeSend.js (1)
  • safeEditReply (178-185)
src/modules/config.js (1)
  • getConfig (282-313)
src/utils/permissions.js (1)
  • isModerator (143-173)
src/modules/scheduler.js (1)
src/modules/pollHandler.js (1)
  • closeExpiredPolls (246-263)
🔇 Additional comments (1)
src/modules/events.js (1)

352-352: Good central registration for the new poll interaction handler.

Wiring this in registerEventHandlers keeps module event bindings consistent and discoverable.

Based on learnings Applies to src/modules/events.js : Module handler functions must be registered in src/modules/events.js via client.on() event listeners.

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

Introduces a /poll feature with persistent storage, button-based voting, live result embeds, and auto-closing via the existing scheduler loop.

Changes:

  • Adds a polls table migration to persist poll state (options, votes, close time, etc.).
  • Implements /poll create|close|list plus a poll handler for voting, embed/button rendering, and auto-close.
  • Registers a button interaction handler and adds an expiry check to the scheduler; includes a dedicated test suite.

Reviewed changes

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

Show a summary per file
File Description
migrations/003_polls.cjs Creates polls table and basic indexes for guild/open polls.
src/commands/poll.js Adds /poll command implementation (create/close/list).
src/modules/pollHandler.js Implements vote handling, embed/button builders, close + expiry logic.
src/modules/events.js Registers a button interaction handler for poll votes.
src/modules/scheduler.js Calls poll expiry close check on each scheduler tick.
tests/commands/poll.test.js Adds test coverage for command behavior and poll handler utilities.

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

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Review Summary

The second commit (77e0f09) addressed several critical issues from earlier reviews (race condition, button label overflow, embed title truncation, guild scoping on votes, closes_at check). Good work on those fixes.

However, 8 issues remain that should be addressed before merge:

🔴 Critical (2)

  1. getPool() throws — null-check is dead code (src/commands/poll.js:64-68) — getPool() throws when DB is uninitialized, so the if (!pool) branch never executes. Needs try/catch.
  2. poll missing from config.json allowedCommands — Without an entry, permissions.js:98 defaults to admin-only. Regular users can't use /poll. Add "poll": "everyone" (file not in diff — needs separate change).

🟡 Warning (5)

  1. No top-level try/catch (src/commands/poll.js:70-79) — Unlike help.js, subcommand dispatch isn't wrapped. Unhandled throws leave the deferred reply dangling.
  2. Uses channel.send() instead of safeSend() (src/commands/poll.js:140-143) — Bypasses mention sanitization. A poll question with @everyone would ping the server.
  3. message.edit() without allowedMentions (src/modules/pollHandler.js:216-218) — Vote embed updates lack mention restrictions.
  4. message.edit() without allowedMentions in closePoll() (src/modules/pollHandler.js:266) — Same issue on poll close.
  5. Poll expiry coupled to scheduled-message success (src/modules/scheduler.js:183) — closeExpiredPolls() is inside the same try block; if scheduled messages throw first, poll expiry is skipped.

🔵 Nitpick (1)

  1. No .setMaxLength(256) on question option (src/commands/poll.js:24) — Slash command allows 6000 chars but embed titles cap at 256. Adding the constraint gives users Discord's built-in validation UI.

Documentation (not in diff)

Per AGENTS.md conventions, new commands/modules require updates to:

  • AGENTS.md — Key Files table (add src/commands/poll.js, src/modules/pollHandler.js, migrations/003_polls.cjs) and Database Tables section (add polls)
  • README.md — command list (add /poll)
  • config.jsonallowedCommands (add "poll": "everyone")
AI Fix Prompt — copy/paste to fix all issues
Fix the following issues in the volvox-bot repo on branch feat/poll-system:

1. src/commands/poll.js:64-68 — Replace `const pool = getPool(); if (!pool) { ... }` with a try/catch:
   let pool;
   try { pool = getPool(); } catch { await safeEditReply(interaction, { content: '❌ Database is not available.' }); return; }

2. src/commands/poll.js:70-79 — Wrap the subcommand dispatch (lines 72-78) in a try/catch that logs with warn() and sends a fallback safeEditReply error message, matching the pattern in help.js:332-353.

3. src/commands/poll.js:14 — Change import to: import { safeEditReply, safeSend } from '../utils/safeSend.js';
   src/commands/poll.js:140-143 — Replace interaction.channel.send({...}) with safeSend(interaction.channel, {...})

4. src/commands/poll.js:24 — Add .setMaxLength(256) to the question string option builder chain.

5. src/modules/pollHandler.js:216-218 — Add allowedMentions: { parse: [] } to interaction.message.edit() call.

6. src/modules/pollHandler.js:266 — Add allowedMentions: { parse: [] } to message.edit() call in closePoll().

7. src/modules/scheduler.js:182-183 — Move closeExpiredPolls(client) out of the existing try block into its own independent try/catch block after the catch on line 184, so it runs even if scheduled-message processing fails.

8. config.json — Add "poll": "everyone" to permissions.allowedCommands (after the "announce" entry).

9. AGENTS.md — Add poll.js, pollHandler.js, and 003_polls.cjs to the Key Files table. Add "polls" to the Database Tables section.

10. README.md — Add /poll to the command list.

@BillChirico BillChirico merged commit e94448c into main Feb 27, 2026
8 of 9 checks passed
@BillChirico BillChirico deleted the feat/poll-system branch February 27, 2026 13:05
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.

Poll system — /poll command for community voting with real-time results

2 participants