#!/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/methodName path = self.path # Strip /bot/ 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()