docs: add issue 31 platform notification design
This commit is contained in:
+140
@@ -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.
|
||||
Reference in New Issue
Block a user