feat: /poll voting system with buttons (#47)#104
Conversation
- 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
|
Claude finished @BillChirico's task in 3m 17s —— View job Code Review: PR #104 — Poll Voting System
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 Remaining issues:
See inline comments for suggested fixes. Documentation updates also needed (AGENTS.md, README.md, config.json) per project conventions. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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. 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughIntroduces 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
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Review Summary
🔴 Critical (1)
- Race condition on vote writes (
src/modules/pollHandler.js:146-172) — Read-modify-write on the JSONBvotescolumn without any locking. Concurrent voters can silently lose votes. NeedsSELECT ... FOR UPDATEin a transaction or atomic JSONB update.
🟡 Warning (3)
- Vote lookup not scoped to guild_id (
src/modules/pollHandler.js:119) — Query fetches poll byidonly; should includeguild_idfor defense-in-depth (consistent with/poll close). - 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. pollmissing fromconfig.jsonallowedCommands — Without an entry, the permission system defaults to admin-only (permissions.js:98). Add"poll": "everyone"(or"moderator") toallowedCommands.
🔵 Nitpick (3)
- No
getPool()null check inhandlePollVote,closePoll,closeExpiredPolls— would throw TypeError if DB is down. questionlength not validated (src/commands/poll.js:84-95) — Embed titles cap at 256 chars; slash command strings allow 6000. Long questions will fail.- 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.jsondocs.
See inline comments for suggested fixes.
|
| 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
Last reviewed commit: 77e0f09
Additional Comments (1)
Prompt To Fix With AIThis 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. |
There was a problem hiding this comment.
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
📒 Files selected for processing (6)
migrations/003_polls.cjssrc/commands/poll.jssrc/modules/events.jssrc/modules/pollHandler.jssrc/modules/scheduler.jstests/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 withimport/exportsyntax; never userequire()
Always usenode: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.jstests/commands/poll.test.jssrc/modules/pollHandler.jssrc/commands/poll.jssrc/modules/scheduler.js
src/**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
src/**/*.{js,ts,jsx,tsx}: Always use Winston for logging viaimport { info, warn, error } from '../logger.js'; never useconsole.log,console.warn,console.error, or anyconsole.*method in src/ files
Pass structured metadata to Winston logging calls (e.g.,info('Message processed', { userId, channelId }))
Use custom error classes fromsrc/utils/errors.jsfor error handling
Always log errors with context before re-throwing
UsegetConfig(guildId?)fromsrc/modules/config.jsto read configuration values
UsesetConfigValue(path, value, guildId?)fromsrc/modules/config.jsto update configuration at runtime
UsesafeSend()utility for all outgoing Discord messages to enforce allowedMentions and prevent mention spam
UsesanitizeMentions()to strip@everyone/@here from outgoing text via zero-width space insertion before sending
UsesplitMessage()utility to handle Discord's 2000-character message limit
onConfigChangecallbacks receive(newValue, oldValue, fullPath, guildId)as parameters
Files:
src/modules/events.jssrc/modules/pollHandler.jssrc/commands/poll.jssrc/modules/scheduler.js
src/modules/events.js
📄 CodeRabbit inference engine (AGENTS.md)
Module handler functions must be registered in
src/modules/events.jsviaclient.on()event listeners
Files:
src/modules/events.js
src/modules/**/*.{js,ts,jsx,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Check
config.yourModule.enabledbefore processing in module handlers
Files:
src/modules/events.jssrc/modules/pollHandler.jssrc/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; runpnpm testbefore every commit andpnpm test:coverageto 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 adataproperty with a SlashCommandBuilder and an asyncexecute(interaction)function
ExportadminOnly = truefor moderator-only slash commands
Duration-based commands (timeout, tempban, slowmode) must useparseDuration()fromsrc/utils/duration.jsfor parsing duration arguments
Always callcheckHierarchy(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.jssrc/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
registerEventHandlerskeeps 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.
There was a problem hiding this comment.
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
pollstable migration to persist poll state (options, votes, close time, etc.). - Implements
/poll create|close|listplus 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.
…embed title truncation
There was a problem hiding this comment.
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)
getPool()throws — null-check is dead code (src/commands/poll.js:64-68) —getPool()throws when DB is uninitialized, so theif (!pool)branch never executes. Needs try/catch.pollmissing fromconfig.jsonallowedCommands — Without an entry,permissions.js:98defaults to admin-only. Regular users can't use/poll. Add"poll": "everyone"(file not in diff — needs separate change).
🟡 Warning (5)
- No top-level try/catch (
src/commands/poll.js:70-79) — Unlikehelp.js, subcommand dispatch isn't wrapped. Unhandled throws leave the deferred reply dangling. - Uses
channel.send()instead ofsafeSend()(src/commands/poll.js:140-143) — Bypasses mention sanitization. A poll question with@everyonewould ping the server. message.edit()withoutallowedMentions(src/modules/pollHandler.js:216-218) — Vote embed updates lack mention restrictions.message.edit()withoutallowedMentionsinclosePoll()(src/modules/pollHandler.js:266) — Same issue on poll close.- 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)
- 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 (addsrc/commands/poll.js,src/modules/pollHandler.js,migrations/003_polls.cjs) and Database Tables section (addpolls)README.md— command list (add/poll)config.json—allowedCommands(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.
Summary
Adds a rich poll system with Discord button-based voting and auto-close.
What's New
Migration
migrations/003_polls.cjs: Createspollstable with JSONB options/votes, indexes for guild and open pollsCommand:
/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 serverVoting
poll_vote_<pollId>_<optionIndex>custom IDs▓▓▓▓▓░░░░░ 50% (5 votes))Auto-Close
Tests
Files Changed
migrations/003_polls.cjs— newsrc/commands/poll.js— newsrc/modules/pollHandler.js— new (embed builder, vote handler, close logic)src/modules/events.js— added poll button handler registrationsrc/modules/scheduler.js— added expired poll close checktests/commands/poll.test.js— new (27 tests)All 1672 tests pass. Biome clean on all changed files.
Closes #47