- telegram_alerts.py: service envoi alertes via Bot API (send_pre_race_alerts,
build_race_alert, send_telegram_message) — gestion gracieuse TELEGRAM_BOT_TOKEN absent
- auth_db.py: migrate_telegram_columns() idempotente (ALTER TABLE + try/except OperationalError)
colonnes: telegram_chat_id, alert_value_bets, alert_top1, alert_quinte_only
- api_v1/routes/user.py: blueprint user_bp GET/POST /api/v1/user/telegram-config
protégé @jwt_required_middleware + @plan_required('premium','pro')
- api_v1/__init__.py: import + register user_bp
- turf_scheduler.py: run_telegram_alerts() + schedule_dynamic_telegram_alerts()
planifiées 30min avant course (même pattern que schedule_dynamic_scoring)
avec try/except Exception + fallback logger
Co-Authored-By: Paperclip <noreply@paperclip.ing>
285 lines
9.7 KiB
Python
285 lines
9.7 KiB
Python
#!/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
|