Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/telegram-business-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@chat-adapter/telegram": minor
---

Add Telegram Business mode support. Opt in via `businessMode: true`.

The adapter handles `business_connection`, `business_message`, and `edited_business_message` updates, encodes business threads as `telegram:biz:{connectionId}:{chatId}`, and passes `business_connection_id` on outbound sends, edits, typing, file uploads, and inline-keyboard callbacks. Business threads are their own channel, slash commands route through `onSlashCommand`, deletes use `deleteBusinessMessages`, and connection state is cached in the state adapter so a revoked connection is honoured by every instance. Reactions on business threads throw a `NotImplementedError`, since the Bot API has no business variant of `setMessageReaction`.
30 changes: 30 additions & 0 deletions apps/docs/content/adapters/official/telegram.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ const telegram = createTelegramAdapter({
description:
"Long polling tuning. Fields: `timeout`, `limit`, `allowedUpdates`, `deleteWebhook`, `dropPendingUpdates`, `retryDelayMs`.",
},
businessMode: {
type: "boolean",
default: "false",
description:
"Enable Telegram Business mode for Connected Business Bots. Handles `business_connection`, `business_message`, and `edited_business_message` updates and passes `business_connection_id` on outbound API calls.",
},
mentionOnReply: {
type: "boolean",
default: "false",
Expand Down Expand Up @@ -196,6 +202,30 @@ const telegram = createTelegramAdapter({

`botToken` is always required. Webhook mode also requires `secretToken` unless `allowUnverifiedWebhooks` is explicitly enabled. Polling mode does not require webhook verification.

## Business mode

Telegram [Connected Business Bots](https://core.telegram.org/api/bots/connected-business-bots) let a bot reply to customer messages on behalf of a business account. Enable it with `businessMode: true`:

```typescript title="lib/bot.ts" lineNumbers
const telegram = createTelegramAdapter({
businessMode: true,
});
```

Business threads use the ID format `telegram:biz:{connectionId}:{chatId}`. Each business conversation is its own channel, separate from any direct chat the same customer has with the bot. Outbound sends, edits, typing, file uploads, and threads created from inline-keyboard callbacks include `business_connection_id`. Slash commands from business chats reach `onSlashCommand` like any other chat.

The adapter ignores messages typed by the business owner and messages the bot sent on the account's behalf, and it stops replying when the connection is disabled or loses `can_reply`. Connection state lives in your state adapter, so a change reaches every instance.

A few Bot API limits apply to business threads:

- `delete()` uses `deleteBusinessMessages`, which needs the `can_delete_sent_messages` right.
- Reactions are not supported. `addReaction` and `removeReaction` throw a `NotImplementedError`.
- `fetchThread()` falls back to the chat details from messages already seen when `getChat` cannot resolve the customer.

When polling, Business mode always sends an explicit `allowed_updates` list (the default update types plus the business ones, or your `longPolling.allowedUpdates` merged with them). Telegram otherwise reuses the list from an earlier call, which can silently exclude business updates. When registering a webhook yourself, add `business_connection`, `business_message`, and `edited_business_message` to `allowed_updates`.

Business thread IDs start with `telegram:biz`, so state adapters that shard by the first two ID segments (such as [Cloudflare Agents](/adapters/vendor-official/cloudflare-agents#state-sharding)) place every business conversation in one shard. Override the sharder with a key that includes the connection ID if that matters for your deployment.

## Authentication

Create a bot via [BotFather](https://t.me/BotFather):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ The default key sharder recognizes these Chat SDK key prefixes:

Unknown keys use the adapter's default shard name, `default`.

Telegram Business threads (`telegram:biz:{connectionId}:{chatId}`) all share the shard `telegram:biz` under this rule. Use a custom `shardKey` that keeps the connection ID if you expect many business conversations.

### Custom sharding

Use `shardKey` to control how thread IDs map to state sub-agent names, and `keyShard` for non-thread-shaped keys that should still route to a provider-specific shard:
Expand Down
13 changes: 13 additions & 0 deletions packages/adapter-telegram/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,17 @@ adapter encodes both:
```
telegram:{chatId} # plain chat / DM
telegram:{chatId}:{messageThreadId} # topic in a forum group
telegram:biz:{connectionId}:{chatId} # Connected Business Bot chat
```

`isDM(threadId)` returns `true` when `chat.type === "private"`.

Business threads are their own channel: `channelIdFromThreadId` returns
the full `telegram:biz:...` ID, never the bare `telegram:{chatId}`, so
they do not share lock or history keys with the customer's direct bot
chat. Note that sharders keyed on the first two segments group every
business thread under `telegram:biz`.

## Webhook flow

`TelegramAdapter.handleWebhook(request, options)` is the entry point.
Expand All @@ -115,6 +122,12 @@ telegram:{chatId}:{messageThreadId} # topic in a forum group
hooks.
- `message_reaction` → `chat.handleReaction`.
- `my_chat_member` / `chat_member` → membership change hooks.
- `business_connection` → cached in the state adapter (before the
`allowedUserIds` gate, since it carries no customer id).
- `business_message` / `edited_business_message` → resolve the
connection from state (falling back to `getBusinessConnection`),
drop owner-typed and `sender_business_bot` echoes, then route
slash commands, media groups, and messages like `message`.
3. **`waitUntil`** — outbound API calls (`sendMessage`,
`editMessageText`, etc.) run inside `waitUntil` so the webhook
response lands quickly.
Expand Down
38 changes: 38 additions & 0 deletions packages/adapter-telegram/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,43 @@ curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/setWebhook" \
}'
```

## Business mode (Connected Business Bots)

Enable Telegram Business mode when your bot manages customer conversations on behalf of a connected business account:

```typescript
const telegram = createTelegramAdapter({
businessMode: true,
});
```

With `businessMode: true`, the adapter:

- Handles `business_connection`, `business_message`, and `edited_business_message` updates
- Skips messages typed by the business owner, messages the bot itself sent on the account's behalf (`sender_business_bot`), and connections without `can_reply`
- Encodes business threads as `telegram:biz:{connectionId}:{chatId}`. Each business conversation is its own channel, kept apart from any direct chat the same customer has with the bot
- Passes `business_connection_id` on outbound `sendMessage`, edits, typing, file uploads, and on threads created from inline-keyboard callbacks
- Routes `/commands` from business chats through `onSlashCommand`, the same as regular chats
- Deletes messages with `deleteBusinessMessages`, which needs the `can_delete_sent_messages` right on the connection
- Stores connection state in the Chat SDK state adapter, so a revoked or disabled connection is honoured by every running instance

Reactions are not available on business threads. The Bot API has no business variant of `setMessageReaction`, so `addReaction` and `removeReaction` throw a `NotImplementedError` there.

When polling, the adapter always sends an explicit `allowed_updates` list in Business mode: the default update types plus the business ones, or your `longPolling.allowedUpdates` merged with the business ones. Telegram otherwise reuses whatever list an earlier call set, which can silently exclude business updates. When registering a webhook yourself, include the business update types:

```json
[
"message",
"business_connection",
"business_message",
"edited_business_message"
]
```

Business thread IDs start with `telegram:biz`, so state adapters that shard by the first two ID segments (such as Cloudflare Agents) place every business conversation in one shard. Override the sharder with a key that includes the connection ID if that matters for your deployment.

Defaults to `false` for backward compatibility.

## Polling (local development)

When developing locally you typically can't expose a public URL for Telegram to deliver webhooks to. Polling mode uses `getUpdates` to fetch messages directly from Telegram instead — no public endpoint needed.
Expand Down Expand Up @@ -161,6 +198,7 @@ Most options are auto-detected from environment variables when not provided. `na
| `secretToken` | Webhook* | Webhook secret token. Auto-detected from `TELEGRAM_WEBHOOK_SECRET_TOKEN` |
| `mode` | No | Adapter mode: `auto` (default), `webhook`, or `polling` |
| `longPolling` | No | Optional long polling config for `getUpdates` (`timeout`, `limit`, `allowedUpdates`, `deleteWebhook`, `dropPendingUpdates`, `retryDelayMs`) |
| `businessMode` | No | Enable Telegram Business mode (`business_connection`, `business_message`, `edited_business_message`). Defaults to `false` |
| `userName` | No | Bot username used for mention detection. Auto-detected from `TELEGRAM_BOT_USERNAME` or `getMe` |
| `mentionOnReply` | No | Treat a reply to one of the bot's own messages as a mention, so it routes to `onNewMention`. Defaults to `false`. Auto-detected from `TELEGRAM_MENTION_ON_REPLY=true`. Implicit forum-topic replies and the bot's own messages never count |
| `nativeStreaming` | No | Stream with Telegram's native draft previews in private chats. Defaults to `false`, which uses post-and-edit in every chat type |
Expand Down
118 changes: 118 additions & 0 deletions packages/adapter-telegram/sample-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -576,3 +576,121 @@ and the story id.
}
}
```

## Business connection (Connected Business Bot)

Sent when the owner connects the bot, changes its rights, or disconnects
it. Arrives on its own; Telegram never combines it with a message in one
update.

```json
{
"update_id": 312744900,
"business_connection": {
"id": "conn-example-abc",
"user": {
"id": 100000010,
"is_bot": false,
"first_name": "Business Owner",
"username": "owner"
},
"user_chat_id": 100000010,
"date": 1756290700,
"is_enabled": true,
"rights": {
"can_reply": true,
"can_read_messages": true,
"can_delete_sent_messages": true
}
}
}
```

## Business message (customer to business account)

```json
{
"update_id": 312744901,
"business_message": {
"message_id": 42,
"from": {
"id": 100000001,
"is_bot": false,
"first_name": "Customer",
"username": "customer"
},
"chat": {
"id": 100000001,
"first_name": "Customer",
"username": "customer",
"type": "private"
},
"date": 1756290701,
"business_connection_id": "conn-example-abc",
"text": "Hi, I need help with my order"
}
}
```

## Business message (sent by the bot on behalf of the account)

Outgoing replies are echoed back as `business_message`. `from` is the
account owner and `sender_business_bot` is the bot that sent it. The
adapter skips these.

```json
{
"update_id": 312744902,
"business_message": {
"message_id": 43,
"from": {
"id": 100000010,
"is_bot": false,
"first_name": "Business Owner",
"username": "owner"
},
"sender_business_bot": {
"id": 100000099,
"is_bot": true,
"first_name": "Support Bot",
"username": "supportbot"
},
"chat": {
"id": 100000001,
"first_name": "Customer",
"username": "customer",
"type": "private"
},
"date": 1756290702,
"business_connection_id": "conn-example-abc",
"text": "Thanks, looking into your order now"
}
}
```

## Edited business message

```json
{
"update_id": 312744903,
"edited_business_message": {
"message_id": 42,
"from": {
"id": 100000001,
"is_bot": false,
"first_name": "Customer",
"username": "customer"
},
"chat": {
"id": 100000001,
"first_name": "Customer",
"username": "customer",
"type": "private"
},
"date": 1756290701,
"edit_date": 1756290760,
"business_connection_id": "conn-example-abc",
"text": "Hi, I need help with my order #4821"
}
}
```
Loading