# Telegram Channel — Configuration Guide

Integrate GoClaw with Telegram so users can talk to the agent in a private chat
or in a group.

Telegram works with **no public URL**: long polling is the default, so a
gateway on `127.0.0.1` or behind NAT can run a live bot.

## Prerequisites

- A Telegram account
- A bot token from [@BotFather](https://t.me/BotFather)
- A public HTTPS endpoint — **only** if you choose webhook mode

## 1. Create the bot

1. Open [@BotFather](https://t.me/BotFather) in Telegram
2. Send `/newbot`, then pick a display name and a username ending in `bot`
3. Copy the token it returns — it looks like `123456789:AAE...`

## 2. Configure GoClaw

```yaml
channels:
  telegram:
    enabled: true
    token: "${TELEGRAM_BOT_TOKEN}"
    mode: "polling"
```

Store the token outside the config file with a secret reference
(`${TELEGRAM_BOT_TOKEN}` from the environment, or `${secret:TELEGRAM_BOT_TOKEN}`
from the encrypted vault).

Restart the gateway. The startup log confirms the bot identity:

```
[telegram] Channel started: @your_bot (mode=polling, require_mention=true)
[telegram] Long polling started (timeout=30s)
```

Verify the token independently with:

```bash
goclaw doctor
```

## 3. Talk to it

Search your bot's `@username` in Telegram and send a message. The first message
from an unknown user goes through the normal pairing flow — see
[Pairing](pairing.md) — unless `channels.dm_policy` is `open`. The same applies
in groups: an unapproved member who addresses the bot gets the pairing reply in
the group.

An edited message is handled as a new message, so correcting a question gets it
answered again.

## Inbound modes

| | `polling` (default) | `webhook` |
|---|---|---|
| Public URL | not needed | required (HTTPS) |
| Setup | token only | token + secret + URL |
| Latency | ~instant (30s long poll) | instant |
| Best for | dev, NAT, single instance | production, multiple replicas |

### Webhook mode

```yaml
channels:
  telegram:
    enabled: true
    token: "${TELEGRAM_BOT_TOKEN}"
    mode: "webhook"
    webhook_secret: "${TELEGRAM_WEBHOOK_SECRET}"
    webhook_url: "https://bot.example.com"
```

`webhook_secret` is **required**. Telegram echoes it back in the
`X-Telegram-Bot-Api-Secret-Token` header, and GoClaw compares it in constant
time; without it, anyone who discovers the hook path could inject fake messages.
The channel refuses to start if it is missing. Telegram accepts 1–256 characters,
only `A-Z`, `a-z`, `0-9`, `_` and `-`; with any other character `setWebhook`
fails and the channel does not start.

When `webhook_url` is set, GoClaw calls `setWebhook` itself on startup and
mounts the receiver at `/hook/telegram` (or `/hook/<slug>/telegram` when
`gateway.webhook_secret_slug` is configured). Leave `webhook_url` empty to
register the endpoint yourself. The registration discards messages that queued
up while the gateway was down, so they are not answered after a restart.

Switching back to `polling` is safe: Telegram refuses `getUpdates` while a
webhook is registered, so the channel clears the stale registration on startup.

## Group chats

Add the bot to a group and, by default, it stays quiet until addressed:

- `@your_bot what is the deploy status?`
- a reply to one of the bot's own messages

Set `require_mention: false` to answer every message in the group — only
sensible for a small, dedicated room. All members of a group share one
conversation with the agent.

> Telegram's privacy mode limits what a bot can see in groups, and it is on by
> default. For `require_mention: false`, or if the bot receives nothing, send
> `/setprivacy` to BotFather and choose **Disable** (or make the bot a group
> admin). Telegram applies the change to a group only after the bot is removed
> and added to it again.

## Attachments

### Inbound

Documents, photos, voice notes, audio and video are saved to `uploads/` in the
workspace (`~/.goclaw/workspace/uploads/`, or under `GOCLAW_STATE_DIR` when it is
set) and referenced in the prompt, so the agent's document tools can read them
and `voice_input` can transcribe voice notes. The Bot API lets a bot download files of up to 20 MB,
so a larger file fails to save and the agent is told so. Filenames are sanitised
before they touch disk.

### Outbound

Files the agent produces are routed by type:

| Condition | Method | Why |
|---|---|---|
| Raster image within Telegram's photo limits | `sendPhoto` | Shows an inline preview |
| Everything else | `sendDocument` | Keeps the original bytes and filename |

The Bot API caps `sendPhoto` at 10 MB, a width + height sum of 10000, and a
20:1 ratio. Images outside those bounds — a tall full-page screenshot, a large
render — are sent as documents instead of being rejected. Dimensions are read
locally before upload; if Telegram still refuses the photo, the file is retried
as a document rather than lost. `sendDocument` accepts up to 50 MB; anything
larger fails with an explicit error instead of a silent drop.

A single file with a short caption (≤1024 characters) carries the text on the
file bubble. Longer text, or text accompanying several files, is sent as its own
message first.

## Formatting

Replies are converted from Markdown to Telegram's HTML parse mode — more
forgiving than MarkdownV2, which rejects a whole message over a single
unescaped character. If Telegram still rejects the markup, the chunk is resent
as plain text rather than dropped.

| Markdown | Telegram |
|---|---|
| `**bold**`, `*italic*`, `~~strike~~` | `<b>`, `<i>`, `<s>` |
| `# Heading` | bold line (Telegram has no heading tag) |
| `` `code` ``, ```` ```lang ```` | `<code>`, `<pre><code class="language-…">` |
| `> quote` | `<blockquote>` |
| `[text](url)` | `<a href="…">` |
| **tables** | aligned `<pre>` block |

### Tables

Telegram has no table tag and renders messages in a proportional font, so a raw
Markdown table arrives as unreadable pipe soup. Tables are re-rendered as
fixed-width text inside `<pre>`, where the columns line up and the block scrolls
horizontally:

```
Date   Provider  Tokens
─────  ────────  ───────
10-01  gemini    120,450
10-01  openai    18,200
```

Column alignment (`:---`, `:---:`, `---:`) is honoured, and widths are measured
in characters so Cyrillic headers align correctly. Full-width CJK text still
misaligns — one character occupies two monospace cells.

Columns only work while the table fits the screen. A phone shows roughly 40–45
monospace characters, and a table whose cells hold sentences blows well past
that — one observed reply padded out to 141 characters, where the separator rule
alone wrapped over four lines. Tables wider than that are laid out as one record
per row instead, which reads correctly at any width:

```
payments-api
Check: "error rate over the last hour"
Result: ❌ Above the threshold, 4.2% of requests failed
```

The row label is bolded and the column names label their values. Because this
layout is ordinary text rather than a `<pre>` block, inline formatting inside
cells is converted properly; in the aligned form it is stripped, since `<pre>`
cannot carry tags and the markers would otherwise show as literal asterisks.

### Long messages

Messages longer than 4096 **characters** (the Bot API's unit, not bytes) are
split on line boundaries. When a split lands inside a fenced code block, the
fence is closed and reopened across the boundary so both halves keep their
monospace formatting and language hint.

Pieces are sized by their **rendered** length, not by the Markdown they came
from. Telegram measures the value it receives — the HTML, tags included — so
sizing the source at 4096 overshoots on tag-heavy content: a 4090-character
source of ordinary bold-and-prose renders to 4438 characters and is rejected.
Because that rejection falls back to plain text, one oversized piece would
otherwise turn an entire reply into raw Markdown. The same rule applies to file
captions.

## Configuration reference

| Key | Default | Description |
|---|---|---|
| `enabled` | `false` | Turn the channel on |
| `token` | — | BotFather token (required) |
| `mode` | `polling` | `polling` or `webhook` |
| `webhook_secret` | — | Required in webhook mode |
| `webhook_url` | — | Public HTTPS base URL; enables auto-`setWebhook` |
| `require_mention` | `true` | In groups, only answer when addressed |
| `bot_name` | from `getMe` | Display name shown in the dashboard |
| `text_chunk_limit` | `4096` | Outbound chunk size |

## Troubleshooting

**`token check failed: telegram API 401`** — the token is wrong or was revoked.
Re-issue it with `/token` in BotFather.

**Bot is silent in a group** — it is not being addressed, or Telegram's privacy
mode is hiding messages. Mention it explicitly, or disable privacy mode via
BotFather's `/setprivacy`.

**`getUpdates failed (retry in 5s): telegram API 409: Conflict: …`** — two
processes are polling the same token. Only one gateway instance can poll a bot;
use webhook mode to run several.

**Nothing arrives in webhook mode** — check that Telegram can reach your URL and
that `webhook_secret` matches on both sides. `getWebhookInfo` reports the last
delivery error:

```bash
curl "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"
```
