docs: update LLM bot dev guide with BotFather chat flow + plaintext auto-detect
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,126 +1,140 @@
|
||||
# featherChat Bot Development Reference
|
||||
|
||||
## Setup
|
||||
## Prerequisites
|
||||
|
||||
Server: `http://HOST:7700`
|
||||
All bot endpoints: `/v1/bot/<TOKEN>/METHOD`
|
||||
|
||||
**Prerequisites:** The server must be started with `--enable-bots` to activate bot functionality.
|
||||
|
||||
### BotFather Registration
|
||||
|
||||
Bots can only be created through `@botfather`. On first server start, BotFather is auto-created and its token is printed in the server logs.
|
||||
|
||||
To create a bot:
|
||||
1. Message `@botfather` in the chat client (or use the BotFather token from server logs for programmatic access).
|
||||
2. BotFather calls `/v1/bot/register` with your request, including a `botfather_token` field for authorization.
|
||||
|
||||
Registration request (sent by BotFather internally):
|
||||
```
|
||||
POST /v1/bot/register
|
||||
{"name":"MyBot","fingerprint":"any_32_hex_chars","botfather_token":"<bf_token>"}
|
||||
→ {"ok":true,"result":{"token":"TOKEN","alias":"@mybot_bot","owner":"<creator_fp>"}}
|
||||
Server must run with `--enable-bots`:
|
||||
```bash
|
||||
warzone-server --bind 0.0.0.0:7700 --enable-bots
|
||||
```
|
||||
|
||||
Bot names must end with Bot/bot/_bot. Token format: `<fp_prefix>:<random_hex>`.
|
||||
## Creating a Bot
|
||||
|
||||
### E2E Bot Mode
|
||||
Message `@botfather` in the chat client (TUI or web):
|
||||
|
||||
Bots can optionally participate in E2E encryption. Pass additional fields during registration:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "SecureBot",
|
||||
"fingerprint": "...",
|
||||
"botfather_token": "...",
|
||||
"e2e": true,
|
||||
"bundle": { "identity_key": "...", "signed_prekey": "...", "signature": "...", "one_time_prekeys": ["..."] },
|
||||
"eth_address": "0x..."
|
||||
}
|
||||
```
|
||||
You: /peer @botfather
|
||||
You: /newbot MyAssistantBot
|
||||
BotFather: Done! Your new bot @myassistantbot is ready.
|
||||
Token: a1b2c3d4e5f6a7b8:9876543210abcdef...
|
||||
Keep this token secret!
|
||||
```
|
||||
|
||||
An E2E bot registers a full prekey bundle and can establish X3DH sessions with users, receiving decryptable messages instead of `raw_encrypted` blobs.
|
||||
BotFather commands:
|
||||
- `/newbot <name>` — create bot (name must end with bot/Bot)
|
||||
- `/mybots` — list your bots
|
||||
- `/deletebot <name>` — delete bot you own
|
||||
- `/token <name>` — show token for your bot
|
||||
- `/help` — show commands
|
||||
|
||||
## How Users Message Bots
|
||||
|
||||
When a user messages a bot alias (`@*bot`, `@*Bot`, `@*_bot`, `@botfather`), the client **automatically sends plaintext** — no E2E encryption. The bot receives readable `text` in getUpdates.
|
||||
|
||||
This is automatic — no configuration needed. The client detects the bot alias suffix.
|
||||
|
||||
## API Base
|
||||
|
||||
```
|
||||
http://SERVER:7700/v1/bot/TOKEN/METHOD
|
||||
```
|
||||
|
||||
## Endpoints
|
||||
|
||||
### getMe
|
||||
```
|
||||
GET /v1/bot/TOKEN/getMe
|
||||
→ {"ok":true,"result":{"id":"fp","is_bot":true,"first_name":"MyBot","username":"MyBot"}}
|
||||
→ {"ok":true,"result":{"id":123456,"id_str":"aabbccdd...","is_bot":true,"first_name":"MyBot"}}
|
||||
```
|
||||
|
||||
### getUpdates (long-poll)
|
||||
### getUpdates
|
||||
```
|
||||
POST /v1/bot/TOKEN/getUpdates
|
||||
{"offset":LAST_UPDATE_ID+1,"timeout":30,"limit":100}
|
||||
→ {"ok":true,"result":[{"update_id":N,"message":{...}}]}
|
||||
{"offset":LAST_ID+1,"timeout":50,"limit":100}
|
||||
```
|
||||
|
||||
offset: skip updates with id < offset (acknowledge processed)
|
||||
timeout: long-poll seconds (max 30)
|
||||
limit: max updates to return (default 100)
|
||||
Response:
|
||||
```json
|
||||
{"ok":true,"result":[
|
||||
{"update_id":1,"message":{
|
||||
"message_id":"uuid",
|
||||
"from":{"id":123456,"id_str":"sender_fp_hex","is_bot":false},
|
||||
"chat":{"id":123456,"id_str":"sender_fp_hex","type":"private"},
|
||||
"date":1711612800,
|
||||
"text":"Hello bot!"
|
||||
}}
|
||||
]}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `offset` — skip updates < offset (acknowledge processed). **Always use this.**
|
||||
- `timeout` — long-poll seconds (max 50, matches Telegram)
|
||||
- `limit` — max updates (default 100)
|
||||
- `from.id` — numeric (i64 hash of fingerprint, for TG library compat)
|
||||
- `from.id_str` — hex fingerprint string
|
||||
|
||||
### sendMessage
|
||||
```
|
||||
POST /v1/bot/TOKEN/sendMessage
|
||||
{
|
||||
"chat_id": "FINGERPRINT",
|
||||
"chat_id": "fingerprint_hex_or_numeric_id",
|
||||
"text": "Hello!",
|
||||
"parse_mode": "HTML", // optional
|
||||
"reply_to_message_id": "MSG_ID", // optional
|
||||
"reply_markup": { // optional, inline keyboard
|
||||
"parse_mode": "HTML",
|
||||
"reply_to_message_id": "msg_uuid",
|
||||
"reply_markup": {
|
||||
"inline_keyboard": [
|
||||
[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]
|
||||
]
|
||||
}
|
||||
}
|
||||
→ {"ok":true,"result":{"message_id":"UUID","delivered":true}}
|
||||
→ {"ok":true,"result":{"message_id":"uuid","delivered":true}}
|
||||
```
|
||||
|
||||
`chat_id` accepts: hex fingerprint string, numeric i64, or `0x` ETH address.
|
||||
`parse_mode` "HTML" renders `<b>`, `<i>`, `<code>`, `<a>` in web client.
|
||||
|
||||
### editMessageText
|
||||
```
|
||||
POST /v1/bot/TOKEN/editMessageText
|
||||
{"chat_id":"..","message_id":"uuid","text":"Updated","reply_markup":{...}}
|
||||
```
|
||||
|
||||
### answerCallbackQuery
|
||||
```
|
||||
POST /v1/bot/TOKEN/answerCallbackQuery
|
||||
{"callback_query_id":"ID","text":"Done!","show_alert":false}
|
||||
{"callback_query_id":"id","text":"Done!","show_alert":false}
|
||||
→ {"ok":true,"result":true}
|
||||
```
|
||||
|
||||
### editMessageText
|
||||
```
|
||||
POST /v1/bot/TOKEN/editMessageText
|
||||
{"chat_id":"FP","message_id":"MSG_ID","text":"Updated text","reply_markup":{...}}
|
||||
```
|
||||
|
||||
### sendDocument
|
||||
```
|
||||
POST /v1/bot/TOKEN/sendDocument
|
||||
{"chat_id":"FP","document":"filename_or_url","caption":"optional"}
|
||||
{"chat_id":"..","document":"filename_or_url","caption":"optional"}
|
||||
```
|
||||
|
||||
### setWebhook / deleteWebhook / getWebhookInfo
|
||||
### Webhooks
|
||||
```
|
||||
POST /v1/bot/TOKEN/setWebhook {"url":"https://mybot.example.com/webhook"}
|
||||
POST /v1/bot/TOKEN/setWebhook {"url":"https://mybot.example.com/hook"}
|
||||
POST /v1/bot/TOKEN/deleteWebhook
|
||||
GET /v1/bot/TOKEN/getWebhookInfo
|
||||
```
|
||||
|
||||
When set, updates are POSTed to the URL instead of queued for getUpdates.
|
||||
|
||||
## Update Types
|
||||
|
||||
Messages from users arrive in getUpdates as:
|
||||
|
||||
**Plaintext (from other bots):**
|
||||
**User message (plaintext — default for bot recipients):**
|
||||
```json
|
||||
{"update_id":1,"message":{"message_id":"id","from":{"id":"fp","is_bot":true},"chat":{"id":"fp","type":"private"},"text":"Hello"}}
|
||||
{"update_id":1,"message":{"message_id":"id","from":{"id":123,"id_str":"fp"},"chat":{"id":123,"id_str":"fp","type":"private"},"text":"Hello bot!","date":1234567890}}
|
||||
```
|
||||
|
||||
**Encrypted (from users with E2E sessions):**
|
||||
**Bot-to-bot message:**
|
||||
```json
|
||||
{"update_id":2,"message":{"message_id":"id","from":{"id":"fp","is_bot":false},"chat":{"id":"fp"},"text":null,"raw_encrypted":"base64..."}}
|
||||
{"update_id":2,"message":{"message_id":"id","from":{"id":456,"is_bot":true},"chat":{"id":456,"type":"private"},"text":"inter-bot msg","date":1234567890}}
|
||||
```
|
||||
Note: v1 bots cannot decrypt E2E messages. They see text=null + raw_encrypted blob.
|
||||
|
||||
**Call signal:**
|
||||
**E2E encrypted (user sent without bot detection — rare):**
|
||||
```json
|
||||
{"update_id":3,"message":{"text":"/call_Offer","call_signal":{"type":"Offer","payload":"..."}}}
|
||||
{"update_id":3,"message":{"text":null,"raw_encrypted":"base64..."}}
|
||||
```
|
||||
|
||||
**File:**
|
||||
@@ -128,28 +142,29 @@ Note: v1 bots cannot decrypt E2E messages. They see text=null + raw_encrypted bl
|
||||
{"update_id":4,"message":{"document":{"file_name":"report.pdf","file_size":1234}}}
|
||||
```
|
||||
|
||||
## Python Examples
|
||||
## Python Echo Bot
|
||||
|
||||
### Echo Bot
|
||||
```python
|
||||
import requests, time
|
||||
|
||||
TOKEN = "YOUR_TOKEN"
|
||||
TOKEN = "YOUR_TOKEN" # from @botfather /newbot
|
||||
API = f"http://localhost:7700/v1/bot/{TOKEN}"
|
||||
offset = 0
|
||||
|
||||
while True:
|
||||
resp = requests.post(f"{API}/getUpdates", json={"offset": offset, "timeout": 30}).json()
|
||||
for update in resp.get("result", []):
|
||||
offset = update["update_id"] + 1
|
||||
msg = update.get("message", {})
|
||||
chat_id = msg.get("chat", {}).get("id", "")
|
||||
r = requests.post(f"{API}/getUpdates", json={"offset": offset, "timeout": 50}).json()
|
||||
for u in r.get("result", []):
|
||||
offset = u["update_id"] + 1
|
||||
msg = u.get("message", {})
|
||||
text = msg.get("text")
|
||||
chat_id = msg.get("chat", {}).get("id", "")
|
||||
if text and chat_id:
|
||||
requests.post(f"{API}/sendMessage", json={"chat_id": chat_id, "text": f"Echo: {text}"})
|
||||
time.sleep(0.1)
|
||||
```
|
||||
|
||||
### Inline Keyboard Bot
|
||||
## Python Menu Bot (Inline Keyboard)
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
@@ -157,101 +172,93 @@ TOKEN = "YOUR_TOKEN"
|
||||
API = f"http://localhost:7700/v1/bot/{TOKEN}"
|
||||
offset = 0
|
||||
|
||||
def send_menu(chat_id):
|
||||
def menu(chat_id):
|
||||
requests.post(f"{API}/sendMessage", json={
|
||||
"chat_id": chat_id,
|
||||
"text": "Choose an option:",
|
||||
"reply_markup": {
|
||||
"inline_keyboard": [
|
||||
[{"text": "Option A", "callback_data": "a"}, {"text": "Option B", "callback_data": "b"}],
|
||||
[{"text": "Help", "callback_data": "help"}]
|
||||
]
|
||||
}
|
||||
"chat_id": chat_id, "text": "Pick one:",
|
||||
"reply_markup": {"inline_keyboard": [
|
||||
[{"text": "A", "callback_data": "a"}, {"text": "B", "callback_data": "b"}]
|
||||
]}
|
||||
})
|
||||
|
||||
while True:
|
||||
resp = requests.post(f"{API}/getUpdates", json={"offset": offset, "timeout": 30}).json()
|
||||
for update in resp.get("result", []):
|
||||
offset = update["update_id"] + 1
|
||||
msg = update.get("message", {})
|
||||
text = msg.get("text", "")
|
||||
chat_id = msg.get("chat", {}).get("id", "")
|
||||
if text == "/start":
|
||||
send_menu(chat_id)
|
||||
elif text:
|
||||
requests.post(f"{API}/sendMessage", json={"chat_id": chat_id, "text": f"You said: {text}"})
|
||||
r = requests.post(f"{API}/getUpdates", json={"offset": offset, "timeout": 50}).json()
|
||||
for u in r.get("result", []):
|
||||
offset = u["update_id"] + 1
|
||||
msg = u.get("message", {})
|
||||
text, cid = msg.get("text", ""), msg.get("chat", {}).get("id", "")
|
||||
if text == "/start": menu(cid)
|
||||
elif text: requests.post(f"{API}/sendMessage", json={"chat_id": cid, "text": f"You said: {text}"})
|
||||
```
|
||||
|
||||
### Node.js Echo Bot
|
||||
## Node.js Echo Bot
|
||||
|
||||
```javascript
|
||||
const API = `http://localhost:7700/v1/bot/${process.env.BOT_TOKEN}`;
|
||||
const TOKEN = process.env.BOT_TOKEN;
|
||||
const API = `http://localhost:7700/v1/bot/${TOKEN}`;
|
||||
let offset = 0;
|
||||
|
||||
async function poll() {
|
||||
(async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const res = await fetch(`${API}/getUpdates`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({offset, timeout: 30})
|
||||
});
|
||||
const data = await res.json();
|
||||
for (const update of data.result || []) {
|
||||
offset = update.update_id + 1;
|
||||
const msg = update.message;
|
||||
if (msg?.text && msg?.chat?.id) {
|
||||
const r = await (await fetch(`${API}/getUpdates`, {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({offset, timeout: 50})
|
||||
})).json();
|
||||
for (const u of r.result || []) {
|
||||
offset = u.update_id + 1;
|
||||
const {text, chat} = u.message || {};
|
||||
if (text && chat?.id)
|
||||
await fetch(`${API}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({chat_id: msg.chat.id, text: `Echo: ${msg.text}`})
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({chat_id: chat.id, text: `Echo: ${text}`})
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error(e); await new Promise(r => setTimeout(r, 3000)); }
|
||||
} catch(e) { console.error(e); await new Promise(r => setTimeout(r, 3000)); }
|
||||
}
|
||||
}
|
||||
poll();
|
||||
})();
|
||||
```
|
||||
|
||||
## Bot Bridge (TG Library Compatibility)
|
||||
|
||||
For unmodified Telegram bots (python-telegram-bot, aiogram, Telegraf):
|
||||
|
||||
```bash
|
||||
python3 tools/bot-bridge.py --server http://localhost:7700 --token YOUR_TOKEN --port 8081
|
||||
```
|
||||
|
||||
Then point your TG bot at the bridge:
|
||||
```python
|
||||
# python-telegram-bot
|
||||
from telegram import Bot
|
||||
bot = Bot(token="TOKEN", base_url="http://localhost:8081/botTOKEN")
|
||||
|
||||
# Telegraf (Node.js)
|
||||
const bot = new Telegraf("TOKEN", { telegram: { apiRoot: "http://localhost:8081" } })
|
||||
```
|
||||
|
||||
The bridge translates numeric chat_id ↔ fingerprints automatically.
|
||||
|
||||
## Differences from Telegram
|
||||
|
||||
| Feature | Telegram | featherChat |
|
||||
|---------|----------|------------|
|
||||
| chat_id | numeric | hex fingerprint string OR numeric (both accepted) |
|
||||
|---------|----------|-------------|
|
||||
| chat_id | integer | string fp, numeric, or 0x ETH (all accepted) |
|
||||
| User→bot messages | plaintext | plaintext (auto-detected by client) |
|
||||
| Bot creation | @BotFather chat | @botfather chat (same flow) |
|
||||
| getUpdates timeout | up to 50s | up to 50s |
|
||||
| User messages | plaintext | E2E encrypted (text=null unless E2E bot) |
|
||||
| Bot messages | plaintext | plaintext (no E2E) unless E2E bot mode |
|
||||
| File upload | multipart form | JSON reference (v1) |
|
||||
| Inline keyboards | full support | stored + delivered, no popup |
|
||||
| Callback queries | full popup | acknowledged, no popup |
|
||||
| Webhooks | full HTTPS | URL stored, updates delivered live (POST to webhook URL) |
|
||||
| Media groups | supported | not yet |
|
||||
| parse_mode | renders HTML/MD | HTML rendered (<b>, <i>, <code>, <a>) |
|
||||
| from.id | integer | integer (hash of fp) + id_str (hex fp) |
|
||||
| File upload | multipart | JSON reference (v1) |
|
||||
| Inline keyboards | full | stored + delivered, no popup |
|
||||
| Webhooks | HTTPS POST | HTTP POST (delivered live) |
|
||||
| parse_mode HTML | rendered | rendered in web client |
|
||||
| Media groups | yes | not yet |
|
||||
|
||||
## Key Patterns
|
||||
## Key Rules
|
||||
|
||||
**Always use offset** — without it, the same messages are returned every poll.
|
||||
|
||||
**chat_id is the sender's fingerprint** — use `msg.chat.id` or `msg.from.id`. Note: `from.id` is now a numeric integer for TG compatibility; use `from.id_str` for the hex fingerprint.
|
||||
|
||||
**Bot alias** — users message bots via `@mybot_bot` which resolves to the bot's fingerprint.
|
||||
|
||||
**Error handling** — all responses have `{"ok": bool}`. Check `ok` before accessing `result`.
|
||||
|
||||
**Rate limits** — 200 concurrent server requests, no per-bot limit (be reasonable).
|
||||
|
||||
## Bot Bridge (`tools/bot-bridge.py`)
|
||||
|
||||
The bot bridge provides a compatibility layer for existing Telegram bot libraries. It translates between featherChat's Bot API and standard TG libraries.
|
||||
|
||||
**Supported libraries:**
|
||||
- **python-telegram-bot** — set `base_url` to `http://your-server:7700/v1/bot/`
|
||||
- **aiogram** — configure the bot session with the featherChat server URL
|
||||
- **Telegraf (Node.js)** — set `telegram.apiRoot` to `http://your-server:7700/v1/bot`
|
||||
|
||||
Usage:
|
||||
```bash
|
||||
python tools/bot-bridge.py --token YOUR_BOT_TOKEN --server http://localhost:7700
|
||||
```
|
||||
|
||||
The bridge handles differences like fingerprint-based chat_id, numeric ID translation, and webhook forwarding.
|
||||
1. **Always use offset** in getUpdates — without it you reprocess messages
|
||||
2. **chat_id** — use `msg.chat.id` (numeric) or `msg.chat.id_str` (hex fingerprint)
|
||||
3. **Bot names** must end with `bot`, `Bot`, or `_bot`
|
||||
4. **Only @botfather** can create bots — direct API registration requires botfather_token
|
||||
5. **Server needs --enable-bots** — without it all bot endpoints return 403
|
||||
6. **Plaintext by default** — user clients auto-detect bot aliases and skip E2E
|
||||
7. **E2E bots** — register with `e2e:true` + bundle for encrypted sessions (advanced)
|
||||
|
||||
@@ -96,7 +96,8 @@ Groups auto-create on join if they don't exist. Server fans out per-member encry
|
||||
- Server sees: metadata (who talks to whom, timestamps), encrypted blobs, presence.
|
||||
- Server cannot read msg content.
|
||||
- Pre-keys: signed pre-key + 10 one-time pre-keys uploaded on register.
|
||||
- Bot API msgs are NOT E2E encrypted (plaintext JSON envelopes).
|
||||
- Bot msgs: clients auto-detect bot aliases, send plaintext (no E2E). Server can read bot msgs.
|
||||
- E2E bots possible (register with seed+bundle) but standard bots are plaintext.
|
||||
|
||||
## Federation
|
||||
|
||||
@@ -131,15 +132,18 @@ no prekeys available | recipient's one-time prekeys exhausted | they need to re-
|
||||
|
||||
## Bot API (Telegram-compatible)
|
||||
|
||||
### BotFather
|
||||
### Creating a Bot
|
||||
|
||||
`@botfather` is the only way to create bots. It is auto-created on first server start (token printed in server logs). Users message `@botfather` to register new bots. The server must be started with `--enable-bots` to activate bot functionality.
|
||||
Server must run with `--enable-bots`. Then in chat:
|
||||
```
|
||||
/peer @botfather
|
||||
/newbot MyWeatherBot
|
||||
→ BotFather replies with token
|
||||
```
|
||||
|
||||
### Registration
|
||||
BotFather commands: /newbot, /mybots, /deletebot <name>, /token <name>, /help
|
||||
|
||||
Bots are created via BotFather, which calls POST /v1/bot/register with a `botfather_token` field. Each bot has an `owner` field linking it to the user who requested creation.
|
||||
|
||||
Bot aliases must end with Bot, bot, or _bot. Non-bots cannot use these.
|
||||
Bot names must end with bot/Bot/_bot. Only @botfather can create bots.
|
||||
|
||||
### Plaintext Bot Messaging
|
||||
|
||||
|
||||
Reference in New Issue
Block a user