Files
baya-monorepo/telegram-otp-bot/README.md
T

7.4 KiB

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/newbot → follow the prompts → copy the token (1234567890:AAH...).

  2. Configure.

    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.

    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:

    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.

    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

{ "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).
  • 502no 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.
  • 503TELEGRAM_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 here is built by the root 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.

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:<published-port>.
  • 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.