diff --git a/warzone/crates/warzone-server/src/routes/aliases.rs b/warzone/crates/warzone-server/src/routes/aliases.rs index b6dbc7e..5fd84b7 100644 --- a/warzone/crates/warzone-server/src/routes/aliases.rs +++ b/warzone/crates/warzone-server/src/routes/aliases.rs @@ -123,6 +123,18 @@ async fn register_alias( return Ok(Json(serde_json::json!({ "error": "alias must be 1-32 alphanumeric chars" }))); } + // Reserve *Bot and *_bot suffixes for bots only + let is_bot_name = alias.ends_with("bot") || alias.ends_with("_bot"); + if is_bot_name { + // Check if this fingerprint is registered as a bot + let bot_key = format!("bot_fp:{}", fp); + let is_registered_bot = state.db.tokens.get(bot_key.as_bytes()) + .ok().flatten().is_some(); + if !is_registered_bot { + return Ok(Json(serde_json::json!({ "error": "aliases ending with 'Bot' or '_bot' are reserved for bots — register via /v1/bot/register first" }))); + } + } + // Check existing record for this alias if let Some(existing) = load_alias_record(&state.db.aliases, &alias) { if existing.fingerprint == fp { diff --git a/warzone/crates/warzone-server/src/routes/bot.rs b/warzone/crates/warzone-server/src/routes/bot.rs index 3578986..1dd71ab 100644 --- a/warzone/crates/warzone-server/src/routes/bot.rs +++ b/warzone/crates/warzone-server/src/routes/bot.rs @@ -100,12 +100,25 @@ async fn register_bot( &token[..token.len().min(20)] ); + // Auto-register bot alias (name must end with Bot or _bot) + let bot_alias = if req.name.ends_with("Bot") || req.name.ends_with("_bot") || req.name.ends_with("bot") { + req.name.to_lowercase() + } else { + format!("{}_bot", req.name.to_lowercase()) + }; + let alias_key = format!("a:{}", bot_alias); + let _ = state.db.aliases.insert(alias_key.as_bytes(), fp.as_bytes()); + let fp_key = format!("fp:{}", fp); + let _ = state.db.aliases.insert(fp_key.as_bytes(), bot_alias.as_bytes()); + tracing::info!("Bot alias @{} registered for {}", bot_alias, fp); + Ok(Json(serde_json::json!({ "ok": true, "result": { "token": token, "name": req.name, "fingerprint": fp, + "alias": format!("@{}", bot_alias), } }))) } diff --git a/warzone/docs/BOT_API.md b/warzone/docs/BOT_API.md new file mode 100644 index 0000000..ea5976e --- /dev/null +++ b/warzone/docs/BOT_API.md @@ -0,0 +1,401 @@ +# featherChat Bot API + +## Overview + +featherChat exposes a **Telegram Bot API-compatible** HTTP interface, allowing +developers to build bots that interact with featherChat users using familiar +patterns. Bots register with the server, receive a token, and communicate via +long-polling (webhook support is planned). + +Key properties of v1: + +- Bot aliases **must** end with `Bot`, `bot`, or `_bot` (auto-enforced on + registration). +- Bots receive encrypted user messages as **base64 blobs** (`raw_encrypted` + field). Plaintext bot-to-bot messages are delivered with a readable `text` + field. +- Bot-sent messages are **plaintext** (not E2E encrypted) in v1. +- `chat_id` values are hex fingerprints (not numeric Telegram-style IDs). + +--- + +## Quick Start + +``` +1. Register your bot: + POST /v1/bot/register + {"name": "WeatherBot", "fingerprint": "aabbccdd..."} + +2. Extract the token from the response. + +3. Poll for updates: + POST /v1/bot//getUpdates + {"timeout": 5} + +4. Send a reply: + POST /v1/bot//sendMessage + {"chat_id": "", "text": "Hello!"} +``` + +--- + +## Endpoints + +### 1. Register a Bot + +``` +POST /v1/bot/register +``` + +Creates a new bot, stores it in the server database, and auto-registers an +alias. + +**Request:** + +```json +{ + "name": "MyBot", + "fingerprint": "aabbccdd1122334455667788aabbccdd" +} +``` + +| Field | Type | Description | +|---------------|--------|----------------------------------------------| +| `name` | string | Display name. Alias suffix auto-added if needed. | +| `fingerprint` | string | Hex-encoded public key fingerprint for the bot. | + +**Response:** + +```json +{ + "ok": true, + "result": { + "token": "aabbccdd11223344:9f8e7d6c5b4a39281706abcdef012345", + "name": "MyBot", + "fingerprint": "aabbccdd1122334455667788aabbccdd", + "alias": "@mybot_bot" + } +} +``` + +**Token format:** `:<32-hex-random-bytes>` + +**Alias rules:** + +- If the name already ends with `Bot`, `bot`, or `_bot`, the alias is the + lowercased name (e.g. `WeatherBot` -> `@weatherbot`). +- Otherwise `_bot` is appended (e.g. `weather` -> `@weather_bot`). +- The alias is registered in both directions (alias -> fingerprint and + fingerprint -> alias). + +--- + +### 2. Get Bot Info + +``` +GET /v1/bot/:token/getMe +``` + +Returns information about the bot in a Telegram-compatible shape. + +**Response (valid token):** + +```json +{ + "ok": true, + "result": { + "id": "aabbccdd1122334455667788aabbccdd", + "is_bot": true, + "first_name": "MyBot", + "username": "MyBot" + } +} +``` + +**Response (invalid token):** + +```json +{ + "ok": false, + "description": "invalid token" +} +``` + +--- + +### 3. Get Updates (Long-Poll) + +``` +POST /v1/bot/:token/getUpdates +``` + +Returns queued messages for the bot and deletes them from the queue. + +**Request:** + +```json +{ + "timeout": 5 +} +``` + +| Field | Type | Description | +|-----------|------|-----------------------------------------------------| +| `timeout` | u64 | Optional. Long-poll wait in seconds. **Capped at 5.** | + +If the queue is empty and `timeout > 0`, the server waits up to `timeout` +seconds (max 5) before returning an empty result, giving new messages a chance +to arrive. + +**Response:** + +```json +{ + "ok": true, + "result": [ ...updates... ] +} +``` + +#### Update Types + +**Encrypted message** (from a user — bot must decrypt if it has a session): + +```json +{ + "update_id": 1, + "message": { + "message_id": "uuid", + "from": { + "id": "sender_fingerprint", + "is_bot": false, + "first_name": "sender_finge" + }, + "chat": { + "id": "sender_fingerprint", + "type": "private" + }, + "date": 1711670400, + "text": null, + "raw_encrypted": "base64-encoded-wiremessage..." + } +} +``` + +**Key exchange** (X3DH session initiation — same shape as encrypted message): + +```json +{ + "update_id": 2, + "message": { + "message_id": "uuid", + "from": { "id": "sender_fp", "is_bot": false, "first_name": "sender_fp..." }, + "chat": { "id": "sender_fp", "type": "private" }, + "date": 1711670400, + "text": null, + "raw_encrypted": "base64-encoded-keyexchange..." + } +} +``` + +**Call signal:** + +```json +{ + "update_id": 3, + "message": { + "message_id": "uuid", + "from": { "id": "sender_fp", "is_bot": false, "first_name": "sender_fp..." }, + "chat": { "id": "sender_fp", "type": "private" }, + "date": 1711670400, + "text": "/call_Offer", + "call_signal": { + "type": "Offer", + "payload": "SDP or ICE data..." + } + } +} +``` + +**File header:** + +```json +{ + "update_id": 4, + "message": { + "message_id": "uuid", + "from": { "id": "sender_fp", "is_bot": false, "first_name": "sender_fp..." }, + "chat": { "id": "sender_fp", "type": "private" }, + "date": 1711670400, + "document": { + "file_name": "report.pdf", + "file_size": 204800 + } + } +} +``` + +**Bot message (plaintext, from another bot via `sendMessage`):** + +```json +{ + "update_id": 5, + "message": { + "message_id": "uuid", + "from": { + "id": "other_bot_fingerprint", + "is_bot": true + }, + "chat": { + "id": "other_bot_fingerprint", + "type": "private" + }, + "date": 1711670400, + "text": "Hello from the other bot!" + } +} +``` + +> **Note:** Receipt and internal wire messages (FileChunk, GroupSenderKey, +> SenderKeyDistribution) are silently skipped and never delivered as updates. + +--- + +### 4. Send Message + +``` +POST /v1/bot/:token/sendMessage +``` + +Sends a **plaintext** message to a user or another bot. + +**Request:** + +```json +{ + "chat_id": "aabbccdd1122334455667788aabbccdd", + "text": "Hello from MyBot!" +} +``` + +| Field | Type | Description | +|-----------|--------|--------------------------------------------------| +| `chat_id` | string | Recipient fingerprint (hex) or Ethereum address. | +| `text` | string | Plaintext message body. | + +Non-hex characters in `chat_id` are stripped and the value is lowercased before +routing. + +**Response:** + +```json +{ + "ok": true, + "result": { + "message_id": "550e8400-e29b-41d4-a716-446655440000", + "chat": { + "id": "aabbccdd1122334455667788aabbccdd", + "type": "private" + }, + "text": "Hello from MyBot!", + "date": 1711670400, + "delivered": true + } +} +``` + +The `delivered` field indicates whether the message was sent over a live +WebSocket connection (`true`) or queued for later retrieval (`false`). + +--- + +## Alias Rules + +| Rule | Detail | +|------|--------| +| Bot aliases **must** end with `Bot`, `bot`, or `_bot` | Enforced at registration time. | +| Non-bot users **cannot** register aliases with these suffixes | Reserved for bots. | +| Auto-registered on bot creation | No separate alias step needed. | +| Users message bots via alias | e.g. `@mybot_bot`, resolved like any other alias. | + +--- + +## Differences from Telegram Bot API + +| Feature | Telegram | featherChat | +|---------|----------|-------------| +| `chat_id` type | Numeric integer | Hex fingerprint string | +| `getUpdates` timeout | Up to 50s | Capped at **5s** | +| Message content | Always plaintext | Encrypted messages arrive as `raw_encrypted` base64; bot must decrypt if it has a session | +| Bot-sent messages | Plaintext | Plaintext (not E2E encrypted) in v1 | +| Inline keyboards / callback queries | Supported | Not yet (planned) | +| Media groups | Supported | Not yet (planned) | +| Webhooks (`setWebhook`) | Supported | Not yet (planned) | +| File download (`getFile`) | Supported | Not yet (planned) | + +--- + +## Example: Simple Echo Bot (Python) + +```python +import requests +import time + +TOKEN = "your_bot_token" +API = f"http://localhost:7700/v1/bot/{TOKEN}" + +while True: + resp = requests.post(f"{API}/getUpdates", json={"timeout": 5}).json() + for update in resp.get("result", []): + msg = update.get("message", {}) + text = msg.get("text") or "[encrypted]" + 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(1) +``` + +### Example: Registration (curl) + +```bash +curl -X POST http://localhost:7700/v1/bot/register \ + -H "Content-Type: application/json" \ + -d '{"name": "EchoBot", "fingerprint": "aabbccdd1122334455667788aabbccdd"}' +``` + +--- + +## Authentication + +All bot endpoints (except `/register`) are authenticated by the **token** in +the URL path. Tokens are generated at registration time and stored server-side. +There is no expiration mechanism in v1 -- tokens remain valid until the server +database is cleared. + +The token grants full access to poll and send messages as the bot. **Treat it +like a password.** + +--- + +## Internal Details + +- Bot info is stored in the `tokens` sled tree under key `bot:`. +- A reverse lookup `bot_fp:` -> `` is also maintained. +- Aliases are stored in the `aliases` sled tree (`a:` -> fingerprint, + `fp:` -> alias). +- Queued messages live in the `messages` sled tree under prefix + `queue::*` and are deleted after `getUpdates` consumes them. +- Messages are delivered via `deliver_or_queue` -- live WebSocket if online, + otherwise queued. + +--- + +## Future Plans + +- **Webhook mode** (`setWebhook`) -- push updates to a URL instead of polling. +- **Inline keyboards and callback queries** -- interactive message buttons. +- **E2E encrypted bot sessions** -- bots participate in X3DH key exchange. +- **File send/receive APIs** -- `sendDocument`, `getFile`. +- **Group bot support** -- bots in group chats with sender-key encryption. diff --git a/warzone/docs/LLM_HELP.md b/warzone/docs/LLM_HELP.md index 0bcd2f8..cbc415d 100644 --- a/warzone/docs/LLM_HELP.md +++ b/warzone/docs/LLM_HELP.md @@ -45,6 +45,8 @@ ETH address | 0x742d35Cc... | derived from same seed, checksum format All 3 formats work in /peer. Aliases resolve to fp via server. One alias per user. Register with /alias, recover with recovery key. +Bot alias reservation: names ending in Bot, bot, or _bot are reserved for the Bot API. Non-bot users cannot register these aliases. + ## Quick Start 1. `warzone init` -- generates seed, saves identity.seed, prints 24-word mnemonic. WRITE IT DOWN. @@ -127,23 +129,44 @@ Problem | Cause | Fix "file too large" | over 10MB | split file manually no prekeys available | recipient's one-time prekeys exhausted | they need to re-register or come online -## Bot API +## Bot API (Telegram-compatible) -Telegram-compatible REST API. Base: /v1/ +Register: POST /v1/bot/register {"name":"MyBot","fingerprint":""} +→ returns token + auto-creates @mybot_bot alias -Endpoint | Method | Body | Returns ---- | --- | --- | --- -/bot/register | POST | {"name":"mybot","fingerprint":"abc..."} | {"token":"...","name":"..."} -/bot/:token/getMe | GET | -- | bot info -/bot/:token/getUpdates | POST | {"timeout":5} | array of Update objects -/bot/:token/sendMessage | POST | {"chat_id":"","text":"hello"} | msg confirmation +Bot aliases must end with Bot, bot, or _bot. Non-bots cannot use these. + +|Endpoint|Method|Body| +|---|---|---| +|/bot/:token/getMe|GET|—| +|/bot/:token/getUpdates|POST|{"timeout":5}| +|/bot/:token/sendMessage|POST|{"chat_id":"","text":"Hello"}| - Token format: fp_prefix:random_hex - getUpdates: long-poll (max 5s), returns then deletes queued msgs - sendMessage: plaintext JSON, NOT E2E encrypted -- Updates include: messages, key exchanges, call signals, file headers - Bot msgs delivered via same routing (WS push or DB queue) +Update types in getUpdates: +- Encrypted msg: text=null, raw_encrypted=base64 +- Bot msg (plaintext): text="actual text", from.is_bot=true +- Call signal: text="/call_Offer", call_signal={type,payload} +- File: document={file_name,file_size} + +v1 limits: sendMessage is plaintext (no E2E), timeout max 5s, no webhooks yet. + +Echo bot (Python): +```python +import requests, time +TOKEN = "your_token" +API = f"http://srv:7700/v1/bot/{TOKEN}" +while True: + for u in requests.post(f"{API}/getUpdates",json={"timeout":5}).json().get("result",[]): + m = u["message"] + if m.get("text"): requests.post(f"{API}/sendMessage",json={"chat_id":m["chat"]["id"],"text":"Echo: "+m["text"]}) + time.sleep(1) +``` + ## Server API (other endpoints) - POST /v1/register -- upload prekey bundle