régler problème avec le bot discord et ajouter un panneau pour gérer les one on one (accepter ,refuser, confirmer)
This commit is contained in:
+184
-56
@@ -10,6 +10,7 @@ import os
|
||||
import logging
|
||||
import asyncio
|
||||
import threading
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
from queue import Queue, Empty
|
||||
@@ -37,15 +38,17 @@ class TeamTryoutsBot(commands.Bot):
|
||||
Handles One on One requests, schedule additions, and daily reminders.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, flask_app=None):
|
||||
intents = Intents.default()
|
||||
intents.message_content = True
|
||||
intents.dm_messages = True
|
||||
intents.dm_reactions = True
|
||||
intents.reactions = True
|
||||
intents.guilds = True
|
||||
intents.members = True
|
||||
|
||||
super().__init__(command_prefix='!', intents=intents)
|
||||
self.flask_app = flask_app
|
||||
self.pending_requests = {} # Maps message_id to {type, id} for reaction handling
|
||||
self.message_queue = Queue() # Thread-safe queue for messages from Flask
|
||||
self.scheduler = AsyncIOScheduler()
|
||||
@@ -57,7 +60,11 @@ class TeamTryoutsBot(commands.Bot):
|
||||
|
||||
async def on_ready(self):
|
||||
"""Log when the bot is ready and start background tasks."""
|
||||
logger.info(f'TeamTryoutsBot is ready! Logged in as {self.user}')
|
||||
try:
|
||||
guilds = [g.name for g in self.guilds]
|
||||
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())
|
||||
@@ -92,10 +99,16 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if item.get('type') == 'one_on_one_request':
|
||||
await self._send_one_on_one_dm(**item['data'])
|
||||
elif item.get('type') == 'schedule_addition':
|
||||
await self._send_schedule_notification(**item['data'])
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self._send_schedule_notification(**item['data'])
|
||||
else:
|
||||
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}")
|
||||
logger.error(f"Error processing queue: {e}\n{traceback.format_exc()}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
def is_dm_channel(self, channel) -> bool:
|
||||
@@ -123,14 +136,30 @@ class TeamTryoutsBot(commands.Bot):
|
||||
|
||||
if emoji_str == CHECK_EMOJI:
|
||||
if handler_type == 'one_on_one':
|
||||
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
|
||||
else:
|
||||
await self.handle_one_on_one_approve(user, message_id, request_id, reaction.message)
|
||||
elif handler_type == 'schedule_addition':
|
||||
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
|
||||
else:
|
||||
await self.handle_attendance_confirm(user, message_id, request_id, reaction.message)
|
||||
elif emoji_str == CROSS_EMOJI:
|
||||
if handler_type == 'one_on_one':
|
||||
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
|
||||
else:
|
||||
await self.handle_one_on_one_reject(user, message_id, request_id, reaction.message)
|
||||
elif handler_type == 'schedule_addition':
|
||||
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
|
||||
else:
|
||||
await self.handle_attendance_decline(user, message_id, request_id, reaction.message)
|
||||
|
||||
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,
|
||||
@@ -188,7 +217,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
"""
|
||||
try:
|
||||
# Look up the DB user to get their Discord user ID
|
||||
from app.models.models import User as DBUser
|
||||
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")
|
||||
@@ -234,13 +263,10 @@ class TeamTryoutsBot(commands.Bot):
|
||||
async def handle_one_on_one_approve(self, coach, message_id, request_id, original_message):
|
||||
"""Handle coach approving a One on One request."""
|
||||
try:
|
||||
from app.models.models import OneOnOneRequest, db
|
||||
from sqlalchemy.orm import joinedload
|
||||
from app.models import OneOnOneRequest
|
||||
from app.extensions import db
|
||||
|
||||
request = OneOnOneRequest.query.options(
|
||||
joinedload(OneOnOneRequest.player),
|
||||
joinedload(OneOnOneRequest.coach)
|
||||
).get(request_id)
|
||||
request = OneOnOneRequest.query.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
@@ -248,11 +274,13 @@ class TeamTryoutsBot(commands.Bot):
|
||||
await original_message.channel.send("⚠️ You are not the intended recipient.")
|
||||
return
|
||||
|
||||
# Capture data before commit (to avoid expired session issues)
|
||||
player = request.player
|
||||
# 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
|
||||
player_full_name = player.full_name
|
||||
player_discord_id = player.discord_user_id
|
||||
|
||||
request.status = 'approved'
|
||||
request.responded_at = datetime.utcnow()
|
||||
@@ -262,29 +290,27 @@ class TeamTryoutsBot(commands.Bot):
|
||||
f"✅ You have **approved** the One on One session with {player_full_name}."
|
||||
)
|
||||
|
||||
# Pass the pre-fetched data to avoid session expiration issues
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
player_discord_id=player_discord_id,
|
||||
player_full_name=player_full_name,
|
||||
coach_full_name=coach_obj.full_name,
|
||||
request=request,
|
||||
approved=True
|
||||
)
|
||||
# Notify player via Discord
|
||||
if player_discord_id:
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
player_discord_id=player_discord_id,
|
||||
player_full_name=player_full_name,
|
||||
coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
|
||||
request=request,
|
||||
approved=True
|
||||
)
|
||||
del self.pending_requests[message_id]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling approval: {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, original_message):
|
||||
"""Handle coach rejecting a One on One request."""
|
||||
try:
|
||||
from app.models.models import OneOnOneRequest, db
|
||||
from sqlalchemy.orm import joinedload
|
||||
from app.models import OneOnOneRequest
|
||||
from app.extensions import db
|
||||
|
||||
request = OneOnOneRequest.query.options(
|
||||
joinedload(OneOnOneRequest.player),
|
||||
joinedload(OneOnOneRequest.coach)
|
||||
).get(request_id)
|
||||
request = OneOnOneRequest.query.get(request_id)
|
||||
if not request:
|
||||
return
|
||||
|
||||
@@ -292,11 +318,12 @@ class TeamTryoutsBot(commands.Bot):
|
||||
await original_message.channel.send("⚠️ You are not the intended recipient.")
|
||||
return
|
||||
|
||||
# Capture data before commit (to avoid expired session issues)
|
||||
player = request.player
|
||||
# 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
|
||||
player_full_name = player.full_name
|
||||
player_discord_id = player.discord_user_id
|
||||
|
||||
refusal_note = None
|
||||
try:
|
||||
@@ -320,23 +347,25 @@ class TeamTryoutsBot(commands.Bot):
|
||||
rejection_msg += "\n\nℹ️ The player has been notified that you are not available."
|
||||
|
||||
await original_message.channel.send(rejection_msg)
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
player_discord_id=player_discord_id,
|
||||
player_full_name=player_full_name,
|
||||
coach_full_name=coach_obj.full_name,
|
||||
request=request,
|
||||
approved=False,
|
||||
refusal_note=refusal_note
|
||||
)
|
||||
if player_discord_id:
|
||||
await self.notify_player_about_one_on_one_direct(
|
||||
player_discord_id=player_discord_id,
|
||||
player_full_name=player_full_name,
|
||||
coach_full_name=coach_obj.full_name if coach_obj else 'Coach',
|
||||
request=request,
|
||||
approved=False,
|
||||
refusal_note=refusal_note
|
||||
)
|
||||
del self.pending_requests[message_id]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling rejection: {e}")
|
||||
logger.error(f"Error handling rejection: {e}\n{traceback.format_exc()}")
|
||||
|
||||
async def handle_attendance_confirm(self, player, message_id, reference_id, original_message):
|
||||
"""Handle player confirming attendance for a match/tryout."""
|
||||
try:
|
||||
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
||||
from app.extensions import db
|
||||
|
||||
request_info = self.pending_requests[message_id]
|
||||
event_type = request_info.get('event_type')
|
||||
@@ -344,10 +373,12 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if event_type == 'match':
|
||||
participant = MatchParticipant.query.get(reference_id)
|
||||
if participant:
|
||||
participant = db.session.merge(participant)
|
||||
participant.attendance_confirmed = True
|
||||
elif event_type == 'tryout':
|
||||
registration = TryoutRegistration.query.get(reference_id)
|
||||
if registration:
|
||||
registration = db.session.merge(registration)
|
||||
registration.attendance_confirmed = True
|
||||
|
||||
db.session.commit()
|
||||
@@ -356,12 +387,13 @@ class TeamTryoutsBot(commands.Bot):
|
||||
del self.pending_requests[message_id]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling attendance confirmation: {e}")
|
||||
logger.error(f"Error handling attendance confirmation: {e}\n{traceback.format_exc()}")
|
||||
|
||||
async def handle_attendance_decline(self, player, message_id, reference_id, original_message):
|
||||
"""Handle player declining attendance for a match/tryout."""
|
||||
try:
|
||||
from app.models.models import MatchParticipant, TryoutRegistration, Match, Tryout, db
|
||||
from app.models import MatchParticipant, TryoutRegistration, Match, Tryout
|
||||
from app.extensions import db
|
||||
|
||||
request_info = self.pending_requests[message_id]
|
||||
event_type = request_info.get('event_type')
|
||||
@@ -369,10 +401,12 @@ class TeamTryoutsBot(commands.Bot):
|
||||
if event_type == 'match':
|
||||
participant = MatchParticipant.query.get(reference_id)
|
||||
if participant:
|
||||
participant = db.session.merge(participant)
|
||||
db.session.delete(participant)
|
||||
elif event_type == 'tryout':
|
||||
registration = TryoutRegistration.query.get(reference_id)
|
||||
if registration:
|
||||
registration = db.session.merge(registration)
|
||||
registration.status = 'no_show'
|
||||
|
||||
db.session.commit()
|
||||
@@ -381,7 +415,7 @@ class TeamTryoutsBot(commands.Bot):
|
||||
del self.pending_requests[message_id]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error handling attendance decline: {e}")
|
||||
logger.error(f"Error handling attendance decline: {e}\n{traceback.format_exc()}")
|
||||
|
||||
async def notify_player_about_one_on_one(self, request, approved=True, refusal_note=None):
|
||||
"""Send confirmation to player about One on One response.
|
||||
@@ -471,7 +505,19 @@ class TeamTryoutsBot(commands.Bot):
|
||||
async def send_daily_reminders(self):
|
||||
"""Send daily reminders at 18:00 EDT for events in 24-48 hours."""
|
||||
try:
|
||||
from app.models.models import Match, Tryout, MatchParticipant, TryoutRegistration, OneOnOneRequest, db
|
||||
if self.flask_app:
|
||||
with self.flask_app.app_context():
|
||||
await self._send_daily_reminders_impl()
|
||||
else:
|
||||
await self._send_daily_reminders_impl()
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending daily reminders: {e}\n{traceback.format_exc()}")
|
||||
|
||||
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.extensions import db
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
now = datetime.now(self.timezone)
|
||||
@@ -540,6 +586,56 @@ class TeamTryoutsBot(commands.Bot):
|
||||
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:
|
||||
"""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"
|
||||
f"Your coach **{coach_full_name}** has approved your request:\n"
|
||||
f"**Date:** {date_str}\n"
|
||||
f"**Time:** {start_time} - {end_time}\n"
|
||||
f"**Discussion Points:** {points or 'No specific points provided'}\n\n"
|
||||
"Please prepare for your session!"
|
||||
)
|
||||
else:
|
||||
if refusal_note:
|
||||
message = (
|
||||
"😞 **One on One Session Rejected**\n\n"
|
||||
f"Your coach **{coach_full_name}** has declined:\n"
|
||||
f"**Reason:** {refusal_note}\n\n"
|
||||
"Please try selecting a different time slot."
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
"😞 **One on One Session Unavailable**\n\n"
|
||||
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})")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending One on One response DM: {e}")
|
||||
return False
|
||||
|
||||
async def send_one_on_one_reminder(self, player, session):
|
||||
"""Send One on One reminder to player."""
|
||||
try:
|
||||
@@ -562,11 +658,13 @@ bot_instance = None
|
||||
bot_thread = None
|
||||
|
||||
|
||||
def get_bot():
|
||||
def get_bot(flask_app=None):
|
||||
"""Get or create the bot instance."""
|
||||
global bot_instance
|
||||
if bot_instance is None:
|
||||
bot_instance = TeamTryoutsBot()
|
||||
bot_instance = TeamTryoutsBot(flask_app=flask_app)
|
||||
elif flask_app is not None and bot_instance.flask_app is None:
|
||||
bot_instance.flask_app = flask_app
|
||||
return bot_instance
|
||||
|
||||
|
||||
@@ -618,11 +716,41 @@ def send_schedule_notification(user_id: int, event_type: str, event_title: str,
|
||||
return False
|
||||
|
||||
|
||||
def start_bot():
|
||||
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,
|
||||
}
|
||||
})
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error queuing One on One response DM: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def start_bot(flask_app=None):
|
||||
"""Start the Discord bot in the background."""
|
||||
global bot_thread
|
||||
|
||||
bot = get_bot()
|
||||
bot = get_bot(flask_app=flask_app)
|
||||
if DISCORD_BOT_TOKEN and bot_thread is None:
|
||||
def run_bot():
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user