#!/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:7000")
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())