This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
"""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',
|
||||
)
|
||||
Reference in New Issue
Block a user