Files
team-tryouts/app/discord/pending.py
T
cedrick2711 e53bc3b80d
CI - Security, Lint & Tests / validate (push) Failing after 25m59s
first cleanup iter
2026-09-09 08:26:47 -04:00

103 lines
3.0 KiB
Python

"""Persistence of pending reaction-request mappings across bot restarts.
The bot uses reaction messages (✅ / ❌) on DMs to let a coach approve or reject
a one-on-one request without visiting the site. The mapping from Discord
message_id → {request_type, primary_key} is stored in a JSON file so that a
bot restart does not orphan reactions that were already sent.
"""
import json
import logging
import os
from datetime import UTC, datetime
from app.discord.constants import PENDING_MAX_AGE_DAYS
from app.discord.queue import enqueue
_logger = logging.getLogger(__name__)
#: File for persisting pending requests across bot restarts.
PENDING_FILE = os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
'discord_pending.json',
)
def load_pending() -> dict:
"""Load pending requests from the JSON file.
Returns:
dict mapping message_id (int) → entry dict.
"""
if not os.path.exists(PENDING_FILE):
return {}
try:
with open(PENDING_FILE, encoding='utf-8') as f:
entries = json.load(f)
except (json.JSONDecodeError, OSError):
_logger.warning('Could not read pending file, starting empty.', exc_info=True)
return {}
if not isinstance(entries, dict):
return {}
cleaned = drop_expired(entries)
if len(cleaned) < len(entries):
save_pending(cleaned)
return cleaned
def drop_expired(entries: dict) -> dict:
"""Remove entries whose creation timestamp is too old.
Args:
entries: dict mapping message_id → entry.
Returns:
Filtered dict.
"""
now = datetime.now(UTC).timestamp()
cutoff = now - PENDING_MAX_AGE_DAYS * 86400
return {mid: e for mid, e in entries.items() if e.get('created_at', 0) >= cutoff}
def save_pending(entries: dict) -> None:
"""Persist pending entries to the JSON file."""
try:
with open(PENDING_FILE, 'w', encoding='utf-8') as f:
json.dump(entries, f, indent=2)
except OSError:
_logger.exception('Could not write pending file')
def forget_pending(entries: dict, message_id: int) -> dict:
"""Remove one message from the pending map and persist.
Args:
entries: The current pending map.
message_id: The message whose entry to remove.
Returns:
Updated dict (already persisted).
"""
entries.pop(str(message_id), None)
save_pending(entries)
return entries
def purge_pending_for_participants(participant_ids: list[int]) -> None:
"""Queue a purge of stale participant pending entries.
Called after a match edit deletes participants. The bot thread removes
matching ``schedule_addition`` pending entries so that a reaction on an
old message cannot produce a false confirmation.
Args:
participant_ids: MatchParticipant primary keys that no longer exist.
"""
if not participant_ids:
return
enqueue(
'purge_pending',
{'participant_ids': participant_ids},
description=f'purge of {len(participant_ids)} stale participant records',
)