# balinyaar-telegram-otp-bot A **standalone** Telegram relay. It is not part of `client/` or `server/` — it is its own tiny Node project with **zero dependencies** (Node 18+ built-ins only: `node:http` + global `fetch`). Its whole job: expose an HTTP endpoint that the .NET API calls, and forward the message to a fixed list of Telegram chat ids. That replaces "read the OTP out of the server log" — you get the code on your phone instead, without an Iranian SMS gateway contract. > **Broadcast, not routing.** *Every* configured recipient receives *every* OTP, regardless of which > phone number requested it. That makes this a shared inbox for a small trusted group, not an SMS > gateway. It is the OTP rail for local development **and** for the pre-launch `balinyaar.ir` demo > deployment — it must be swapped for `Seams:Sms:Provider = kavenegar` before anyone outside that > trusted group can request a code. --- ## Setup 1. **Create the bot.** Message [@BotFather](https://t.me/BotFather) → `/newbot` → follow the prompts → copy the token (`1234567890:AAH...`). 2. **Configure.** ```bash cd telegram-otp-bot cp .env.example .env # PowerShell: Copy-Item .env.example .env node -e "console.log(require('crypto').randomBytes(24).toString('hex'))" ``` Paste the token into `TELEGRAM_BOT_TOKEN` and the generated secret into `API_KEY` (**required** — the process refuses to start without it). Leave `TELEGRAM_CHAT_IDS` empty for now. 3. **Run it.** ```bash npm start ``` No `npm install` needed — there are no dependencies. On boot it prints the bot's `@username`, which confirms the token works. 4. **Discover the chat ids.** Each recipient opens the bot in Telegram and presses **Start** (a bot cannot open a conversation — the user must message it first). Then: ```bash curl -H "X-Api-Key: $API_KEY" http://localhost:5010/chat_ids ``` Copy the returned `chat_id` values into `TELEGRAM_CHAT_IDS` (comma-separated) and restart. 5. **Test it.** ```bash curl -X POST http://localhost:5010/send_otp \ -H "X-Api-Key: $API_KEY" \ -H 'content-type: application/json' \ -d '{"phone":"09120000001","code":"123456"}' ``` --- ## HTTP API Base URL: `http://127.0.0.1:5010` (configurable via `HOST`/`PORT`). **Every route except `GET /health` requires the `X-Api-Key` header**, compared against `API_KEY` in constant time. There is no way to disable it: `API_KEY` is mandatory (min 16 chars) and the process exits at boot without one, so the relay is never reachable unauthenticated. A rejected request is logged with the caller's address. | Route | Auth | Body | Purpose | | --- | --- | --- | --- | | `GET /health` | — | — | Liveness only; echoes no configuration. | | `GET /chat_ids` | `X-Api-Key` | — | Chat ids that recently messaged the bot (setup helper). | | `POST /send_otp` | `X-Api-Key` | `{ "phone": "...", "code": "..." }` | Broadcast a login code. | | `POST /send` | `X-Api-Key` | `{ "phone": "...", "message": "..." }` | Broadcast a free-form transactional message. | The two POST routes mirror the server's `ISmsSender` (`SendOtpAsync` / `SendAsync`) one-for-one, so a `TelegramSmsSender` adapter is a thin HTTP call per method. **Response** ```json { "ok": true, "delivered": ["11111111"], "failed": [{ "chatId": "22222222", "error": "chat not found" }] } ``` - `200` — at least one recipient received it (partial delivery still counts; one reachable reader is enough to complete a login). - `502` — **no** recipient received it. The caller should treat this as a delivery failure so the OTP command fails loudly rather than pretending an undeliverable code was sent. - `503` — `TELEGRAM_CHAT_IDS` is empty. - `400` / `401` — bad body / missing-or-wrong `X-Api-Key`. --- ## Configuration | Variable | Default | Meaning | | --- | --- | --- | | `TELEGRAM_BOT_TOKEN` | — | **Required.** From @BotFather. Process exits without it. | | `API_KEY` | — | **Required**, min 16 chars. Shared secret expected in `X-Api-Key`. Process exits without it. | | `TELEGRAM_CHAT_IDS` | — | Comma-separated recipient chat ids. Empty ⇒ every send returns `503`. | | `PORT` | `5010` | HTTP port. | | `HOST` | `127.0.0.1` | Bind address. Loopback locally; the Dockerfile sets `0.0.0.0` so the API container can reach it. | | `REDACT_CODE_IN_LOGS` | `false` | Keep the code out of *this process's* stdout (still delivered). | | `TELEGRAM_PROXY_URL` | — | Optional outbound proxy for the Telegram hop — see below. | The API key is the only access control — there is no IP allow-list and no TLS. Keep `HOST` on loopback when the API runs on the same machine; if you must expose it, put it behind something that terminates TLS, or the key travels in clear text. Values come from `.env` (git-ignored) or from real environment variables, which take precedence. In the deployed stack there is no `.env` at all — the root `docker-compose.yml` supplies every variable directly (and `.dockerignore` keeps a local `.env` out of the image, so it can't silently win). ## Running in Docker The [`Dockerfile`](Dockerfile) here is built by the root [`docker-compose.yml`](../docker-compose.yml) as the `otp-relay` service. Nothing is published to the host: the API reaches it as `http://balinyaar-otp-relay:5010` over the shared `caddy_net` network, and its own hop to Telegram goes through the proxy container on that same network. See [DEPLOY.md](../DEPLOY.md). ## Troubleshooting | Symptom | Cause | | --- | --- | | `chat not found` in `failed[]` | That user never messaged the bot. Press **Start** in Telegram. | | `GET /chat_ids` returns nothing | No message in the last 24h, or a webhook is set on the bot (`getUpdates` returns nothing while a webhook is registered — call `deleteWebhook`). | | `token check FAILED` / `fetch failed` | Wrong/revoked token, or no outbound access to `api.telegram.org` — see below. | | `401` on every call | The caller isn't sending `X-Api-Key`, or its value differs from `API_KEY`. | ### Reaching Telegram from Iran — the proxy option `api.telegram.org` is filtered, so the machine running this usually needs a proxy for **its own** hop to Telegram. (The API → relay hop is loopback/LAN and never proxied.) Set one URL in `.env`: ``` TELEGRAM_PROXY_URL=http://127.0.0.1:10809 # or socks5://127.0.0.1:10808 ``` - **Opt-in.** Unset ⇒ the relay connects directly, byte-for-byte as before. The boot banner prints which it is (`proxy: socks5://127.0.0.1:10808` or `proxy: (none — direct to api.telegram.org)`). - **Schemes:** `http`/`https` (an HTTP `CONNECT` tunnel) and `socks5`/`socks5h`. Credentials go in the URL (`socks5://user:pass@host:1080`) and are never logged. - **Local machine with a VPN client** → point it at that client's HTTP or SOCKS listener. **VPS with a proxy client in a docker container** → put the relay and the proxy on the same docker network and use the container name, e.g. `http://proxy:1080`; from the host, `http://127.0.0.1:`. - SOCKS5 requests are sent with the hostname (not a pre-resolved IP), so DNS resolves at the proxy — local DNS is filtered too. - `HTTPS_PROXY` / `ALL_PROXY` (either case) are used as a fallback when `TELEGRAM_PROXY_URL` is unset, so a container that already sets them needs no extra config. `NODE_USE_ENV_PROXY` is not needed and not read — the tunnel is handled in-process, so behaviour is the same on every Node ≥ 18. - A malformed proxy URL is fatal **at boot**, not silently at the first OTP.