feat: complete Telegram-compatible Bot API + bot dev guide
Bot API (routes/bot.rs — full rewrite): - getUpdates: persistent update_id counter, offset acknowledgement, limit (max 100), long-poll up to 30s with 1s intervals - sendMessage: parse_mode, reply_to_message_id, reply_markup (inline keyboards) - answerCallbackQuery: acknowledge button clicks - editMessageText: update sent messages - setWebhook / deleteWebhook / getWebhookInfo: webhook configuration - sendDocument: file reference with caption - Bot queue: raw messages migrated to bot_queue:<fp>:<update_id> for ordering Web client (routes/web.rs): - Bot messages rendered properly (was showing "[message could not be decrypted]") - Handles bot_message, bot_edit, bot_document as both Text and Binary WS frames - Inline keyboard buttons rendered as bracketed text - Missed call notifications handled in Text frame path Docs: - LLM_BOT_DEV.md: token-optimized bot dev reference for coding assistant LLM (Python + Node.js examples, all endpoints, TG compatibility table) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
214
warzone/docs/LLM_BOT_DEV.md
Normal file
214
warzone/docs/LLM_BOT_DEV.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# featherChat Bot Development Reference
|
||||
|
||||
## Setup
|
||||
|
||||
Server: `http://HOST:7700`
|
||||
All bot endpoints: `/v1/bot/<TOKEN>/METHOD`
|
||||
|
||||
Register bot:
|
||||
```
|
||||
POST /v1/bot/register
|
||||
{"name":"MyBot","fingerprint":"any_32_hex_chars"}
|
||||
→ {"ok":true,"result":{"token":"TOKEN","alias":"@mybot_bot"}}
|
||||
```
|
||||
|
||||
Bot names must end with Bot/bot/_bot. Token format: `<fp_prefix>:<random_hex>`.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### getMe
|
||||
```
|
||||
GET /v1/bot/TOKEN/getMe
|
||||
→ {"ok":true,"result":{"id":"fp","is_bot":true,"first_name":"MyBot","username":"MyBot"}}
|
||||
```
|
||||
|
||||
### getUpdates (long-poll)
|
||||
```
|
||||
POST /v1/bot/TOKEN/getUpdates
|
||||
{"offset":LAST_UPDATE_ID+1,"timeout":30,"limit":100}
|
||||
→ {"ok":true,"result":[{"update_id":N,"message":{...}}]}
|
||||
```
|
||||
|
||||
offset: skip updates with id < offset (acknowledge processed)
|
||||
timeout: long-poll seconds (max 30)
|
||||
limit: max updates to return (default 100)
|
||||
|
||||
### sendMessage
|
||||
```
|
||||
POST /v1/bot/TOKEN/sendMessage
|
||||
{
|
||||
"chat_id": "FINGERPRINT",
|
||||
"text": "Hello!",
|
||||
"parse_mode": "HTML", // optional
|
||||
"reply_to_message_id": "MSG_ID", // optional
|
||||
"reply_markup": { // optional, inline keyboard
|
||||
"inline_keyboard": [
|
||||
[{"text":"Yes","callback_data":"yes"},{"text":"No","callback_data":"no"}]
|
||||
]
|
||||
}
|
||||
}
|
||||
→ {"ok":true,"result":{"message_id":"UUID","delivered":true}}
|
||||
```
|
||||
|
||||
### answerCallbackQuery
|
||||
```
|
||||
POST /v1/bot/TOKEN/answerCallbackQuery
|
||||
{"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"}
|
||||
```
|
||||
|
||||
### setWebhook / deleteWebhook / getWebhookInfo
|
||||
```
|
||||
POST /v1/bot/TOKEN/setWebhook {"url":"https://mybot.example.com/webhook"}
|
||||
POST /v1/bot/TOKEN/deleteWebhook
|
||||
GET /v1/bot/TOKEN/getWebhookInfo
|
||||
```
|
||||
|
||||
## Update Types
|
||||
|
||||
Messages from users arrive in getUpdates as:
|
||||
|
||||
**Plaintext (from other bots):**
|
||||
```json
|
||||
{"update_id":1,"message":{"message_id":"id","from":{"id":"fp","is_bot":true},"chat":{"id":"fp","type":"private"},"text":"Hello"}}
|
||||
```
|
||||
|
||||
**Encrypted (from users with E2E sessions):**
|
||||
```json
|
||||
{"update_id":2,"message":{"message_id":"id","from":{"id":"fp","is_bot":false},"chat":{"id":"fp"},"text":null,"raw_encrypted":"base64..."}}
|
||||
```
|
||||
Note: v1 bots cannot decrypt E2E messages. They see text=null + raw_encrypted blob.
|
||||
|
||||
**Call signal:**
|
||||
```json
|
||||
{"update_id":3,"message":{"text":"/call_Offer","call_signal":{"type":"Offer","payload":"..."}}}
|
||||
```
|
||||
|
||||
**File:**
|
||||
```json
|
||||
{"update_id":4,"message":{"document":{"file_name":"report.pdf","file_size":1234}}}
|
||||
```
|
||||
|
||||
## Python Examples
|
||||
|
||||
### Echo Bot
|
||||
```python
|
||||
import requests, time
|
||||
|
||||
TOKEN = "YOUR_TOKEN"
|
||||
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", "")
|
||||
text = msg.get("text")
|
||||
if text and chat_id:
|
||||
requests.post(f"{API}/sendMessage", json={"chat_id": chat_id, "text": f"Echo: {text}"})
|
||||
```
|
||||
|
||||
### Inline Keyboard Bot
|
||||
```python
|
||||
import requests
|
||||
|
||||
TOKEN = "YOUR_TOKEN"
|
||||
API = f"http://localhost:7700/v1/bot/{TOKEN}"
|
||||
offset = 0
|
||||
|
||||
def send_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"}]
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
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}"})
|
||||
```
|
||||
|
||||
### Node.js Echo Bot
|
||||
```javascript
|
||||
const API = `http://localhost:7700/v1/bot/${process.env.BOT_TOKEN}`;
|
||||
let offset = 0;
|
||||
|
||||
async function poll() {
|
||||
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) {
|
||||
await fetch(`${API}/sendMessage`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({chat_id: msg.chat.id, text: `Echo: ${msg.text}`})
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error(e); await new Promise(r => setTimeout(r, 3000)); }
|
||||
}
|
||||
}
|
||||
poll();
|
||||
```
|
||||
|
||||
## Differences from Telegram
|
||||
|
||||
| Feature | Telegram | featherChat |
|
||||
|---------|----------|------------|
|
||||
| chat_id | numeric | hex fingerprint string |
|
||||
| getUpdates timeout | up to 50s | up to 30s |
|
||||
| User messages | plaintext | E2E encrypted (text=null in v1) |
|
||||
| Bot messages | plaintext | plaintext (no E2E) |
|
||||
| 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, delivery planned |
|
||||
| Media groups | supported | not yet |
|
||||
| parse_mode | renders HTML/MD | stored, not rendered (v1) |
|
||||
|
||||
## Key Patterns
|
||||
|
||||
**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`.
|
||||
|
||||
**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).
|
||||
Reference in New Issue
Block a user