#!/usr/bin/env python3 """ Telegram Alerts — Service d'alertes pré-course pour les utilisateurs Premium/Pro HRT-79: Alertes Telegram configurables (Premium) Fonctionnement : - 30 minutes avant chaque course détectée, envoie un message Telegram aux utilisateurs Premium/Pro ayant configuré leur chat_id. - Les préférences individuelles (value_bets, top1, quinte_only) sont respectées. - Requiert la variable d'environnement TELEGRAM_BOT_TOKEN. """ import os import logging import sqlite3 from datetime import datetime from typing import Optional import requests logger = logging.getLogger(__name__) DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db") BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_API_BASE = "https://api.telegram.org/bot{token}/sendMessage" # ── Helpers ─────────────────────────────────────────────────────────────────── def _get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def send_telegram_message(chat_id: str, text: str) -> bool: """ Envoie un message Telegram à un chat_id donné. Returns True si succès, False sinon. Ne lève pas d'exception pour ne pas crasher le scheduler. """ if not BOT_TOKEN: logger.warning("[TELEGRAM] TELEGRAM_BOT_TOKEN non configuré — envoi ignoré") return False url = TELEGRAM_API_BASE.format(token=BOT_TOKEN) payload = { "chat_id": chat_id, "text": text, "parse_mode": "Markdown", "disable_web_page_preview": True, } try: resp = requests.post(url, json=payload, timeout=10) if resp.status_code == 200: return True logger.warning( "[TELEGRAM] Echec envoi chat_id=%s status=%d body=%s", chat_id, resp.status_code, resp.text[:200], ) return False except requests.RequestException as exc: logger.error("[TELEGRAM] Exception HTTP chat_id=%s: %s", chat_id, exc) return False # ── Alert builder ───────────────────────────────────────────────────────────── def build_race_alert(race_data: dict, predictions: list) -> str: """ Construit le message Markdown de l'alerte pré-course. Args: race_data: dict avec les clés 'hippo', 'num_course', 'heure', 'type_course' predictions: liste de dicts {'num_cheval', 'nom_cheval', 'prob_top3', 'is_value_bet', 'ml_score'} Returns: texte Markdown formaté """ hippo = race_data.get("hippo", "?") num_course = race_data.get("num_course", "?") heure = race_data.get("heure", "?") type_course = race_data.get("type_course", "") lines = [ f"🏇 *Alerte course — {hippo} R{num_course}*", f"⏰ Départ prévu : *{heure}*", ] if type_course: lines.append(f"📋 Type : {type_course}") lines.append("") top3 = [p for p in predictions if p.get("prob_top3", 0) > 0][:3] value_bets = [p for p in predictions if p.get("is_value_bet")] if top3: lines.append("📊 *Top-3 ML :*") for i, p in enumerate(top3, 1): nom = p.get("nom_cheval", f"#{p.get('num_cheval', '?')}") prob = p.get("prob_top3", 0) lines.append(f" {i}. {nom} — {prob:.0%} prob top-3") lines.append("") if value_bets: lines.append("💡 *Value bets :*") for p in value_bets[:3]: nom = p.get("nom_cheval", f"#{p.get('num_cheval', '?')}") score = p.get("ml_score", 0) lines.append(f" ✅ {nom} (score {score:.2f})") lines.append("") lines.append("_Alerte automatique Turf SaaS — 30min avant départ_") return "\n".join(lines) # ── Main send function ──────────────────────────────────────────────────────── def send_pre_race_alerts(minutes_before: int = 30) -> dict: """ Interroge la DB pour récupérer les courses du jour, puis envoie des alertes Telegram aux utilisateurs Premium/Pro éligibles. Args: minutes_before: non utilisé directement (la planification est gérée par le scheduler), présent pour documentation. Returns: dict {'sent': int, 'skipped': int, 'errors': int} """ if not BOT_TOKEN: logger.warning( "[TELEGRAM] TELEGRAM_BOT_TOKEN absent — send_pre_race_alerts ignoré" ) return {"sent": 0, "skipped": 0, "errors": 0} stats = {"sent": 0, "skipped": 0, "errors": 0} try: conn = _get_db() today = datetime.now().strftime("%Y-%m-%d") # Récupère les courses du jour try: courses_rows = conn.execute( """ SELECT DISTINCT hippo, num_course, heure_depart, type_course FROM pmu_courses WHERE date_programme = ? AND heure_depart IS NOT NULL ORDER BY heure_depart ASC LIMIT 20 """, (today,), ).fetchall() except sqlite3.OperationalError as exc: logger.warning("[TELEGRAM] Table pmu_courses introuvable: %s", exc) conn.close() return stats if not courses_rows: logger.info("[TELEGRAM] Aucune course aujourd'hui — pas d'alerte") conn.close() return stats # Récupère les utilisateurs Premium/Pro avec chat_id configuré try: users = conn.execute( """ SELECT id, telegram_chat_id, alert_value_bets, alert_top1, alert_quinte_only FROM users WHERE plan IN ('premium', 'pro') AND is_active = 1 AND telegram_chat_id IS NOT NULL AND telegram_chat_id != '' """, ).fetchall() except sqlite3.OperationalError as exc: logger.warning( "[TELEGRAM] Colonnes Telegram absentes (migration non appliquée?): %s", exc, ) conn.close() return stats if not users: logger.info("[TELEGRAM] Aucun utilisateur avec chat_id configuré") conn.close() return stats for course_row in courses_rows: hippo = course_row["hippo"] or "?" num_course = course_row["num_course"] or "?" heure_ts = course_row["heure_depart"] type_course = course_row["type_course"] or "" try: dt = datetime.fromtimestamp(heure_ts / 1000) heure_str = dt.strftime("%H:%M") except Exception: heure_str = str(heure_ts) race_data = { "hippo": hippo, "num_course": num_course, "heure": heure_str, "type_course": type_course, } # Récupère les prédictions ML pour cette course predictions = [] try: pred_rows = conn.execute( """ SELECT num_cheval, nom_cheval, prob_top3, is_value_bet, ml_score FROM ml_predictions_cache WHERE date = ? AND hippo = ? AND num_course = ? ORDER BY prob_top3 DESC LIMIT 10 """, (today, hippo, num_course), ).fetchall() predictions = [dict(r) for r in pred_rows] except sqlite3.OperationalError: pass # table absente, on envoie quand même avec données minimales is_quinte = ( "quinté" in type_course.lower() or "quinte" in type_course.lower() ) for user in users: chat_id = user["telegram_chat_id"] alert_quinte_only = bool(user["alert_quinte_only"]) alert_top1 = bool(user["alert_top1"]) alert_value_bets = bool(user["alert_value_bets"]) # Filtre quinte_only if alert_quinte_only and not is_quinte: stats["skipped"] += 1 continue # Construit le message selon préférences filtered_preds = [] if predictions: for p in predictions: include = False if alert_top1 and p.get("prob_top3", 0) > 0: include = True if alert_value_bets and p.get("is_value_bet"): include = True if include: filtered_preds.append(p) text = build_race_alert(race_data, filtered_preds) ok = send_telegram_message(chat_id, text) if ok: stats["sent"] += 1 else: stats["errors"] += 1 conn.close() except Exception as exc: logger.error("[TELEGRAM] Erreur inattendue dans send_pre_race_alerts: %s", exc) import traceback traceback.print_exc() stats["errors"] += 1 logger.info( "[TELEGRAM] Alertes pré-course: %d envoyées, %d ignorées, %d erreurs", stats["sent"], stats["skipped"], stats["errors"], ) return stats