diff --git a/warzone/docs/LLM_BOT_DEV.md b/warzone/docs/LLM_BOT_DEV.md index 0e81e0e..182dab2 100644 --- a/warzone/docs/LLM_BOT_DEV.md +++ b/warzone/docs/LLM_BOT_DEV.md @@ -1,126 +1,140 @@ # featherChat Bot Development Reference -## Setup +## Prerequisites -Server: `http://HOST:7700` -All bot endpoints: `/v1/bot//METHOD` - -**Prerequisites:** The server must be started with `--enable-bots` to activate bot functionality. - -### BotFather Registration - -Bots can only be created through `@botfather`. On first server start, BotFather is auto-created and its token is printed in the server logs. - -To create a bot: -1. Message `@botfather` in the chat client (or use the BotFather token from server logs for programmatic access). -2. BotFather calls `/v1/bot/register` with your request, including a `botfather_token` field for authorization. - -Registration request (sent by BotFather internally): -``` -POST /v1/bot/register -{"name":"MyBot","fingerprint":"any_32_hex_chars","botfather_token":""} -→ {"ok":true,"result":{"token":"TOKEN","alias":"@mybot_bot","owner":""}} +Server must run with `--enable-bots`: +```bash +warzone-server --bind 0.0.0.0:7700 --enable-bots ``` -Bot names must end with Bot/bot/_bot. Token format: `:`. +## Creating a Bot -### E2E Bot Mode +Message `@botfather` in the chat client (TUI or web): -Bots can optionally participate in E2E encryption. Pass additional fields during registration: - -```json -{ - "name": "SecureBot", - "fingerprint": "...", - "botfather_token": "...", - "e2e": true, - "bundle": { "identity_key": "...", "signed_prekey": "...", "signature": "...", "one_time_prekeys": ["..."] }, - "eth_address": "0x..." -} +``` +You: /peer @botfather +You: /newbot MyAssistantBot +BotFather: Done! Your new bot @myassistantbot is ready. + Token: a1b2c3d4e5f6a7b8:9876543210abcdef... + Keep this token secret! ``` -An E2E bot registers a full prekey bundle and can establish X3DH sessions with users, receiving decryptable messages instead of `raw_encrypted` blobs. +BotFather commands: +- `/newbot ` — create bot (name must end with bot/Bot) +- `/mybots` — list your bots +- `/deletebot ` — delete bot you own +- `/token ` — 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":"fp","is_bot":true,"first_name":"MyBot","username":"MyBot"}} +→ {"ok":true,"result":{"id":123456,"id_str":"aabbccdd...","is_bot":true,"first_name":"MyBot"}} ``` -### getUpdates (long-poll) +### getUpdates ``` POST /v1/bot/TOKEN/getUpdates -{"offset":LAST_UPDATE_ID+1,"timeout":30,"limit":100} -→ {"ok":true,"result":[{"update_id":N,"message":{...}}]} +{"offset":LAST_ID+1,"timeout":50,"limit":100} ``` -offset: skip updates with id < offset (acknowledge processed) -timeout: long-poll seconds (max 30) -limit: max updates to return (default 100) +Response: +```json +{"ok":true,"result":[ + {"update_id":1,"message":{ + "message_id":"uuid", + "from":{"id":123456,"id_str":"sender_fp_hex","is_bot":false}, + "chat":{"id":123456,"id_str":"sender_fp_hex","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 (i64 hash of fingerprint, for TG library compat) +- `from.id_str` — hex fingerprint string ### sendMessage ``` POST /v1/bot/TOKEN/sendMessage { - "chat_id": "FINGERPRINT", + "chat_id": "fingerprint_hex_or_numeric_id", "text": "Hello!", - "parse_mode": "HTML", // optional - "reply_to_message_id": "MSG_ID", // optional - "reply_markup": { // optional, inline keyboard + "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}} +→ {"ok":true,"result":{"message_id":"uuid","delivered":true}} +``` + +`chat_id` accepts: hex fingerprint string, numeric i64, or `0x` ETH address. +`parse_mode` "HTML" renders ``, ``, ``, `` 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} +{"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"} +{"chat_id":"..","document":"filename_or_url","caption":"optional"} ``` -### setWebhook / deleteWebhook / getWebhookInfo +### Webhooks ``` -POST /v1/bot/TOKEN/setWebhook {"url":"https://mybot.example.com/webhook"} +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 -Messages from users arrive in getUpdates as: - -**Plaintext (from other bots):** +**User message (plaintext — default for bot recipients):** ```json -{"update_id":1,"message":{"message_id":"id","from":{"id":"fp","is_bot":true},"chat":{"id":"fp","type":"private"},"text":"Hello"}} +{"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}} ``` -**Encrypted (from users with E2E sessions):** +**Bot-to-bot message:** ```json -{"update_id":2,"message":{"message_id":"id","from":{"id":"fp","is_bot":false},"chat":{"id":"fp"},"text":null,"raw_encrypted":"base64..."}} +{"update_id":2,"message":{"message_id":"id","from":{"id":456,"is_bot":true},"chat":{"id":456,"type":"private"},"text":"inter-bot msg","date":1234567890}} ``` -Note: v1 bots cannot decrypt E2E messages. They see text=null + raw_encrypted blob. -**Call signal:** +**E2E encrypted (user sent without bot detection — rare):** ```json -{"update_id":3,"message":{"text":"/call_Offer","call_signal":{"type":"Offer","payload":"..."}}} +{"update_id":3,"message":{"text":null,"raw_encrypted":"base64..."}} ``` **File:** @@ -128,28 +142,29 @@ Note: v1 bots cannot decrypt E2E messages. They see text=null + raw_encrypted bl {"update_id":4,"message":{"document":{"file_name":"report.pdf","file_size":1234}}} ``` -## Python Examples +## Python Echo Bot -### Echo Bot ```python import requests, time -TOKEN = "YOUR_TOKEN" +TOKEN = "YOUR_TOKEN" # from @botfather /newbot 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", "") + 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) ``` -### Inline Keyboard Bot +## Python Menu Bot (Inline Keyboard) + ```python import requests @@ -157,101 +172,93 @@ TOKEN = "YOUR_TOKEN" API = f"http://localhost:7700/v1/bot/{TOKEN}" offset = 0 -def send_menu(chat_id): +def 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"}] - ] - } + "chat_id": chat_id, "text": "Pick one:", + "reply_markup": {"inline_keyboard": [ + [{"text": "A", "callback_data": "a"}, {"text": "B", "callback_data": "b"}] + ]} }) 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}"}) + 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 +## Node.js Echo Bot + ```javascript -const API = `http://localhost:7700/v1/bot/${process.env.BOT_TOKEN}`; +const TOKEN = process.env.BOT_TOKEN; +const API = `http://localhost:7700/v1/bot/${TOKEN}`; let offset = 0; -async function poll() { +(async () => { 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) { + 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: msg.chat.id, text: `Echo: ${msg.text}`}) + 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)); } + } catch(e) { console.error(e); await new Promise(r => setTimeout(r, 3000)); } } -} -poll(); +})(); ``` +## 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 | numeric | hex fingerprint string OR numeric (both accepted) | +|---------|----------|-------------| +| 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 | -| User messages | plaintext | E2E encrypted (text=null unless E2E bot) | -| Bot messages | plaintext | plaintext (no E2E) unless E2E bot mode | -| 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, updates delivered live (POST to webhook URL) | -| Media groups | supported | not yet | -| parse_mode | renders HTML/MD | HTML rendered (, , , ) | +| from.id | integer | integer (hash of fp) + id_str (hex fp) | +| 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 Patterns +## Key Rules -**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`. Note: `from.id` is now a numeric integer for TG compatibility; use `from.id_str` for the hex fingerprint. - -**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). - -## Bot Bridge (`tools/bot-bridge.py`) - -The bot bridge provides a compatibility layer for existing Telegram bot libraries. It translates between featherChat's Bot API and standard TG libraries. - -**Supported libraries:** -- **python-telegram-bot** — set `base_url` to `http://your-server:7700/v1/bot/` -- **aiogram** — configure the bot session with the featherChat server URL -- **Telegraf (Node.js)** — set `telegram.apiRoot` to `http://your-server:7700/v1/bot` - -Usage: -```bash -python tools/bot-bridge.py --token YOUR_BOT_TOKEN --server http://localhost:7700 -``` - -The bridge handles differences like fingerprint-based chat_id, numeric ID translation, and webhook forwarding. +1. **Always use offset** in getUpdates — without it you reprocess messages +2. **chat_id** — use `msg.chat.id` (numeric) or `msg.chat.id_str` (hex fingerprint) +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) diff --git a/warzone/docs/LLM_HELP.md b/warzone/docs/LLM_HELP.md index 0b0e408..70af2e2 100644 --- a/warzone/docs/LLM_HELP.md +++ b/warzone/docs/LLM_HELP.md @@ -96,7 +96,8 @@ Groups auto-create on join if they don't exist. Server fans out per-member encry - Server sees: metadata (who talks to whom, timestamps), encrypted blobs, presence. - Server cannot read msg content. - Pre-keys: signed pre-key + 10 one-time pre-keys uploaded on register. -- Bot API msgs are NOT E2E encrypted (plaintext JSON envelopes). +- Bot msgs: clients auto-detect bot aliases, send plaintext (no E2E). Server can read bot msgs. +- E2E bots possible (register with seed+bundle) but standard bots are plaintext. ## Federation @@ -131,15 +132,18 @@ no prekeys available | recipient's one-time prekeys exhausted | they need to re- ## Bot API (Telegram-compatible) -### BotFather +### Creating a Bot -`@botfather` is the only way to create bots. It is auto-created on first server start (token printed in server logs). Users message `@botfather` to register new bots. The server must be started with `--enable-bots` to activate bot functionality. +Server must run with `--enable-bots`. Then in chat: +``` +/peer @botfather +/newbot MyWeatherBot +→ BotFather replies with token +``` -### Registration +BotFather commands: /newbot, /mybots, /deletebot , /token , /help -Bots are created via BotFather, which calls POST /v1/bot/register with a `botfather_token` field. Each bot has an `owner` field linking it to the user who requested creation. - -Bot aliases must end with Bot, bot, or _bot. Non-bots cannot use these. +Bot names must end with bot/Bot/_bot. Only @botfather can create bots. ### Plaintext Bot Messaging