docs(#22): обновить Архитектуру — neutral rendering, v1.10.0
@@ -6,102 +6,52 @@ GM-Relay состоит из нескольких .NET-проектов и об
|
||||
|
||||
- `src/GmRelay.AppHost` — Aspire AppHost. Поднимает PostgreSQL с PgAdmin и запускает bot/web проекты.
|
||||
- `src/GmRelay.Bot` — Worker Service с Telegram long polling, миграциями, планировщиком и вертикальными срезами бота.
|
||||
- `src/GmRelay.Web` — Blazor Server UI для ГМа, включая Telegram Login Widget и Telegram Mini App вход.
|
||||
- `src/GmRelay.Shared` — общие доменные значения и rendering-код Telegram-сообщений.
|
||||
- `src/GmRelay.ServiceDefaults` — общие Aspire defaults: OpenTelemetry, service discovery, resilience, health checks.
|
||||
- `src/GmRelay.Web` — Blazor Server UI для ГМа, включая Telegram Login Widget и Telegram Mini App.
|
||||
- `src/GmRelay.Shared` — чисто нейтральные доменные типы, DTO и platform-agnostic рендеринг. **НЕ зависит от Telegram.Bot**.
|
||||
- `src/GmRelay.ServiceDefaults` — Aspire defaults: OpenTelemetry, service discovery, resilience, health checks.
|
||||
|
||||
## Архитектурное решение
|
||||
## Вертикальная архитектура
|
||||
|
||||
В проекте принят подход Vertical Slice Architecture. Логика бота лежит в `src/GmRelay.Bot/Features/*`, где каждый сценарий содержит свой handler и локальные DTO.
|
||||
Все use-cases живут в `src/GmRelay.Bot/Features/*/`. Каждый фича-срез содержит свои handler, DTO и тесты. Зависимости идут только к `Shared`. См. `docs/adr/0001-use-vertical-slice-native-aot-and-aspire.md`.
|
||||
|
||||
Причины такого выбора зафиксированы в `docs/adr/0001-use-vertical-slice-native-aot-and-aspire.md`:
|
||||
## Telegram updates
|
||||
|
||||
- сценариев немного, отдельный Clean Architecture слой был бы лишним;
|
||||
- зависимости Npgsql и Telegram.Bot стабильны;
|
||||
- Native AOT требует явной регистрации зависимостей и аккуратного SQL-маппинга;
|
||||
- BackgroundService + PeriodicTimer проще и надёжнее Quartz.NET для периодических проверок расписания и дедлайнов голосования.
|
||||
`TelegramBotService` получает updates через long polling и маршрутизирует их в `UpdateRouter`.
|
||||
|
||||
## Поток Telegram updates
|
||||
Команды и callback:
|
||||
- `/start`, `/help`, `/newsession`, `/listsessions`, `/exportcalendar`
|
||||
- `join_session:<sessionId>`, `leave_session:<sessionId>`, `cancel_session:<sessionId>`
|
||||
- `reschedule_session:<sessionId>`, `reschedule_vote:<optionId>`, `rsvp:<confirm|decline>:<sessionId>`
|
||||
|
||||
`TelegramBotService` получает updates через long polling и передаёт их в `UpdateRouter`.
|
||||
## Рендеринг батчей (v1.10.0+)
|
||||
|
||||
`UpdateRouter` явно маршрутизирует:
|
||||
Рендеринг разделён на platform-neutral View Builder и platform-specific Renderers:
|
||||
|
||||
- `/start`, `/help`, `/newsession`, `/listsessions`, `/exportcalendar`;
|
||||
- callback `join_session:<sessionId>`;
|
||||
- callback `leave_session:<sessionId>`;
|
||||
- callback `cancel_session:<sessionId>`;
|
||||
- callback `delete_session:<sessionId>`;
|
||||
- callback `reschedule_session:<sessionId>`;
|
||||
- callback `reschedule_vote:<optionId>`;
|
||||
- callback `rsvp:<confirm|decline>:<sessionId>`;
|
||||
- обычные текстовые сообщения ГМа как ввод 2-3 вариантов времени и дедлайна для активного переноса.
|
||||
- `SessionBatchViewBuilder` (`GmRelay.Shared.Rendering`) — собирает нейтральную view model (`SessionBatchViewModel`) из доменных DTO. Не зависит от Telegram.
|
||||
- `TelegramSessionBatchRenderer` (`GmRelay.Bot.Infrastructure.Telegram` / `GmRelay.Web.Services`) — рендерит HTML + `InlineKeyboardMarkup` для Telegram.
|
||||
- `DiscordSessionBatchRenderer` (`GmRelay.Shared.Rendering`) — заглушка для Discord renderer (issue #26).
|
||||
- `BatchMessageEditor` (`GmRelay.Bot` / `GmRelay.Web`) — редактирует batch-сообщения в Telegram (текст или caption).
|
||||
|
||||
`/newsession` может прийти как обычный text message или как caption к прикреплённому фото. Вариант с caption используется для обложек batch-поста: `CreateSessionHandler` отправляет фото отдельным сообщением перед текстовым расписанием, чтобы обновления состава продолжали редактировать обычный `batch_message_id`.
|
||||
|
||||
Маршрутизация не использует reflection или assembly scanning.
|
||||
|
||||
## Планировщик
|
||||
|
||||
`SessionSchedulerService` — stateless background service. Он запускается сразу при старте, затем раз в минуту:
|
||||
|
||||
1. Ищет сессии `Planned`, у которых `scheduled_at - 24h <= now()`.
|
||||
2. Вызывает `SendConfirmationHandler` и переводит сессию в `ConfirmationSent`.
|
||||
3. Ищет сессии `Confirmed` или `ConfirmationSent`, у которых `scheduled_at - 1h <= now()` и `one_hour_reminder_processed_at IS NULL`.
|
||||
4. Вызывает `SendOneHourReminderHandler`, который учитывает `sessions.notification_mode`.
|
||||
5. Ищет сессии `Confirmed`, у которых `scheduled_at - 5min <= now()` и `link_message_id IS NULL`.
|
||||
6. Вызывает `SendJoinLinkHandler`.
|
||||
|
||||
Состояние хранится в PostgreSQL, поэтому после рестарта сервис продолжает работу без in-memory очередей.
|
||||
|
||||
`RescheduleVotingDeadlineService` — отдельный stateless background service. Он раз в минуту ищет `reschedule_proposals` в статусе `Voting`, у которых `voting_deadline_at <= now()`, выбирает вариант с наибольшим числом голосов, применяет перенос или отклоняет голосование при ничьей/нуле голосов, обновляет Telegram-сообщения и сбрасывает RSVP при успешном переносе.
|
||||
Цепочка использования:
|
||||
```csharp
|
||||
var view = SessionBatchViewBuilder.Build(title, sessions, participants);
|
||||
var (text, markup) = TelegramSessionBatchRenderer.Render(view);
|
||||
```
|
||||
|
||||
## Web UI
|
||||
|
||||
Blazor Server-приложение использует cookie auth и Telegram Login Widget.
|
||||
Начиная с `1.9.0`, тот же Web UI открывается как Telegram Mini App через `/miniapp`; в `1.9.9` основной вход по-прежнему проверяет Telegram `initData`, а fallback `/login` использует callback-mode Telegram Login Widget и endpoint `/auth/telegram-login`, чтобы cookie выставлялась внутри активного Mini App WebView без ручного закрытия и повторного открытия.
|
||||
Blazor Server-side с cookie auth и Telegram Login Widget.
|
||||
|
||||
Основные сервисы:
|
||||
Ключевые endpoints:
|
||||
- `/login` — Telegram Login Widget
|
||||
- `/miniapp` — Telegram Mini App WebView
|
||||
- `/auth/telegram-login` — callback endpoint для Telegram Login Widget
|
||||
- `/auth/status` — проверка статуса аутентификации
|
||||
- `/` — дашборд (только owner и co-GM)
|
||||
- `/templates` — шаблоны кампаний
|
||||
- `/group/{GroupId}` — сессии группы, bulk-операции
|
||||
- `/session/edit/{SessionId}` — редактирование сессии
|
||||
|
||||
- `TelegramAuthService` валидирует HMAC-SHA256 подпись Telegram Login Widget, Telegram WebApp `initData` и `auth_date`.
|
||||
- `SessionService` читает и обновляет группы/сессии через Dapper, включая шаблоны кампаний и bulk-операции по `batch_id`.
|
||||
- `SessionService` сохраняет режим уведомлений batch и дублирует Web-уведомления в ЛС, если включён `GroupAndDirect`.
|
||||
- `SessionService` читает `group_managers`, проверяет роли `Owner`/`CoGm` и сохраняет назначения co-GM.
|
||||
- `BatchSchedulePlanner` задаёт детерминированные правила fixed-interval переноса, повторяющихся расписаний и clone-смещения batch.
|
||||
- `AuthorizedSessionService` проверяет, что текущий Telegram ID является owner или co-GM группы; owner-only операции делегирования проходят через отдельную проверку роли.
|
||||
## C4-диаграммы
|
||||
|
||||
Основные страницы:
|
||||
|
||||
- `/login` — вход через Telegram.
|
||||
- `/miniapp` — вход из Telegram Mini App через серверную проверку `initData`.
|
||||
- `/auth/telegram-login` — callback-вход Telegram Login Widget: принимает JSON payload, проверяет Telegram HMAC и выдаёт cookie-сессию внутри текущего WebView.
|
||||
- `/auth/status` — лёгкая проверка текущей cookie-сессии для Mini App fallback-flow.
|
||||
- `/` — список групп, где пользователь owner или co-GM.
|
||||
- `/templates` — отдельная вкладка управления шаблонами кампаний по доступным группам.
|
||||
- `/group/{GroupId}` — управление группой, применение шаблонов кампаний, будущие сессии и batch bulk-операции.
|
||||
- `/session/edit/{SessionId}` — редактирование названия, времени и ссылки.
|
||||
|
||||
Поток Mini App:
|
||||
|
||||
1. Бот открывает URL `Telegram:MiniAppUrl` через menu button или inline WebApp-кнопку `/start`.
|
||||
2. Страница `/miniapp` ждёт `Telegram.WebApp.initData` из Telegram JS API.
|
||||
3. Blazor отправляет initData на `/auth/telegram-webapp`.
|
||||
4. Если `initData` недоступен, fallback `/login` регистрирует `data-onauth`, получает Telegram Login Widget payload в JavaScript и отправляет его на `/auth/telegram-login` без внешнего redirect-flow.
|
||||
5. Shell вызывает `Telegram.WebApp.ready()` / `expand()`, синхронизирует `safeAreaInset` и `contentSafeAreaInset` в CSS-переменные и слушает `safeAreaChanged`, `contentSafeAreaChanged`, `viewportChanged`.
|
||||
6. Сервер проверяет подпись через ключ `WebAppData` + токен бота, извлекает Telegram ID из `user` и создаёт стандартную auth-cookie.
|
||||
7. Дальше пользователь работает с теми же страницами и сервисами, что и в обычном Web Dashboard.
|
||||
|
||||
## Общий домен
|
||||
|
||||
`GmRelay.Shared` содержит:
|
||||
|
||||
- `SessionStatus`: `Planned`, `ConfirmationSent`, `Confirmed`, `Cancelled`.
|
||||
- `RsvpStatus`: `Pending`, `Confirmed`, `Declined`.
|
||||
- `SessionNotificationMode`: `GroupAndDirect`, `GroupOnly`.
|
||||
- `GroupManagerRole`: `Owner`, `CoGm`.
|
||||
- `MoscowTime`: парсинг и форматирование МСК (`UTC+3`).
|
||||
- `SessionBatchRenderer`: единый renderer для Telegram-сообщения пачки сессий.
|
||||
|
||||
## Диаграммы
|
||||
|
||||
C4-диаграммы хранятся в `docs/c4-system-context.md`. Там описаны system context, container view и component view для `GmRelay.Bot`.
|
||||
Хранятся в `docs/c4-system-context.md`. Описывают system context, container view и component view для `GmRelay.Bot`.
|
||||
Reference in New Issue
Block a user