manual improvement 2 & add telegram bot

This commit is contained in:
hamid
2026-07-27 23:58:16 +03:30
parent baa3cc63cd
commit e6a8f93a1e
57 changed files with 4181 additions and 2484 deletions
+153
View File
@@ -0,0 +1,153 @@
# Prompt — wire the Telegram OTP relay into the .NET API
> Paste everything below into a fresh Claude Code session opened at the repo root.
---
Implement a **Development-only Telegram OTP delivery channel** on the server, behind the existing
`ISmsSender` seam. It must be **opt-in from `appsettings`** — an environment that does not configure it
behaves byte-for-byte as it does today.
## Context you should read first
- `server/CLAUDE.md` → the "External rails go real — config-selected vendor adapters" paragraph and the
"Startup wiring" section.
- `server/src/Core/Baya.Application/Contracts/Common/ISmsSender.cs` — the contract (`SendOtpAsync`, `SendAsync`).
- `server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/Real/KavenegarSmsSender.cs` — the shape
every real SMS adapter follows. Mirror it.
- `server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/SeamOptions.cs``SmsOptions` + `SeamProviders`.
- `server/src/Infrastructure/Baya.Infrastructure.CrossCutting/ServiceConfiguration/ServiceCollectionExtension.cs`
→ the `RegisterSms` method — the config-selected registration pattern.
- `telegram-otp-bot/README.md` — the relay's HTTP contract (it already exists and runs; do not modify it).
## What the relay already is
A standalone zero-dependency Node service at `telegram-otp-bot/` (its own project — **not** part of
`client/` or `server/`). It forwards messages to a fixed list of Telegram chat ids. Contract:
| Route | Auth | Request | Success | Failure |
| --- | --- | --- | --- | --- |
| `POST /send_otp` | `X-Api-Key` header | `{"phone":"...","code":"..."}` | `200` `{"ok":true,"delivered":[...],"failed":[...]}` | `502` when **no** recipient got it; `503` no recipients configured; `401` bad key; `400` bad body |
| `POST /send` | `X-Api-Key` header | `{"phone":"...","message":"..."}` | same | same |
| `GET /health` | none | — | `200` `{"ok":true}` | — |
It is a **broadcast**, not per-user routing: every configured Telegram recipient receives every code,
regardless of which phone requested it. That is exactly why this is Development-only.
## Requirements
### 1. Config surface — opt-in, default off
Add to `SmsOptions` (`SeamOptions.cs`) a nested `TelegramOptions Telegram { get; set; } = new();` with:
- `BaseUrl` — the relay root, e.g. `http://127.0.0.1:5010`.
- `ApiKey`**the shared secret sent as the `X-Api-Key` header. It is a secret**: the committed
`appsettings*.json` carries an empty string or the repo's `SET_VIA_USER_SECRETS_OR_ENV` placeholder,
never a real value. Real value via user-secrets / environment only.
- `TimeoutSeconds` — default `10`.
Add `public const string Telegram = "telegram";` to `SeamProviders`. Document on the options class, in the
`SmsOptions` XML doc, and in the `SeamProviders` SMS-gateway group that `telegram` is a **Development
convenience channel, not an SMS gateway**.
The feature is selected exactly like every other rail:
```jsonc
// server/src/API/Baya.Web.Api/appsettings.Development.json
"Seams": {
"Sms": {
"Provider": "telegram",
"Telegram": {
"BaseUrl": "http://127.0.0.1:5010",
"ApiKey": "", // real value via user-secrets: Seams:Sms:Telegram:ApiKey
"TimeoutSeconds": 10
}
}
}
```
**Default must stay `mock`.** Do not change the committed `Provider` value in any shared
`appsettings.json` — document the opt-in instead (see §5). An unconfigured environment must resolve
`LoggingSmsSender` exactly as it does now.
### 2. The adapter
New `TelegramSmsSender : ISmsSender` in
`server/src/Infrastructure/Baya.Infrastructure.CrossCutting/Seams/Real/`, following `KavenegarSmsSender`'s
shape — primary-constructor injection of `HttpClient` + `IOptions<SeamOptions>` + `ILogger<T>`,
`System.Text.Json`, **no new NuGet package**.
- `SendOtpAsync(phone, code, ct)``POST {BaseUrl}/send_otp` with `{"phone":…,"code":…}`.
- `SendAsync(phone, message, ct)``POST {BaseUrl}/send` with `{"phone":…,"message":…}`.
- Send `X-Api-Key: {ApiKey}` on every request.
- Snake-case JSON body keys (`phone`, `code`, `message`) — the relay reads exactly those names.
- **Non-2xx, or a body with `ok != true`, is a delivery failure** — log a warning and throw
`InvalidOperationException`, exactly as `KavenegarSmsSender` does, so `RequestOtpCommand` reports a real
send failure instead of silently "succeeding". A `502` from the relay means nobody received the code and
must not be swallowed.
- **Never log the OTP code.** Log the phone tail only — reuse Kavenegar's `Tail(phone)` helper approach.
- Guard against an unconfigured `ApiKey`: throw a clear startup/first-call error naming
`Seams:Sms:Telegram:ApiKey` rather than sending an unauthenticated request that the relay will 401.
### 3. Registration
In `ServiceCollectionExtension.RegisterSms`, add a branch **before** the `smsir`/`ghasedak`
`NotSupportedException`:
```csharp
else if (Is(provider, SeamProviders.Telegram))
{
services.AddHttpClient(HttpClients.Telegram, c => { /* BaseAddress + Timeout from options */ });
services.AddSingleton<ISmsSender>(sp => new TelegramSmsSender(...));
}
```
Add the `HttpClients.Telegram` named-client constant alongside the existing ones. Follow the file's own
`Is(...)` / `BaseOrDefault(...)` / `Client(sp, name)` helpers — do not invent a parallel style.
### 4. Keep the `/dev/last_otp` bridge working
`Program.cs` currently disables the Development OTP-capture decorator whenever a non-mock SMS provider is
selected (`usingMockSms`), because a real gateway must never have the code logged/captured. **Telegram is
the exception**: it is a Development-only channel, and the `GET /api/v1/dev/last_otp/{phone}` helper and its
e2e tests should keep working alongside it.
Widen that condition to allow the capture bridge for `mock` **or** `telegram` (keep it strictly disallowed
for `kavenegar` and any future real gateway), and update the explanatory comment above it to say why —
the current comment asserts the bridge is off for *every* non-mock provider, and that will become wrong.
Rename the local to something accurate (e.g. `otpCaptureAllowedProvider`). The `IsDevelopment()` guard stays.
### 5. Docs — same change, non-negotiable
- `server/CLAUDE.md` → the "External rails go real" paragraph: add `TelegramSmsSender`
(`Sms:Provider=telegram`) to the adapter list, flagged **Development-only, broadcast, not a gateway**,
and note that it is the one non-mock SMS provider that keeps the OTP-capture bridge enabled.
- `server/CLAUDE.md` → "Startup wiring": update the `AddDevelopmentOtpCapture()` comment to match the new
condition.
- `dev/post-phase/refinement/RUNBOOK.md` → a short "OTP over Telegram" subsection: start the relay
(`cd telegram-otp-bot && npm start`), set the same secret on both sides
(`dotnet user-secrets set "Seams:Sms:Telegram:ApiKey" "<value>"`), flip `Seams:Sms:Provider` to
`telegram` in `appsettings.Development.json`, log in, read the code in Telegram. Link to
`telegram-otp-bot/README.md` for bot creation and chat-id discovery.
### 6. Tests
Add unit tests for `TelegramSmsSender` next to the existing seam tests (`Baya.Test.Foundation`), using a
stubbed `HttpMessageHandler` — no network:
- `SendOtpAsync` posts to `/send_otp` with the right body **and** the `X-Api-Key` header.
- A `502` / `{"ok":false}` response throws.
- The OTP code never appears in the log output.
- Registration: `Provider = "mock"` (and an unset provider) still resolves `LoggingSmsSender`; `Provider =
"telegram"` resolves `TelegramSmsSender`. **This is the regression that matters** — the feature must be
invisible unless opted in.
## Constraints
- Server-side only. Do not touch `client/`.
- No handler changes — `RequestOtpCommand` depends on `ISmsSender` and must stay untouched.
- No new NuGet packages; versions are centrally pinned in `Directory.Packages.props` regardless.
- Follow `server/CONVENTIONS.md`: `sealed` classes, no unused usings/locals, *why*-comments only.
- Never commit the API key or the bot token.
- Finish with `dotnet build Baya.sln` (zero new warnings) and `dotnet test Baya.sln` (all green), and report
the actual output.