"""Reminders: nag via ntfy when present-but-unpointed or departed-but-not-depointed.""" import asyncio import logging import urllib.request from datetime import datetime from models import list_users, load_notif_config, load_presence, load_week_pointages logger = logging.getLogger("pointeuse.notifications") CHECK_INTERVAL_SECONDS = 300 def send_ntfy(server: str, topic: str, message: str, title: str): req = urllib.request.Request( f"{server}/{topic}", data=message.encode("utf-8"), headers={"Title": title.encode("utf-8"), "Priority": "high"}, method="POST", ) try: urllib.request.urlopen(req, timeout=10).close() logger.info("notif ntfy envoyée: server=%r topic=%r title=%r", server, topic, title) except Exception: logger.exception("échec de l'envoi ntfy: server=%r topic=%r title=%r", server, topic, title) # best-effort: a missed reminder isn't worth crashing the loop def _todays_pointage(user_id: str, today: datetime) -> dict: iso = today.isocalendar() pointages = load_week_pointages(iso.year, iso.week, user_id) date_str = today.strftime("%Y-%m-%d") return next((p for p in pointages if p["date"] == date_str), {}) async def check_user(user_id: str, now: datetime): notif = load_notif_config(user_id) topic = notif.get("ntfy_topic") server = notif.get("ntfy_server", "https://ntfy.sh") debut_h = notif.get("rappel_debut_h", 7) fin_h = notif.get("rappel_fin_h", 20) if not topic: logger.debug("check_user(%r): pas de topic ntfy configuré, ignoré", user_id) return if now.weekday() >= 5 or not (debut_h <= now.hour < fin_h): logger.debug("check_user(%r): hors plage de rappel (%s, plage %sh-%sh)", user_id, now, debut_h, fin_h) return presence = load_presence(user_id) jour = _todays_pointage(user_id, now) logger.debug( "check_user(%r): presence=%s matin_entree=%r aprem_sortie=%r", user_id, presence.get("present"), jour.get("matin_entree"), jour.get("aprem_sortie"), ) if presence.get("present") and not jour.get("matin_entree"): logger.info("rappel 'pense à pointer' pour %r", user_id) await asyncio.to_thread(send_ntfy, server, topic, "Tu es dans les locaux mais pas encore pointé ce matin.", "⏰ Pense à pointer") elif not presence.get("present") and jour.get("matin_entree") and not jour.get("aprem_sortie"): logger.info("rappel 'pense à dépointer' pour %r", user_id) await asyncio.to_thread(send_ntfy, server, topic, "Tu as quitté les locaux sans dépointer la sortie.", "⏰ Pense à dépointer") async def reminder_loop(): logger.info("boucle de rappels démarrée (intervalle=%ss)", CHECK_INTERVAL_SECONDS) while True: now = datetime.now() users = list_users() logger.debug("cycle de vérification pour %d utilisateur(s)", len(users)) for user_id in users: await check_user(user_id, now) await asyncio.sleep(CHECK_INTERVAL_SECONDS)