9.9 KiB
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
SessionSchedulerServiceremains 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: aPlatformUserplus 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:
messageThreadIdhandling for forum topics;- HTML parse mode where the existing flow uses HTML;
- current confirmation and RSVP button callback payloads;
confirmation_message_idandlink_message_idstorage insessions;- direct notification behavior controlled by
SessionNotificationMode; - warning-and-continue behavior for failed direct messages;
- existing schedule rendering through
TelegramSessionBatchRendererandBatchMessageEditor.
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
DiscordInteractionReplyCachepattern; - 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_messageswhere 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
SessionSchedulerService.TickAsyncasksISessionTriggerStorefor due confirmation, one-hour reminder, and join-link session ids.- Each handler loads the session, group platform identity, message refs, participants, RSVP state, and notification mode.
- The handler builds a semantic platform notification DTO and calls
IPlatformMessenger. - The messenger renders and sends/updates platform messages.
- The handler persists sent message ids where required, using legacy
sessions.confirmation_message_idandsessions.link_message_idfor Telegram andplatform_messagesfor Discord refs that need later updates. - Telegram callback queries and Discord component interactions both call the same platform-neutral RSVP handler.
- Reschedule deadline services use
RescheduleVotingFinalizer, then callIPlatformMessengerfor 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:
IPlatformMessengerexposes semantic notification methods without referencing Telegram or Discord assemblies fromGmRelay.Shared.SendConfirmationHandler,SendOneHourReminderHandler,SendJoinLinkHandler,HandleRsvpHandler, and reschedule deadline services do not callITelegramBotClient,BatchMessageEditor, or DiscordRestClientdirectly 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
PlatformUseridentity, 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, anddotnet 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.propscompose.yamlimage tags for bot, discord, and web.gitea/workflows/deploy.ymlVERSIONsrc/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, orlink_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.