style: formater le depot avec ruff format
QUA-002, premiere moitie. **Ce commit ne fait que reformater** : aucun changement de comportement, aucune ligne de logique touchee. 72 fichiers, 4 restaient deja conformes. Il est isole exprès, pour que `git log -p` sur les commits voisins reste lisible. `quote-style = "preserve"` etait deja pose dans pyproject.toml, ce qui evite le brassage guillemets simples / doubles : le diff porte sur les retours a la ligne, l indentation des appels longs et les virgules finales, pas sur le style de chaine. Verification : 263 tests passent avant et apres, ruff check propre. L activation en CI arrive dans le commit suivant, separement, pour que ce diff-ci ne contienne rien d autre. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+281
-181
@@ -28,19 +28,21 @@ DISCORD_BOT_TOKEN = os.getenv('DISCORD_BOT_TOKEN')
|
||||
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.abspath(__file__))), 'discord_pending.json')
|
||||
PENDING_FILE = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'discord_pending.json'
|
||||
)
|
||||
|
||||
# Emoji constants
|
||||
CHECK_EMOJI = '✅' # Green checkmark
|
||||
CROSS_EMOJI = '❌' # Red X
|
||||
CROSS_EMOJI = '❌' # Red X
|
||||
|
||||
|
||||
class TeamTryoutsBot(commands.Bot):
|
||||
"""Unified Discord bot for Team Tryouts notifications.
|
||||
|
||||
|
||||
Handles One on One requests, schedule additions, and daily reminders.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self, flask_app=None):
|
||||
# Only what the bot actually reads. `members` — the privileged
|
||||
# GUILD_MEMBERS intent — was requested and never used: nothing here
|
||||
@@ -62,7 +64,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
self.message_queue = Queue() # Thread-safe queue for messages from Flask
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
self.timezone = ZoneInfo('America/Toronto') # EDT timezone
|
||||
|
||||
|
||||
def _load_pending(self):
|
||||
"""Load pending requests from the JSON file."""
|
||||
try:
|
||||
@@ -71,12 +73,14 @@ class TeamTryoutsBot(commands.Bot):
|
||||
data = json.load(f)
|
||||
# Convert string keys back to int
|
||||
self.pending_requests = {int(k): v for k, v in data.items()}
|
||||
logger.info(f"Loaded {len(self.pending_requests)} pending requests from {PENDING_FILE}")
|
||||
logger.info(
|
||||
f"Loaded {len(self.pending_requests)} pending requests from {PENDING_FILE}"
|
||||
)
|
||||
else:
|
||||
logger.info("No pending requests file found, starting fresh.")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading pending requests: {e}")
|
||||
|
||||
|
||||
def _save_pending(self):
|
||||
"""Save pending requests to the JSON file."""
|
||||
try:
|
||||
@@ -84,12 +88,12 @@ class TeamTryoutsBot(commands.Bot):
|
||||
json.dump(self.pending_requests, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving pending requests: {e}")
|
||||
|
||||
|
||||
async def setup_hook(self):
|
||||
"""Called when the bot is ready."""
|
||||
self._load_pending()
|
||||
logger.info(f'TeamTryoutsBot logged in as {self.user}')
|
||||
|
||||
|
||||
async def on_ready(self):
|
||||
"""Log when the bot is ready and start background tasks."""
|
||||
try:
|
||||
@@ -97,13 +101,13 @@ class TeamTryoutsBot(commands.Bot):
|
||||
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user} | Guilds: {guilds}')
|
||||
except Exception:
|
||||
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}')
|
||||
|
||||
|
||||
# Start the queue processing task
|
||||
self.loop.create_task(self.process_queue())
|
||||
|
||||
|
||||
# Start the daily reminder scheduler
|
||||
self.loop.create_task(self.start_scheduler())
|
||||
|
||||
|
||||
async def start_scheduler(self):
|
||||
"""Start the APScheduler for daily reminders."""
|
||||
try:
|
||||
@@ -111,13 +115,13 @@ class TeamTryoutsBot(commands.Bot):
|
||||
self.send_daily_reminders,
|
||||
trigger=CronTrigger(hour=18, minute=0, timezone=self.timezone),
|
||||
id='daily_reminders',
|
||||
replace_existing=True
|
||||
replace_existing=True,
|
||||
)
|
||||
self.scheduler.start()
|
||||
logger.info('Daily reminder scheduler started (18:00 EDT)')
|
||||
except Exception as e:
|
||||
logger.error(f'Error starting scheduler: {e}')
|
||||
|
||||
|
||||
async def process_queue(self):
|
||||
"""Process messages from the queue (runs continuously)."""
|
||||
while True:
|
||||
@@ -127,7 +131,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
except Empty:
|
||||
await asyncio.sleep(0.5)
|
||||
continue
|
||||
|
||||
|
||||
if item.get('type') == 'one_on_one_request':
|
||||
await self._send_one_on_one_dm(**item['data'])
|
||||
elif item.get('type') == 'schedule_addition':
|
||||
@@ -138,91 +142,116 @@ class TeamTryoutsBot(commands.Bot):
|
||||
await self._send_schedule_notification(**item['data'])
|
||||
elif item.get('type') == 'one_on_one_response':
|
||||
await self._send_one_on_one_response_dm(**item['data'])
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing queue: {e}\n{traceback.format_exc()}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
def is_dm_channel(self, channel) -> bool:
|
||||
"""Check if a channel is a DM channel."""
|
||||
return hasattr(channel, 'recipient') or hasattr(channel, 'recipients')
|
||||
|
||||
|
||||
async def on_raw_reaction_add(self, payload):
|
||||
"""Handle when a reaction is added to a message (works even after bot restart)."""
|
||||
# Ignore bot's own reactions
|
||||
if payload.user_id == self.user.id:
|
||||
return
|
||||
|
||||
|
||||
# Check if this is a pending request we're tracking
|
||||
if payload.message_id not in self.pending_requests:
|
||||
return
|
||||
|
||||
|
||||
# Fetch the channel and check if it's a DM
|
||||
try:
|
||||
channel = await self.fetch_channel(payload.channel_id)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
if not self.is_dm_channel(channel):
|
||||
return
|
||||
|
||||
|
||||
# Fetch the user who reacted
|
||||
try:
|
||||
user = await self.fetch_user(payload.user_id)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
if user is None:
|
||||
return
|
||||
|
||||
|
||||
request_info = self.pending_requests[payload.message_id]
|
||||
emoji_str = str(payload.emoji)
|
||||
|
||||
|
||||
handler_type = request_info.get('type')
|
||||
request_id = request_info.get('id')
|
||||
|
||||
|
||||
if emoji_str == CHECK_EMOJI:
|
||||
if handler_type == 'one_on_one':
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_one_on_one_approve(user, payload.message_id, request_id, channel)
|
||||
await self.handle_one_on_one_approve(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
else:
|
||||
await self.handle_one_on_one_approve(user, payload.message_id, request_id, channel)
|
||||
await self.handle_one_on_one_approve(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
elif handler_type == 'schedule_addition':
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_attendance_confirm(user, payload.message_id, request_id, channel)
|
||||
await self.handle_attendance_confirm(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
else:
|
||||
await self.handle_attendance_confirm(user, payload.message_id, request_id, channel)
|
||||
await self.handle_attendance_confirm(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
elif emoji_str == CROSS_EMOJI:
|
||||
if handler_type == 'one_on_one':
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_one_on_one_reject(user, payload.message_id, request_id, channel)
|
||||
await self.handle_one_on_one_reject(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
else:
|
||||
await self.handle_one_on_one_reject(user, payload.message_id, request_id, channel)
|
||||
await self.handle_one_on_one_reject(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
elif handler_type == 'schedule_addition':
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_attendance_decline(user, payload.message_id, request_id, channel)
|
||||
await self.handle_attendance_decline(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
else:
|
||||
await self.handle_attendance_decline(user, payload.message_id, request_id, channel)
|
||||
|
||||
async def _send_one_on_one_dm(self, coach_name: str, coach_discord_id: str, player_name: str,
|
||||
team_name: str, date_str: str, start_time: str, end_time: str,
|
||||
points: str, request_id: int) -> int:
|
||||
await self.handle_attendance_decline(
|
||||
user, payload.message_id, request_id, channel
|
||||
)
|
||||
|
||||
async def _send_one_on_one_dm(
|
||||
self,
|
||||
coach_name: str,
|
||||
coach_discord_id: str,
|
||||
player_name: str,
|
||||
team_name: str,
|
||||
date_str: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
points: str,
|
||||
request_id: int,
|
||||
) -> int:
|
||||
"""Send a One on One request DM to a coach with reactions."""
|
||||
try:
|
||||
user_id = int(coach_discord_id)
|
||||
except (ValueError, TypeError):
|
||||
logger.warning(f"Invalid coach_discord_id '{coach_discord_id}'")
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
user = await self.fetch_user(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
|
||||
message = (
|
||||
"📅 **One on One Request**\n\n"
|
||||
f"**Player:** {player_name}\n"
|
||||
@@ -234,27 +263,33 @@ class TeamTryoutsBot(commands.Bot):
|
||||
f"{CHECK_EMOJI} - Confirm the meeting\n"
|
||||
f"{CROSS_EMOJI} - Decline (you can add a reason by replying before clicking)"
|
||||
)
|
||||
|
||||
|
||||
msg = await user.send(message)
|
||||
await msg.add_reaction(CHECK_EMOJI)
|
||||
await msg.add_reaction(CROSS_EMOJI)
|
||||
|
||||
|
||||
# Track this pending request
|
||||
self.pending_requests[msg.id] = {'type': 'one_on_one', 'id': request_id}
|
||||
self._save_pending()
|
||||
|
||||
|
||||
logger.info(f"Sent One on One DM with reactions, message_id={msg.id}")
|
||||
return msg.id
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending One on One DM: {e}")
|
||||
return None
|
||||
|
||||
async def _send_schedule_notification(self, user_id: int, event_type: str,
|
||||
event_title: str, event_date: str,
|
||||
event_time: str, reference_id: int) -> int:
|
||||
|
||||
async def _send_schedule_notification(
|
||||
self,
|
||||
user_id: int,
|
||||
event_type: str,
|
||||
event_title: str,
|
||||
event_date: str,
|
||||
event_time: str,
|
||||
reference_id: int,
|
||||
) -> int:
|
||||
"""Send a schedule addition notification to a player.
|
||||
|
||||
|
||||
Args:
|
||||
user_id: Database primary key of the User (NOT Discord user ID).
|
||||
event_type: 'match' or 'tryout'.
|
||||
@@ -266,23 +301,24 @@ class TeamTryoutsBot(commands.Bot):
|
||||
try:
|
||||
# Look up the DB user to get their Discord user ID
|
||||
from app.models import User as DBUser
|
||||
|
||||
db_user = DBUser.query.get(user_id)
|
||||
if not db_user:
|
||||
logger.warning(f"DB user {user_id} not found for schedule notification")
|
||||
return None
|
||||
|
||||
|
||||
if not db_user.discord_user_id:
|
||||
logger.warning(f"User {db_user.username} has no Discord user ID, cannot send DM")
|
||||
return None
|
||||
|
||||
|
||||
discord_uid = int(db_user.discord_user_id)
|
||||
user = await self.fetch_user(discord_uid)
|
||||
if not user:
|
||||
logger.warning(f"Could not fetch Discord user {discord_uid}")
|
||||
return None
|
||||
|
||||
|
||||
event_name = "Match" if event_type == 'match' else "Tryout"
|
||||
|
||||
|
||||
message = (
|
||||
f"📅 **{event_name} Scheduled**\n\n"
|
||||
f"You have been added to the following {event_type}:\n"
|
||||
@@ -293,52 +329,58 @@ class TeamTryoutsBot(commands.Bot):
|
||||
f"{CHECK_EMOJI} - Confirm attendance\n"
|
||||
f"{CROSS_EMOJI} - Decline"
|
||||
)
|
||||
|
||||
|
||||
msg = await user.send(message)
|
||||
await msg.add_reaction(CHECK_EMOJI)
|
||||
await msg.add_reaction(CROSS_EMOJI)
|
||||
|
||||
|
||||
# Track this pending request
|
||||
self.pending_requests[msg.id] = {'type': 'schedule_addition', 'id': reference_id, 'event_type': event_type}
|
||||
self.pending_requests[msg.id] = {
|
||||
'type': 'schedule_addition',
|
||||
'id': reference_id,
|
||||
'event_type': event_type,
|
||||
}
|
||||
self._save_pending()
|
||||
|
||||
logger.info(f"Sent {event_type} schedule notification to {db_user.username}, message_id={msg.id}")
|
||||
|
||||
logger.info(
|
||||
f"Sent {event_type} schedule notification to {db_user.username}, message_id={msg.id}"
|
||||
)
|
||||
return msg.id
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending schedule notification: {e}")
|
||||
return None
|
||||
|
||||
|
||||
async def handle_one_on_one_approve(self, coach, message_id, request_id, channel):
|
||||
"""Handle coach approving a One on One request."""
|
||||
try:
|
||||
from app.models import OneOnOneRequest
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
request = OneOnOneRequest.query.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
|
||||
if request.coach.discord_user_id != str(coach.id):
|
||||
await channel.send("⚠️ You are not the intended recipient.")
|
||||
return
|
||||
|
||||
|
||||
# Re-attach to current session (object may be detached across app contexts)
|
||||
request = db.session.merge(request)
|
||||
|
||||
|
||||
# Capture data before commit
|
||||
player_full_name = request.player.full_name if request.player else 'Unknown'
|
||||
player_discord_id = request.player.discord_user_id if request.player else None
|
||||
coach_obj = request.coach
|
||||
|
||||
|
||||
request.status = 'approved'
|
||||
request.responded_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
|
||||
await channel.send(
|
||||
f"✅ You have **approved** the One on One session with {player_full_name}."
|
||||
)
|
||||
|
||||
|
||||
# Notify player via Discord
|
||||
if player_discord_id:
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
@@ -346,56 +388,62 @@ class TeamTryoutsBot(commands.Bot):
|
||||
player_full_name=player_full_name,
|
||||
coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
|
||||
request=request,
|
||||
approved=True
|
||||
approved=True,
|
||||
)
|
||||
del self.pending_requests[message_id]
|
||||
self._save_pending()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling approval: {e}\n{traceback.format_exc()}")
|
||||
|
||||
|
||||
async def handle_one_on_one_reject(self, coach, message_id, request_id, channel):
|
||||
"""Handle coach rejecting a One on One request."""
|
||||
try:
|
||||
from app.models import OneOnOneRequest
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
request = OneOnOneRequest.query.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
|
||||
if request.coach.discord_user_id != str(coach.id):
|
||||
await channel.send("⚠️ You are not the intended recipient.")
|
||||
return
|
||||
|
||||
|
||||
# Re-attach to current session (object may be detached across app contexts)
|
||||
request = db.session.merge(request)
|
||||
|
||||
|
||||
player_full_name = request.player.full_name if request.player else 'Unknown'
|
||||
player_discord_id = request.player.discord_user_id if request.player else None
|
||||
coach_obj = request.coach
|
||||
|
||||
|
||||
refusal_note = None
|
||||
try:
|
||||
async for reply in channel.history(limit=20):
|
||||
if reply.author.id == coach.id and reply.reference and reply.reference.message_id == message_id:
|
||||
if (
|
||||
reply.author.id == coach.id
|
||||
and reply.reference
|
||||
and reply.reference.message_id == message_id
|
||||
):
|
||||
refusal_note = reply.content
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not check for reply message: {e}")
|
||||
|
||||
|
||||
request.status = 'rejected'
|
||||
request.responded_at = datetime.utcnow()
|
||||
if refusal_note:
|
||||
request.coach_rejection_message = refusal_note
|
||||
db.session.commit()
|
||||
|
||||
rejection_msg = f"❌ You have **rejected** the One on One session with {player_full_name}."
|
||||
|
||||
rejection_msg = (
|
||||
f"❌ You have **rejected** the One on One session with {player_full_name}."
|
||||
)
|
||||
if refusal_note:
|
||||
rejection_msg += f"\n**Reason:** {refusal_note}"
|
||||
else:
|
||||
rejection_msg += "\n\nℹ️ The player has been notified that you are not available."
|
||||
|
||||
|
||||
await channel.send(rejection_msg)
|
||||
if player_discord_id:
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
@@ -404,23 +452,23 @@ class TeamTryoutsBot(commands.Bot):
|
||||
coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
|
||||
request=request,
|
||||
approved=False,
|
||||
refusal_note=refusal_note
|
||||
refusal_note=refusal_note,
|
||||
)
|
||||
del self.pending_requests[message_id]
|
||||
self._save_pending()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}")
|
||||
|
||||
|
||||
async def handle_attendance_confirm(self, player, message_id, reference_id, channel):
|
||||
"""Handle player confirming attendance for a match/tryout."""
|
||||
try:
|
||||
from app.models import MatchParticipant, TryoutRegistration
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
request_info = self.pending_requests[message_id]
|
||||
event_type = request_info.get('event_type')
|
||||
|
||||
|
||||
if event_type == 'match':
|
||||
participant = MatchParticipant.query.get(reference_id)
|
||||
if participant:
|
||||
@@ -431,25 +479,25 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if registration:
|
||||
registration = db.session.merge(registration)
|
||||
registration.attendance_confirmed = True
|
||||
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
await channel.send("✅ Your attendance has been confirmed!")
|
||||
del self.pending_requests[message_id]
|
||||
self._save_pending()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}")
|
||||
|
||||
|
||||
async def handle_attendance_decline(self, player, message_id, reference_id, channel):
|
||||
"""Handle player declining attendance for a match/tryout."""
|
||||
try:
|
||||
from app.models import MatchParticipant, TryoutRegistration
|
||||
from app.extensions import db
|
||||
|
||||
|
||||
request_info = self.pending_requests[message_id]
|
||||
event_type = request_info.get('event_type')
|
||||
|
||||
|
||||
if event_type == 'match':
|
||||
participant = MatchParticipant.query.get(reference_id)
|
||||
if participant:
|
||||
@@ -460,24 +508,30 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if registration:
|
||||
registration = db.session.merge(registration)
|
||||
registration.status = 'no_show'
|
||||
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
||||
await channel.send("❌ Your attendance has been declined.")
|
||||
del self.pending_requests[message_id]
|
||||
self._save_pending()
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling attendance decline: {e}\n{traceback.format_exc()}")
|
||||
|
||||
async def notify_player_about_one_on_one_direct(self, player_discord_id, player_full_name,
|
||||
coach_full_name, request,
|
||||
approved=True, refusal_note=None):
|
||||
|
||||
async def notify_player_about_one_on_one_direct(
|
||||
self,
|
||||
player_discord_id,
|
||||
player_full_name,
|
||||
coach_full_name,
|
||||
request,
|
||||
approved=True,
|
||||
refusal_note=None,
|
||||
):
|
||||
"""Send confirmation to player about One on One response using pre-fetched data.
|
||||
|
||||
|
||||
This method avoids session expiration issues by using data captured before
|
||||
the database commit.
|
||||
|
||||
|
||||
Args:
|
||||
player_discord_id: The player's Discord user ID string.
|
||||
player_full_name: The player's full name.
|
||||
@@ -490,12 +544,12 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if not player_discord_id:
|
||||
logger.warning(f"Player has no Discord user ID for request {request.id}")
|
||||
return
|
||||
|
||||
|
||||
player_user = await self.fetch_user(int(player_discord_id))
|
||||
if not player_user:
|
||||
logger.warning(f"Could not fetch Discord user {player_discord_id}")
|
||||
return
|
||||
|
||||
|
||||
if approved:
|
||||
message = (
|
||||
"🎉 **One on One Session Confirmed!**\n\n"
|
||||
@@ -519,13 +573,15 @@ class TeamTryoutsBot(commands.Bot):
|
||||
f"Your coach **{coach_full_name}** is not available.\n\n"
|
||||
"Please try selecting a different time slot."
|
||||
)
|
||||
|
||||
|
||||
await player_user.send(message)
|
||||
logger.info(f"Sent One on One notification to player {player_full_name} (request {request.id})")
|
||||
|
||||
logger.info(
|
||||
f"Sent One on One notification to player {player_full_name} (request {request.id})"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in direct One on One notification: {e}")
|
||||
|
||||
|
||||
async def send_daily_reminders(self):
|
||||
"""Send daily reminders at 18:00 EDT for events in 24-48 hours."""
|
||||
try:
|
||||
@@ -540,12 +596,18 @@ class TeamTryoutsBot(commands.Bot):
|
||||
async def _send_daily_reminders_impl(self):
|
||||
"""Internal implementation of daily reminders with proper app context."""
|
||||
try:
|
||||
from app.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest
|
||||
from app.models import (
|
||||
Match,
|
||||
Tryout,
|
||||
MatchParticipant,
|
||||
TryoutRegistration,
|
||||
OneOnOneRequest,
|
||||
)
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
|
||||
now = datetime.now(self.timezone)
|
||||
tomorrow = now.date() + timedelta(days=1)
|
||||
|
||||
|
||||
# Find matches for tomorrow
|
||||
matches = Match.query.filter(Match.date == tomorrow).all()
|
||||
for match in matches:
|
||||
@@ -553,7 +615,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
for participant in participants:
|
||||
if participant.player.discord_user_id:
|
||||
await self.send_match_reminder(participant.player, match)
|
||||
|
||||
|
||||
# Find tryouts for tomorrow
|
||||
tryouts = Tryout.query.filter(Tryout.date == tomorrow).all()
|
||||
for tryout in tryouts:
|
||||
@@ -561,22 +623,22 @@ class TeamTryoutsBot(commands.Bot):
|
||||
for reg in registrations:
|
||||
if reg.player.discord_user_id:
|
||||
await self.send_tryout_reminder(reg.player, tryout)
|
||||
|
||||
|
||||
# Find One on One sessions for tomorrow (only approved ones)
|
||||
one_on_ones = OneOnOneRequest.query.options(
|
||||
joinedload(OneOnOneRequest.player),
|
||||
joinedload(OneOnOneRequest.coach)
|
||||
).filter(
|
||||
OneOnOneRequest.date == tomorrow,
|
||||
OneOnOneRequest.status == 'approved'
|
||||
).all()
|
||||
one_on_ones = (
|
||||
OneOnOneRequest.query.options(
|
||||
joinedload(OneOnOneRequest.player), joinedload(OneOnOneRequest.coach)
|
||||
)
|
||||
.filter(OneOnOneRequest.date == tomorrow, OneOnOneRequest.status == 'approved')
|
||||
.all()
|
||||
)
|
||||
for session in one_on_ones:
|
||||
if session.player and session.player.discord_user_id:
|
||||
await self.send_one_on_one_reminder(session.player, session)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending daily reminders: {e}")
|
||||
|
||||
|
||||
async def send_match_reminder(self, player, match):
|
||||
"""Send match reminder to player."""
|
||||
try:
|
||||
@@ -593,7 +655,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
await player_user.send(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending match reminder: {e}")
|
||||
|
||||
|
||||
async def send_tryout_reminder(self, player, tryout):
|
||||
"""Send tryout reminder to player."""
|
||||
try:
|
||||
@@ -608,25 +670,33 @@ class TeamTryoutsBot(commands.Bot):
|
||||
await player_user.send(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending tryout reminder: {e}")
|
||||
|
||||
async def _send_one_on_one_response_dm(self, player_discord_id: str, player_full_name: str,
|
||||
coach_full_name: str, date_str: str, start_time: str,
|
||||
end_time: str, points: str, approved: bool,
|
||||
refusal_note: str = None) -> bool:
|
||||
|
||||
async def _send_one_on_one_response_dm(
|
||||
self,
|
||||
player_discord_id: str,
|
||||
player_full_name: str,
|
||||
coach_full_name: str,
|
||||
date_str: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
points: str,
|
||||
approved: bool,
|
||||
refusal_note: str = None,
|
||||
) -> bool:
|
||||
"""Send a DM to a player notifying them of their One on One request response.
|
||||
|
||||
|
||||
Called from the message queue when a coach accepts/rejects via the web app.
|
||||
"""
|
||||
try:
|
||||
if not player_discord_id:
|
||||
logger.warning("Cannot send response DM: no player_discord_id")
|
||||
return False
|
||||
|
||||
|
||||
player_user = await self.fetch_user(int(player_discord_id))
|
||||
if not player_user:
|
||||
logger.warning(f"Could not fetch Discord user {player_discord_id}")
|
||||
return False
|
||||
|
||||
|
||||
if approved:
|
||||
message = (
|
||||
"🎉 **One on One Session Confirmed!**\n\n"
|
||||
@@ -650,11 +720,13 @@ class TeamTryoutsBot(commands.Bot):
|
||||
f"Your coach **{coach_full_name}** is not available.\n\n"
|
||||
"Please try selecting a different time slot."
|
||||
)
|
||||
|
||||
|
||||
await player_user.send(message)
|
||||
logger.info(f"Sent One on One response DM to player {player_full_name} (approved={approved})")
|
||||
logger.info(
|
||||
f"Sent One on One response DM to player {player_full_name} (approved={approved})"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending One on One response DM: {e}")
|
||||
return False
|
||||
@@ -691,78 +763,105 @@ def get_bot(flask_app=None):
|
||||
return bot_instance
|
||||
|
||||
|
||||
def send_one_on_one_dm(coach_name: str, coach_discord_id: str, player_name: str,
|
||||
team_name: str, date_str: str, start_time: str, end_time: str,
|
||||
points: str, request_id: int) -> bool:
|
||||
def send_one_on_one_dm(
|
||||
coach_name: str,
|
||||
coach_discord_id: str,
|
||||
player_name: str,
|
||||
team_name: str,
|
||||
date_str: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
points: str,
|
||||
request_id: int,
|
||||
) -> bool:
|
||||
"""Queue a One on One request DM to be sent by the bot."""
|
||||
bot = get_bot()
|
||||
try:
|
||||
bot.message_queue.put({
|
||||
'type': 'one_on_one_request',
|
||||
'data': {
|
||||
'coach_name': coach_name,
|
||||
'coach_discord_id': coach_discord_id,
|
||||
'player_name': player_name,
|
||||
'team_name': team_name,
|
||||
'date_str': date_str,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'points': points,
|
||||
'request_id': request_id
|
||||
bot.message_queue.put(
|
||||
{
|
||||
'type': 'one_on_one_request',
|
||||
'data': {
|
||||
'coach_name': coach_name,
|
||||
'coach_discord_id': coach_discord_id,
|
||||
'player_name': player_name,
|
||||
'team_name': team_name,
|
||||
'date_str': date_str,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'points': points,
|
||||
'request_id': request_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error queuing One on One DM: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_schedule_notification(user_id: int, event_type: str, event_title: str,
|
||||
event_date: str, event_time: str, reference_id: int) -> bool:
|
||||
def send_schedule_notification(
|
||||
user_id: int,
|
||||
event_type: str,
|
||||
event_title: str,
|
||||
event_date: str,
|
||||
event_time: str,
|
||||
reference_id: int,
|
||||
) -> bool:
|
||||
"""Queue a schedule addition notification to be sent by the bot."""
|
||||
bot = get_bot()
|
||||
try:
|
||||
bot.message_queue.put({
|
||||
'type': 'schedule_addition',
|
||||
'data': {
|
||||
'user_id': user_id,
|
||||
'event_type': event_type,
|
||||
'event_title': event_title,
|
||||
'event_date': event_date,
|
||||
'event_time': event_time,
|
||||
'reference_id': reference_id
|
||||
bot.message_queue.put(
|
||||
{
|
||||
'type': 'schedule_addition',
|
||||
'data': {
|
||||
'user_id': user_id,
|
||||
'event_type': event_type,
|
||||
'event_title': event_title,
|
||||
'event_date': event_date,
|
||||
'event_time': event_time,
|
||||
'reference_id': reference_id,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error queuing schedule notification: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_one_on_one_response(player_discord_id: str, player_full_name: str,
|
||||
coach_full_name: str, date_str: str, start_time: str,
|
||||
end_time: str, points: str, approved: bool,
|
||||
refusal_note: str = None) -> bool:
|
||||
def send_one_on_one_response(
|
||||
player_discord_id: str,
|
||||
player_full_name: str,
|
||||
coach_full_name: str,
|
||||
date_str: str,
|
||||
start_time: str,
|
||||
end_time: str,
|
||||
points: str,
|
||||
approved: bool,
|
||||
refusal_note: str = None,
|
||||
) -> bool:
|
||||
"""Queue a One on One response DM to be sent to the player by the bot.
|
||||
|
||||
|
||||
Called from Flask routes when a coach accepts/rejects via the web app.
|
||||
"""
|
||||
bot = get_bot()
|
||||
try:
|
||||
bot.message_queue.put({
|
||||
'type': 'one_on_one_response',
|
||||
'data': {
|
||||
'player_discord_id': player_discord_id,
|
||||
'player_full_name': player_full_name,
|
||||
'coach_full_name': coach_full_name,
|
||||
'date_str': date_str,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'points': points,
|
||||
'approved': approved,
|
||||
'refusal_note': refusal_note,
|
||||
bot.message_queue.put(
|
||||
{
|
||||
'type': 'one_on_one_response',
|
||||
'data': {
|
||||
'player_discord_id': player_discord_id,
|
||||
'player_full_name': player_full_name,
|
||||
'coach_full_name': coach_full_name,
|
||||
'date_str': date_str,
|
||||
'start_time': start_time,
|
||||
'end_time': end_time,
|
||||
'points': points,
|
||||
'approved': approved,
|
||||
'refusal_note': refusal_note,
|
||||
},
|
||||
}
|
||||
})
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error queuing One on One response DM: {e}")
|
||||
@@ -772,17 +871,18 @@ def send_one_on_one_response(player_discord_id: str, player_full_name: str,
|
||||
def start_bot(flask_app=None):
|
||||
"""Start the Discord bot in the background."""
|
||||
global bot_thread
|
||||
|
||||
|
||||
bot = get_bot(flask_app=flask_app)
|
||||
if DISCORD_BOT_TOKEN and bot_thread is None:
|
||||
|
||||
def run_bot():
|
||||
try:
|
||||
bot.run(DISCORD_BOT_TOKEN)
|
||||
except Exception as e:
|
||||
logger.error(f"Bot error: {e}")
|
||||
|
||||
|
||||
bot_thread = threading.Thread(target=run_bot, daemon=True)
|
||||
bot_thread.start()
|
||||
logger.info("TeamTryoutsBot started in background thread")
|
||||
elif not DISCORD_BOT_TOKEN:
|
||||
logger.warning("DISCORD_BOT_TOKEN not set, bot not started")
|
||||
logger.warning("DISCORD_BOT_TOKEN not set, bot not started")
|
||||
|
||||
Reference in New Issue
Block a user