# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Build, Test, and Development Commands This is a .NET 10 solution using the modern XML-based `.slnx` format. The global SDK version is `10.0.100` with `rollForward: latestFeature`. **Build the solution:** ```bash dotnet build ``` **Build individual projects (the CI does this to include SAST via SecurityCodeScan):** ```bash dotnet build src/GmRelay.Shared/GmRelay.Shared.csproj --no-restore dotnet build src/GmRelay.Bot/GmRelay.Bot.csproj --no-restore dotnet build src/GmRelay.Web/GmRelay.Web.csproj --no-restore ``` **Run all tests:** ```bash dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --verbosity normal ``` **Run a single test class or method:** ```bash dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --filter "FullyQualifiedName~YourTestClassName" ``` **Lint and format:** ```bash dotnet format --verify-no-changes --verbosity diagnostic # CI enforcement dotnet format # Apply fixes ``` **Check for vulnerable packages:** ```bash dotnet list package --vulnerable --include-transitive ``` **Restore with lock file verification:** The repo enforces `RestorePackagesWithLockFile=true`. After adding or updating packages, commit the updated `packages.lock.json` files or the Trivy scan in CI will fail. **Run locally with Aspire (dev orchestration):** ```bash dotnet run --project src/GmRelay.AppHost/GmRelay.AppHost.csproj ``` This automatically starts PostgreSQL in a container, the Bot, and the Web dashboard. **Run locally with Docker Compose (production-like):** ```bash cp .env.example .env # Edit .env with your TELEGRAM_BOT_TOKEN, TELEGRAM_BOT_USERNAME, POSTGRES_PASSWORD docker compose up -d ``` ## High-Level Architecture ### Project Roles and Runtime Model | Project | Runtime | Key Trait | |---|---|---| | `GmRelay.Bot` | `Microsoft.NET.Sdk.Worker` | **Native AOT** binary. Telegram long polling bot + stateless scheduler. | | `GmRelay.Web` | `Microsoft.NET.Sdk.Web` | Blazor Server dashboard. Cookie auth via Telegram Login Widget / Mini App `initData`. | | `GmRelay.Shared` | Plain library | Domain models and platform-neutral view builders. **Must not depend on `Telegram.Bot`**. | | `GmRelay.ServiceDefaults` | Aspire shared project | OpenTelemetry, health checks, HTTP resilience. Referenced by both Bot and Web. | | `GmRelay.AppHost` | Aspire orchestrator | Dev-only. Spins up PostgreSQL and wires Bot + Web with service discovery. | **Important:** `README.md` references `GmRelay.Migrator` and `GmRelay.Worker`, but these projects do not exist. Migrations (`DbUp`) and background workers (`BackgroundService`) live inside `GmRelay.Bot`. ### Vertical Slice Architecture with Explicit DI Each use case is a self-contained vertical slice: a C# record (Command/Query) + Handler class with all logic (SQL, Telegram API calls, validation). There are no abstract repository interfaces or service layers. Because the Bot is compiled as Native AOT (`PublishAot=true`, `EnableTrimAnalyzer=true`), **all DI registrations are explicit** in `src/GmRelay.Bot/Program.cs`. There is no assembly scanning or reflection-based discovery. When adding a new handler, you must register it manually in Program.cs. ### Database Access: Npgsql + Dapper.AOT + DbUp **No EF Core** — it is incompatible with Native AOT. The stack is: - **Npgsql** ADO.NET for connections. - **Dapper 2.1.72** with **Dapper.AOT 1.0.48** for compile-time source-generated mapping (AOT-safe). - **DbUp 7.0.1** for migrations. SQL scripts are embedded resources in `src/GmRelay.Bot/Migrations/` (V001 through V015). - `DbMigrator.MigrateUp()` runs on every Bot startup. Both Bot and Web share the same PostgreSQL database. Web registers `NpgsqlDataSource` via `builder.AddNpgsqlDataSource("gmrelaydb")` (Aspire integration), while Bot registers it manually to avoid reflection-based Aspire configuration at AOT time. ### Platform-Neutral Rendering (ADR-002) Rendering is split into two stages: 1. **View Builder** (`GmRelay.Shared`) — platform-agnostic view model from domain DTOs. 2. **Platform Renderer** — `TelegramSessionBatchRenderer` lives in both `GmRelay.Bot` and `GmRelay.Web` (temporary duplication until a third Telegram consumer justifies extracting `GmRelay.Shared.Telegram`). This means `GmRelay.Shared` must remain free of `Telegram.Bot` types. If you need to add rendering logic that produces `InlineKeyboardMarkup`, it belongs in the Bot or Web project, not Shared. ### Stateless Scheduling The session scheduler (`SessionSchedulerService`) is a `BackgroundService` with a `PeriodicTimer(TimeSpan.FromMinutes(1))`. On each tick it queries PostgreSQL for sessions needing action (T-24h confirmation, T-5min join link) and updates their status. There is no in-memory state — the database is the single source of truth. This design was chosen specifically because Quartz.NET is incompatible with Native AOT. ### Health Checks - **Bot:** Custom `BotHealthCheckHostedService` listens on port 8081. The Docker health check hits `localhost:8081/health`. - **Web:** Standard ASP.NET Core health checks on `/health` (JSON response with status and timestamp) and `/alive` (liveness probe tag filter). Exposed via `GmRelay.ServiceDefaults`. ### Authentication and Security - **Telegram Login Widget** and **Mini App `initData`** verification via HMAC-SHA256. Cookie auth is hardened (`HttpOnly`, `SecurePolicy.Always`, `SameSite.Strict`). - Web Data Protection keys are persisted to `/app/dataprotection-keys` (Docker volume `web_keys`). - Security headers middleware (`X-Content-Type-Options`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`) is applied globally in Web. - `SecurityCodeScan.VS2019` (5.6.7) is included in all projects via `Directory.Build.props` for SAST at build time. - Connection string passwords are redacted in logs via `SecretRedactor`. ### CI/CD Pipeline `.gitea/workflows/pr-checks.yml` runs on every PR to `main`: 1. `dotnet restore` 2. Verify `packages.lock.json` files exist for Trivy 3. `dotnet format --verify-no-changes` 4. `dotnet list package --vulnerable` 5. Trivy filesystem scan (`vuln,misconfig,secret`, HIGH/CRITICAL) 6. Build Shared → Bot → Web 7. Run tests `.gitea/workflows/deploy.yml` runs on push to `main`: 1. Build and push `gmrelay-bot` and `gmrelay-web` images to `git.codeanddice.ru/toutsu/...` 2. Trivy image scan on both images (HIGH/CRITICAL, exit-code 1) 3. Create `.env` from secrets and run `docker compose up -d` ### Environment Configuration Key environment variables (see `.env.example`): - `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_USERNAME`, `TELEGRAM_MINI_APP_URL` - `POSTGRES_PASSWORD` - `GMRELAY_WEB_PORT` (default 8080) - `ConnectionStrings__gmrelaydb` — used by both Bot and Web The Bot reads config as `Telegram:BotToken` (colon) which maps from `Telegram__BotToken` (double underscore) via environment variables. ### Docker Images - **Bot:** Multi-stage Dockerfile. Build stage uses `sdk:10.0-noble` with `clang` and `zlib1g-dev` for AOT compilation. Final stage uses `runtime-deps:10.0-noble`. Exposes 8081. - **Web:** Multi-stage Dockerfile. Build stage uses `sdk:10.0-noble`. Final stage uses `aspnet:10.0-noble` with `libgssapi-krb5-2` and `wget`. Exposes 8080. Both images are built for multi-arch (`linux/amd64`, `linux/arm64`) to support Raspberry Pi 5 (ARM64) deployment.