#!/usr/bin/env python3 """ 🦊 Foxy Dev Team β€” Telegram Bot v3 (API-Backed) ================================================= Rewritten to consume the central FastAPI backend instead of direct filesystem access. All state operations go through HTTP calls to the API. Usage : python3 foxy-telegram-bot.py Service: systemctl --user start foxy-telegram-bot """ import json import os import sys import signal import logging import asyncio import httpx # ─── CONFIG ──────────────────────────────────────────────────────────────────── TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "") API_BASE_URL = os.environ.get("FOXY_API_URL", "http://localhost:8000") POLL_TIMEOUT = 30 # ─── LOGGING ─────────────────────────────────────────────────────────────────── logging.basicConfig( level=logging.INFO, format="[%(asctime)s] %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%SZ", ) log = logging.getLogger("foxy-telegram-bot") # ─── SIGNAL ──────────────────────────────────────────────────────────────────── _running = True def handle_signal(sig, frame): global _running log.info("πŸ›‘ Signal reΓ§u β€” arrΓͺt du bot...") _running = False signal.signal(signal.SIGTERM, handle_signal) signal.signal(signal.SIGINT, handle_signal) # ─── Status Emojis ───────────────────────────────────────────────────────────── STATUS_EMOJI = { "AWAITING_CONDUCTOR": "⏳ En attente β€” Conductor", "CONDUCTOR_RUNNING": "πŸ€– Conductor travaille...", "AWAITING_ARCHITECT": "⏳ En attente β€” Architect", "ARCHITECT_RUNNING": "πŸ€– Architect travaille...", "AWAITING_DEV": "⏳ En attente β€” Dev", "DEV_RUNNING": "πŸ€– Dev travaille...", "AWAITING_UIUX": "⏳ En attente β€” UI/UX", "UIUX_RUNNING": "πŸ€– UI/UX travaille...", "AWAITING_QA": "⏳ En attente β€” QA", "QA_RUNNING": "πŸ€– QA travaille...", "AWAITING_DEPLOY": "⏳ En attente β€” DΓ©ploiement", "DEPLOY_RUNNING": "πŸš€ DΓ©ploiement en cours...", "COMPLETED": "βœ… TerminΓ©", "FAILED": "❌ Γ‰chouΓ©", "PAUSED": "⏸️ En pause", } WORKFLOW_LABELS = { "SOFTWARE_DESIGN": "πŸ—οΈ Conception logicielle", "SYSADMIN_DEBUG": "πŸ› DΓ©bogage Sysadmin", "DEVOPS_SETUP": "🐳 DevOps Setup", "SYSADMIN_ADJUST": "πŸ”§ Ajustement Sysadmin", } # ─── Telegram API ────────────────────────────────────────────────────────────── class TelegramClient: def __init__(self, token: str): self.token = token self.base = f"https://api.telegram.org/bot{token}" self.client = httpx.AsyncClient(timeout=POLL_TIMEOUT + 10) async def send(self, chat_id: str, text: str) -> bool: try: resp = await self.client.post( f"{self.base}/sendMessage", data={"chat_id": chat_id, "text": text, "parse_mode": "HTML"}, ) return resp.status_code == 200 except Exception as e: log.warning(f"Telegram send error: {e}") return False async def get_updates(self, offset: int) -> list: try: resp = await self.client.post( f"{self.base}/getUpdates", data={ "offset": offset, "timeout": POLL_TIMEOUT, "allowed_updates": '["message"]', }, ) if resp.status_code == 200: data = resp.json() if data.get("ok"): return data.get("result", []) except Exception as e: log.warning(f"Telegram getUpdates error: {e}") return [] async def get_me(self) -> dict | None: try: resp = await self.client.post(f"{self.base}/getMe") if resp.status_code == 200: data = resp.json() if data.get("ok"): return data.get("result") except Exception: pass return None async def close(self): await self.client.aclose() # ─── Foxy API Client ────────────────────────────────────────────────────────── class FoxyAPI: def __init__(self, base_url: str): self.base = base_url.rstrip("/") self.client = httpx.AsyncClient(timeout=30) async def list_projects(self) -> list: resp = await self.client.get(f"{self.base}/api/projects") resp.raise_for_status() return resp.json() async def create_project(self, name: str, description: str, workflow_type: str = "SOFTWARE_DESIGN", test_mode: bool = False) -> dict: resp = await self.client.post( f"{self.base}/api/projects", json={"name": name, "description": description, "workflow_type": workflow_type, "test_mode": test_mode}, ) resp.raise_for_status() return resp.json() async def start_project(self, project_id: int) -> dict: resp = await self.client.post(f"{self.base}/api/projects/{project_id}/start") resp.raise_for_status() return resp.json() async def reset_project(self, project_id: int) -> dict: resp = await self.client.post(f"{self.base}/api/projects/{project_id}/reset") resp.raise_for_status() return resp.json() async def list_agents(self) -> list: resp = await self.client.get(f"{self.base}/api/agents") resp.raise_for_status() return resp.json() async def health(self) -> dict: resp = await self.client.get(f"{self.base}/api/health") resp.raise_for_status() return resp.json() async def close(self): await self.client.aclose() # ─── Command Handlers ───────────────────────────────────────────────────────── async def cmd_start(tg: TelegramClient, chat_id: str): await tg.send(chat_id, "🦊 Foxy Dev Team Bot v3\n\n" "ConnectΓ© Γ  l'API centralisΓ©e.\n\n" "Commandes :\n" "/projets β€” Statut de tous les projets\n" "/agents β€” Γ‰tat des agents\n" "/nouveau <nom> | <description> β€” Nouveau projet\n" "/test β€” Projet de test pipeline\n" "/reset <id> β€” RΓ©initialiser un projet\n" "/aide β€” Aide complΓ¨te\n\n" "━━━━━━━━━━━━━━━━━━━━\n" "Toutes les actions passent par l'API centralisΓ©e." ) async def cmd_aide(tg: TelegramClient, chat_id: str): await tg.send(chat_id, "🦊 Commandes Foxy Bot v3\n\n" "/start β€” Bienvenue\n" "/projets β€” Portrait de tous les projets\n" "/agents β€” Γ‰tat des agents en temps rΓ©el\n" "/nouveau nom | description β€” CrΓ©er un projet\n" "/test β€” Lancer un projet de test\n" "/reset ID β€” RΓ©initialiser un projet\n" "/aide β€” Cette aide\n\n" "━━━━━━━━━━━━━━━━━━━━\n" f"🌐 Dashboard : Ouvrir" ) async def cmd_projets(tg: TelegramClient, foxy: FoxyAPI, chat_id: str): try: projects = await foxy.list_projects() except Exception as e: await tg.send(chat_id, f"❌ Erreur API : {e}") return if not projects: await tg.send(chat_id, "πŸ“­ Aucun projet.") return await tg.send(chat_id, f"🦊 Statut des projets ({len(projects)} projet(s))") for p in projects: status = p.get("status", "?") status_label = STATUS_EMOJI.get(status, f"❓ {status}") wf_label = WORKFLOW_LABELS.get(p.get("workflow_type", ""), p.get("workflow_type", "")) total = p.get("task_count", 0) done = p.get("tasks_done", 0) pct = int(done / total * 100) if total > 0 else 0 filled = int(10 * pct / 100) bar = "β–ˆ" * filled + "β–‘" * (10 - filled) await tg.send(chat_id, f"━━━━━━━━━━━━━━━━━━━━\n" f"πŸ“‹ {p['name']} (#{p['id']})\n" f"πŸ“Š {status_label}\n" f"πŸ”„ {wf_label}\n" f"[{bar}] {pct}% ({done}/{total})\n" f"πŸ• {p.get('updated_at', '?')[:16]}" ) async def cmd_agents(tg: TelegramClient, foxy: FoxyAPI, chat_id: str): try: agents = await foxy.list_agents() except Exception as e: await tg.send(chat_id, f"❌ Erreur API : {e}") return lines = ["🦊 Γ‰tat des Agents\n"] for a in agents: status_icon = "⚑" if a["current_status"] == "running" else "❌" if a["current_status"] == "failed" else "πŸ’€" lines.append( f"{status_icon} {a['display_name']}\n" f" ModΓ¨le: {a['model']} | " f"Total: {a['total_executions']} | " f"βœ… {a['success_count']} | ❌ {a['failure_count']}" ) await tg.send(chat_id, "\n".join(lines)) async def cmd_test(tg: TelegramClient, foxy: FoxyAPI, chat_id: str): try: project = await foxy.create_project( name="test-pipeline", description="PROJET DE TEST AUTOMATIQUE β€” Pipeline Foxy Dev Team. Simulation sans code rΓ©el.", workflow_type="SOFTWARE_DESIGN", test_mode=True, ) await tg.send(chat_id, f"🦊 Projet de test créé !\n\n" f"πŸ“‹ {project['name']} (#{project['id']})\n" f"πŸ“Š {project['status']}\n\n" f"Utilisez /projets pour suivre la progression." ) except Exception as e: await tg.send(chat_id, f"❌ Erreur crΓ©ation test : {e}") async def cmd_nouveau(tg: TelegramClient, foxy: FoxyAPI, chat_id: str, text: str): parts = text.split(maxsplit=1) if len(parts) < 2 or not parts[1].strip(): await tg.send(chat_id, "🦊 Nouveau projet\n\n" "Usage : /nouveau nom | description du projet\n\n" "Exemple :\n" "/nouveau api-users | CrΓ©er une API REST pour gΓ©rer les utilisateurs" ) return raw = parts[1].strip() if "|" in raw: name, desc = raw.split("|", 1) name = name.strip() desc = desc.strip() else: name = raw[:50].replace(" ", "-").lower() desc = raw try: project = await foxy.create_project(name=name, description=desc) await tg.send(chat_id, f"🦊 Projet créé !\n\n" f"πŸ“‹ {project['name']} (#{project['id']})\n" f"πŸ“Š {project['status']}\n" f"πŸ”„ {project['workflow_type']}\n\n" f"Le moteur de workflow prendra en charge ce projet." ) except Exception as e: await tg.send(chat_id, f"❌ Erreur : {e}") async def cmd_reset(tg: TelegramClient, foxy: FoxyAPI, chat_id: str, text: str): parts = text.split() if len(parts) < 2: # Reset all running projects try: projects = await foxy.list_projects() resets = [] for p in projects: if p["status"].endswith("_RUNNING") or p["status"].startswith("AWAITING_"): result = await foxy.reset_project(p["id"]) resets.append(f" β€’ {p['name']} (#{p['id']})") if resets: await tg.send(chat_id, "πŸ”„ Reset effectuΓ©\n\n" + "\n".join(resets)) else: await tg.send(chat_id, "βœ… Aucun projet Γ  resetter.") except Exception as e: await tg.send(chat_id, f"❌ Erreur : {e}") return try: project_id = int(parts[1]) result = await foxy.reset_project(project_id) await tg.send(chat_id, f"πŸ”„ Projet #{project_id} reset β†’ {result['status']}") except ValueError: await tg.send(chat_id, "❌ ID invalide. Usage : /reset 1") except Exception as e: await tg.send(chat_id, f"❌ Erreur : {e}") # ─── Routing ────────────────────────────────────────────────────────────────── SIMPLE_COMMANDS = { "/start": cmd_start, "/aide": cmd_aide, "/help": cmd_aide, } FOXY_COMMANDS = { "/projets": cmd_projets, "/projets-statut": cmd_projets, "/status": cmd_projets, "/agents": cmd_agents, "/test": cmd_test, } async def handle_update(tg: TelegramClient, foxy: FoxyAPI, update: dict): msg = update.get("message", {}) if not msg: return chat_id = str(msg.get("chat", {}).get("id", "")) text = msg.get("text", "").strip() username = msg.get("from", {}).get("username", "inconnu") if not text or not chat_id: return if chat_id != TELEGRAM_CHAT_ID: log.warning(f"Message from unauthorized chat: {chat_id} (@{username})") await tg.send(chat_id, "β›” Chat non autorisΓ©.") return log.info(f"πŸ“© Message: '{text[:80]}' from @{username}") if text.startswith("/"): cmd = text.split()[0].split("@")[0].lower() # Simple commands (no API needed) handler = SIMPLE_COMMANDS.get(cmd) if handler: await handler(tg, chat_id) return # API-backed commands foxy_handler = FOXY_COMMANDS.get(cmd) if foxy_handler: await foxy_handler(tg, foxy, chat_id) return # Commands with arguments if cmd == "/nouveau" or cmd == "/foxy-conductor": await cmd_nouveau(tg, foxy, chat_id, text) return if cmd == "/reset": await cmd_reset(tg, foxy, chat_id, text) return # Free text β€” show help await tg.send(chat_id, "🦊 Tape /aide pour voir les commandes disponibles.") # ─── Main Loop ───────────────────────────────────────────────────────────────── async def run_bot(): if not TELEGRAM_BOT_TOKEN: log.error("❌ TELEGRAM_BOT_TOKEN non dΓ©fini. Exiting.") sys.exit(1) if not TELEGRAM_CHAT_ID: log.error("❌ TELEGRAM_CHAT_ID non dΓ©fini. Exiting.") sys.exit(1) tg = TelegramClient(TELEGRAM_BOT_TOKEN) foxy = FoxyAPI(API_BASE_URL) log.info("=" * 50) log.info("🦊 FOXY TELEGRAM BOT v3 (API-Backed) β€” DΓ‰MARRΓ‰") log.info(f" API Backend : {API_BASE_URL}") log.info(f" Chat autorisΓ© : {TELEGRAM_CHAT_ID}") log.info("=" * 50) # Verify connections me = await tg.get_me() if me: log.info(f"βœ… Bot connectΓ© : @{me.get('username', '?')}") else: log.error("❌ Impossible de contacter l'API Telegram.") sys.exit(1) try: health = await foxy.health() log.info(f"βœ… API Backend connectΓ©e : {health}") except Exception as e: log.warning(f"⚠️ API Backend non disponible : {e}") log.warning(" Le bot fonctionnera mais les commandes API Γ©choueront.") await tg.send(TELEGRAM_CHAT_ID, f"🦊 Foxy Bot v3 dΓ©marrΓ© (@{me.get('username', '?')})\n" f"🌐 API : {API_BASE_URL}\n" "Tape /aide pour les commandes." ) offset = 0 while _running: try: updates = await tg.get_updates(offset) for update in updates: offset = update["update_id"] + 1 await handle_update(tg, foxy, update) except Exception as e: log.error(f"Erreur boucle principale: {e}", exc_info=True) await asyncio.sleep(5) log.info("πŸ›‘ Bot arrΓͺtΓ© proprement.") await tg.send(TELEGRAM_CHAT_ID, "πŸ›‘ Foxy Bot arrΓͺtΓ©") await tg.close() await foxy.close() if __name__ == "__main__": asyncio.run(run_bot())