-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbot.py
More file actions
380 lines (303 loc) · 12.7 KB
/
bot.py
File metadata and controls
380 lines (303 loc) · 12.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
import discord
from discord.ext import commands
from discord.ui import View, Button, Modal, TextInput
import os
from dotenv import load_dotenv
import asyncio
import traceback
# ---- CONFIG ----
load_dotenv()
TOKEN = os.getenv("DISCORD_TOKEN")
STUDY_ROLE_NAME = "Studying"
# ---- INTENTS ----
intents = discord.Intents.default()
intents.members = True
intents.voice_states = True # Required for voice channel operations
STUDY_VOICE_CHANNEL = "Etude 🤓"
bot = commands.Bot(command_prefix="!", intents=intents)
# ---- SESSION STORAGE ----
# Structure: {user_id: {'task': asyncio.Task, 'guild_id': int, 'minutes': int, 'locked': bool}}
active_sessions: dict[int, dict] = {}
# ---- EVENTS ----
@bot.event
async def on_ready():
print(f"✅ Connecté comme {bot.user}")
try:
synced = await bot.tree.sync()
print(f"✅ {len(synced)} commande(s) synchronisée(s)")
except Exception as e:
print(f"❌ Échec de la synchronisation: {e}")
# ---- HELPER FUNCTIONS ----
async def get_study_role(guild: discord.Guild) -> discord.Role | None:
"""Get the study role from the guild"""
return discord.utils.get(guild.roles, name=STUDY_ROLE_NAME)
async def add_study_role(member: discord.Member) -> bool:
"""Add the study role to a member"""
role = await get_study_role(member.guild)
if not role:
print(f"❌ Rôle '{STUDY_ROLE_NAME}' introuvable")
return False
try:
await member.add_roles(role)
print(f"✅ Rôle ajouté à {member.name}")
return True
except discord.Forbidden:
print(f"❌ Permission refusée pour ajouter le rôle à {member.name}")
return False
except Exception as e:
print(f"❌ Erreur lors de l'ajout du rôle: {e}")
return False
async def remove_study_role(member: discord.Member) -> bool:
"""Remove the study role from a member"""
role = await get_study_role(member.guild)
if not role or role not in member.roles:
return True
try:
await member.remove_roles(role)
print(f"✅ Rôle retiré de {member.name}")
return True
except discord.Forbidden:
print(f"❌ Permission refusée pour retirer le rôle de {member.name}")
return False
except Exception as e:
print(f"❌ Erreur lors du retrait du rôle: {e}")
return False
async def get_study_voice_channel(guild: discord.Guild) -> discord.VoiceChannel | None:
"""Get the study voice channel from the guild"""
return discord.utils.get(guild.voice_channels, name=STUDY_VOICE_CHANNEL)
async def move_member_to_study_channel(member: discord.Member) -> bool:
"""Move a member to the study voice channel if they're in another VC"""
study_vc = await get_study_voice_channel(member.guild)
if not study_vc:
print(f"❌ Salon vocal '{STUDY_VOICE_CHANNEL}' introuvable")
return False
# Check if user is in a voice channel but not the study one
if member.voice and member.voice.channel and member.voice.channel != study_vc:
try:
await member.move_to(study_vc)
print(f"✅ {member.name} déplacé vers {STUDY_VOICE_CHANNEL}")
return True
except discord.Forbidden:
print(f"❌ Permission refusée pour déplacer {member.name}")
return False
except Exception as e:
print(f"❌ Erreur lors du déplacement: {e}")
return False
return True
# ---- MODALS ----
class DurationModal(Modal, title="Durée de la session"):
"""Modal for entering custom study duration"""
minutes_input = TextInput(
label="Durée (en minutes)",
placeholder="Ex: 25, 45, 90...",
required=True,
min_length=1,
max_length=3
)
def __init__(self, lock_session: bool = False):
super().__init__()
self.lock_session = lock_session
async def on_submit(self, interaction: discord.Interaction):
try:
# Parse and validate minutes
minutes = int(self.minutes_input.value)
if minutes < 1:
await interaction.response.send_message(
"❌ La durée doit être au moins 1 minute.",
ephemeral=True
)
return
if minutes > 300: # Max 5 hours
await interaction.response.send_message(
"❌ La durée maximale est de 300 minutes (5 heures).",
ephemeral=True
)
return
# Start the session
await self.start_session(interaction, minutes)
except ValueError:
await interaction.response.send_message(
"❌ Veuillez entrer un nombre valide.",
ephemeral=True
)
async def start_session(self, interaction: discord.Interaction, minutes: int):
"""Start the study session"""
user_id = interaction.user.id
guild = interaction.guild
member = interaction.user
print(f"📚 Durée sélectionnée: {minutes} min par {member.name} (verrouillée={self.lock_session})")
# Check for existing session
if user_id in active_sessions:
await interaction.response.send_message(
"⚠️ Tu as déjà une session en cours. Utilise `/stopstudy` pour l'arrêter.",
ephemeral=True
)
return
# Respond immediately
lock_msg = "🔒 **Session verrouillée** - tu ne pourras pas l'arrêter avant la fin !" if self.lock_session else ""
await interaction.response.send_message(
f"📚 Session de **{minutes} minutes** lancée ! Bon courage 💪\n{lock_msg}",
ephemeral=True
)
# Add study role
await add_study_role(member)
# Move to study channel if in another VC
await move_member_to_study_channel(member)
# Create study session task
task = asyncio.create_task(
run_study_session(guild.id, user_id, minutes)
)
# Store session
active_sessions[user_id] = {
'task': task,
'guild_id': guild.id,
'minutes': minutes,
'locked': self.lock_session
}
print(f"✅ Session enregistrée pour {member.name}")
print(f" Sessions actives: {list(active_sessions.keys())}")
# ---- VIEWS ----
class LockWarningView(View):
"""View for confirming session lock"""
def __init__(self):
super().__init__(timeout=60)
@discord.ui.button(label="✅ Oui, verrouiller", style=discord.ButtonStyle.danger, custom_id="lock_yes")
async def btn_yes(self, interaction: discord.Interaction, button: Button):
await interaction.response.send_modal(DurationModal(lock_session=True))
@discord.ui.button(label="❌ Non, laisser déverrouillé", style=discord.ButtonStyle.secondary, custom_id="lock_no")
async def btn_no(self, interaction: discord.Interaction, button: Button):
await interaction.response.send_modal(DurationModal(lock_session=False))
# ---- SESSION LOGIC ----
async def run_study_session(guild_id: int, user_id: int, minutes: int):
"""Run the study session timer"""
try:
print(f"⏳ Timer de {minutes} min démarré pour user {user_id}")
await asyncio.sleep(minutes * 60)
print(f"✅ Timer terminé pour user {user_id}")
# Session completed normally
await end_session(guild_id, user_id, cancelled=False)
except asyncio.CancelledError:
print(f"⏹️ Timer annulé pour user {user_id}")
# Session was cancelled
await end_session(guild_id, user_id, cancelled=True)
async def end_session(guild_id: int, user_id: int, cancelled: bool = False):
"""End a study session and clean up"""
print(f"🧹 Fin de session pour user {user_id} (annulée={cancelled})")
# Get session data before removing
session = active_sessions.pop(user_id, None)
if not session:
print(f"⚠️ Aucune session trouvée pour user {user_id}")
return
guild = bot.get_guild(guild_id)
if not guild:
print(f"❌ Serveur {guild_id} introuvable")
return
# Get member
try:
member = await guild.fetch_member(user_id)
except discord.NotFound:
print(f"❌ Membre {user_id} introuvable")
return
except discord.HTTPException as e:
print(f"❌ Erreur lors de la récupération du membre: {e}")
return
# Remove study role
await remove_study_role(member)
# Send DM
try:
if cancelled:
await member.send("⏹️ Session annulée. J'espère que t'as bien étudié mon mignon 📚")
else:
await member.send("✅ Ta session est terminée, bien ouej ! 🎉")
print(f"✅ DM envoyé à {member.name}")
except discord.Forbidden:
print(f"❌ Impossible d'envoyer un DM à {member.name}")
except discord.HTTPException as e:
print(f"❌ Erreur lors de l'envoi du DM: {e}")
# ---- SLASH COMMANDS ----
@bot.tree.command(name="study", description="Démarre une session d'étude")
async def study(interaction: discord.Interaction):
"""Start a study session with custom duration"""
try:
user_id = interaction.user.id
print(f"📖 /study utilisé par {interaction.user.name}")
# Check for existing session
if user_id in active_sessions:
await interaction.response.send_message(
"⚠️ Tu as déjà une session en cours. Utilise `/stopstudy` pour l'arrêter d'abord.",
ephemeral=True
)
return
# Show lock warning
await interaction.response.send_message(
"🔒 **Veux-tu verrouiller cette session ?**\n\n"
"Si tu verrouilles, tu ne pourras **pas** utiliser `/stopstudy` pour l'arrêter avant la fin.\n"
"Cela t'aidera à rester concentré sans tentation d'abandonner ! 💪",
view=LockWarningView(),
ephemeral=True
)
except Exception as e:
print(f"❌ Erreur dans /study: {e}")
traceback.print_exc()
@bot.tree.command(name="stopstudy", description="Arrête ta session d'étude en cours")
async def stopstudy(interaction: discord.Interaction):
"""Stop the current study session"""
try:
user_id = interaction.user.id
print(f"🔍 /stopstudy par {interaction.user.name}")
session = active_sessions.get(user_id)
if not session:
await interaction.response.send_message(
"❌ Aucune session en cours.",
ephemeral=True
)
return
# Check if session is locked
if session.get('locked', False):
await interaction.response.send_message(
"🔒 Cette session est verrouillée ! Tu dois attendre la fin du timer.\n"
"Allez, tu peux le faire ! 💪",
ephemeral=True
)
return
# Cancel the task (this triggers end_session via CancelledError)
if session.get('task'):
session['task'].cancel()
await interaction.response.send_message(
"⏹️ Session annulée.",
ephemeral=True
)
except Exception as e:
print(f"❌ Erreur dans /stopstudy: {e}")
traceback.print_exc()
@bot.tree.command(name="mystatus", description="Vérifie si tu as une session en cours")
async def mystatus(interaction: discord.Interaction):
"""Check your current study status"""
try:
session = active_sessions.get(interaction.user.id)
if not session:
await interaction.response.send_message(
"📖 Tu n'as pas de session en cours.",
ephemeral=True
)
else:
minutes = session['minutes']
locked_status = "🔒 Verrouillée" if session.get('locked', False) else "🔓 Déverrouillée"
await interaction.response.send_message(
f"📚 Session de **{minutes} minutes** en cours...\n{locked_status}",
ephemeral=True
)
except Exception as e:
print(f"❌ Erreur dans /mystatus: {e}")
traceback.print_exc()
# ---- ERROR HANDLING ----
@bot.event
async def on_command_error(ctx, error):
print(f"❌ Erreur de commande: {error}")
traceback.print_exc()
# ---- RUN ----
if __name__ == "__main__":
print("🚀 Démarrage du bot...")
print(f"🎭 Rôle: {STUDY_ROLE_NAME}")
print(f"🔊 Salon vocal: {STUDY_VOICE_CHANNEL}")
bot.run(TOKEN)