Skip to main content
Glama
mia-oc

grokbot-telegram-bridge

by mia-oc
README.md
# grokbot-telegram-bridge

Open-source **Telegram ↔ Grok Bot** bridge: a local webhook listener, on-disk spool, and stdio [MCP](https://modelcontextprotocol.io/) server so Grok Bot can drain Telegram messages and reply **immediately** when woken.

## What this is / what it is not

**Is**

- A small Node.js service that receives Telegram Bot API webhooks on `127.0.0.1:8787`
- An atomic on-disk **spool** of inbound updates (`spool/<update_id>.json`)
- A **stdio MCP** server exposing Telegram helpers (`tg_send_message`, `tg_edit_message`, `tg_list_spool`, …) that Grok Bot can call as a custom MCP
- Designed to **wake Grok Bot via a webhook routine** on each new Telegram message (NOT cron)
- **OpenClaw-style progress drafts**: one editable status message while the agent works, then delete + final answer

**Is not**

- Native Telegram support inside Grok Bot (there is none)
- A hosted SaaS or always-on cloud bot framework
- A cron-first design — **polling/cron drain was the wrong architecture**; use a webhook routine
- A replacement for BotFather, Telegram clients, or Grok Bot itself
- Full OpenClaw tool-stream UI (this bridge approximates `streaming.mode=progress` with edit/delete, not native Telegram streaming)

Architecture in one line: **Telegram → HTTPS relay → local listener → spool + progress draft + keepalive ticker → wake POST → Grok Bot webhook routine → MCP (`tg_progress` hold / `tg_delete_message` / `tg_send_message`)**.

## Progress drafts (OpenClaw `streaming.mode=progress`)

OpenClaw’s Telegram channel can show a live **progress** message that is edited as the agent works, then cleared when the final answer lands. This bridge mirrors that UX.

### Telegram twin of Grok in-app commentary

The progress draft is the **Telegram twin** of Grok Bot’s in-app working commentary (the short status lines you see in the app while tools run — e.g. `Running a few commands`).

- At each beat, the agent must call `tg_progress` (or `tg_edit_message`) with the **same wording** as that in-app commentary — not a paraphrase, not a custom “status protocol”.
- Example: when the app shows `Running a few commands`, Telegram should show `Running a few commands` (optionally with a leading `● ` if you keep the draft’s bullet style consistent).
- Before the final answer: **delete** the draft (`tg_delete_message`), then `tg_send_message` with the real reply.
- The listener’s first line is `● waking Grok Bot`, then the **listener keepalive** cycles Grok-style lines until the agent’s first `tg_progress` / `tg_edit_message` sets `hold: true` (or the draft is deleted / spool acked / ~90s). After hold, **agent edits** should be exact commentary mirrors.

| Phase | What happens |
|-------|----------------|
| **Inbound** | Listener sends **one** status draft (`● waking Grok Bot`). Persists `{ chat_id, progress_message_id }` to `spool/<update_id>.meta.json` (mode `0600`). Idempotent redelivery does **not** send another draft or start another ticker. |
| **Listener keepalive** | After a successful draft, the listener starts a **non-blocking** ticker (~every 2.5s, up to ~90s) that `editMessageText`s the draft through Grok-style lines (e.g. `● Running a few commands` → `● Reading files` → `● Searching the web` → `● Writing a reply`). This keeps the user seeing motion even when the wake agent is slow or skips `tg_progress`. |
| **Agent hold** | When `tg_progress` / `tg_edit_message` succeeds for a chat whose spool `*.meta.json` matches that `progress_message_id`, the MCP server sets `hold: true` on that meta (best-effort). The listener ticker stops overwriting agent commentary. |
| **Working** | Agent **edits that same message** via `tg_progress` / `tg_edit_message` with the **exact** in-app Grok commentary at each beat (e.g. `Running a few commands`). First successful edit holds the ticker. |
| **Done** | Agent **deletes** the progress draft (`tg_delete_message`), then sends the **final answer as a new normal message** (`tg_send_message`). Delete also stops the ticker (Telegram message-not-found). |
| **Typing** | Optional/secondary keepalive only — **progress text is the signal of life**, not “typing…”. |

**Keepalive stop conditions:** spool file for that `update_id` is gone (acked); Telegram returns message-not-found (agent deleted draft); `meta.json` has `hold: true` (agent took over); ~90s timeout. The ticker never blocks the webhook `200` or the wake POST. Never logs the bot token.

A static “Queued…” receipt is **not** the design. Progress text that updates — mirroring in-app commentary — is.

### Agent routine behaviour (recommended)

When woken, the webhook routine should:

1. Call `tg_list_spool` (preview includes `progress_message_id` when present; wake body may also carry it)
2. If empty → stay silent
3. While working: at each commentary beat, `tg_progress(chat_id, progress_message_id, "<exact in-app wording>")` (e.g. `Running a few commands`)
4. When done: `tg_delete_message(chat_id, progress_message_id)` → then `tg_send_message` with the final answer
5. `tg_ack_spool` for each handled `update_id`

Suggested prompt sketch:

> On wake: call `tg_list_spool`. If `count` is 0, do nothing. Otherwise, for each pending item, use `progress_message_id` from the preview (or wake payload). The progress draft is the Telegram twin of Grok in-app commentary: whenever the app would show a working line (e.g. `Running a few commands`), call `tg_progress` / `tg_edit_message` with that **same wording**. When finished: **delete** the progress draft with `tg_delete_message`, then send the final answer as a **new** `tg_send_message`. Ack with `tg_ack_spool`. Never print tokens or secrets. Typing alone is not progress — mirror commentary into the draft.

## Prerequisites

- **Node.js 18+** (20+ recommended)
- A Telegram bot from [@BotFather](https://t.me/BotFather) (API token)
- Your numeric Telegram **chat id** (allowlisted)
- **Grok Bot desktop** (to register the stdio MCP and create a **webhook routine**)
- A public HTTPS front-door for Telegram webhooks (see [Public HTTPS](#public-https--prefer-smeeio))

## Install

```bash
git clone <this-repo-url> grokbot-telegram-bridge
cd grokbot-telegram-bridge
npm install
```

### Secrets (never commit)

Create secrets **outside the repo** or only in **gitignored** paths. Modes should be `0600`.

Helper (creates empty/`minted` files in the package directory — they are gitignored):

```bash
npm run setup-secrets
# or: bash scripts/setup-secrets.sh
```

Then edit:

| File | Purpose |
|------|---------|
| `token` | BotFather token (or set `TELEGRAM_BOT_TOKEN`) |
| `webhook-secret` | Random `secret_token` for `setWebhook` / `X-Telegram-Bot-Api-Secret-Token` |
| `ALLOWED_CHAT_ID` | Your numeric chat id (progress draft, typing, and wake respect this) |
| `grokbot-wake-url` | Full HTTPS URL of the Grok Bot **webhook routine** |
| `grokbot-wake-secret` | Sender key / secret for that routine (Authorization Bearer + `X-Webhook-Secret`) |

`example.env.example` shows placeholder env keys only:

```bash
TELEGRAM_BOT_TOKEN=
ALLOWED_CHAT_ID=
# Optional override: GROKBOT_WAKE_HEADER="Header-Name: value"
```

**Do not** put real tokens in the repo, in README snippets, or in chat. If a token is ever pasted into chat, **revoke/rotate it in BotFather**.

## Run the listener

Bind address is hard-coded to loopback:

- `GET  http://127.0.0.1:8787/healthz` → `200 ok`
- `POST http://127.0.0.1:8787/telegram-webhook` → Telegram updates (requires secret header)

Foreground:

```bash
npm run listener
# or: node listener.mjs
```

Optional supervisor (restart-friendly, writes `listener.pid` / `listener.log`):

```bash
npm run supervise
# or: bash supervisor.sh
```

Confirm health:

```bash
curl -sS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8787/healthz
# expect 200
```

## Public HTTPS — prefer smee.io

Telegram requires a public **HTTPS** webhook URL. Your listener stays on `127.0.0.1:8787`.

**Cloudflare Tunnel** often fails Telegram’s DNS/resolve checks for some setups. Prefer a relay such as **[smee.io](https://smee.io/)**:

1. Create a smee channel; note the public HTTPS URL.
2. Run the smee client locally, forwarding to `http://127.0.0.1:8787/telegram-webhook`.
3. Point Telegram’s webhook at the **smee (or other relay) public URL** that ultimately POSTs to `/telegram-webhook`.

Any stable HTTPS reverse proxy/tunnel that Telegram can resolve is fine; smee is the documented default recommendation here because Cloudflare tunnels frequently fail Telegram resolve.

Do **not** paste your public smee/relay URL into shared issue trackers or chat logs.

## setWebhook (and never mix with getUpdates)

With the listener running and the relay forwarding:

1. Ensure `webhook-secret` exists (`npm run setup-secrets`).
2. Call Telegram `setWebhook` with:
   - `url` = your public HTTPS webhook URL
   - `secret_token` = contents of `webhook-secret` (same value the listener checks on `X-Telegram-Bot-Api-Secret-Token`)

Via MCP (once registered):

- Tool: `tg_set_webhook`
- Arg: `public_url` = your public HTTPS URL

Or via curl (do **not** echo the token/secret into shell history logs you share):

```bash
# Illustrative only — load token/secret from files; do not paste into chat
curl -sS -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/setWebhook" \
  -H 'Content-Type: application/json' \
  -d "{\"url\":\"https://YOUR_PUBLIC_HOST/telegram-webhook\",\"secret_token\":\"${WEBHOOK_SECRET}\"}"
```

**Important:** While a webhook is active, **do not call `getUpdates`**. Telegram rejects long-polling when a webhook is set. Use `tg_get_updates` only for pre-webhook debugging. Inspect webhook state with `tg_webhook_info`.

## Register as a Grok Bot custom MCP (stdio)

In Grok Bot → custom MCP / Add MCP server (stdio):

| Field | Suggested value |
|-------|-----------------|
| Name | `grokbot-telegram-bridge` (or `telegram`) |
| Command | `node` |
| Args | `/ABS/PATH/TO/grokbot-telegram-bridge/mcp-server.mjs` |
| CWD (if offered) | `/ABS/PATH/TO/grokbot-telegram-bridge` |

Dogfood / box path example:

- Command: `node`
- Args: `/home/box/.local/telegram-mcp/mcp-server.mjs`

The MCP process reads `token` / `webhook-secret` / `spool/` relative to the script directory (or `TELEGRAM_BOT_TOKEN` from the environment). **Never** put the token in the MCP args.

### MCP tools

| Tool | Purpose |
|------|---------|
| `tg_get_me` | Bot identity |
| `tg_send_message` | `chat_id`, `text` — final answers |
| `tg_edit_message` | `chat_id`, `message_id`, `text` — edit progress draft |
| `tg_delete_message` | `chat_id`, `message_id` — clear progress draft before final reply |
| `tg_progress` | Alias of `tg_edit_message` for status updates |
| `tg_send_chat_action` | `chat_id`, `action` (default `typing`) — optional keepalive |
| `tg_list_spool` | Pending inbound updates (preview includes `progress_message_id` when present) |
| `tg_ack_spool` | Archive `update_id` → `spool/done/` (also moves `.meta.json`) |
| `tg_webhook_info` | Telegram `getWebhookInfo` |
| `tg_get_updates` | Pre-webhook long-poll only |
| `tg_set_webhook` | Set webhook; reads `webhook-secret` |

## Wake Grok Bot with a **webhook routine** (primary path)

Cron/polling drain was the wrong design. Wire immediacy like this:

1. In Grok Bot, create a **webhook routine** (not a scheduled/cron routine).
2. Copy the routine’s **HTTPS URL** into `grokbot-wake-url` (mode `0600`).
3. Copy the routine’s **sender key / secret** into `grokbot-wake-secret` (mode `0600`).
4. Restart the listener (or supervisor) so it can read the new files.

On each **new** spool write (`created === true`), after Telegram already got `200 ok`, the listener sends the progress draft (when allowlisted), then asynchronously POSTs:

```json
{
  "source": "telegram-bridge",
  "update_id": "...",
  "chat_id": 123,
  "progress_message_id": 456
}
```

`progress_message_id` is included when the draft send succeeded. Auth headers (default): both `Authorization: Bearer <secret>` and `X-Webhook-Secret: <secret>`.  
Optional override: set env `GROKBOT_WAKE_HEADER` to `Name: value` to send that single header instead.

If either wake file is missing/empty: spool + progress draft + typing still run; wake is skipped with log line `wake skipped: missing grokbot-wake-url/secret`. Wake failures never fail the Telegram webhook response. Only allowlisted chats (`ALLOWED_CHAT_ID`) trigger a wake (same gate as the progress draft).

Optional smoke test (does not print secrets):

```bash
node scripts/send-wake-test.mjs
```

## Security

- **Never commit** `token`, `webhook-secret`, `grokbot-wake-url`, `grokbot-wake-secret`, `ALLOWED_CHAT_ID`, `allowed-chat-id`, `.env`, `spool/`, or logs
- File mode **0600** for secrets and `.meta.json`; spool dirs preferably `0700`
- **Allowlist** your chat id; do not run an open relay for the world
- Rotate the BotFather token if it was pasted into chat, committed, or leaked
- Listener binds **127.0.0.1 only**; expose it only through a deliberate HTTPS relay
- Validate Telegram’s `X-Telegram-Bot-Api-Secret-Token` (built-in) — keep `webhook-secret` long and random
- Do not log tokens, secrets, wake URLs, or full public relay URLs in shared issue trackers

## Dogfood checklist

- [ ] `npm install` succeeds on Node 18+
- [ ] `npm run setup-secrets` then fill `token` + `ALLOWED_CHAT_ID` (0600)
- [ ] Create Grok Bot **webhook routine**; paste URL + sender key into `grokbot-wake-url` / `grokbot-wake-secret` (0600)
- [ ] `npm run supervise` (or `npm run listener`) — `GET /healthz` returns **200**
- [ ] HTTPS relay (prefer smee.io) forwards to `http://127.0.0.1:8787/telegram-webhook`
- [ ] `tg_set_webhook` / `setWebhook` with `secret_token`; `tg_webhook_info` shows the URL
- [ ] Send yourself a Telegram message → file under `spool/` + progress draft (`● waking Grok Bot`) cycling keepalive lines + `.meta.json` + wake POST; agent `tg_progress` sets `hold: true`
- [ ] MCP registered in Grok Bot (`node` + absolute `mcp-server.mjs`)
- [ ] Webhook routine: edit progress → delete draft → `tg_send_message` final → `tg_ack_spool`
- [ ] Empty spool runs stay silent
- [ ] Confirm `.gitignore` excludes secrets; no secrets under version control

## Layout

```
grokbot-telegram-bridge/
  listener.mjs          # HTTP webhook + spool + progress draft + keepalive + wake POST
  mcp-server.mjs        # stdio MCP tools (incl. edit/delete/progress)
  supervisor.sh         # simple process keeper
  package.json
  scripts/setup-secrets.sh
  scripts/send-wake-test.mjs
  example.env.example
  LICENSE               # MIT
  README.md
```

Runtime data (gitignored): `token`, `webhook-secret`, `grokbot-wake-url`, `grokbot-wake-secret`, `ALLOWED_CHAT_ID`, `spool/` (incl. `*.meta.json`), `listener.pid`, `*.log`.

## License

MIT — see [LICENSE](./LICENSE).

Maintenance

ActivityMaintained
ResponsivenessNo issues