#!/usr/bin/env python3 """ Orchestrator — single entry point for `nix run`. 1. Loads BOT_TOKEN (env var → ~/.secrets → .env) 2. Starts the API + Mini App server 3. Optionally starts a cloudflared Quick Tunnel for a public HTTPS URL (skipped if WEBAPP_URL is already set, e.g. fronted by a reverse proxy) 4. Cleans up on Ctrl+C The slash-command bot was removed: the Mini App is the only interface. The bot's identity (token) only matters now for validating Telegram WebApp initData HMACs and for the menu-button URL — both handled in server.py. """ import os import re import signal import subprocess import sys import threading import time import pathlib # Flush prints line-by-line so the tunnel URL etc. show up promptly in # systemd journals (stdout is block-buffered when not connected to a TTY). sys.stdout.reconfigure(line_buffering=True) SCRIPT_DIR = pathlib.Path(__file__).resolve().parent SECRETS_FILE = pathlib.Path.home() / ".secrets" / "bigbiggerbiggestbot" def load_token() -> str: """Load bot token: BOT_TOKEN env → secrets file → .env in cwd. Env var wins so multiple instances on the same host (prod + shipyard staging) can each get a distinct token via systemd EnvironmentFile. """ # 1. Already in environment (systemd EnvironmentFile sets this for staging) token = os.environ.get("BOT_TOKEN", "").strip() if token: print(" Token loaded from BOT_TOKEN env var") return token # 2. Default secrets file if SECRETS_FILE.is_file(): token = SECRETS_FILE.read_text().strip() if token: print(f" Token loaded from {SECRETS_FILE}") return token # 3. .env in working directory env_file = pathlib.Path.cwd() / ".env" if env_file.exists(): for line in env_file.read_text().splitlines(): line = line.strip() if line.startswith("BOT_TOKEN="): token = line.split("=", 1)[1].strip().strip("\"'") if token: print(f" Token loaded from {env_file}") return token print("\n No bot token found!") print(f" Set BOT_TOKEN env var, or put it in {SECRETS_FILE}, or in a .env file.\n") sys.exit(1) def start_server(port: int, bot_token: str) -> subprocess.Popen: env = {**os.environ, "API_PORT": str(port), "BOT_TOKEN": bot_token} return subprocess.Popen( [sys.executable, str(SCRIPT_DIR / "server.py")], env=env, ) def start_tunnel(port: int) -> tuple[subprocess.Popen, str]: print(f" Starting tunnel to port {port}...") proc = subprocess.Popen( ["cloudflared", "tunnel", "--url", f"http://localhost:{port}", "--protocol", "http2"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) url = None connected = False deadline = time.time() + 30 while time.time() < deadline: line = proc.stdout.readline() if not line: if proc.poll() is not None: print(" Tunnel process exited unexpectedly.") break continue line = line.strip() if line: print(f" [tunnel] {line}") if not url: match = re.search(r"https://\S+\.trycloudflare\.com", line) if match: url = match.group(0) if "Registered tunnel connection" in line: connected = True break if not url or not connected: proc.kill() print("\n Could not get a tunnel URL.") print(" Make sure cloudflared is installed: cloudflared tunnel --url http://localhost:8080\n") sys.exit(1) # Keep draining cloudflared output so its pipe buffer doesn't fill up # (which would block the process and kill the tunnel) def _drain(): for _line in proc.stdout: pass threading.Thread(target=_drain, daemon=True).start() return proc, url def main(): port = int(os.environ.get("API_PORT", "8080")) procs: list[subprocess.Popen] = [] def cleanup(sig=None, frame=None): print("\nShutting down...") for p in procs: try: p.terminate() except OSError: pass for p in procs: try: p.wait(timeout=5) except subprocess.TimeoutExpired: p.kill() sys.exit(0) signal.signal(signal.SIGINT, cleanup) signal.signal(signal.SIGTERM, cleanup) print() print(" ==========================================") print(" BigBiggerBiggest — Mini App backend") print(" ==========================================") print() # 1. Load token (used by server.py to validate initData HMACs) bot_token = load_token() masked = bot_token[:5] + "..." + bot_token[-4:] print(f" BOT_TOKEN: {masked}") # 2. Start API server print(f"\n Starting API server on port {port}...") server = start_server(port, bot_token) procs.append(server) time.sleep(1) if server.poll() is not None: print(" Server failed to start!") sys.exit(1) # 3. Tunnel — skipped if WEBAPP_URL is already provided (e.g. fronted # by an external reverse proxy that terminates TLS and proxies back to # localhost:$API_PORT over a private network). env_webapp_url = os.environ.get("WEBAPP_URL", "").strip() if env_webapp_url: webapp_url = env_webapp_url print(f" WEBAPP_URL from environment: {webapp_url} (skipping cloudflared)") else: tunnel, webapp_url = start_tunnel(port) procs.append(tunnel) print("\n ==========================================") print(" All systems go!") print(f" Mini App: {webapp_url}") print(f" API: http://localhost:{port}") print(" Press Ctrl+C to stop") print(" ==========================================\n") proc_names = {id(server): "Server"} if procs[-1] is not server: proc_names[id(procs[-1])] = "Tunnel" while True: for p in procs: ret = p.poll() if ret is not None: name = proc_names.get(id(p), "?") print(f"\n {name} exited with code {ret}") cleanup() time.sleep(1) if __name__ == "__main__": main()