Files
featherChat/warzone/docs/LLM_BOT_DEV.md
Siavash Sameni c2be68ca20 docs: comprehensive update all docs to v0.0.46
11 files updated to reflect current state (v0.0.22 → v0.0.46):

ARCHITECTURE.md:
- Ring tones, group calls, read receipts, markdown rendering sections
- Bot API expanded (BotFather, numeric IDs, Telegram compat)
- Admin commands, known issues, 155 tests

TASK_PLAN.md:
- All P1-P4 marked DONE with version numbers
- Additional completed work section (bots, ETH, ring tones, group calls)
- New FC-P7 (Voice & Transport): cpal, Sender Keys, WebTransport
- FC-P6-T9/T10 added

PROGRESS.md:
- Full version history table v0.0.22 through v0.0.46
- Known issues section

README.md:
- Voice calls, ring tones, group calls, read receipts, markdown, 155 tests

SECURITY.md:
- Bot API security, voice call security, admin commands sections
- Updated protection tables

USAGE.md:
- Group calls, read receipts, markdown formatting, admin commands

CLIENT.md:
- Call commands, read receipts, markdown rendering

LLM_HELP.md + LLM_BOT_DEV.md:
- Call/group call/admin commands, ring tones, per-bot numeric IDs

TESTING_E2E.md:
- Tests 16-18: ring tones, group calls, admin commands

CLAUDE.md:
- Ring tone notes, group signal endpoint, MLS roadmap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 09:47:13 +04:00

9.2 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 and Group Calls

Bots cannot initiate or participate in voice calls or group 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. Group call signals (/gcall, /gjoin, etc.) are similarly not actionable by bots.

Markdown Rendering

Bot replies support inline markdown formatting in both the web and TUI clients:

  • **bold** or <b>bold</b> (with parse_mode: "HTML")
  • *italic* or <i>italic</i>
  • `inline code` or <code>code</code>
  • [link text](url) or <a href="url">text</a>
  • ```block``` for code blocks

When using parse_mode: "HTML", the HTML tags are rendered. Without parse_mode, the web client renders markdown syntax natively. Both paths produce styled output.

Per-Bot Numeric IDs

Each bot sees a unique numeric ID for each user (from.id in updates). These IDs are:

  • Deterministic: the same user always maps to the same numeric ID for a given bot
  • Per-bot unique: different bots see different numeric IDs for the same user
  • Privacy-preserving: bots cannot correlate users across bots or recover raw fingerprints from the numeric ID
  • Derived via HMAC of the user's fingerprint keyed with the bot's token prefix

Use from.id (or chat.id) as-is for replies. Do not attempt to reverse it to a fingerprint.

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)