466 lines
16 KiB
Python
466 lines
16 KiB
Python
#!/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,
|
|
"🦊 <b>Foxy Dev Team Bot v3</b>\n\n"
|
|
"Connecté à l'API centralisée.\n\n"
|
|
"<b>Commandes :</b>\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,
|
|
"🦊 <b>Commandes Foxy Bot v3</b>\n\n"
|
|
"<b>/start</b> — Bienvenue\n"
|
|
"<b>/projets</b> — Portrait de tous les projets\n"
|
|
"<b>/agents</b> — État des agents en temps réel\n"
|
|
"<b>/nouveau</b> nom | description — Créer un projet\n"
|
|
"<b>/test</b> — Lancer un projet de test\n"
|
|
"<b>/reset</b> ID — Réinitialiser un projet\n"
|
|
"<b>/aide</b> — Cette aide\n\n"
|
|
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
f"🌐 Dashboard : <a href='{API_BASE_URL.replace(':8000', ':5173')}'>Ouvrir</a>"
|
|
)
|
|
|
|
|
|
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"🦊 <b>Statut des projets</b> ({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"📋 <b>{p['name']}</b> (#{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 = ["🦊 <b>État des Agents</b>\n"]
|
|
for a in agents:
|
|
status_icon = "⚡" if a["current_status"] == "running" else "❌" if a["current_status"] == "failed" else "💤"
|
|
lines.append(
|
|
f"{status_icon} <b>{a['display_name']}</b>\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"🦊 <b>Projet de test créé !</b>\n\n"
|
|
f"📋 <b>{project['name']}</b> (#{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,
|
|
"🦊 <b>Nouveau projet</b>\n\n"
|
|
"Usage : <code>/nouveau nom | description du projet</code>\n\n"
|
|
"Exemple :\n"
|
|
"<code>/nouveau api-users | Créer une API REST pour gérer les utilisateurs</code>"
|
|
)
|
|
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"🦊 <b>Projet créé !</b>\n\n"
|
|
f"📋 <b>{project['name']}</b> (#{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, "🔄 <b>Reset effectué</b>\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"🦊 <b>Foxy Bot v3 démarré</b> (@{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, "🛑 <b>Foxy Bot arrêté</b>")
|
|
await tg.close()
|
|
await foxy.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(run_bot())
|