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>
196 lines
6.4 KiB
Python
Executable File
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()
|