2026-02-06 22:23:20 +01:00
import discord
import json
import os
import aiohttp
from datetime import datetime
2026-06-13 14:46:02 +00:00
WARNINGS_FILE = ' data/warnings.json '
2026-02-06 22:23:20 +01:00
def load_warnings ( ) :
if os . path . exists ( WARNINGS_FILE ) :
with open ( WARNINGS_FILE , ' r ' , encoding = ' utf-8 ' ) as f :
content = f . read ( ) . strip ( )
if content :
return json . loads ( content )
return { }
def save_warnings ( warnings ) :
with open ( WARNINGS_FILE , ' w ' , encoding = ' utf-8 ' ) as f :
json . dump ( warnings , f , indent = 4 , ensure_ascii = False )
async def rephrase_reason ( original_reason , api_url , api_key , model , timeout , temperature ) :
try :
payload = {
" model " : model ,
" prompt " : f " Reformule la raison suivante d ' avertissement en français de manière polie et claire : \n { original_reason } \n Réformulation : " ,
" stream " : False ,
" options " : { " temperature " : temperature }
}
headers = { }
if api_key :
headers [ " Authorization " ] = f " Bearer { api_key } "
async with aiohttp . ClientSession ( ) as session :
async with session . post ( api_url , json = payload , headers = headers , timeout = aiohttp . ClientTimeout ( total = 30 ) ) as response :
response . raise_for_status ( )
try :
data = await response . json ( )
raw_response = data . get ( ' response ' , ' ' ) . strip ( )
# If the response is JSON (some models do that), try to parse it
try :
json_resp = json . loads ( raw_response )
if isinstance ( json_resp , dict ) and ' response ' in json_resp :
rephrased = json_resp [ ' response ' ] . strip ( )
else :
rephrased = raw_response
except json . JSONDecodeError :
rephrased = raw_response
except Exception :
# If not JSON, try to get text
content = await response . text ( )
try :
data = json . loads ( content )
raw_response = data . get ( ' response ' , ' ' ) . strip ( )
try :
json_resp = json . loads ( raw_response )
rephrased = json_resp . get ( ' response ' , raw_response ) if isinstance ( json_resp , dict ) else raw_response
except json . JSONDecodeError :
rephrased = raw_response
except json . JSONDecodeError :
rephrased = content . strip ( )
return rephrased if rephrased else None
except Exception as e :
print ( f " Erreur lors de la reformulation : { e } " )
return None
async def generate_dm_message ( original_reason , warning_count , api_url , api_key , model , timeout , temperature ) :
try :
payload = {
" model " : model ,
" prompt " : f " Écris seulement le message d ' avertissement poli et concis pour un user en DM, sans ajouter de métadonnées ou de JSON. Le message doit commencer par ' Bonjour, ' et finir par ' Cordialement, le Superviseur ' . Raison originale : { original_reason } , nombre d ' avertissements : { warning_count } . " ,
" stream " : False ,
" options " : { " temperature " : temperature }
}
headers = { }
if api_key :
headers [ " Authorization " ] = f " Bearer { api_key } "
async with aiohttp . ClientSession ( ) as session :
async with session . post ( api_url , json = payload , headers = headers , timeout = aiohttp . ClientTimeout ( total = 30 ) ) as response :
response . raise_for_status ( )
try :
data = await response . json ( )
raw_message = data . get ( ' response ' , ' ' ) . strip ( )
# If the response is JSON (some models do that), try to parse it
try :
json_resp = json . loads ( raw_message )
if isinstance ( json_resp , dict ) :
final_message = json_resp . get ( ' generated_message ' ) or json_resp . get ( ' response ' , raw_message )
else :
final_message = raw_message
except json . JSONDecodeError :
final_message = raw_message
except Exception :
# If not JSON, try to get text
content = await response . text ( )
try :
data = json . loads ( content )
raw_message = data . get ( ' response ' , ' ' ) . strip ( )
try :
json_resp = json . loads ( raw_message )
final_message = json_resp . get ( ' generated_message ' ) or json_resp . get ( ' response ' , raw_message ) if isinstance ( json_resp , dict ) else raw_message
except json . JSONDecodeError :
final_message = raw_message
except json . JSONDecodeError :
final_message = content . strip ( )
# Ensure message is not too long (Discord DM limit is 2000 chars)
if len ( final_message ) > 1950 :
final_message = final_message [ : 1950 ] + " ... "
return final_message if final_message else None
except Exception as e :
print ( f " Erreur lors de la génération du message DM : { e } " )
return None
async def execute ( bot , params , message ) :
warnings_list = params . get ( ' warnings ' , [ ] ) # warnings is param name
if not warnings_list :
# Fallback for single warning
warnings_list = [ params ]
warned_count = 0
all_warnings = load_warnings ( )
guild_id = str ( message . guild . id )
if guild_id not in all_warnings :
all_warnings [ guild_id ] = { }
for warn_params in warnings_list :
target_mention = warn_params . get ( ' target_user_mention ' )
original_reason = warn_params . get ( ' reason ' , ' Aucune raison spécifiée ' )
# Rephrase the reason using Ollama
2026-06-13 14:46:02 +00:00
ollama_url = os . environ . get ( " OLLAMA_BASE_URL " , " http://localhost:11434 " ) + " /api/generate "
ollama_model = os . environ . get ( " OLLAMA_MODEL " , " gpt-oss:20b " )
rephrased_reason = await rephrase_reason ( original_reason , ollama_url , " " , ollama_model , 30 , 0.7 )
2026-02-06 22:23:20 +01:00
reason = rephrased_reason if rephrased_reason else original_reason
# Parse mention or ID
if target_mention . startswith ( ' <@ ' ) and target_mention . endswith ( ' > ' ) :
user_id = int ( target_mention [ 2 : - 1 ] )
elif target_mention . startswith ( ' <@! ' ) and target_mention . endswith ( ' > ' ) :
user_id = int ( target_mention [ 3 : - 1 ] )
elif target_mention . startswith ( ' @ ' ) :
# Handle @username
username = target_mention [ 1 : ] # Remove @
member = discord . utils . find ( lambda m : m . name == username or m . display_name == username , message . guild . members )
if not member :
await message . channel . send ( f " Utilisateur { target_mention } introuvable. " )
continue
user_id = member . id
else :
try :
user_id = int ( target_mention )
except ValueError :
await message . channel . send ( f " Mention ou ID invalide: { target_mention } " )
continue
member = message . guild . get_member ( user_id )
if not member :
await message . channel . send ( f " Utilisateur { target_mention } introuvable. " )
continue
# Add warning to record
user_id_str = str ( user_id )
if user_id_str not in all_warnings [ guild_id ] :
all_warnings [ guild_id ] [ user_id_str ] = [ ]
all_warnings [ guild_id ] [ user_id_str ] . append ( {
' reason ' : reason ,
' timestamp ' : datetime . now ( ) . isoformat ( ) ,
' issued_by ' : str ( message . author . id )
} )
save_warnings ( all_warnings )
warning_count = len ( all_warnings [ guild_id ] [ user_id_str ] )
# Send warning message in channel with count
embed = discord . Embed (
title = f " Avertissement # { warning_count } " ,
description = f " { member . mention } , { reason } \n "
f " Nombre total d ' avertissements : { warning_count } " ,
color = discord . Color . orange ( )
)
await message . channel . send ( embed = embed )
# Generate full DM message using AI with original reason for polite reformulation
2026-06-13 14:46:02 +00:00
dm_message = await generate_dm_message ( original_reason , warning_count , ollama_url , " " , ollama_model , 30 , 0.7 )
2026-02-06 22:23:20 +01:00
dm_content = dm_message if dm_message else f " Avertissement : { reason } . Nombre total : { warning_count } . "
# Send DM to user
try :
await member . send ( dm_content )
warned_count + = 1
except discord . Forbidden :
await message . channel . send ( f " Impossible d ' envoyer un DM à { member . mention } , avertissement public. " )
warned_count + = 1 # Still count as warned
# If 3+ warnings, auto-ban
if warning_count > = 3 :
try :
await member . ban ( reason = f " 3 avertissements accumulés. Dernière raison: { reason } " )
embed . add_field ( name = " Action automatique " , value = " Utilisateur banni pour accumulation d ' avertissements. " , inline = False )
await message . channel . send ( embed = embed )
await message . channel . send ( f " { member . mention } a été banni pour accumulation d ' avertissements. " )
continue # Skip further processing for this user
except discord . Forbidden :
embed . add_field ( name = " Action suggérée " , value = " Utilisateur a 3+ avertissements, mais bot n ' a pas les permissions pour bannir. " , inline = False )
await message . channel . send ( embed = embed )
except discord . HTTPException as e :
embed . add_field ( name = " Erreur de bannissement " , value = f " Impossible de bannir: { e } " , inline = False )
await message . channel . send ( embed = embed )
save_warnings ( all_warnings ) # Save at end
if warned_count > 0 :
await message . channel . send ( f " { warned_count } utilisateur(s) averti(s). " )