Files
featherChat/warzone/docs/LLM_BOT_DEV.md
Siavash Sameni 81954b1b0c v0.0.44: web UI polish — ETH display, peer input, call fixes, docs
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>
2026-03-30 08:32:31 +04:00

8.0 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

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)