2026-08-16 19:37:15 +02:00
import discord
from discord import app_commands
from discord . ext import commands , tasks
import dateparser
from datetime import datetime , timezone
Modularize bot: per-guild config system for roles/channels/categories
- Add utils/guild_config.py: centralized config loader per guild (data/guilds/{id}.json)
- Add commandes/config.py: /config set_role, /config set_channel, /config set_category, /config voir, /config reset
- Replace ALL hardcoded role IDs (banquier, gouvernement, citoyen, staff, ems, permis, fbm, ursaff, etc.) with guild_config lookups
- Replace ALL hardcoded channel IDs (welcome, candidature, questions, mp_reception) with guild_config lookups
- Replace ALL hardcoded category IDs (ticket) with guild_config lookups
- Remove duplicate commandes/welcome_message.py (kept utils/ version)
- Remove 20+ hardcoded snowflake IDs across 22 files
- Guilds configure via /config on Discord, no code changes needed
2026-08-17 00:12:57 +02:00
from utils . guild_config import get_role_id
2026-08-16 19:37:15 +02:00
presence_data = { }
class PresenceView ( discord . ui . View ) :
def __init__ ( self , message_id ) :
super ( ) . __init__ ( timeout = None )
self . message_id = message_id
async def update_message ( self , interaction , state ) :
user_id = interaction . user . id
if self . message_id not in presence_data :
presence_data [ self . message_id ] = {
" present " : set ( ) , " absent " : set ( ) , " retard " : set ( ) ,
" date " : None , " sent_dm " : False ,
" reminder1 " : None , " reminder2 " : None ,
" reminder1_sent " : False , " reminder2_sent " : False ,
" channel_id " : None , " guild_id " : None
}
for key in [ " present " , " absent " , " retard " ] :
presence_data [ self . message_id ] [ key ] . discard ( user_id )
presence_data [ self . message_id ] [ state ] . add ( user_id )
n_present = len ( presence_data [ self . message_id ] [ " present " ] )
n_absent = len ( presence_data [ self . message_id ] [ " absent " ] )
n_retard = len ( presence_data [ self . message_id ] [ " retard " ] )
msg = await interaction . channel . fetch_message ( self . message_id )
lines = msg . content . split ( ' \n ' )
# On modifie uniquement les lignes de présence
for i , line in enumerate ( lines ) :
if line . startswith ( " > - Je serai présent " ) :
lines [ i ] = f " > - Je serai présent : <:check:1297296859206778911> **( { n_present } )** "
elif line . startswith ( " > - Je serai absent " ) :
lines [ i ] = f " > - Je serai absent : <:uncheck:1297296882778902598> **( { n_absent } )** "
elif line . startswith ( " > - Je serai en retard " ) :
lines [ i ] = f " > - Je serai en retard : ⏰ **( { n_retard } )** "
new_content = ' \n ' . join ( lines )
await msg . edit ( content = new_content , view = self )
@discord.ui.button ( label = " Présent " , style = discord . ButtonStyle . success , emoji = " ✅ " , custom_id = " presence_present " )
async def present ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
await self . update_message ( interaction , " present " )
await interaction . response . send_message ( " Merci, tu as été noté comme **présent** à la session RP ! " , ephemeral = True )
@discord.ui.button ( label = " Absent " , style = discord . ButtonStyle . danger , emoji = " ❌ " , custom_id = " presence_absent " )
async def absent ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
await self . update_message ( interaction , " absent " )
await interaction . response . send_message ( " Merci, tu as été noté comme **absent** à la session RP. " , ephemeral = True )
@discord.ui.button ( label = " En retard " , style = discord . ButtonStyle . primary , emoji = " ⏰ " , custom_id = " presence_retard " )
async def retard ( self , interaction : discord . Interaction , button : discord . ui . Button ) :
await self . update_message ( interaction , " retard " )
await interaction . response . send_message ( " Merci, tu as été noté comme **en retard** pour la session RP. " , ephemeral = True )
class AnnonceSession ( commands . Cog ) :
def __init__ ( self , bot ) :
self . bot = bot
self . check_sessions . start ( )
def cog_unload ( self ) :
self . check_sessions . cancel ( )
@app_commands.command (
name = " annonce_session " ,
description = " Annonce une session RP avec image, rappels et boutons de présence. "
)
@app_commands.describe (
date = " Date de la session (ex: Vendredi 20 Juin à 20h45) " ,
reminder1 = " Date/heure du 1er rappel (ex: Jeudi 19 Juin à 19h) " ,
reminder2 = " Date/heure du 2e rappel (ex: Vendredi 20 Juin à 15h) " ,
image = " Image à joindre (obligatoire) "
)
async def annonce_session (
self ,
interaction : discord . Interaction ,
date : str ,
reminder1 : str ,
reminder2 : str ,
image : discord . Attachment
) :
if not image . content_type or not image . content_type . startswith ( " image/ " ) :
await interaction . response . send_message (
" ❌ Le fichier joint doit être une image valide. " , ephemeral = True
)
return
# On parse toutes les dates en Europe/Paris
session_date = dateparser . parse (
date , languages = [ ' fr ' ] ,
settings = { " TIMEZONE " : " Europe/Paris " , " RETURN_AS_TIMEZONE_AWARE " : True }
)
reminder1_date = dateparser . parse (
reminder1 , languages = [ ' fr ' ] ,
settings = { " TIMEZONE " : " Europe/Paris " , " RETURN_AS_TIMEZONE_AWARE " : True }
)
reminder2_date = dateparser . parse (
reminder2 , languages = [ ' fr ' ] ,
settings = { " TIMEZONE " : " Europe/Paris " , " RETURN_AS_TIMEZONE_AWARE " : True }
)
if not session_date or not reminder1_date or not reminder2_date :
await interaction . response . send_message (
" ❌ Une des dates n ' a pas été comprise. Vérifie le format. " , ephemeral = True
)
return
session_date = session_date . astimezone ( timezone . utc )
reminder1_date = reminder1_date . astimezone ( timezone . utc )
reminder2_date = reminder2_date . astimezone ( timezone . utc )
Modularize bot: per-guild config system for roles/channels/categories
- Add utils/guild_config.py: centralized config loader per guild (data/guilds/{id}.json)
- Add commandes/config.py: /config set_role, /config set_channel, /config set_category, /config voir, /config reset
- Replace ALL hardcoded role IDs (banquier, gouvernement, citoyen, staff, ems, permis, fbm, ursaff, etc.) with guild_config lookups
- Replace ALL hardcoded channel IDs (welcome, candidature, questions, mp_reception) with guild_config lookups
- Replace ALL hardcoded category IDs (ticket) with guild_config lookups
- Remove duplicate commandes/welcome_message.py (kept utils/ version)
- Remove 20+ hardcoded snowflake IDs across 22 files
- Guilds configure via /config on Discord, no code changes needed
2026-08-17 00:12:57 +02:00
ping_role_id = get_role_id ( interaction . guild . id , " citoyen " )
reminder_role_id = get_role_id ( interaction . guild . id , " session_reminders " )
2026-08-16 19:37:15 +02:00
message = (
" # <:VV:1339237574593609780> - **SESSION VICTORIA’ S VICE** \n "
" \n "
" ```📅 - HORAIRE DE LA SESSION``` \n "
f " > - Nous vous donnons rendez-vous le ** { date } ** pour une **session officielle** sur Victoria’ s Vice. 🗓️ \n "
" --- \n "
" ```🤔 - COMMENT REJOINDRE LA SESSION ?``` \n "
" > - Pour lancer la Map, veuillez entrer le code ci-dessous sur la barre de recherche de Fortnite, par la suite lancer la Map en **public** afin de pouvoir nous rejoindre. \n "
" > - **`CODE DE LA MAP`** <:arrow:1297491509201080362> [0986-1836-1625] \n "
" > - **`LIEN DIRECT DE LA MAP`** <:arrow:1297491509201080362> <https://play.fn.gg/island/0986-1836-1625> \n "
" --- \n "
" ```🖐 - SEREZ-VOUS PRÉSENT(E) EN SESSION ?``` \n "
" > - Je serai présent : <:check:1297296859206778911> **(0)** \n "
" > - Je serai absent : <:uncheck:1297296882778902598> **(0)** \n "
" > - Je serai en retard : ⏰ **(0)** \n "
" --- "
Modularize bot: per-guild config system for roles/channels/categories
- Add utils/guild_config.py: centralized config loader per guild (data/guilds/{id}.json)
- Add commandes/config.py: /config set_role, /config set_channel, /config set_category, /config voir, /config reset
- Replace ALL hardcoded role IDs (banquier, gouvernement, citoyen, staff, ems, permis, fbm, ursaff, etc.) with guild_config lookups
- Replace ALL hardcoded channel IDs (welcome, candidature, questions, mp_reception) with guild_config lookups
- Replace ALL hardcoded category IDs (ticket) with guild_config lookups
- Remove duplicate commandes/welcome_message.py (kept utils/ version)
- Remove 20+ hardcoded snowflake IDs across 22 files
- Guilds configure via /config on Discord, no code changes needed
2026-08-17 00:12:57 +02:00
f " > - **`PING`** : <@& { ping_role_id } > "
2026-08-16 19:37:15 +02:00
)
await interaction . response . send_message (
content = message ,
files = [ await image . to_file ( ) ]
)
msg = await interaction . original_response ( )
presence_data [ msg . id ] = {
" present " : set ( ) ,
" absent " : set ( ) ,
" retard " : set ( ) ,
" date " : session_date ,
" sent_dm " : False ,
" reminder1 " : reminder1_date ,
" reminder2 " : reminder2_date ,
" reminder1_sent " : False ,
" reminder2_sent " : False ,
" channel_id " : msg . channel . id ,
" guild_id " : msg . guild . id
}
await msg . edit ( view = PresenceView ( msg . id ) )
@app_commands.command (
name = " liste_presence " ,
description = " Liste les présences pour une session RP. "
)
@app_commands.describe (
message_id = " ID du message d ' annonce "
)
async def liste_presence ( self , interaction : discord . Interaction , message_id : str ) :
try :
mid = int ( message_id )
except ValueError :
await interaction . response . send_message ( " ID de message invalide. " , ephemeral = True )
return
if mid not in presence_data :
await interaction . response . send_message ( " Aucune donnée de présence pour ce message. " , ephemeral = True )
return
data = presence_data [ mid ]
def mentions ( ids ) :
return ' , ' . join ( f " <@ { uid } > " for uid in ids ) if ids else " Aucun "
await interaction . response . send_message (
f " **Présents ( { len ( data [ ' present ' ] ) } )** : { mentions ( data [ ' present ' ] ) } \n "
f " **Absents ( { len ( data [ ' absent ' ] ) } )** : { mentions ( data [ ' absent ' ] ) } \n "
f " **En retard ( { len ( data [ ' retard ' ] ) } )** : { mentions ( data [ ' retard ' ] ) } " ,
ephemeral = True
)
@tasks.loop ( seconds = 30 )
async def check_sessions ( self ) :
now = datetime . now ( timezone . utc )
for msg_id , data in list ( presence_data . items ( ) ) :
# Premier rappel
if data . get ( " reminder1 " ) and not data . get ( " reminder1_sent " , False ) and now > = data [ " reminder1 " ] :
await self . send_reminder ( data , msg_id , " Premier rappel ! " )
data [ " reminder1_sent " ] = True
# Deuxième rappel
if data . get ( " reminder2 " ) and not data . get ( " reminder2_sent " , False ) and now > = data [ " reminder2 " ] :
await self . send_reminder ( data , msg_id , " Deuxième rappel ! " )
data [ " reminder2_sent " ] = True
# Envoi du MP à l'heure de la session
if data [ " date " ] and not data . get ( " sent_dm " , False ) and now > = data [ " date " ] :
for user_id in data [ " present " ] :
try :
user = await self . bot . fetch_user ( user_id )
channel = self . bot . get_channel ( data [ " channel_id " ] )
if channel :
msg = await channel . fetch_message ( msg_id )
await user . send ( f " La session RP commence ! Voici le lien de l ' annonce : { msg . jump_url } " )
except Exception as e :
print ( f " Erreur lors de l ' envoi du DM à { user_id } : { e } " )
data [ " sent_dm " ] = True
async def send_reminder ( self , data , msg_id , label ) :
channel = self . bot . get_channel ( data [ " channel_id " ] )
if channel :
msg = await channel . fetch_message ( msg_id )
Modularize bot: per-guild config system for roles/channels/categories
- Add utils/guild_config.py: centralized config loader per guild (data/guilds/{id}.json)
- Add commandes/config.py: /config set_role, /config set_channel, /config set_category, /config voir, /config reset
- Replace ALL hardcoded role IDs (banquier, gouvernement, citoyen, staff, ems, permis, fbm, ursaff, etc.) with guild_config lookups
- Replace ALL hardcoded channel IDs (welcome, candidature, questions, mp_reception) with guild_config lookups
- Replace ALL hardcoded category IDs (ticket) with guild_config lookups
- Remove duplicate commandes/welcome_message.py (kept utils/ version)
- Remove 20+ hardcoded snowflake IDs across 22 files
- Guilds configure via /config on Discord, no code changes needed
2026-08-17 00:12:57 +02:00
reminder_role_id = get_role_id ( data . get ( " guild_id " , 0 ) , " session_reminders " )
2026-08-16 19:37:15 +02:00
await channel . send (
Modularize bot: per-guild config system for roles/channels/categories
- Add utils/guild_config.py: centralized config loader per guild (data/guilds/{id}.json)
- Add commandes/config.py: /config set_role, /config set_channel, /config set_category, /config voir, /config reset
- Replace ALL hardcoded role IDs (banquier, gouvernement, citoyen, staff, ems, permis, fbm, ursaff, etc.) with guild_config lookups
- Replace ALL hardcoded channel IDs (welcome, candidature, questions, mp_reception) with guild_config lookups
- Replace ALL hardcoded category IDs (ticket) with guild_config lookups
- Remove duplicate commandes/welcome_message.py (kept utils/ version)
- Remove 20+ hardcoded snowflake IDs across 22 files
- Guilds configure via /config on Discord, no code changes needed
2026-08-17 00:12:57 +02:00
f " <@& { reminder_role_id } > ⏰ ** { label } ** \n La session RP approche ! \n Annonce : { msg . jump_url } "
2026-08-16 19:37:15 +02:00
)
@check_sessions.before_loop
async def before_check_sessions ( self ) :
await self . bot . wait_until_ready ( )
async def setup ( bot ) :
await bot . add_cog ( AnnonceSession ( bot ) )