feat: add polls feature #148
No reviewers
Labels
No labels
beginner friendly
bug
chore
documentation
duplicate
feature
help wanted
invalid
needs-triage
question
size/L
size/M
size/S
size/XL
size/XS
urgent
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
ScottyLabs/dalmatian!148
Loading…
Reference in a new issue
No description provided.
Delete branch "polls"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Adds
/pollcommand with six subcommands:setup(configure guild polls channel)create(create a poll with up to 10 options, optional multi-select)results(view vote counts)delete(for admins or poll authors to remove polls)close(to manually close the poll before expiry)myvote(check your own vote on an anonymous poll)Closes https://github.com/ScottyLabs/dalmatian/issues/48
just some things, not sure if some are feasible at all
Poll by <@649388957007609879>•Today at 16:21instead of mentioning the user/poll myvoteto check your vote on anonymous pollswait how do you unvote
it doesn't seem like i'm able to send a selection with 0 options selected or anything
nevermind seems like that's not a thing, i just added an unvote button instead
Wait does this have ranked choice implemented
anish lock in bro
Polls: scaling, rendering, and UX fixes
Addresses review feedback on the polls feature (scaling to more options/voters, visual
polish, permission/validation gaps, and the
/pollsetupflow), plus three follow-uprounds of feedback after each pass.
Scaling to more options
"Add option" / "Remove last," so fixing a typo in option 3 meant deleting everything
after it and retyping. Editing now opens a dedicated "Options" view where every
existing option gets its own delete button (
✕-style, one per option, laid out fiveper row).
caps messages at 40 total components. A delete button per option alongside the
rest of the form (question, duration, settings, role selects) doesn't fit once
there are more than a handful of options — a
Section(text + accessory button)per option would cost 3 components each, blowing the budget well before 20
options. Plain buttons in the dedicated view cost roughly 1.2 components per
option, which fits.
Progress bars and the results table
block) instead of the old stacked "label, then bar on the next line" layout, so bars
line up in a column regardless of label length.
Padding past the cap would make one long option blow up the whole table's width, so
labels beyond the cap just don't get further padding — but the full text is always
shown.
code block (e.g. a full sentence) used to drag its bar and percentage along with it,
leaving them visually stranded mid-wrap. Any label over the cap now gets its
bar/percentage moved to their own indented line underneath instead of trailing it
inline — short options still get the single-line aligned layout, long ones just wrap
cleanly with their stats underneath.
-#small-text markdown trick, so apoll with a lot of voters doesn't produce a wall of full-size
@mentions.Option markers (letters/numbers)
or a number/keycap emoji (1️⃣, 2️⃣, …) — shown beside the option in both the results
table and the voting dropdown.
are later displayed sorted by vote percentage, so "option B" always means the same
option.
/pollsetup, snapshottedonto each poll at creation time so changing the guild default later doesn't change
how existing polls render.
11+isbuilt by concatenating per-digit keycaps (e.g.
1️⃣1️⃣) — this only works as plain text,not as a button/select-option icon (Discord only accepts one emoji there), which is
why the marker has both a text form and an "icon" form used in different places.
/pollsetupredesign/poll setup(a subcommand) into a standalone/pollsetupcommand.This was required, not stylistic: Discord's native permission system
(
.setDefaultMemberPermissions()) only exists on top-level slash commands, not onsubcommands, and the review asked for native permission checks instead of a manual
if (!hasPermission)check./pollsetupnow declaresManageChannelsas itsrequired permission natively.
/poll create, instead ofthe generic form wizard used elsewhere in the bot.
"Progress bars off") rather than one button that toggles and relabels itself, so the
current choice is always visible at a glance without reading a label.
didn't add information.
Permissions and input validation
Manage Messagesinstead ofAdministrator— a better fit for the action, since you don't need full admin rights to moderate a
poll.
message_idinput no longer throws.message_idis stored as abigintcolumn; passing something like"a"straight into a query threw a rawPostgres cast error (caught by the bot's top-level handler, but with no useful
message to the user). Every subcommand that takes a
message_idnow validates itagainst a snowflake pattern (
/^\d{1,20}$/) first and replies with a clear errorotherwise.
.orderBy(asc(...)), since Postgresdoesn't guarantee row order without one — needed for option markers to stay
consistently assigned across requests.
Visual polish
the title and the rest of the poll, "not just dashes" — classic Discord
embeds/messages have no native horizontal rule, so getting a real one required
converting the live/closed poll message from an
EmbedBuilderto a Components V2ContainerBuilder, which supports a genuineSeparatorBuilderdivider. (Everythingthat sends or edits that message — poll creation, vote updates, and closing — was
updated to use the new container instead of an embed.)
separators after the "Poll by" line and after the options table, which turned out to
be more dividers than needed.
×,—,·) with plain equivalents throughout.EmbedBuilder(which has abuilt-in
.setTimestamp(), unlike a container) — is back, rendered as a Discordtimestamp (
<t:...:f>).an embed footer, since footers can't render markdown. It now shares one small-text
line with the timestamp (
Whitelisted: role - <timestamp>) instead of twoseparate lines.
Notification pings
Poll by @user) and any restricted roles, whichwas pinging the poll's creator and role members every time. Every send/edit/reply
that can contain a mention (poll creation, vote updates, closing, viewing voters) now
sets
allowedMentions: { parse: [] }, so the mention still renders as a normal pillbut doesn't trigger a notification.
Bug fixes
for its submission using a fixed
customId; if the modal got triggered twice in arow (e.g. a double-tapped button), both listeners matched the same submission and
both processed it. Each modal invocation now gets a unique, single-use
customId(via a per-call nonce), so only the listener for the modal actually shown to the user
can ever match its submission.
/poll createsessions for the same user would cross-talk — editingoptions on one would sometimes affect the other. The component collector was scoped
to the whole channel rather than the specific reply message, so it picked up
interactions from either session. Collectors are now scoped to their own message, and
starting a new
/poll createsession cancels any session already active for that userin that guild (editing the old session's message to say it was superseded).
Data model
poll_config(guild-wide defaults) andpolls(per-poll snapshot) both gainedshow_progress_bars(boolean, defaulttrue) andoption_marker_style(
"letter" | "number", default"letter"). Snapshotting ontopollsmeans changinga guild's defaults later doesn't retroactively change how already-created polls look.
Commit structure
This PR is organized as 5 commits rather than one, so each concern reviews on its own:
chore: bump devenv and flake lockfilesfeat(db): add progress bar and option marker style poll settingsfeat(poll-setup): add standalone /pollsetup command with buttons UIfix(poll): validate message_id input, weaken close/delete permissionfeat(polls): rework poll creation and renderingView command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.