- DiscordPermissionChecker: removed dead-code userRoles overload; now only uses resolvedPermissions bitflag (Administrator = 0x8). - DiscordNewSessionCommand: computes resolved permissions from guild user roles via Context.Guild.Users[Id].RoleIds + guild.Roles. - DiscordNewSessionHandler: updated signature to accept ulong resolvedPermissions instead of unused userRoles. - Added ILogger to command for diagnostics on unexpected errors. - Added test: regular user with ManageServer (but not Admin) is rejected. Refs issue #28 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
7.3 KiB
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:
dotnet build
Build individual projects (the CI does this to include SAST via SecurityCodeScan):
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:
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --verbosity normal
Run a single test class or method:
dotnet test tests/GmRelay.Bot.Tests/GmRelay.Bot.Tests.csproj --filter "FullyQualifiedName~YourTestClassName"
Lint and format:
dotnet format --verify-no-changes --verbosity diagnostic # CI enforcement
dotnet format # Apply fixes
Check for vulnerable packages:
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):
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):
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:
- View Builder (
GmRelay.Shared) — platform-agnostic view model from domain DTOs. - Platform Renderer —
TelegramSessionBatchRendererlives in bothGmRelay.BotandGmRelay.Web(temporary duplication until a third Telegram consumer justifies extractingGmRelay.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
BotHealthCheckHostedServicelistens on port 8081. The Docker health check hitslocalhost:8081/health. - Web: Standard ASP.NET Core health checks on
/health(JSON response with status and timestamp) and/alive(liveness probe tag filter). Exposed viaGmRelay.ServiceDefaults.
Authentication and Security
- Telegram Login Widget and Mini App
initDataverification via HMAC-SHA256. Cookie auth is hardened (HttpOnly,SecurePolicy.Always,SameSite.Strict). - Web Data Protection keys are persisted to
/app/dataprotection-keys(Docker volumeweb_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 viaDirectory.Build.propsfor 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:
dotnet restore- Verify
packages.lock.jsonfiles exist for Trivy dotnet format --verify-no-changesdotnet list package --vulnerable- Trivy filesystem scan (
vuln,misconfig,secret, HIGH/CRITICAL) - Build Shared → Bot → Web
- Run tests
.gitea/workflows/deploy.yml runs on push to main:
- Build and push
gmrelay-botandgmrelay-webimages togit.codeanddice.ru/toutsu/... - Trivy image scan on both images (HIGH/CRITICAL, exit-code 1)
- Create
.envfrom secrets and rundocker compose up -d
Environment Configuration
Key environment variables (see .env.example):
TELEGRAM_BOT_TOKEN,TELEGRAM_BOT_USERNAME,TELEGRAM_MINI_APP_URLPOSTGRES_PASSWORDGMRELAY_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-noblewithclangandzlib1g-devfor AOT compilation. Final stage usesruntime-deps:10.0-noble. Exposes 8081. - Web: Multi-stage Dockerfile. Build stage uses
sdk:10.0-noble. Final stage usesaspnet:10.0-noblewithlibgssapi-krb5-2andwget. Exposes 8080.
Both images are built for multi-arch (linux/amd64, linux/arm64) to support Raspberry Pi 5 (ARM64) deployment.