198 lines
9.9 KiB
Python
198 lines
9.9 KiB
Python
import discord
|
|
import json
|
|
import os
|
|
import aiohttp
|
|
from datetime import datetime
|
|
|
|
WARNINGS_FILE = 'data/warnings.json'
|
|
|
|
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}\nRé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
|
|
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)
|
|
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
|
|
dm_message = await generate_dm_message(original_reason, warning_count, ollama_url, "", ollama_model, 30, 0.7)
|
|
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).")
|