From 742600093744c22c27b88229c4cbab9186827ba9 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Wed, 20 May 2026 14:38:27 +0300 Subject: [PATCH] docs: add issue 31 platform notification design --- ...essenger-scheduler-notifications-design.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-20-platform-messenger-scheduler-notifications-design.md diff --git a/docs/superpowers/specs/2026-05-20-platform-messenger-scheduler-notifications-design.md b/docs/superpowers/specs/2026-05-20-platform-messenger-scheduler-notifications-design.md new file mode 100644 index 0000000..820d9a7 --- /dev/null +++ b/docs/superpowers/specs/2026-05-20-platform-messenger-scheduler-notifications-design.md @@ -0,0 +1,140 @@ +# Platform Messenger Scheduler Notifications Design + +## Goal + +Issue #31 moves scheduler-driven notifications and reschedule deadline message updates behind `IPlatformMessenger`, preserving Telegram behavior and adding full Discord support instead of no-op placeholders. + +## Scope + +- `SessionSchedulerService` remains the trigger orchestrator, but scheduler handlers stop depending on Telegram API types for outbound notification work. +- Confirmation requests, one-hour reminders, join-link notifications, RSVP follow-up messages, and reschedule deadline updates use platform-neutral contracts. +- Telegram keeps the current user-visible behavior: same message content, RSVP buttons, direct messages, topic/thread targeting, and stored legacy message ids. +- Discord receives full channel and direct notifications: + - confirmation requests are sent to the Discord channel with RSVP buttons; + - Discord RSVP button clicks update participant RSVP state, refresh the confirmation message, and send the same group/GM outcome notifications where applicable; + - one-hour reminders and join-link notifications are sent as Discord DMs when direct notifications are enabled; + - join-link notifications also post the channel message with participant mentions; + - reschedule deadline processing updates Discord vote and schedule messages through the same messenger boundary. +- Discord DM failures are non-fatal: log a warning and continue without posting a public fallback message. + +## Architecture + +The platform boundary should be semantic, not Telegram-shaped. `GmRelay.Shared.Platform` already owns `PlatformKind`, `PlatformUser`, `PlatformGroup`, `PlatformMessageRef`, and `IPlatformMessenger`; issue #31 extends that layer with notification-specific DTOs and messenger methods. + +The scheduler handlers own database queries and notification eligibility. They load platform-neutral groups, users, message refs, and session data, then ask the platform messenger to send or update the platform message. Platform implementations own rendering details: Telegram renders HTML and inline keyboards; Discord renders embeds, components, channel messages, mentions, and DMs. + +RSVP handling should become platform-neutral enough for both Telegram and Discord. The current `HandleRsvpHandler` logic is not duplicated. Its command changes from Telegram ids to `PlatformUser`, `PlatformGroup`, `PlatformMessageRef`, and `InteractionId`. Telegram update routing maps callback queries into that command; Discord component routing maps RSVP button interactions into the same command. + +Reschedule finalization already has shared database logic in `RescheduleVotingFinalizer`. The remaining platform-specific deadline services should stop editing messages through `ITelegramBotClient` or Discord `RestClient` directly. They should load message refs and call `IPlatformMessenger` to update vote messages, schedule messages, and direct result notifications. + +## Platform Contracts + +Add semantic notification records in `GmRelay.Shared.Platform`, with names finalized during implementation planning: + +- `PlatformSessionParticipant`: a `PlatformUser` plus RSVP, registration, and display metadata needed by notification renderers. +- `PlatformSessionNotification`: common session title, time, join link, notification mode, group, optional existing message, and participants. +- `PlatformConfirmationRequest`: confirmation-specific session notification with RSVP actions. +- `PlatformJoinLinkNotification`: join-link group/direct notification data. +- `PlatformOneHourReminder`: one-hour direct reminder data. +- `PlatformRsvpMessageUpdate`: refreshed confirmation message state after a participant responds. +- `PlatformRescheduleVoteUpdate`: finalized reschedule vote message state, including selected option or rejection reason. + +Extend `IPlatformMessenger` with methods for these semantic operations while keeping existing schedule, group, private, interaction, and calendar methods intact for current flows: + +- send and update confirmation request messages; +- send one-hour reminder direct notifications; +- send join-link channel and direct notifications; +- update finalized reschedule vote messages; +- send RSVP outcome messages to the group and GM recipients. + +The exact method names should be chosen in the implementation plan after tests define the desired API, but each method should accept platform-neutral DTOs and return `PlatformMessageRef` when the caller must persist a sent message id. + +## Telegram Behavior + +Telegram implementation lives in `GmRelay.Bot.Infrastructure.Telegram.TelegramPlatformMessenger`. + +It must preserve: + +- `messageThreadId` handling for forum topics; +- HTML parse mode where the existing flow uses HTML; +- current confirmation and RSVP button callback payloads; +- `confirmation_message_id` and `link_message_id` storage in `sessions`; +- direct notification behavior controlled by `SessionNotificationMode`; +- warning-and-continue behavior for failed direct messages; +- existing schedule rendering through `TelegramSessionBatchRenderer` and `BatchMessageEditor`. + +Telegram-specific inbound parsing remains at the Telegram boundary. `UpdateRouter` can still use `Telegram.Bot.Types`, but the command it passes into the RSVP handler should be platform-neutral. + +## Discord Behavior + +Discord implementation lives in `GmRelay.DiscordBot.Infrastructure.Discord.DiscordPlatformMessenger`. + +It must support: + +- channel messages through the configured channel id in `PlatformGroup.ExternalChannelId`; +- interactive RSVP buttons routed by `DiscordSessionInteractionModule`; +- ephemeral interaction replies via the existing `DiscordInteractionReplyCache` pattern; +- DMs through Discord user ids in `PlatformUser.ExternalUserId`; +- non-fatal DM failures with warning logs; +- Discord-friendly rendering, not raw Telegram HTML; +- persistence of Discord schedule and notification message refs in `platform_messages` where later updates need them. + +The current Discord reschedule deadline service directly uses `RestClient` for vote and schedule message edits. This should be folded into `DiscordPlatformMessenger` so deadline services and future platform handlers do not need to know Discord API details. + +## Data Flow + +1. `SessionSchedulerService.TickAsync` asks `ISessionTriggerStore` for due confirmation, one-hour reminder, and join-link session ids. +2. Each handler loads the session, group platform identity, message refs, participants, RSVP state, and notification mode. +3. The handler builds a semantic platform notification DTO and calls `IPlatformMessenger`. +4. The messenger renders and sends/updates platform messages. +5. The handler persists sent message ids where required, using legacy `sessions.confirmation_message_id` and `sessions.link_message_id` for Telegram and `platform_messages` for Discord refs that need later updates. +6. Telegram callback queries and Discord component interactions both call the same platform-neutral RSVP handler. +7. Reschedule deadline services use `RescheduleVotingFinalizer`, then call `IPlatformMessenger` for vote message updates, schedule updates, and direct result notifications. + +## Error Handling + +- A failed trigger query still logs and lets the scheduler continue to the next trigger category. +- A failed send/update for one session logs and does not stop other sessions in the same tick. +- DM failures are warning-level and non-fatal for Telegram and Discord. +- A missing platform message ref logs a warning and skips only the update that needs the ref. +- Unsupported platform values throw at the messenger boundary, not inside scheduler orchestration. +- If Discord cannot parse a stored channel, message, or user id, it logs the bad external id and skips that platform send/update. + +## Testing + +Use TDD for implementation. + +Focused tests should cover: + +- `IPlatformMessenger` exposes semantic notification methods without referencing Telegram or Discord assemblies from `GmRelay.Shared`. +- `SendConfirmationHandler`, `SendOneHourReminderHandler`, `SendJoinLinkHandler`, `HandleRsvpHandler`, and reschedule deadline services do not call `ITelegramBotClient`, `BatchMessageEditor`, or Discord `RestClient` directly for notification output. +- Telegram source/regression tests preserve thread ids, callback payloads, message id persistence, and direct notification mode behavior. +- Discord source tests verify registration of scheduler handlers, RSVP component routes, and messenger methods. +- RSVP flow tests run through platform-neutral `PlatformUser` identity, including Discord users without Telegram ids. +- Discord messenger tests verify DMs are attempted, DM failures are swallowed after logging, channel notifications include buttons or mentions as appropriate, and message refs are returned. +- Full regression: `dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj`, `dotnet build`, and `dotnet format --verify-no-changes --verbosity diagnostic`. + +## Versioning + +Current repository version is `2.6.0`. Although the Gitea issue is labeled `type:refactor`, the approved scope adds full Discord notification behavior. Proposed bump: `2.6.0` to `2.7.0`. + +Synchronize: + +- `Directory.Build.props` +- `compose.yaml` image tags for bot, discord, and web +- `.gitea/workflows/deploy.yml` `VERSION` +- `src/GmRelay.Web/Components/Layout/NavMenu.razor` + +## Out Of Scope + +- Moving the entire scheduler hosted service into `GmRelay.Shared`. +- Removing legacy Telegram columns such as `telegram_chat_id`, `confirmation_message_id`, or `link_message_id`. +- Reworking Web dashboard Telegram behavior. +- Public fallback messages when a Discord DM is blocked. + +## Self-Review + +- Spec coverage: every issue acceptance criterion is represented by scheduler handler boundaries, messenger contracts, Telegram behavior preservation, and Discord implementation requirements. +- Placeholder scan: no TBD/TODO/fill-in-later sections remain. +- Internal consistency: the design uses semantic platform DTOs consistently and keeps SDK-specific rendering in platform implementations. +- Scope check: the work is large but still one coherent platform-notification refactor; moving the whole scheduler to shared remains explicitly out of scope.