Privacy: from.id is now Hash(bot_token + user_fp) → different bots see different numeric IDs for the same user. Prevents cross-bot user correlation. Removed id_str (raw hex fingerprint) from all bot API responses. Updated LLM_BOT_DEV.md and LLM_HELP.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.7 KiB
featherChat Bot Development Reference
Prerequisites
Server must run with --enable-bots:
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:
{"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):
{"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:
{"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):
{"update_id":3,"message":{"text":null,"raw_encrypted":"base64..."}}
File:
{"update_id":4,"message":{"document":{"file_name":"report.pdf","file_size":1234}}}
Python Echo Bot
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)
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
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):
python3 tools/bot-bridge.py --server http://localhost:7700 --token YOUR_TOKEN --port 8081
Then point your TG bot at the bridge:
# 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 |
Key Rules
- Always use offset in getUpdates — without it you reprocess messages
- chat_id — use
msg.chat.id(numeric, per-bot unique) for replies - Bot names must end with
bot,Bot, or_bot - Only @botfather can create bots — direct API registration requires botfather_token
- Server needs --enable-bots — without it all bot endpoints return 403
- Plaintext by default — user clients auto-detect bot aliases and skip E2E
- E2E bots — register with
e2e:true+ bundle for encrypted sessions (advanced)