v0.0.27: TG-compatible bots — plaintext send, numeric IDs, webhooks, BotFather
Bot compatibility: - Clients send plaintext bot_message to bot aliases (no E2E encryption) - Numeric chat_id: fp_to_numeric_id() deterministic hash, accept string/number - Webhook delivery: POST updates to bot's webhook URL (async, fire-and-forget) - getUpdates timeout raised to 50s (was 30, TG uses 50) - parse_mode HTML rendered in web client - E2E bot registration: optional seed + bundle for encrypted bot sessions BotFather + instance control: - --enable-bots CLI flag (default: disabled) - BotFather auto-created on first start (@botfather alias) - Bot ownership: owner fingerprint stored in bot_info - All bot endpoints return 403 when disabled Bot Bridge: - tools/bot-bridge.py: TG-compatible proxy for unmodified TG bots - Translates chat_id int↔string, proxies getUpdates/sendMessage - README with python-telegram-bot and Telegraf examples Test fixes: - Updated tests for ETH address display in header/messages Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
38
warzone/tools/README.md
Normal file
38
warzone/tools/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# featherChat Bot Tools
|
||||
|
||||
## bot-bridge.py
|
||||
|
||||
Proxy server that makes featherChat compatible with Telegram bot libraries.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# 1. Register a bot on featherChat
|
||||
curl -X POST http://server:7700/v1/bot/register \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"MyBot","fingerprint":"aabbccddaabbccddaabbccddaabbccdd"}'
|
||||
|
||||
# 2. Start the bridge
|
||||
python3 tools/bot-bridge.py --server http://server:7700 --token YOUR_TOKEN --port 8081
|
||||
|
||||
# 3. Point your TG bot at the bridge
|
||||
# Python (python-telegram-bot):
|
||||
# bot = Bot(token="TOKEN", base_url="http://localhost:8081/botTOKEN")
|
||||
# Node (Telegraf):
|
||||
# const bot = new Telegraf("TOKEN", { telegram: { apiRoot: "http://localhost:8081" } })
|
||||
```
|
||||
|
||||
### What it does
|
||||
|
||||
- Translates Telegram API calls to featherChat Bot API
|
||||
- Converts numeric chat_id <-> fingerprint hex strings
|
||||
- Proxies getUpdates long-polling
|
||||
- Passes through sendMessage, editMessageText, etc.
|
||||
|
||||
### Future: E2E Mode
|
||||
|
||||
When E2E bot support is complete, the bridge will:
|
||||
- Hold the bot's seed/keypair
|
||||
- Decrypt incoming E2E messages before forwarding to the TG bot
|
||||
- Encrypt outgoing messages with the user's ratchet session
|
||||
- The TG bot sees plaintext; the server sees only ciphertext
|
||||
175
warzone/tools/bot-bridge.py
Executable file
175
warzone/tools/bot-bridge.py
Executable file
@@ -0,0 +1,175 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
featherChat E2E Bot Bridge
|
||||
|
||||
Runs a local Telegram-compatible API server that proxies to featherChat.
|
||||
Your Telegram bot connects to this bridge instead of api.telegram.org.
|
||||
|
||||
Usage:
|
||||
python bot-bridge.py --server http://featherchat:7700 --token YOUR_BOT_TOKEN --port 8081
|
||||
|
||||
Your bot code:
|
||||
# Instead of: bot = Bot(token="...", base_url="https://api.telegram.org")
|
||||
# Use: bot = Bot(token="...", base_url="http://localhost:8081")
|
||||
|
||||
Architecture:
|
||||
[TG Bot] <--HTTP--> [Bridge :8081] <--HTTP--> [featherChat :7700]
|
||||
|
||||
The bridge translates between Telegram API format and featherChat Bot API,
|
||||
handling the chat_id type differences and other incompatibilities.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from urllib.parse import urlparse
|
||||
from urllib.request import Request, urlopen
|
||||
from urllib.error import URLError
|
||||
|
||||
class BotBridgeHandler(BaseHTTPRequestHandler):
|
||||
server_url = ""
|
||||
bot_token = ""
|
||||
|
||||
def log_message(self, format, *args):
|
||||
print(f"[bridge] {args[0]}" if args else "")
|
||||
|
||||
def do_GET(self):
|
||||
self._proxy()
|
||||
|
||||
def do_POST(self):
|
||||
self._proxy()
|
||||
|
||||
def _proxy(self):
|
||||
# Extract the method from URL: /bot<token>/methodName
|
||||
path = self.path
|
||||
|
||||
# Strip /bot<token>/ prefix if present (TG libraries send this)
|
||||
if path.startswith(f'/bot{self.bot_token}/'):
|
||||
method = path[len(f'/bot{self.bot_token}/'):]
|
||||
elif path.startswith('/bot'):
|
||||
# Library might send a different token format
|
||||
parts = path.split('/', 3)
|
||||
method = parts[3] if len(parts) > 3 else parts[-1]
|
||||
else:
|
||||
method = path.lstrip('/')
|
||||
|
||||
# Read request body
|
||||
content_length = int(self.headers.get('Content-Length', 0))
|
||||
body = self.rfile.read(content_length) if content_length > 0 else b''
|
||||
|
||||
# Transform request for featherChat
|
||||
fc_url = f"{self.server_url}/v1/bot/{self.bot_token}/{method}"
|
||||
|
||||
# Transform body if needed
|
||||
if body and method == 'sendMessage':
|
||||
body = self._transform_send_message(body)
|
||||
|
||||
try:
|
||||
req = Request(fc_url, data=body if body else None, method=self.command)
|
||||
req.add_header('Content-Type', 'application/json')
|
||||
|
||||
with urlopen(req, timeout=60) as resp:
|
||||
response_body = resp.read()
|
||||
|
||||
# Transform response
|
||||
if method == 'getUpdates':
|
||||
response_body = self._transform_updates(response_body)
|
||||
|
||||
self.send_response(resp.status)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(response_body)
|
||||
|
||||
except URLError as e:
|
||||
self.send_response(502)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({
|
||||
"ok": False,
|
||||
"description": f"Bridge error: {e}"
|
||||
}).encode())
|
||||
|
||||
def _transform_send_message(self, body):
|
||||
"""Transform sendMessage: convert numeric chat_id to string if needed."""
|
||||
try:
|
||||
data = json.loads(body)
|
||||
# chat_id: featherChat accepts both string and number now
|
||||
# No transformation needed -- pass through
|
||||
return json.dumps(data).encode()
|
||||
except:
|
||||
return body
|
||||
|
||||
def _transform_updates(self, body):
|
||||
"""Transform getUpdates response: ensure chat_id is integer for TG libs."""
|
||||
try:
|
||||
data = json.loads(body)
|
||||
if data.get('ok') and data.get('result'):
|
||||
for update in data['result']:
|
||||
msg = update.get('message', {})
|
||||
# Convert string IDs to numeric for TG library compatibility
|
||||
if 'from' in msg and isinstance(msg['from'].get('id'), str):
|
||||
fp = msg['from']['id']
|
||||
msg['from']['id_str'] = fp
|
||||
msg['from']['id'] = _fp_to_numeric(fp)
|
||||
if 'chat' in msg and isinstance(msg['chat'].get('id'), str):
|
||||
fp = msg['chat']['id']
|
||||
msg['chat']['id_str'] = fp
|
||||
msg['chat']['id'] = _fp_to_numeric(fp)
|
||||
return json.dumps(data).encode()
|
||||
except:
|
||||
return body
|
||||
|
||||
|
||||
def _fp_to_numeric(fp: str) -> int:
|
||||
"""Convert fingerprint hex string to positive i64 (same as server's fp_to_numeric_id)."""
|
||||
clean = ''.join(c for c in fp if c in '0123456789abcdefABCDEF')[:16]
|
||||
if len(clean) >= 16:
|
||||
return int(clean, 16) & 0x7FFFFFFFFFFFFFFF
|
||||
return 0
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='featherChat E2E Bot Bridge')
|
||||
parser.add_argument('--server', required=True, help='featherChat server URL (e.g., http://localhost:7700)')
|
||||
parser.add_argument('--token', required=True, help='Bot token from /v1/bot/register')
|
||||
parser.add_argument('--port', type=int, default=8081, help='Local port for TG-compatible API (default: 8081)')
|
||||
args = parser.parse_args()
|
||||
|
||||
BotBridgeHandler.server_url = args.server.rstrip('/')
|
||||
BotBridgeHandler.bot_token = args.token
|
||||
|
||||
# Verify bot token
|
||||
try:
|
||||
req = Request(f"{args.server}/v1/bot/{args.token}/getMe")
|
||||
with urlopen(req, timeout=5) as resp:
|
||||
data = json.loads(resp.read())
|
||||
if not data.get('ok'):
|
||||
print(f"ERROR: Invalid bot token")
|
||||
sys.exit(1)
|
||||
bot_name = data['result'].get('first_name', '?')
|
||||
print(f"Bot: {bot_name}")
|
||||
except Exception as e:
|
||||
print(f"ERROR: Cannot reach server: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
server = HTTPServer(('127.0.0.1', args.port), BotBridgeHandler)
|
||||
print(f"Bridge running on http://127.0.0.1:{args.port}")
|
||||
print(f"Proxying to {args.server}")
|
||||
print(f"")
|
||||
print(f"Configure your bot:")
|
||||
print(f" base_url = 'http://127.0.0.1:{args.port}/bot{args.token}'")
|
||||
print(f"")
|
||||
print(f"Example (python-telegram-bot):")
|
||||
print(f" from telegram import Bot")
|
||||
print(f" bot = Bot(token='{args.token}', base_url='http://127.0.0.1:{args.port}/bot{args.token}')")
|
||||
|
||||
try:
|
||||
server.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
print("\nBridge stopped.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user