Files
baya-monorepo/telegram-otp-bot
2026-07-28 22:25:15 +03:30
..
2026-07-28 22:25:15 +03:30
2026-07-28 22:25:15 +03:30
2026-07-28 22:25:15 +03:30

balinyaar-telegram-otp-bot

A standalone, dev-only 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" during manual testing — you get the code on your phone instead, without paying an Iranian SMS gateway.

Development only. There is no per-user routing: every configured recipient receives every OTP, regardless of which phone number requested it. That is fine for a test group; it is not an SMS gateway. Do not point a real environment at this.


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. Keep it loopback unless the API runs on another machine.
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.

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.