Web UI: - Peer input Enter key now resolves ETH/@alias (like /peer command) - ETH address stored and shown everywhere instead of raw fingerprint - Call UI shows ETH address: "Calling 0x0021...", "In call with 0x9D70..." - Server URL color: #444 → #666 (readable on dark background) - Peer input placeholder: "ETH address, fingerprint, or @alias" - peerEthAddr persisted in localStorage across sessions Server: - WS binary header: strip zero-padding from 64-char to 32-char fingerprint - Call routing now works (was failing due to padded fingerprint lookup) - startCall() resolves ETH/alias before sending CallSignal::Offer - Audio bridge sends auth token to wzp-web as first WS message - Deterministic room name: sorted fingerprint pair (both peers same room) Docs updated: - SERVER.md: WZP integration section (components, running, TLS, auth flow) - USAGE.md: voice call usage for web and TUI - LLM_HELP.md: call architecture, key files, environment vars - LLM_BOT_DEV.md: note that bots cannot participate in calls - TESTING_E2E.md: updated WZP prerequisites with correct flags Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
269 lines
8.0 KiB
Markdown
269 lines
8.0 KiB
Markdown
# featherChat Bot Development Reference
|
|
|
|
## Prerequisites
|
|
|
|
Server must run with `--enable-bots`:
|
|
```bash
|
|
warzone-server --bind 0.0.0.0:7700 --enable-bots
|
|
```
|
|
|
|
## Creating a Bot
|
|
|
|
Message `@botfather` in the chat client (TUI or web):
|
|
|
|
```
|
|
You: /peer @botfather
|
|
You: /newbot MyAssistantBot
|
|
BotFather: Done! Your new bot @myassistantbot is ready.
|
|
Token: a1b2c3d4e5f6a7b8:9876543210abcdef...
|
|
Keep this token secret!
|
|
```
|
|
|
|
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":123456,"id_str":"aabbccdd...","is_bot":true,"first_name":"MyBot"}}
|
|
```
|
|
|
|
### getUpdates
|
|
```
|
|
POST /v1/bot/TOKEN/getUpdates
|
|
{"offset":LAST_ID+1,"timeout":50,"limit":100}
|
|
```
|
|
|
|
Response:
|
|
```json
|
|
{"ok":true,"result":[
|
|
{"update_id":1,"message":{
|
|
"message_id":"uuid",
|
|
"from":{"id":123456,"is_bot":false},
|
|
"chat":{"id":123456,"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 (per-bot unique hash, different bots see different IDs for same user)
|
|
- No raw fingerprint exposed to bots (privacy: bots can't correlate users cross-bot)
|
|
|
|
### sendMessage
|
|
```
|
|
POST /v1/bot/TOKEN/sendMessage
|
|
{
|
|
"chat_id": "fingerprint_hex_or_numeric_id",
|
|
"text": "Hello!",
|
|
"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}}
|
|
```
|
|
|
|
`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}
|
|
→ {"ok":true,"result":true}
|
|
```
|
|
|
|
### sendDocument
|
|
```
|
|
POST /v1/bot/TOKEN/sendDocument
|
|
{"chat_id":"..","document":"filename_or_url","caption":"optional"}
|
|
```
|
|
|
|
### Webhooks
|
|
```
|
|
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
|
|
|
|
**User message (plaintext — default for bot recipients):**
|
|
```json
|
|
{"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}}
|
|
```
|
|
|
|
**Bot-to-bot message:**
|
|
```json
|
|
{"update_id":2,"message":{"message_id":"id","from":{"id":456,"is_bot":true},"chat":{"id":456,"type":"private"},"text":"inter-bot msg","date":1234567890}}
|
|
```
|
|
|
|
**E2E encrypted (user sent without bot detection — rare):**
|
|
```json
|
|
{"update_id":3,"message":{"text":null,"raw_encrypted":"base64..."}}
|
|
```
|
|
|
|
**File:**
|
|
```json
|
|
{"update_id":4,"message":{"document":{"file_name":"report.pdf","file_size":1234}}}
|
|
```
|
|
|
|
## Python Echo Bot
|
|
|
|
```python
|
|
import requests, time
|
|
|
|
TOKEN = "YOUR_TOKEN" # from @botfather /newbot
|
|
API = f"http://localhost:7700/v1/bot/{TOKEN}"
|
|
offset = 0
|
|
|
|
while True:
|
|
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)
|
|
```
|
|
|
|
## Python Menu Bot (Inline Keyboard)
|
|
|
|
```python
|
|
import requests
|
|
|
|
TOKEN = "YOUR_TOKEN"
|
|
API = f"http://localhost:7700/v1/bot/{TOKEN}"
|
|
offset = 0
|
|
|
|
def menu(chat_id):
|
|
requests.post(f"{API}/sendMessage", json={
|
|
"chat_id": chat_id, "text": "Pick one:",
|
|
"reply_markup": {"inline_keyboard": [
|
|
[{"text": "A", "callback_data": "a"}, {"text": "B", "callback_data": "b"}]
|
|
]}
|
|
})
|
|
|
|
while True:
|
|
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
|
|
|
|
```javascript
|
|
const TOKEN = process.env.BOT_TOKEN;
|
|
const API = `http://localhost:7700/v1/bot/${TOKEN}`;
|
|
let offset = 0;
|
|
|
|
(async () => {
|
|
while (true) {
|
|
try {
|
|
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: chat.id, text: `Echo: ${text}`})
|
|
});
|
|
}
|
|
} catch(e) { console.error(e); await new Promise(r => setTimeout(r, 3000)); }
|
|
}
|
|
})();
|
|
```
|
|
|
|
## 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 | 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 |
|
|
| from.id | integer | integer (per-bot unique hash, no raw fp exposed) |
|
|
| 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 |
|
|
|
|
## Voice Calls
|
|
|
|
Bots cannot initiate or participate in voice calls. Voice is peer-to-peer only between human clients (web or TUI). Call signaling messages (`CallSignal` type) are delivered to bots via getUpdates as `text="/call_Offer"` etc., but bots should ignore them -- there is no audio path for bots.
|
|
|
|
## Key Rules
|
|
|
|
1. **Always use offset** in getUpdates — without it you reprocess messages
|
|
2. **chat_id** — use `msg.chat.id` (numeric, per-bot unique) for replies
|
|
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)
|