Add persistent RSVP cards and reconciliation for scheduled Party sessions - #70
Merged
Merged
Conversation
…ions Closes #67. Confirming a scheduled night now auto-publishes a persistent, button- first RSVP card (Going/Maybe/Can't Make It) to the Play channel instead of requiring players to know an RSVP command. First-time Going shows the same lightweight Primary/Secondary/Fill picker as joining an ad-hoc queue; a returning player with saved roles RSVPs in one click with a Change Roles escape hatch. Maybe is a genuinely separate response that never occupies a seat or waitlist slot. Every RSVP mutation - from the card or the /party rsvp/unrsvp fallback commands - refreshes the one canonical card via its durable delivery reference, never whichever message triggered the interaction. A weekly night rolls the *same* Discord message forward to each next occurrence with a fresh roster instead of posting a new card every week; RSVP history for the occurrence that just converted stays durable on its own row for audit even though the public projection has moved on. A one-time night's card gets one final edit into a "session started" terminal state instead. Startup recovery and every periodic sweep reconcile every guild's scheduled-night cards (post-if-missing, repost-if-deleted, never duplicated when still live); the new `/party session-refresh` command provides the same reconciliation for one named event on demand, gated to its organizer or a server manager. Domain changes (utils/party_schedule.py): - ScheduledNight gains delivery_channel_id/delivery_message_id (the durable card location) and a `maybe` tuple, tracked entirely separately from rsvps/waitlist. - scheduled_rsvps gets a `response` column (going/maybe); only `going` responses ever occupy a seat or waitlist position. rsvp() now promotes an existing Maybe to Going; new rsvp_maybe() releases (and reindexes) any held Going seat first, since Maybe is never a stronger commitment than Going. cancel_rsvp() handles both. - mark_converted() carries the delivery ref forward to the newly created weekly successor row (tracked via a new predecessor_event_id column) in the same transaction, and clears it on the now-CONVERTED row — this is what lets the RSVP card follow the series instead of accumulating. New find_successor() and list_all_upcoming() (cross-guild, for the reconciliation sweep). New utils/schedule_rsvp.py (ScheduleRsvpService) and utils/schedule_views.py (ScheduleRsvpView) mirror the existing PartyLobbyService/RecruitingCardView pattern: injected Discord-UI- agnostic collaborators, embed-footer-based event_id routing, and the same ensure/refresh-card idioms already established for the queue card. schedule_commands.py wires auto-publish into confirm, card refresh into rsvp/unrsvp, rollover-or-terminal-refresh into open-scheduled, and the new session-refresh command. ScheduleLifecycle gained on_startup and extended on_cleanup to sweep list_all_upcoming() through the same reconcile() path used by recovery — rsvp_service is optional so pre-existing reminder-only callers/tests are unaffected. 39 new/updated tests across test_schedule_rsvp_cards.py (new), test_party_schedule.py, and test_party_schedule_commands_characterization.py. Full suite: 689 passing. Version bumped to 2.3.0-rc.5 per RELEASE_PROCESS.md.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c4e3bade6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- ensure_rsvp_card's publish call (channel.send) was unguarded, so a Forbidden/HTTPException there would propagate out of the shared periodic sweep and abort reconciliation for every other guild (and reminder delivery) behind it. Now wrapped best-effort like every other public-card post. - The same method's repost-detection treated Forbidden/HTTPException as "message is gone" alongside NotFound, so a persistent permission gap would repost a brand-new duplicate card on every 5-minute sweep forever. Only a confirmed NotFound now triggers a repost; anything else leaves the delivery ref untouched for the next sweep to retry. - rsvp()'s already-Going branch no-op-returned instead of applying the new role/fill/captain values, silently discarding the "Change Roles" card action even though the player received a success ack. Fixing it also surfaced a second, independent bug: the early return sat inside the transaction's `with` block, so self.get() ran on a separate connection before the transaction had committed and read back stale data. Restructured so the read-back always happens after commit. Added direct regression coverage for all three.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #67.
Summary
Confirming a scheduled night now auto-publishes a persistent, button-first RSVP card (Going / Maybe / Can't Make It) to the Play channel instead of requiring players to know an RSVP command. First-time Going shows the same lightweight Primary/Secondary/Fill picker as joining an ad-hoc queue; a returning player with saved roles RSVPs in one click with a Change Roles escape hatch. Maybe is a genuinely separate response that never occupies a seat or waitlist slot. Every RSVP mutation — from the card or the
/party rsvp/unrsvpfallback commands — refreshes the one canonical card via its durable delivery reference, never whichever message triggered the interaction.A weekly night rolls the same Discord message forward to each next occurrence with a fresh roster instead of posting a new card every week; RSVP history for the occurrence that just converted stays durable on its own row for audit even though the public projection has moved on. A one-time night's card gets one final edit into a "session started" terminal state instead. Startup recovery and every periodic sweep reconcile every guild's scheduled-night cards (post-if-missing, repost-if-deleted, never duplicated when still live); a new
/party session-refreshcommand provides the same reconciliation for one named event on demand, gated to its organizer or a server manager.Player/organizer-facing UX changes
/party rsvp/unrsvpremain working fallbacks./party session-refresh EVENT_IDadmin/organizer recovery command.Domain changes (
utils/party_schedule.py)ScheduledNightgainsdelivery_channel_id/delivery_message_id(the durable card location) and amaybetuple, tracked entirely separately fromrsvps/waitlist.scheduled_rsvpsgets aresponsecolumn (going/maybe); onlygoingresponses ever occupy a seat or waitlist position.rsvp()now promotes an existing Maybe to Going; newrsvp_maybe()releases (and reindexes) any held Going seat first, since Maybe is never a stronger commitment than Going.cancel_rsvp()handles both.mark_converted()carries the delivery ref forward to the newly created weekly successor row (tracked via a newpredecessor_event_idcolumn) in the same transaction, and clears it on the now-CONVERTEDrow — this is what lets the RSVP card follow the series instead of accumulating. Newfind_successor()andlist_all_upcoming()(cross-guild, for the reconciliation sweep).New modules
utils/schedule_rsvp.py(ScheduleRsvpService) andutils/schedule_views.py(ScheduleRsvpView) mirror the existingPartyLobbyService/RecruitingCardViewpattern: injected Discord-UI-agnostic collaborators, embed-footer-basedevent_idrouting, and the same ensure/refresh-card idioms already established for the queue card.schedule_commands.pywires auto-publish intoconfirm, card refresh intorsvp/unrsvp, rollover-or-terminal-refresh intoopen-scheduled, and the newsession-refreshcommand.ScheduleLifecyclegainedon_startupand extendedon_cleanupto sweeplist_all_upcoming()through the samereconcile()path used by recovery — the newrsvp_servicecollaborator is optional so pre-existing reminder-only callers/tests are unaffected.Tests
New
tests/unit/test_schedule_rsvp_cards.py(18 tests) plus updates totest_party_schedule.pyandtest_party_schedule_commands_characterization.py, covering: auto-publish on confirm, first-time vs returning RSVP, Maybe (including releasing a held seat and promoting the waitlist), Can't Make It, card refresh via delivery ref regardless of trigger message, reconciliation (post-if-missing/repost-if-deleted/no-duplicate-if-live/skip-unconfirmed), the cross-guild periodic/startup sweep, thesession-refreshcommand's permission gate and both outcomes, weekly rollover (same message, fresh roster, historical data preserved), one-time terminal-state conversion, and multi-session isolation.Full suite: 689 passing.
Version
Bumped to
2.3.0-rc.5perRELEASE_PROCESS.md.Manual Discord test checklist
/party schedulea one-time night, then/party confirmit — confirm an RSVP card is posted to the Play channel with Going/Maybe/Can't Make It buttons./party rsvp//party unrsvpinstead of the buttons — confirm the same public card still updates./party session-refresh EVENT_ID— confirm exactly one replacement card is posted. Run it again while the card is still live — confirm it's reconciled in place, not duplicated./party schedulea weekly night, confirm it, RSVP a few players, then/party open-scheduledit — confirm the same card message now shows the next week's occurrence with an empty roster, and that the original occurrence's RSVP data is still queryable for history./party open-scheduled— confirm its card is edited to a "session started" state with no buttons, linking to the lobby if resolvable.🤖 Generated with Claude Code
Generated by Claude Code