Compare commits
4 Commits
feature/HR
...
feature/HR
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8604dc78b1 | ||
|
|
30464fb40c | ||
|
|
31db3a8260 | ||
|
|
278245cd7c |
@@ -3,6 +3,7 @@
|
|||||||
API v1 Blueprint package — Turf SaaS
|
API v1 Blueprint package — Turf SaaS
|
||||||
Sprint 3-4: HRT-29 — Refacto API /v1/
|
Sprint 3-4: HRT-29 — Refacto API /v1/
|
||||||
Sprint 5-6: HRT-31 — Billing Stripe
|
Sprint 5-6: HRT-31 — Billing Stripe
|
||||||
|
HRT-79: Alertes Telegram configurables (user blueprint)
|
||||||
|
|
||||||
Registers sub-blueprints:
|
Registers sub-blueprints:
|
||||||
/api/v1/health — public health-check
|
/api/v1/health — public health-check
|
||||||
@@ -13,6 +14,7 @@ Registers sub-blueprints:
|
|||||||
/api/v1/export/ — export CSV (pro)
|
/api/v1/export/ — export CSV (pro)
|
||||||
/api/v1/metrics — métriques perf ML (premium+)
|
/api/v1/metrics — métriques perf ML (premium+)
|
||||||
/api/v1/billing/ — Stripe checkout, portal, webhook, status
|
/api/v1/billing/ — Stripe checkout, portal, webhook, status
|
||||||
|
/api/v1/user/ — config utilisateur, alertes Telegram (premium+)
|
||||||
/api/v1/docs — Swagger UI (via flasgger, registered on app)
|
/api/v1/docs — Swagger UI (via flasgger, registered on app)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -26,6 +28,7 @@ from .routes.backtest import backtest_bp
|
|||||||
from .routes.export import export_bp
|
from .routes.export import export_bp
|
||||||
from .routes.metrics import metrics_bp
|
from .routes.metrics import metrics_bp
|
||||||
from .routes.billing import billing_bp
|
from .routes.billing import billing_bp
|
||||||
|
from .routes.user import user_bp
|
||||||
|
|
||||||
# Master blueprint that aggregates all sub-routes under /api/v1
|
# Master blueprint that aggregates all sub-routes under /api/v1
|
||||||
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
|
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")
|
||||||
@@ -41,3 +44,4 @@ def register_api_v1(app):
|
|||||||
app.register_blueprint(export_bp)
|
app.register_blueprint(export_bp)
|
||||||
app.register_blueprint(metrics_bp)
|
app.register_blueprint(metrics_bp)
|
||||||
app.register_blueprint(billing_bp)
|
app.register_blueprint(billing_bp)
|
||||||
|
app.register_blueprint(user_bp)
|
||||||
|
|||||||
216
api_v1/routes/user.py
Normal file
216
api_v1/routes/user.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
User route for API v1 — Telegram alert configuration
|
||||||
|
HRT-79: Alertes Telegram configurables (Premium)
|
||||||
|
|
||||||
|
GET /api/v1/user/telegram-config — Lire la config Telegram de l'utilisateur connecté
|
||||||
|
POST /api/v1/user/telegram-config — Mettre à jour la config Telegram
|
||||||
|
|
||||||
|
Accès : Premium / Pro uniquement (@jwt_required_middleware + @plan_required)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sqlite3
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
|
||||||
|
from api_v1.utils import internal_error, bad_request
|
||||||
|
from auth import jwt_required_middleware, plan_required
|
||||||
|
|
||||||
|
user_bp = Blueprint("v1_user", __name__, url_prefix="/api/v1/user")
|
||||||
|
|
||||||
|
# DB_PATH est résolu via la même variable d'env que auth_db.py
|
||||||
|
import os
|
||||||
|
|
||||||
|
_DB_PATH = os.environ.get("TURF_SAAS_DB", "/home/h3r7/turf_saas/turf_saas.db")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_db():
|
||||||
|
conn = sqlite3.connect(_DB_PATH)
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
# ── GET /api/v1/user/telegram-config ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route("/telegram-config", methods=["GET"])
|
||||||
|
@jwt_required_middleware
|
||||||
|
@plan_required("premium", "pro")
|
||||||
|
def get_telegram_config():
|
||||||
|
"""
|
||||||
|
Retourne la configuration Telegram de l'utilisateur connecté.
|
||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Utilisateur
|
||||||
|
summary: Lire la config alertes Telegram (premium+)
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Configuration Telegram courante
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
telegram_chat_id:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
alert_value_bets:
|
||||||
|
type: boolean
|
||||||
|
alert_top1:
|
||||||
|
type: boolean
|
||||||
|
alert_quinte_only:
|
||||||
|
type: boolean
|
||||||
|
401:
|
||||||
|
description: Token invalide
|
||||||
|
403:
|
||||||
|
description: Plan insuffisant
|
||||||
|
"""
|
||||||
|
user_id = request.user_id # injecté par jwt_required_middleware
|
||||||
|
|
||||||
|
conn = _get_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT telegram_chat_id, alert_value_bets, alert_top1, alert_quinte_only
|
||||||
|
FROM users
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(user_id,),
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return jsonify({"error": "Utilisateur introuvable"}), 404
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"telegram_chat_id": row["telegram_chat_id"],
|
||||||
|
"alert_value_bets": bool(row["alert_value_bets"]),
|
||||||
|
"alert_top1": bool(row["alert_top1"]),
|
||||||
|
"alert_quinte_only": bool(row["alert_quinte_only"]),
|
||||||
|
}
|
||||||
|
), 200
|
||||||
|
|
||||||
|
except sqlite3.OperationalError as exc:
|
||||||
|
# Colonnes absentes : migration non appliquée
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"telegram_chat_id": None,
|
||||||
|
"alert_value_bets": True,
|
||||||
|
"alert_top1": True,
|
||||||
|
"alert_quinte_only": False,
|
||||||
|
"_warning": "Migration Telegram non appliquée",
|
||||||
|
}
|
||||||
|
), 200
|
||||||
|
except Exception as exc:
|
||||||
|
return internal_error(str(exc))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
# ── POST /api/v1/user/telegram-config ─────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@user_bp.route("/telegram-config", methods=["POST"])
|
||||||
|
@jwt_required_middleware
|
||||||
|
@plan_required("premium", "pro")
|
||||||
|
def update_telegram_config():
|
||||||
|
"""
|
||||||
|
Met à jour la configuration Telegram de l'utilisateur connecté.
|
||||||
|
---
|
||||||
|
tags:
|
||||||
|
- Utilisateur
|
||||||
|
summary: Configurer les alertes Telegram (premium+)
|
||||||
|
security:
|
||||||
|
- Bearer: []
|
||||||
|
parameters:
|
||||||
|
- in: body
|
||||||
|
name: body
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
telegram_chat_id:
|
||||||
|
type: string
|
||||||
|
description: Chat ID Telegram (ou null pour désactiver)
|
||||||
|
alert_value_bets:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
alert_top1:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
alert_quinte_only:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
responses:
|
||||||
|
200:
|
||||||
|
description: Configuration mise à jour
|
||||||
|
400:
|
||||||
|
description: Paramètres invalides
|
||||||
|
401:
|
||||||
|
description: Token invalide
|
||||||
|
403:
|
||||||
|
description: Plan insuffisant
|
||||||
|
"""
|
||||||
|
user_id = request.user_id # injecté par jwt_required_middleware
|
||||||
|
|
||||||
|
data = request.get_json(silent=True)
|
||||||
|
if not data:
|
||||||
|
return bad_request("Corps JSON requis")
|
||||||
|
|
||||||
|
# Validation et extraction des champs
|
||||||
|
telegram_chat_id = data.get("telegram_chat_id")
|
||||||
|
if telegram_chat_id is not None and not isinstance(telegram_chat_id, str):
|
||||||
|
return bad_request("telegram_chat_id doit être une chaîne ou null")
|
||||||
|
if isinstance(telegram_chat_id, str):
|
||||||
|
telegram_chat_id = telegram_chat_id.strip() or None
|
||||||
|
|
||||||
|
alert_value_bets = data.get("alert_value_bets", True)
|
||||||
|
alert_top1 = data.get("alert_top1", True)
|
||||||
|
alert_quinte_only = data.get("alert_quinte_only", False)
|
||||||
|
|
||||||
|
if not isinstance(alert_value_bets, bool):
|
||||||
|
return bad_request("alert_value_bets doit être un booléen")
|
||||||
|
if not isinstance(alert_top1, bool):
|
||||||
|
return bad_request("alert_top1 doit être un booléen")
|
||||||
|
if not isinstance(alert_quinte_only, bool):
|
||||||
|
return bad_request("alert_quinte_only doit être un booléen")
|
||||||
|
|
||||||
|
conn = _get_db()
|
||||||
|
try:
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
UPDATE users
|
||||||
|
SET telegram_chat_id = ?,
|
||||||
|
alert_value_bets = ?,
|
||||||
|
alert_top1 = ?,
|
||||||
|
alert_quinte_only = ?
|
||||||
|
WHERE id = ?
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
telegram_chat_id,
|
||||||
|
int(alert_value_bets),
|
||||||
|
int(alert_top1),
|
||||||
|
int(alert_quinte_only),
|
||||||
|
user_id,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"telegram_chat_id": telegram_chat_id,
|
||||||
|
"alert_value_bets": alert_value_bets,
|
||||||
|
"alert_top1": alert_top1,
|
||||||
|
"alert_quinte_only": alert_quinte_only,
|
||||||
|
}
|
||||||
|
), 200
|
||||||
|
|
||||||
|
except sqlite3.OperationalError as exc:
|
||||||
|
return jsonify(
|
||||||
|
{
|
||||||
|
"error": "Migration Telegram non appliquée — contacter le support",
|
||||||
|
"detail": str(exc),
|
||||||
|
}
|
||||||
|
), 500
|
||||||
|
except Exception as exc:
|
||||||
|
return internal_error(str(exc))
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
31
auth_db.py
31
auth_db.py
@@ -2,6 +2,7 @@
|
|||||||
"""
|
"""
|
||||||
Auth DB — users and subscriptions schema for turf_saas.db
|
Auth DB — users and subscriptions schema for turf_saas.db
|
||||||
Sprint 2-3: Auth JWT + Multi-tenant (HRT-28)
|
Sprint 2-3: Auth JWT + Multi-tenant (HRT-28)
|
||||||
|
HRT-79: migration Telegram columns
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -63,6 +64,36 @@ def init_auth_tables():
|
|||||||
conn.close()
|
conn.close()
|
||||||
print("[auth_db] Tables users, subscriptions, refresh_tokens created/verified.")
|
print("[auth_db] Tables users, subscriptions, refresh_tokens created/verified.")
|
||||||
|
|
||||||
|
# Apply Telegram columns migration (idempotent)
|
||||||
|
migrate_telegram_columns()
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_telegram_columns():
|
||||||
|
"""
|
||||||
|
Migration idempotente : ajoute les colonnes Telegram à la table users.
|
||||||
|
Utilise ALTER TABLE ... ADD COLUMN avec try/except OperationalError
|
||||||
|
pour être safe si les colonnes existent déjà (SQLite ne supporte pas IF NOT EXISTS).
|
||||||
|
HRT-79
|
||||||
|
"""
|
||||||
|
conn = get_db()
|
||||||
|
c = conn.cursor()
|
||||||
|
columns = [
|
||||||
|
("telegram_chat_id", "TEXT DEFAULT NULL"),
|
||||||
|
("alert_value_bets", "INTEGER DEFAULT 1"),
|
||||||
|
("alert_top1", "INTEGER DEFAULT 1"),
|
||||||
|
("alert_quinte_only", "INTEGER DEFAULT 0"),
|
||||||
|
]
|
||||||
|
for col, definition in columns:
|
||||||
|
try:
|
||||||
|
c.execute(f"ALTER TABLE users ADD COLUMN {col} {definition}")
|
||||||
|
print(f"[auth_db] Colonne '{col}' ajoutée.")
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
# Column already exists — safe to ignore
|
||||||
|
pass
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("[auth_db] Migration Telegram columns OK.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
init_auth_tables()
|
init_auth_tables()
|
||||||
|
|||||||
1266
dashboard_saas.html
1266
dashboard_saas.html
File diff suppressed because it is too large
Load Diff
284
telegram_alerts.py
Normal file
284
telegram_alerts.py
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
#!/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
|
||||||
@@ -193,6 +193,65 @@ def schedule_dynamic_scoring():
|
|||||||
logger.info("ℹ️ [SCHEDULER] Pas de course aujourd'hui, pas de scoring dynamique")
|
logger.info("ℹ️ [SCHEDULER] Pas de course aujourd'hui, pas de scoring dynamique")
|
||||||
|
|
||||||
|
|
||||||
|
def run_telegram_alerts():
|
||||||
|
"""Envoie les alertes Telegram pré-course aux utilisateurs Premium/Pro"""
|
||||||
|
logger.info("📨 [SCHEDULER] Envoi alertes Telegram pré-course...")
|
||||||
|
try:
|
||||||
|
os.chdir("/home/h3r7/turf_saas")
|
||||||
|
import telegram_alerts
|
||||||
|
|
||||||
|
stats = telegram_alerts.send_pre_race_alerts(minutes_before=30)
|
||||||
|
logger.info(
|
||||||
|
"✅ [SCHEDULER] Alertes Telegram: %d envoyées, %d ignorées, %d erreurs",
|
||||||
|
stats.get("sent", 0),
|
||||||
|
stats.get("skipped", 0),
|
||||||
|
stats.get("errors", 0),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"❌ [SCHEDULER] Erreur alertes Telegram: {e}")
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
|
def schedule_dynamic_telegram_alerts():
|
||||||
|
"""Planifie les alertes Telegram 30min avant la course (même pattern que schedule_dynamic_scoring)"""
|
||||||
|
race_time = get_todays_race_time()
|
||||||
|
|
||||||
|
if race_time:
|
||||||
|
try:
|
||||||
|
# Convertir timestamp ms en datetime
|
||||||
|
dt = datetime.fromtimestamp(race_time / 1000)
|
||||||
|
race_hour = dt.hour
|
||||||
|
race_min = dt.minute
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"📅 [SCHEDULER] Alertes Telegram — course à {race_hour:02d}:{race_min:02d}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Alertes 30min avant la course
|
||||||
|
pre_min = race_min - 30
|
||||||
|
pre_hour = race_hour
|
||||||
|
if pre_min < 0:
|
||||||
|
pre_min += 60
|
||||||
|
pre_hour -= 1
|
||||||
|
|
||||||
|
alert_time = f"{pre_hour:02d}:{pre_min:02d}"
|
||||||
|
schedule.every().day.at(alert_time).do(run_telegram_alerts).tag(
|
||||||
|
"telegram", "dynamic"
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"📅 [SCHEDULER] Alertes Telegram planifiées à {alert_time} (30min avant la course)"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"⚠️ Impossible de planifier les alertes Telegram: {e}")
|
||||||
|
else:
|
||||||
|
logger.info(
|
||||||
|
"ℹ️ [SCHEDULER] Pas de course aujourd'hui, pas d'alertes Telegram dynamiques"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def schedule_dynamic_results():
|
def schedule_dynamic_results():
|
||||||
"""Planifie le scraping des résultats à H+1 (1h après la course)"""
|
"""Planifie le scraping des résultats à H+1 (1h après la course)"""
|
||||||
race_time = get_todays_race_time()
|
race_time = get_todays_race_time()
|
||||||
@@ -245,6 +304,9 @@ def main():
|
|||||||
# Scoring dynamique (15min avant course)
|
# Scoring dynamique (15min avant course)
|
||||||
schedule_dynamic_scoring()
|
schedule_dynamic_scoring()
|
||||||
|
|
||||||
|
# Alertes Telegram dynamiques (30min avant course)
|
||||||
|
schedule_dynamic_telegram_alerts()
|
||||||
|
|
||||||
# Résultats dynamiques (H+1)
|
# Résultats dynamiques (H+1)
|
||||||
schedule_dynamic_results()
|
schedule_dynamic_results()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user