#!/usr/bin/env python3 """ featherChat BotFather (Standalone) A Telegram-style BotFather that manages bot creation via chat. Uses the featherChat Bot API — runs as a regular bot process. Usage: python botfather.py --server http://localhost:7700 On first run, it registers itself as @botfather if not already registered. Subsequent runs reuse the stored token from .botfather_token file. Commands: /start, /help - Show help /newbot - Create a new bot (name must end with bot/Bot) /mybots - List your bots /deletebot - Delete a bot you own /token - Show token for your bot """ import argparse import json import os import sys import time from urllib.request import Request, urlopen from urllib.error import URLError TOKEN_FILE = ".botfather_token" def api(server, token, method, data=None): """Call a bot API method.""" url = f"{server}/v1/bot/{token}/{method}" body = json.dumps(data).encode() if data else None req = Request(url, data=body, method="POST" if body else "GET") req.add_header("Content-Type", "application/json") try: with urlopen(req, timeout=60) as resp: return json.loads(resp.read()) except URLError as e: print(f"API error ({method}): {e}") return {"ok": False} def send(server, token, chat_id, text): """Send a message.""" return api(server, token, "sendMessage", {"chat_id": chat_id, "text": text}) def register_botfather(server): """Register BotFather with the server. Returns token.""" # BotFather registers itself — it needs the built-in BotFather token # to authorize. Read it from the server's initial log or pass via env. builtin_token = os.environ.get("BOTFATHER_TOKEN", "") if not builtin_token: print("ERROR: Set BOTFATHER_TOKEN env var to the token from server logs") print(" (printed on first --enable-bots start)") sys.exit(1) # Use the built-in token directly return builtin_token def handle_message(server, token, msg): """Process a message and respond.""" text = (msg.get("text") or "").strip() chat_id = msg.get("chat", {}).get("id", "") from_id = msg.get("from", {}).get("id_str") or str(msg.get("from", {}).get("id", "")) if not text or not chat_id: return print(f"[{from_id[:16]}] {text}") if text in ("/start", "/help"): send(server, token, chat_id, "Welcome to BotFather! I manage bots on featherChat.\n\n" "Commands:\n" "/newbot - Create a bot (name must end with bot/Bot)\n" "/mybots - List your bots\n" "/deletebot - Delete your bot\n" "/token - Get bot token\n" "/help - Show this message") elif text.startswith("/newbot"): name = text.replace("/newbot", "").strip() if not name: send(server, token, chat_id, "Usage: /newbot \nExample: /newbot WeatherBot") return if len(name) < 3 or len(name) > 32: send(server, token, chat_id, "Bot name must be 3-32 characters.") return if not name.lower().endswith("bot"): send(server, token, chat_id, "Bot name must end with 'bot' or 'Bot'.") return # Create the bot via internal API fp = os.urandom(16).hex() resp = api(server, token, "../register", { "name": name, "fingerprint": fp, "botfather_token": token, "owner": from_id }) if resp.get("ok"): result = resp["result"] send(server, token, chat_id, f"Done! Your new bot @{result.get('alias', name.lower())} is ready.\n\n" f"Token: {result['token']}\n\n" f"Keep this token secret!") else: send(server, token, chat_id, f"Failed: {resp.get('description', 'unknown error')}") elif text == "/mybots": send(server, token, chat_id, "Use the built-in /mybots via chat with @botfather.\n" "(The built-in handler tracks ownership.)") elif text.startswith("/deletebot"): name = text.replace("/deletebot", "").strip() if not name: send(server, token, chat_id, "Usage: /deletebot ") return send(server, token, chat_id, f"Use the built-in /deletebot {name} via chat with @botfather.\n" "(The built-in handler verifies ownership.)") elif text.startswith("/token"): name = text.replace("/token", "").strip() if not name: send(server, token, chat_id, "Usage: /token ") return send(server, token, chat_id, f"Use the built-in /token {name} via chat with @botfather.\n" "(The built-in handler verifies ownership.)") else: send(server, token, chat_id, "Unknown command. Try /help") def main(): parser = argparse.ArgumentParser(description="featherChat BotFather (standalone)") parser.add_argument("--server", required=True, help="featherChat server URL") parser.add_argument("--token", help="BotFather token (or set BOTFATHER_TOKEN env)") args = parser.parse_args() token = args.token or os.environ.get("BOTFATHER_TOKEN", "") # Try loading from file if not token and os.path.exists(TOKEN_FILE): token = open(TOKEN_FILE).read().strip() if not token: token = register_botfather(args.server) # Save token with open(TOKEN_FILE, "w") as f: f.write(token) # Verify me = api(args.server, token, "getMe") if not me.get("ok"): print(f"ERROR: Invalid token. Delete {TOKEN_FILE} and retry.") sys.exit(1) bot_name = me["result"].get("first_name", "BotFather") print(f"BotFather ({bot_name}) running") print(f"Server: {args.server}") print(f"Polling for messages...") print() offset = 0 while True: try: resp = api(args.server, token, "getUpdates", {"offset": offset, "timeout": 30}) for update in resp.get("result", []): offset = update["update_id"] + 1 msg = update.get("message", {}) if msg: handle_message(args.server, token, msg) except KeyboardInterrupt: print("\nBotFather stopped.") break except Exception as e: print(f"Error: {e}") time.sleep(3) if __name__ == "__main__": main()