docs: add issue 31 platform notification design

This commit is contained in:
2026-05-20 14:38:27 +03:00
parent 0c62631ab6
commit 7426000937
@@ -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.