547 lines
22 KiB
Python
547 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
🦊 Foxy Dev Team — Telegram Bot v2
|
|
====================================
|
|
Routing des messages :
|
|
/start, /test, /projets-statut, /reset, /aide → handlers locaux
|
|
/foxy-conductor <texte> → agent foxy-conductor
|
|
Tout autre message (libre ou commande inconnue) → agent foxy (principal)
|
|
|
|
Usage : python3 foxy-telegram-bot.py
|
|
Service: systemctl --user start foxy-telegram-bot
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import fcntl
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
import signal
|
|
import logging
|
|
import urllib.request
|
|
import urllib.parse
|
|
import urllib.error
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
# ─── CONFIG ────────────────────────────────────────────────────────────────────
|
|
|
|
TELEGRAM_BOT = "8686313703:AAEGUunkJWbJx7njX_NUrW9HcyrZqXzA3KQ"
|
|
TELEGRAM_CHAT = "8379645618"
|
|
WORKSPACE = Path("/home/openclaw/.openclaw/workspace")
|
|
LOG_FILE = Path("/home/openclaw/.openclaw/logs/foxy-telegram-bot.log")
|
|
PID_FILE = Path("/home/openclaw/.openclaw/logs/foxy-telegram-bot.pid")
|
|
POLL_TIMEOUT = 30
|
|
OPENCLAW_AGENT_DEFAULT = "foxy" # messages libres → agent principal
|
|
OPENCLAW_AGENT_CONDUCTOR = "foxy-conductor" # /foxy-conductor → soumission projet
|
|
|
|
# Statuts avec emoji
|
|
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é",
|
|
}
|
|
|
|
TASK_STATUS_EMOJI = {
|
|
"PENDING": "⏳",
|
|
"IN_REVIEW": "🔍",
|
|
"READY_FOR_DEPLOY": "✅",
|
|
"DONE": "🎉",
|
|
"BLOCKED": "🚫",
|
|
}
|
|
|
|
RUNNING_TO_AWAITING = {
|
|
"CONDUCTOR_RUNNING": "AWAITING_CONDUCTOR",
|
|
"ARCHITECT_RUNNING": "AWAITING_ARCHITECT",
|
|
"DEV_RUNNING": "AWAITING_DEV",
|
|
"UIUX_RUNNING": "AWAITING_UIUX",
|
|
"QA_RUNNING": "AWAITING_QA",
|
|
"DEPLOY_RUNNING": "AWAITING_DEPLOY",
|
|
}
|
|
|
|
# ─── LOGGING ───────────────────────────────────────────────────────────────────
|
|
|
|
UTC = timezone.utc
|
|
|
|
def utcnow_iso() -> str:
|
|
return datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
|
|
LOG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
log = logging.getLogger("foxy-telegram-bot")
|
|
if not log.handlers:
|
|
log.setLevel(logging.INFO)
|
|
fmt = logging.Formatter("[%(asctime)s] %(levelname)s %(message)s", datefmt="%Y-%m-%dT%H:%M:%SZ")
|
|
fh = logging.FileHandler(LOG_FILE)
|
|
fh.setFormatter(fmt)
|
|
log.addHandler(fh)
|
|
# Stdout seulement si on n'est pas sous systemd (évite le double-logging)
|
|
if not os.environ.get("INVOCATION_ID"):
|
|
sh = logging.StreamHandler(sys.stdout)
|
|
sh.setFormatter(fmt)
|
|
log.addHandler(sh)
|
|
log.propagate = False
|
|
|
|
# ─── 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)
|
|
|
|
# ─── PID LOCK ──────────────────────────────────────────────────────────────────
|
|
|
|
_pid_lock_fh = None
|
|
|
|
def acquire_pid_lock() -> bool:
|
|
global _pid_lock_fh
|
|
try:
|
|
PID_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
_pid_lock_fh = open(PID_FILE, "w")
|
|
fcntl.flock(_pid_lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
_pid_lock_fh.write(str(os.getpid()))
|
|
_pid_lock_fh.flush()
|
|
return True
|
|
except BlockingIOError:
|
|
try:
|
|
existing = PID_FILE.read_text().strip()
|
|
print(f"❌ Une instance est déjà en cours (PID {existing}). Abandon.")
|
|
except Exception:
|
|
print("❌ Une instance est déjà en cours. Abandon.")
|
|
return False
|
|
except Exception as e:
|
|
print(f"❌ Impossible d'acquérir le PID lock: {e}")
|
|
return False
|
|
|
|
# ─── TELEGRAM API ──────────────────────────────────────────────────────────────
|
|
|
|
def tg_request(method: str, params: dict, timeout: int = 10) -> dict | None:
|
|
try:
|
|
url = f"https://api.telegram.org/bot{TELEGRAM_BOT}/{method}"
|
|
data = urllib.parse.urlencode(params).encode()
|
|
req = urllib.request.Request(url, data=data, method="POST")
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
return json.loads(resp.read())
|
|
except urllib.error.HTTPError as e:
|
|
log.warning(f"Telegram HTTP {e.code} sur {method}: {e.read().decode()[:200]}")
|
|
return None
|
|
except Exception as e:
|
|
log.warning(f"Telegram error sur {method}: {e}")
|
|
return None
|
|
|
|
def send(chat_id: str, text: str, parse_mode: str = "HTML") -> bool:
|
|
result = tg_request("sendMessage", {
|
|
"chat_id": chat_id, "text": text, "parse_mode": parse_mode,
|
|
})
|
|
return result is not None and result.get("ok", False)
|
|
|
|
def get_updates(offset: int) -> list:
|
|
result = tg_request("getUpdates", {
|
|
"offset": offset, "timeout": POLL_TIMEOUT,
|
|
"allowed_updates": '["message"]',
|
|
}, timeout=POLL_TIMEOUT + 5)
|
|
if result and result.get("ok"):
|
|
return result.get("result", [])
|
|
return []
|
|
|
|
# ─── STATE HELPERS ─────────────────────────────────────────────────────────────
|
|
|
|
def load_state(state_file: Path) -> dict | None:
|
|
for attempt in range(3):
|
|
try:
|
|
with open(state_file) as f:
|
|
state = json.load(f)
|
|
# Filtrer les tasks corrompues (strings au lieu de dicts)
|
|
state["tasks"] = [t for t in state.get("tasks", []) if isinstance(t, dict)]
|
|
return state
|
|
except json.JSONDecodeError:
|
|
if attempt < 2:
|
|
time.sleep(0.3)
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
def save_state(state_file: Path, state: dict) -> bool:
|
|
backup = state_file.with_suffix(".json.bak")
|
|
try:
|
|
if state_file.exists():
|
|
state_file.rename(backup)
|
|
with open(state_file, "w") as f:
|
|
json.dump(state, f, indent=2, ensure_ascii=False)
|
|
return True
|
|
except Exception as e:
|
|
log.error(f"Erreur sauvegarde {state_file}: {e}")
|
|
if backup.exists():
|
|
backup.rename(state_file)
|
|
return False
|
|
|
|
def find_project_states() -> list[Path]:
|
|
states = []
|
|
try:
|
|
for proj_dir in sorted(WORKSPACE.iterdir()):
|
|
if proj_dir.is_dir() and not proj_dir.name.startswith("."):
|
|
sf = proj_dir / "project_state.json"
|
|
if sf.exists():
|
|
states.append(sf)
|
|
except Exception:
|
|
pass
|
|
return states
|
|
|
|
# ─── SESSION ENV (pour openclaw agent) ────────────────────────────────────────
|
|
|
|
def _get_session_env() -> dict:
|
|
env = {**os.environ, "HOME": "/home/openclaw"}
|
|
try:
|
|
gw = subprocess.run(
|
|
["pgrep", "-u", "openclaw", "-x", "openclaw-gateway"],
|
|
capture_output=True, text=True
|
|
)
|
|
gw_pid = gw.stdout.strip().splitlines()[0] if gw.stdout.strip() else None
|
|
if gw_pid:
|
|
with open(f"/proc/{gw_pid}/environ", "rb") as f:
|
|
for item in f.read().split(b"\x00"):
|
|
if b"=" not in item:
|
|
continue
|
|
k, _, v = item.partition(b"=")
|
|
if k.decode() in {"DBUS_SESSION_BUS_ADDRESS", "XDG_RUNTIME_DIR", "DISPLAY"}:
|
|
env[k.decode()] = v.decode()
|
|
except Exception:
|
|
pass
|
|
uid = subprocess.run(["id", "-u"], capture_output=True, text=True).stdout.strip()
|
|
env.setdefault("XDG_RUNTIME_DIR", f"/run/user/{uid}")
|
|
env.setdefault("DBUS_SESSION_BUS_ADDRESS", f"unix:path=/run/user/{uid}/bus")
|
|
return env
|
|
|
|
# ─── COMMANDES LOCALES ─────────────────────────────────────────────────────────
|
|
|
|
def cmd_start(chat_id: str):
|
|
send(chat_id,
|
|
"🦊 <b>Foxy Dev Team Bot</b>\n\n"
|
|
"Je suis le panneau de contrôle de ton pipeline.\n\n"
|
|
"<b>Commandes :</b>\n"
|
|
"/projets-statut — Portrait de tous les projets\n"
|
|
"/test — Lancer un projet de test complet\n"
|
|
"/reset — Débloquer un projet bloqué\n"
|
|
"/foxy-conductor — Soumettre un projet à Conductor\n"
|
|
"/aide — Aide complète\n\n"
|
|
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
"💬 Tu peux aussi m'écrire librement — je transmets à l'agent principal.\n"
|
|
"Exemple : <i>\"Quel est l'état du pipeline ?\"</i>"
|
|
)
|
|
|
|
def cmd_aide(chat_id: str):
|
|
send(chat_id,
|
|
"🦊 <b>Commandes Foxy Bot</b>\n\n"
|
|
"<b>/start</b> — Bienvenue\n\n"
|
|
"<b>/projets-statut</b>\n"
|
|
" Portrait de tous les projets actifs\n\n"
|
|
"<b>/test</b>\n"
|
|
" Crée un projet de test et déclenche le pipeline\n\n"
|
|
"<b>/reset</b>\n"
|
|
" Remet les projets RUNNING en AWAITING (déblocage)\n\n"
|
|
"<b>/foxy-conductor <description></b>\n"
|
|
" Soumet une demande directement à Foxy-Conductor\n"
|
|
" Ex: <code>/foxy-conductor Créer une API REST pour les users</code>\n\n"
|
|
"<b>/aide</b> — Cette aide\n\n"
|
|
"━━━━━━━━━━━━━━━━━━━━\n"
|
|
f"💬 <b>Message libre</b> → agent principal (<code>{OPENCLAW_AGENT_DEFAULT}</code>)\n"
|
|
f"🔧 <b>Commande inconnue</b> → agent principal (<code>{OPENCLAW_AGENT_DEFAULT}</code>)"
|
|
)
|
|
|
|
def cmd_projets_statut(chat_id: str):
|
|
state_files = find_project_states()
|
|
if not state_files:
|
|
send(chat_id, "📭 Aucun projet dans le workspace.")
|
|
return
|
|
|
|
send(chat_id, f"🦊 <b>Statut des projets</b> ({len(state_files)} projet(s))")
|
|
|
|
for sf in state_files:
|
|
state = load_state(sf)
|
|
if not state:
|
|
send(chat_id, f"⚠️ <code>{sf.parent.name}</code> — JSON illisible")
|
|
continue
|
|
|
|
project_name = state.get("project_name", sf.parent.name)
|
|
status = state.get("status", "?")
|
|
description = state.get("description", "")[:100]
|
|
updated_at = state.get("updated_at", "?")
|
|
tasks = state.get("tasks", [])
|
|
|
|
total = len(tasks)
|
|
done = sum(1 for t in tasks if t.get("status") in ("DONE", "READY_FOR_DEPLOY"))
|
|
pct = int(done / total * 100) if total > 0 else 0
|
|
filled = int(10 * pct / 100)
|
|
bar = "█" * filled + "░" * (10 - filled)
|
|
|
|
status_label = STATUS_EMOJI.get(status, f"❓ {status}")
|
|
|
|
try:
|
|
dt = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
|
elapsed = int((datetime.now(UTC) - dt).total_seconds() / 60)
|
|
last = f"{elapsed}min" if elapsed < 60 else f"{elapsed//60}h{elapsed%60:02d}min"
|
|
except Exception:
|
|
last = updated_at
|
|
|
|
task_lines = []
|
|
for t in tasks:
|
|
emoji = TASK_STATUS_EMOJI.get(t.get("status", ""), "❓")
|
|
task_lines.append(
|
|
f" {emoji} <code>{t.get('task_id','?')}</code> {t.get('title','')[:40]}\n"
|
|
f" → {t.get('assigned_to','?')} [{t.get('status','?')}]"
|
|
)
|
|
|
|
send(chat_id,
|
|
f"━━━━━━━━━━━━━━━━━━━━\n"
|
|
f"📋 <b>{project_name}</b>\n"
|
|
f"📊 {status_label}\n"
|
|
f"⏱️ Dernière activité : {last}\n"
|
|
f"🔄 [{bar}] {pct}% ({done}/{total})\n"
|
|
f"\n📝 <i>{description}</i>\n"
|
|
f"\n<b>Tâches :</b>\n" + ("\n".join(task_lines) if task_lines else " (aucune tâche)")
|
|
)
|
|
|
|
def cmd_test(chat_id: str):
|
|
ts = datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
project_slug = f"test-pipeline-{ts}"
|
|
proj_dir = WORKSPACE / project_slug
|
|
state_file = proj_dir / "project_state.json"
|
|
|
|
description = (
|
|
"PROJET DE TEST AUTOMATIQUE — Pipeline Foxy Dev Team. "
|
|
"Chaque agent doit : lire project_state.json, simuler son travail, "
|
|
"mettre à jour le statut vers l'étape suivante, ajouter une entrée audit_log. "
|
|
"Aucun code réel. Test réussi si statut = COMPLETED."
|
|
)
|
|
|
|
try:
|
|
proj_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(state_file, "w") as f:
|
|
json.dump({
|
|
"project_name": project_slug,
|
|
"description": description,
|
|
"status": "AWAITING_CONDUCTOR",
|
|
"created_at": utcnow_iso(),
|
|
"updated_at": utcnow_iso(),
|
|
"test_mode": True,
|
|
"tasks": [],
|
|
"audit_log": [{"timestamp": utcnow_iso(), "action": "PROJECT_SUBMITTED",
|
|
"agent": "foxy-telegram-bot", "details": "Créé via /test"}]
|
|
}, f, indent=2, ensure_ascii=False)
|
|
except Exception as e:
|
|
send(chat_id, f"❌ Impossible de créer le projet de test : {e}")
|
|
return
|
|
|
|
send(chat_id,
|
|
f"🦊 <b>Projet de test créé !</b>\n\n"
|
|
f"📋 <code>{project_slug}</code>\n"
|
|
f"📊 ⏳ AWAITING_CONDUCTOR\n\n"
|
|
f"Le daemon prendra en charge ce projet dans ≤30s.\n"
|
|
f"Utilise /projets-statut pour suivre la progression."
|
|
)
|
|
log.info(f"📋 Projet de test créé : {project_slug}")
|
|
|
|
def cmd_reset(chat_id: str):
|
|
state_files = find_project_states()
|
|
resets = []
|
|
for sf in state_files:
|
|
state = load_state(sf)
|
|
if not state:
|
|
continue
|
|
status = state.get("status", "")
|
|
if status in RUNNING_TO_AWAITING:
|
|
reset_to = RUNNING_TO_AWAITING[status]
|
|
project_name = state.get("project_name", sf.parent.name)
|
|
state["status"] = reset_to
|
|
state["updated_at"] = utcnow_iso()
|
|
state.setdefault("audit_log", []).append({
|
|
"timestamp": utcnow_iso(), "action": "MANUAL_RESET",
|
|
"agent": "foxy-telegram-bot",
|
|
"details": f"{status} → {reset_to} via /reset"
|
|
})
|
|
if save_state(sf, state):
|
|
resets.append(f" • {project_name} : {status} → {reset_to}")
|
|
log.info(f"Reset : {project_name} {status} → {reset_to}")
|
|
|
|
if resets:
|
|
send(chat_id, "🔄 <b>Reset effectué</b>\n\n" + "\n".join(resets) +
|
|
"\n\nLe daemon reprendra ces projets au prochain cycle.")
|
|
else:
|
|
send(chat_id, "✅ Aucun projet RUNNING à resetter.")
|
|
|
|
def cmd_foxy_conductor(chat_id: str, text: str, username: str):
|
|
"""Soumet un message directement à Foxy-Conductor."""
|
|
parts = text.split(maxsplit=1)
|
|
if len(parts) < 2 or not parts[1].strip():
|
|
send(chat_id,
|
|
"🦊 <b>Foxy-Conductor</b>\n\n"
|
|
"Usage : <code>/foxy-conductor <description du projet></code>\n\n"
|
|
"Exemple :\n"
|
|
"<code>/foxy-conductor Créer une API REST pour gérer les utilisateurs</code>\n\n"
|
|
"Conductor va analyser la demande et initialiser le pipeline."
|
|
)
|
|
return
|
|
forward_to_openclaw(chat_id, parts[1].strip(), username, agent=OPENCLAW_AGENT_CONDUCTOR)
|
|
|
|
# ─── FORWARDING VERS OPENCLAW ──────────────────────────────────────────────────
|
|
|
|
def forward_to_openclaw(chat_id: str, text: str, username: str,
|
|
agent: str = OPENCLAW_AGENT_DEFAULT):
|
|
"""Transmet un message à un agent openclaw et renvoie la réponse dans Telegram."""
|
|
log.info(f"📨 Forwarding à openclaw ({agent}): '{text[:80]}'")
|
|
|
|
env = _get_session_env()
|
|
full_message = f"[Message Telegram de @{username}] {text}"
|
|
cmd = ["openclaw", "agent", "--agent", agent, "--message", full_message]
|
|
|
|
try:
|
|
send(chat_id, f"⏳ <i>Openclaw traite ta demande... (agent: {agent})</i>")
|
|
result = subprocess.run(
|
|
cmd, capture_output=True, text=True,
|
|
timeout=120, env=env,
|
|
cwd="/home/openclaw/.openclaw/workspace"
|
|
)
|
|
response = (result.stdout or "").strip()
|
|
stderr = (result.stderr or "").strip()
|
|
|
|
if result.returncode != 0:
|
|
log.error(f"openclaw agent erreur (code {result.returncode}): {stderr[:200]}")
|
|
send(chat_id,
|
|
f"❌ <b>Openclaw a rencontré une erreur</b>\n"
|
|
f"<code>{stderr[:300]}</code>"
|
|
)
|
|
return
|
|
|
|
if not response:
|
|
send(chat_id, "🤷 <i>Openclaw n'a pas retourné de réponse.</i>")
|
|
return
|
|
|
|
# Découper si > 4000 chars (limite Telegram)
|
|
for i in range(0, len(response), 4000):
|
|
send(chat_id, response[i:i+4000])
|
|
|
|
log.info(f"✅ Réponse openclaw ({agent}) envoyée ({len(response)} chars)")
|
|
|
|
except subprocess.TimeoutExpired:
|
|
log.error(f"openclaw agent ({agent}) timeout 120s")
|
|
send(chat_id, f"⏱️ <b>Timeout</b> — <code>{agent}</code> n'a pas répondu en 120s.")
|
|
except FileNotFoundError:
|
|
send(chat_id, "❌ <code>openclaw</code> introuvable dans PATH.")
|
|
except Exception as e:
|
|
log.error(f"Erreur forward_to_openclaw: {e}")
|
|
send(chat_id, f"❌ Erreur inattendue : {e}")
|
|
|
|
# ─── ROUTING ───────────────────────────────────────────────────────────────────
|
|
|
|
# Commandes locales gérées par le bot (sans le texte du message)
|
|
LOCAL_COMMANDS = {
|
|
"/start": cmd_start,
|
|
"/aide": cmd_aide,
|
|
"/help": cmd_aide,
|
|
"/projets-statut": cmd_projets_statut,
|
|
"/status": cmd_projets_statut,
|
|
"/test": cmd_test,
|
|
"/reset": cmd_reset,
|
|
}
|
|
|
|
def handle_update(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:
|
|
log.warning(f"Message ignoré de chat non autorisé: {chat_id} (@{username})")
|
|
send(chat_id, "⛔ Chat non autorisé.")
|
|
return
|
|
|
|
log.info(f"📩 Message reçu: '{text[:80]}' de @{username}")
|
|
|
|
if text.startswith("/"):
|
|
cmd = text.split()[0].split("@")[0].lower()
|
|
|
|
# /foxy-conductor a besoin du texte complet
|
|
if cmd == "/foxy-conductor":
|
|
cmd_foxy_conductor(chat_id, text, username)
|
|
return
|
|
|
|
# Commande locale connue
|
|
handler = LOCAL_COMMANDS.get(cmd)
|
|
if handler:
|
|
try:
|
|
handler(chat_id)
|
|
except Exception as e:
|
|
log.error(f"Erreur handler {cmd}: {e}", exc_info=True)
|
|
send(chat_id, f"❌ Erreur lors de <code>{cmd}</code> : {e}")
|
|
return
|
|
|
|
# Tout le reste (message libre + commande inconnue) → agent principal
|
|
forward_to_openclaw(chat_id, text, username)
|
|
|
|
# ─── DAEMON ────────────────────────────────────────────────────────────────────
|
|
|
|
def run_bot():
|
|
if not acquire_pid_lock():
|
|
sys.exit(1)
|
|
|
|
log.info("=" * 50)
|
|
log.info("🦊 FOXY TELEGRAM BOT v2 — DÉMARRÉ")
|
|
log.info(f" Chat autorisé : {TELEGRAM_CHAT}")
|
|
log.info(f" Agent par défaut : {OPENCLAW_AGENT_DEFAULT}")
|
|
log.info(f" Agent conductor : {OPENCLAW_AGENT_CONDUCTOR}")
|
|
log.info(f" Workspace : {WORKSPACE}")
|
|
log.info("=" * 50)
|
|
|
|
me = tg_request("getMe", {})
|
|
if me and me.get("ok"):
|
|
bot_name = me["result"].get("username", "?")
|
|
log.info(f"✅ Bot connecté : @{bot_name}")
|
|
send(TELEGRAM_CHAT,
|
|
f"🦊 <b>Foxy Bot v2 démarré</b> (@{bot_name})\n"
|
|
f"Agent par défaut : <code>{OPENCLAW_AGENT_DEFAULT}</code>\n"
|
|
"Tape /aide pour les commandes.")
|
|
else:
|
|
log.error("❌ Impossible de contacter l'API Telegram.")
|
|
sys.exit(1)
|
|
|
|
offset = 0
|
|
while _running:
|
|
try:
|
|
updates = get_updates(offset)
|
|
for update in updates:
|
|
offset = update["update_id"] + 1
|
|
handle_update(update)
|
|
except Exception as e:
|
|
log.error(f"Erreur boucle principale: {e}", exc_info=True)
|
|
time.sleep(5)
|
|
|
|
log.info("🛑 Bot arrêté proprement.")
|
|
send(TELEGRAM_CHAT, "🛑 <b>Foxy Bot arrêté</b>")
|
|
|
|
# ─── ENTRY POINT ───────────────────────────────────────────────────────────────
|
|
|
|
if __name__ == "__main__":
|
|
run_bot()
|