Files
featherChat/warzone/tools/botfather.py
Siavash Sameni 362e7a765b v0.0.29: BotFather — create bots by messaging @botfather
Built-in BotFather (Rust, server-side):
- Intercepts messages to @botfather in deliver_or_queue
- Commands: /newbot <name>, /mybots, /deletebot <name>, /token <name>
- Creates bot with fingerprint, token, alias, tracks ownership
- Replies via push_to_client or queue (works offline)
- Only active when --enable-bots is set

Standalone BotFather (Python):
- tools/botfather.py: uses bot API (getUpdates/sendMessage)
- Delegates core ops to built-in handler
- Extensible for additional features
- Reads token from BOTFATHER_TOKEN env or .botfather_token file

Flow: User messages @botfather → "/newbot MyBot" → gets token back

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 11:08:35 +04:00

196 lines
6.4 KiB
Python
Executable File

#!/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 <name> - Create a new bot (name must end with bot/Bot)
/mybots - List your bots
/deletebot <n> - Delete a bot you own
/token <name> - 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 <name> - Create a bot (name must end with bot/Bot)\n"
"/mybots - List your bots\n"
"/deletebot <name> - Delete your bot\n"
"/token <name> - 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 <botname>\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 <botname>")
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 <botname>")
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()