103 lines
3.0 KiB
Python
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',
|
|
)
|