diff --git a/%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0.md b/%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0.md index 54693ef..23a7231 100644 --- a/%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0.md +++ b/%D0%90%D1%80%D1%85%D0%B8%D1%82%D0%B5%D0%BA%D1%82%D1%83%D1%80%D0%B0.md @@ -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:`, `leave_session:`, `cancel_session:` +- `reschedule_session:`, `reschedule_vote:`, `rsvp::` -`TelegramBotService` получает updates через long polling и передаёт их в `UpdateRouter`. +## Рендеринг батчей (v1.10.0+) -`UpdateRouter` явно маршрутизирует: +Рендеринг разделён на platform-neutral View Builder и platform-specific Renderers: -- `/start`, `/help`, `/newsession`, `/listsessions`, `/exportcalendar`; -- callback `join_session:`; -- callback `leave_session:`; -- callback `cancel_session:`; -- callback `delete_session:`; -- callback `reschedule_session:`; -- callback `reschedule_vote:`; -- callback `rsvp::`; -- обычные текстовые сообщения ГМа как ввод 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`. \ No newline at end of file