chatbot.py 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228
  1. import os
  2. import json
  3. import logging
  4. import base64
  5. from io import BytesIO
  6. import asyncio
  7. import random
  8. import mysql.connector
  9. import pytz
  10. from mysql.connector import Error
  11. from PIL import Image
  12. import tiktoken
  13. import discord
  14. from discord.ext import commands
  15. from discord import app_commands
  16. from datetime import datetime, timezone, timedelta
  17. from dotenv import load_dotenv
  18. from openai import AsyncOpenAI, OpenAIError
  19. from discord.utils import get
  20. # =================================
  21. # Configuration et Initialisation
  22. # =================================
  23. # Charger les variables d'environnement depuis le fichier .env
  24. load_dotenv()
  25. DISCORD_TOKEN = os.getenv('DISCORD_TOKEN')
  26. OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
  27. DISCORD_CHANNEL_ID = os.getenv('DISCORD_CHANNEL_ID')
  28. PERSONALITY_PROMPT_FILE = os.getenv('PERSONALITY_PROMPT_FILE', 'personality_prompt.txt')
  29. IMAGE_ANALYSIS_PROMPT_FILE = os.getenv('IMAGE_ANALYSIS_PROMPT_FILE', 'image_analysis_prompt.txt')
  30. BOT_NAME = os.getenv('BOT_NAME', 'ChatBot')
  31. BOT_VERSION = "3.0.0"
  32. GUILD_ID = os.getenv('GUILD_ID')
  33. SPECIFIC_ROLE_NAME = os.getenv('SPECIFIC_ROLE_NAME')
  34. # Validation des variables d'environnement
  35. required_env_vars = {
  36. 'DISCORD_TOKEN': DISCORD_TOKEN,
  37. 'OPENAI_API_KEY': OPENAI_API_KEY,
  38. 'DISCORD_CHANNEL_ID': DISCORD_CHANNEL_ID,
  39. 'IMAGE_ANALYSIS_PROMPT_FILE': IMAGE_ANALYSIS_PROMPT_FILE,
  40. 'GUILD_ID': GUILD_ID,
  41. 'SPECIFIC_ROLE_NAME': SPECIFIC_ROLE_NAME
  42. }
  43. missing_vars = [var for var, val in required_env_vars.items() if val is None]
  44. if missing_vars:
  45. raise ValueError(f"Les variables d'environnement suivantes ne sont pas définies: {', '.join(missing_vars)}")
  46. # Convertir l'ID du canal Discord en entier
  47. try:
  48. chatgpt_channel_id = int(DISCORD_CHANNEL_ID)
  49. except ValueError:
  50. raise ValueError("L'ID du canal Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
  51. # Convertir l'ID de la guild en entier
  52. try:
  53. GUILD_ID = int(GUILD_ID)
  54. except ValueError:
  55. raise ValueError("L'ID de la guild Discord est invalide. Assurez-vous qu'il s'agit d'un entier.")
  56. # Vérification de l'existence des fichiers de prompt
  57. for file_var, file_path in [('PERSONALITY_PROMPT_FILE', PERSONALITY_PROMPT_FILE),
  58. ('IMAGE_ANALYSIS_PROMPT_FILE', IMAGE_ANALYSIS_PROMPT_FILE)]:
  59. if not os.path.isfile(file_path):
  60. raise FileNotFoundError(f"Le fichier de prompt '{file_var}' '{file_path}' est introuvable.")
  61. # Lire les prompts depuis les fichiers
  62. with open(PERSONALITY_PROMPT_FILE, 'r', encoding='utf-8') as f:
  63. PERSONALITY_PROMPT = f.read().strip()
  64. with open(IMAGE_ANALYSIS_PROMPT_FILE, 'r', encoding='utf-8') as f:
  65. IMAGE_ANALYSIS_PROMPT = f.read().strip()
  66. # Configurer les logs
  67. LOG_FORMAT = '%(asctime)s : %(name)s : %(levelname)s : %(message)s'
  68. logging.basicConfig(
  69. handlers=[
  70. logging.FileHandler("./chatbot.log", mode='a', encoding='utf-8'),
  71. logging.StreamHandler()
  72. ],
  73. format=LOG_FORMAT,
  74. level=logging.INFO
  75. )
  76. logger = logging.getLogger(BOT_NAME)
  77. logger.setLevel(logging.INFO)
  78. logging.getLogger('httpx').setLevel(logging.WARNING) # Réduire le niveau de log pour 'httpx'
  79. # Initialiser les intents Discord
  80. intents = discord.Intents.default()
  81. intents.message_content = True
  82. intents.members = True
  83. intents.presences = True
  84. # Initialiser le client OpenAI asynchrone
  85. openai_client = AsyncOpenAI(api_key=OPENAI_API_KEY)
  86. # ============================
  87. # Commande Décorateur Admin
  88. # ============================
  89. def admin_command(func):
  90. """Décorateur pour marquer une commande comme réservée aux administrateurs."""
  91. func.is_admin = True
  92. return func
  93. # =====================================
  94. # Gestion de la Base de Données MariaDB
  95. # =====================================
  96. class DatabaseManager:
  97. def __init__(self):
  98. self.connection = self.create_db_connection()
  99. def create_db_connection(self):
  100. try:
  101. connection = mysql.connector.connect(
  102. host=os.getenv('DB_HOST'),
  103. user=os.getenv('DB_USER'),
  104. password=os.getenv('DB_PASSWORD'),
  105. database=os.getenv('DB_NAME'),
  106. charset='utf8mb4',
  107. collation='utf8mb4_unicode_ci'
  108. )
  109. if connection.is_connected():
  110. logger.info("Connexion réussie à MariaDB")
  111. return connection
  112. except Error as e:
  113. logger.error(f"Erreur de connexion à MariaDB: {e}")
  114. return None
  115. def load_conversation_history(self):
  116. global conversation_history
  117. try:
  118. with self.connection.cursor(dictionary=True) as cursor:
  119. cursor.execute("SELECT role, content FROM conversation_history ORDER BY id ASC")
  120. rows = cursor.fetchall()
  121. conversation_history = [
  122. row for row in rows
  123. if not (row['role'] == "system" and row['content'] == PERSONALITY_PROMPT)
  124. ]
  125. logger.info("Historique chargé depuis la base de données")
  126. except Error as e:
  127. logger.error(f"Erreur lors du chargement de l'historique depuis la base de données: {e}")
  128. conversation_history = []
  129. def save_message(self, role, content):
  130. try:
  131. with self.connection.cursor() as cursor:
  132. sql = "INSERT INTO conversation_history (role, content) VALUES (%s, %s)"
  133. cursor.execute(sql, (role, json.dumps(content, ensure_ascii=False) if isinstance(content, (dict, list)) else content))
  134. self.connection.commit()
  135. logger.debug(f"Message sauvegardé dans la base de données: {role} - {content[:50]}...")
  136. except Error as e:
  137. logger.error(f"Erreur lors de la sauvegarde du message dans la base de données: {e}")
  138. def delete_old_image_analyses(self):
  139. try:
  140. with self.connection.cursor() as cursor:
  141. cursor.execute("DELETE FROM conversation_history WHERE role = 'system' AND content LIKE '__IMAGE_ANALYSIS__:%'")
  142. self.connection.commit()
  143. logger.info("Toutes les anciennes analyses d'image ont été supprimées de la base de données.")
  144. except Error as e:
  145. logger.error(f"Erreur lors de la suppression des analyses d'image: {e}")
  146. def reset_history(self):
  147. try:
  148. with self.connection.cursor() as cursor:
  149. cursor.execute("DELETE FROM conversation_history")
  150. self.connection.commit()
  151. logger.info("Historique des conversations réinitialisé.")
  152. except Error as e:
  153. logger.error(f"Erreur lors de la réinitialisation de l'historique: {e}")
  154. def delete_old_messages(self, limit):
  155. try:
  156. with self.connection.cursor() as cursor:
  157. cursor.execute("DELETE FROM conversation_history ORDER BY id ASC LIMIT %s", (limit,))
  158. self.connection.commit()
  159. logger.debug(f"{limit} messages les plus anciens ont été supprimés de la base de données pour maintenir l'historique à 150 messages.")
  160. except Error as e:
  161. logger.error(f"Erreur lors de la suppression des anciens messages: {e}")
  162. def close_connection(self):
  163. if self.connection and self.connection.is_connected():
  164. self.connection.close()
  165. logger.info("Connexion à la base de données fermée.")
  166. def add_reminder(self, user_id, channel_id, remind_at, content):
  167. try:
  168. with self.connection.cursor() as cursor:
  169. sql = """
  170. INSERT INTO reminders (user_id, channel_id, remind_at, content)
  171. VALUES (%s, %s, %s, %s)
  172. """
  173. cursor.execute(sql, (user_id, channel_id, remind_at, content))
  174. self.connection.commit()
  175. logger.info(f"Rappel ajouté pour l'utilisateur {user_id} à {remind_at}")
  176. except Error as e:
  177. logger.error(f"Erreur lors de l'ajout du rappel: {e}")
  178. def get_due_reminders(self, current_time):
  179. try:
  180. with self.connection.cursor(dictionary=True) as cursor:
  181. sql = "SELECT * FROM reminders WHERE remind_at <= %s"
  182. cursor.execute(sql, (current_time,))
  183. reminders = cursor.fetchall()
  184. return reminders
  185. except Error as e:
  186. logger.error(f"Erreur lors de la récupération des rappels: {e}")
  187. return []
  188. def delete_reminder(self, reminder_id):
  189. try:
  190. with self.connection.cursor() as cursor:
  191. sql = "DELETE FROM reminders WHERE id = %s"
  192. cursor.execute(sql, (reminder_id,))
  193. self.connection.commit()
  194. logger.info(f"Rappel ID {reminder_id} supprimé")
  195. except Error as e:
  196. logger.error(f"Erreur lors de la suppression du rappel ID {reminder_id}: {e}")
  197. def get_user_reminders(self, user_id):
  198. """Récupère tous les rappels futurs pour un utilisateur spécifique."""
  199. try:
  200. with self.connection.cursor(dictionary=True) as cursor:
  201. sql = """
  202. SELECT * FROM reminders
  203. WHERE user_id = %s AND remind_at > NOW()
  204. ORDER BY remind_at ASC
  205. """
  206. cursor.execute(sql, (user_id,))
  207. reminders = cursor.fetchall()
  208. logger.info(f"{len(reminders)} rappels récupérés pour l'utilisateur {user_id}.")
  209. return reminders
  210. except Error as e:
  211. logger.error(f"Erreur lors de la récupération des rappels de l'utilisateur {user_id}: {e}")
  212. return []
  213. def get_reminder_by_id(self, reminder_id):
  214. """Récupère un rappel spécifique par son ID."""
  215. try:
  216. with self.connection.cursor(dictionary=True) as cursor:
  217. sql = "SELECT * FROM reminders WHERE id = %s"
  218. cursor.execute(sql, (reminder_id,))
  219. reminder = cursor.fetchone()
  220. return reminder
  221. except Error as e:
  222. logger.error(f"Erreur lors de la récupération du rappel ID {reminder_id}: {e}")
  223. return None
  224. # ===============================
  225. # Gestion de l'Historique des Messages
  226. # ===============================
  227. conversation_history = []
  228. last_analysis_index = None
  229. messages_since_last_analysis = 0
  230. # ====================
  231. # Fonctions Utilitaires
  232. # ====================
  233. def split_message(message, max_length=2000):
  234. """Divise un message en plusieurs segments de longueur maximale spécifiée."""
  235. if len(message) <= max_length:
  236. return [message]
  237. parts = []
  238. current_part = ""
  239. for line in message.split('\n'):
  240. if len(current_part) + len(line) + 1 > max_length:
  241. parts.append(current_part)
  242. current_part = line + '\n'
  243. else:
  244. current_part += line + '\n'
  245. if current_part:
  246. parts.append(current_part)
  247. return parts
  248. def has_text(text):
  249. """Détermine si le texte fourni est non vide après suppression des espaces."""
  250. return bool(text.strip())
  251. def resize_image(image_bytes, mode='high', attachment_filename=None):
  252. """Redimensionne l'image selon le mode spécifié."""
  253. try:
  254. with Image.open(BytesIO(image_bytes)) as img:
  255. original_format = img.format # Stocker le format original
  256. if mode == 'high':
  257. img.thumbnail((2000, 2000))
  258. if min(img.size) < 768:
  259. scale = 768 / min(img.size)
  260. new_size = tuple(int(x * scale) for x in img.size)
  261. img = img.resize(new_size, Image.Resampling.LANCZOS)
  262. elif mode == 'low':
  263. img = img.resize((512, 512))
  264. buffer = BytesIO()
  265. img_format = img.format or _infer_image_format(attachment_filename)
  266. img.save(buffer, format=img_format)
  267. return buffer.getvalue()
  268. except Exception as e:
  269. logger.error(f"Erreur lors du redimensionnement de l'image : {e}")
  270. raise
  271. def _infer_image_format(filename):
  272. """Déduit le format de l'image basé sur l'extension du fichier."""
  273. if filename:
  274. _, ext = os.path.splitext(filename)
  275. ext = ext.lower()
  276. format_mapping = {
  277. '.jpg': 'JPEG',
  278. '.jpeg': 'JPEG',
  279. '.png': 'PNG',
  280. '.gif': 'GIF',
  281. '.bmp': 'BMP',
  282. '.tiff': 'TIFF'
  283. }
  284. return format_mapping.get(ext, 'PNG')
  285. return 'PNG'
  286. def extract_text_from_message(message):
  287. """Extrait le texte du message."""
  288. content = message.get("content", "")
  289. if isinstance(content, list):
  290. texts = [part.get("text", "") for part in content if isinstance(part, dict) and part.get("text")]
  291. return ' '.join(texts)
  292. elif isinstance(content, str):
  293. return content
  294. return ""
  295. def calculate_cost(usage, model='gpt-4o-mini'):
  296. """Calcule le coût basé sur l'utilisation des tokens."""
  297. input_tokens = usage.get('prompt_tokens', 0)
  298. output_tokens = usage.get('completion_tokens', 0)
  299. model_costs = {
  300. 'gpt-4o': {
  301. 'input_rate': 5.00 / 1_000_000, # 5$ pour 1M tokens d'entrée
  302. 'output_rate': 15.00 / 1_000_000 # 15$ pour 1M tokens de sortie
  303. },
  304. 'gpt-4o-mini': {
  305. 'input_rate': 0.150 / 1_000_000, # 0.150$ pour 1M tokens d'entrée
  306. 'output_rate': 0.600 / 1_000_000 # 0.600$ pour 1M tokens de sortie
  307. }
  308. }
  309. rates = model_costs.get(model, model_costs['gpt-4o-mini'])
  310. input_cost = input_tokens * rates['input_rate']
  311. output_cost = output_tokens * rates['output_rate']
  312. total_cost = input_cost + output_cost
  313. if model not in model_costs:
  314. logger.warning(f"Modèle inconnu '{model}'. Utilisation des tarifs par défaut pour 'gpt-4o-mini'.")
  315. return input_tokens, output_tokens, total_cost
  316. async def read_text_file(attachment):
  317. """Lit le contenu d'un fichier texte attaché."""
  318. file_bytes = await attachment.read()
  319. return file_bytes.decode('utf-8')
  320. async def encode_image_from_attachment(attachment, mode='high'):
  321. """Encode une image depuis une pièce jointe en base64 après redimensionnement."""
  322. image_data = await attachment.read()
  323. resized_image = resize_image(image_data, mode=mode, attachment_filename=attachment.filename)
  324. return base64.b64encode(resized_image).decode('utf-8')
  325. # =================================
  326. # Interaction avec OpenAI
  327. # =================================
  328. # Charger l'encodeur pour le modèle GPT-4o mini
  329. encoding = tiktoken.get_encoding("o200k_base")
  330. async def call_openai_model(model, messages, max_tokens, temperature=0.8):
  331. """Appelle un modèle OpenAI avec les paramètres spécifiés et gère la réponse."""
  332. try:
  333. response = await openai_client.chat.completions.create(
  334. model=model,
  335. messages=messages,
  336. max_tokens=max_tokens,
  337. temperature=temperature
  338. )
  339. if response and response.choices:
  340. reply = response.choices[0].message.content
  341. # Ne pas logger les réponses de 'gpt-4o-mini' et 'gpt-4o'
  342. if model not in ["gpt-4o-mini", "gpt-4o"]:
  343. logger.info(f"Réponse de {model}: {reply[:100]}...")
  344. if hasattr(response, 'usage') and response.usage:
  345. usage = {
  346. 'prompt_tokens': response.usage.prompt_tokens,
  347. 'completion_tokens': response.usage.completion_tokens
  348. }
  349. _, _, total_cost = calculate_cost(usage, model=model)
  350. # Log avec les tokens d'entrée et de sortie
  351. logger.info(f"Coût de l'utilisation de {model}: ${total_cost:.4f} / Input: {usage['prompt_tokens']} / Output: {usage['completion_tokens']}")
  352. else:
  353. logger.warning(f"Informations d'utilisation non disponibles pour {model}.")
  354. return reply
  355. except OpenAIError as e:
  356. logger.error(f"Erreur lors de l'appel à l'API OpenAI avec {model}: {e}")
  357. except Exception as e:
  358. logger.error(f"Erreur inattendue lors de l'appel à l'API OpenAI avec {model}: {e}")
  359. return None
  360. async def call_gpt4o_for_image_analysis(image_data, user_text=None, detail='high'):
  361. """Appelle GPT-4o pour analyser une image."""
  362. prompt = IMAGE_ANALYSIS_PROMPT
  363. if user_text:
  364. prompt += f" Voici ce que l'on te décrit : \"{user_text}\"."
  365. message_to_send = {
  366. "role": "user",
  367. "content": [
  368. {"type": "text", "text": prompt},
  369. {
  370. "type": "image_url",
  371. "image_url": {
  372. "url": f"data:image/jpeg;base64,{image_data}",
  373. "detail": detail
  374. }
  375. }
  376. ]
  377. }
  378. # Obtenir la date et l'heure actuelles
  379. tz = pytz.timezone('Europe/Paris')
  380. current_datetime = datetime.now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
  381. # Créer un message système avec la date et l'heure
  382. date_message = {
  383. "role": "system",
  384. "content": f"Date et heure actuelles : {current_datetime}"
  385. }
  386. # Construire la liste des messages avec le message de date ajouté
  387. messages = [
  388. {"role": "system", "content": PERSONALITY_PROMPT},
  389. date_message # Ajout du message de date et heure
  390. ] + conversation_history + [message_to_send]
  391. analysis = await call_openai_model(
  392. model="gpt-4o",
  393. messages=messages,
  394. max_tokens=4096,
  395. temperature=1.0
  396. )
  397. if analysis:
  398. logger.info(f"Analyse de l'image par GPT-4o : {analysis}")
  399. return analysis
  400. async def call_gpt4o_mini_with_analysis(analysis_text, user_name, user_question, has_text_flag):
  401. """Appelle GPT-4o Mini pour générer une réponse basée sur l'analyse de l'image."""
  402. system_messages = [
  403. {"role": "system", "content": PERSONALITY_PROMPT},
  404. {
  405. "role": "system",
  406. "content": f"L'analyse de l'image fournie est la suivante :\n{analysis_text}\n\n"
  407. }
  408. ]
  409. if has_text_flag:
  410. user_content = (
  411. f"{user_name} a posté un message contenant une image et a écrit avec : '{user_question}'. "
  412. "Réponds à l'utilisateur en te basant sur l'analyse, avec ta personnalité. "
  413. "Ne mentionne pas explicitement que l'analyse est pré-existante, fais comme si tu l'avais faite toi-même."
  414. )
  415. else:
  416. user_content = (
  417. f"{user_name} a partagé une image sans texte additionnel. "
  418. "Commente l'image en te basant sur l'analyse, avec ta personnalité. "
  419. "Ne mentionne pas que l'analyse a été fournie à l'avance, réagis comme si tu l'avais toi-même effectuée."
  420. )
  421. user_message = {"role": "user", "content": user_content}
  422. messages = system_messages + conversation_history + [user_message]
  423. reply = await call_openai_model(
  424. model="gpt-4o-mini",
  425. messages=messages,
  426. max_tokens=4096,
  427. temperature=1.0
  428. )
  429. return reply
  430. async def call_openai_api(user_text, user_name, image_data=None, detail='high'):
  431. """Appelle l'API OpenAI pour générer une réponse basée sur le texte et/ou l'image."""
  432. text = f"{user_name} dit : {user_text}"
  433. if image_data:
  434. text += " (a posté une image.)"
  435. message_to_send = {
  436. "role": "user",
  437. "content": [
  438. {"type": "text", "text": text}
  439. ]
  440. }
  441. if image_data:
  442. message_to_send["content"].append({
  443. "type": "image_url",
  444. "image_url": {
  445. "url": f"data:image/jpeg;base64,{image_data}",
  446. "detail": detail
  447. }
  448. })
  449. # Obtenir la date et l'heure actuelles dans le fuseau horaire 'Europe/Paris'
  450. tz = pytz.timezone('Europe/Paris')
  451. current_datetime = datetime.now(tz).strftime('%d/%m/%Y %H:%M:%S %Z')
  452. # Créer un message système avec la date et l'heure
  453. date_message = {
  454. "role": "system",
  455. "content": f"Date et heure actuelles : {current_datetime}"
  456. }
  457. # Construire la liste des messages avec le message de date ajouté
  458. messages = [
  459. {"role": "system", "content": PERSONALITY_PROMPT},
  460. date_message # Ajout du message de date et heure
  461. ] + conversation_history + [message_to_send]
  462. reply = await call_openai_model(
  463. model="gpt-4o-mini",
  464. messages=messages,
  465. max_tokens=4096,
  466. temperature=1.0
  467. )
  468. return reply
  469. # =====================================
  470. # Gestion du Contenu de l'Historique
  471. # =====================================
  472. async def remove_old_image_analyses(db_manager, new_analysis=False):
  473. """Supprime les anciennes analyses d'images de l'historique."""
  474. global conversation_history, last_analysis_index, messages_since_last_analysis
  475. if new_analysis:
  476. logger.debug("Nouvelle analyse détectée. Suppression des anciennes analyses.")
  477. conversation_history = [
  478. msg for msg in conversation_history
  479. if not (msg.get("role") == "system" and msg.get("content", "").startswith("__IMAGE_ANALYSIS__:"))
  480. ]
  481. last_analysis_index = len(conversation_history)
  482. messages_since_last_analysis = 0
  483. # Supprimer les analyses d'images de la base de données
  484. db_manager.delete_old_image_analyses()
  485. else:
  486. # Exemple de logique additionnelle si nécessaire
  487. pass
  488. async def add_to_conversation_history(db_manager, new_message):
  489. global conversation_history, last_analysis_index, messages_since_last_analysis
  490. # Exclure le PERSONALITY_PROMPT de l'historique
  491. if new_message.get("role") == "system" and new_message.get("content") == PERSONALITY_PROMPT:
  492. logger.debug("PERSONALITY_PROMPT système non ajouté à l'historique.")
  493. return
  494. # Gérer les analyses d'images
  495. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  496. await remove_old_image_analyses(db_manager, new_analysis=True)
  497. # Ajouter le message à l'historique en mémoire
  498. conversation_history.append(new_message)
  499. # Sauvegarder dans la base de données
  500. db_manager.save_message(new_message.get("role"), new_message.get("content"))
  501. logger.debug(f"Message ajouté à l'historique. Taille actuelle : {len(conversation_history)}")
  502. # Mettre à jour les indices pour les analyses d'images
  503. if new_message.get("role") == "system" and new_message.get("content", "").startswith("__IMAGE_ANALYSIS__:"):
  504. last_analysis_index = len(conversation_history) - 1
  505. messages_since_last_analysis = 0
  506. else:
  507. await remove_old_image_analyses(db_manager, new_analysis=False)
  508. # Limiter l'historique à 50 messages
  509. if len(conversation_history) > 50:
  510. excess = len(conversation_history) - 50
  511. conversation_history = conversation_history[excess:]
  512. # Supprimer les messages les plus anciens de la base de données
  513. db_manager.delete_old_messages(excess)
  514. # =====================================
  515. # Gestion des Événements Discord
  516. # =====================================
  517. class MyDiscordBot(commands.Bot):
  518. def __init__(self, db_manager, **kwargs):
  519. super().__init__(**kwargs)
  520. self.db_manager = db_manager
  521. self.message_queue = asyncio.Queue()
  522. self.reminder_task = None
  523. self.random_message_delay = 240
  524. self.inactivity_task = None
  525. self.last_activity = datetime.now(pytz.timezone('Europe/Paris'))
  526. self.guild_id = GUILD_ID
  527. async def setup_hook(self):
  528. """Hook d'initialisation asynchrone pour configurer des tâches supplémentaires."""
  529. self.processing_task = asyncio.create_task(self.process_messages())
  530. self.reminder_task = asyncio.create_task(self.process_reminders())
  531. self.inactivity_task = asyncio.create_task(self.monitor_inactivity())
  532. # Charger les commandes slash
  533. await self.add_cog(AdminCommands(self, self.db_manager))
  534. await self.add_cog(ReminderCommands(self, self.db_manager))
  535. await self.add_cog(HelpCommands(self))
  536. await self.tree.sync() # Synchroniser les commandes slash
  537. async def close(self):
  538. if openai_client:
  539. await openai_client.close()
  540. self.db_manager.close_connection()
  541. self.processing_task.cancel()
  542. if self.reminder_task:
  543. self.reminder_task.cancel()
  544. if self.inactivity_task:
  545. self.inactivity_task.cancel()
  546. await super().close()
  547. async def get_personalized_reminder(self, content, user):
  548. """Utilise l'API OpenAI pour personnaliser le contenu du rappel."""
  549. messages = [
  550. {"role": "system", "content": PERSONALITY_PROMPT},
  551. {"role": "user", "content": f"Personnalise le rappel suivant pour {user.name} : {content}"}
  552. ]
  553. reply = await call_openai_model(
  554. model="gpt-4o-mini",
  555. messages=messages,
  556. max_tokens=4096,
  557. temperature=1.0
  558. )
  559. return reply if reply else content
  560. async def on_ready(self):
  561. """Événement déclenché lorsque le bot est prêt."""
  562. logger.info(f'{BOT_NAME} connecté en tant que {self.user}')
  563. if not conversation_history:
  564. logger.info("Aucun historique trouvé. L'historique commence vide.")
  565. # Envoyer un message de version dans le canal Discord
  566. channel = self.get_channel(chatgpt_channel_id)
  567. if channel:
  568. try:
  569. embed = discord.Embed(
  570. title="Bot Démarré",
  571. description=f"🎉 {BOT_NAME} est en ligne ! Version {BOT_VERSION}",
  572. color=0x00ff00 # Vert
  573. )
  574. await channel.send(embed=embed)
  575. logger.info(f"Message de connexion envoyé dans le canal ID {chatgpt_channel_id}")
  576. except discord.Forbidden:
  577. logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal ID {chatgpt_channel_id}.")
  578. except discord.HTTPException as e:
  579. logger.error(f"Erreur lors de l'envoi du message de connexion : {e}")
  580. else:
  581. logger.error(f"Canal avec ID {chatgpt_channel_id} non trouvé.")
  582. async def on_message(self, message):
  583. """Événement déclenché lorsqu'un message est envoyé dans un canal suivi."""
  584. # Ignorer les messages provenant d'autres canaux ou du bot lui-même
  585. if message.channel.id != chatgpt_channel_id or message.author == self.user:
  586. return
  587. # Mettre à jour le dernier temps d'activité
  588. self.last_activity = datetime.now(pytz.timezone('Europe/Paris'))
  589. await self.message_queue.put(message)
  590. async def monitor_inactivity(self):
  591. """Tâche en arrière-plan pour surveiller l'inactivité et envoyer des messages aléatoires."""
  592. await self.wait_until_ready()
  593. while not self.is_closed():
  594. try:
  595. # Calculer le temps écoulé depuis la dernière activité
  596. now = datetime.now(pytz.timezone('Europe/Paris'))
  597. elapsed = (now - self.last_activity).total_seconds() / 60 # en minutes
  598. if elapsed >= self.random_message_delay:
  599. # Vérifier si on est en dehors des heures silencieuses (minuit à 7h)
  600. if not (now.hour >= 0 and now.hour < 7):
  601. await self.perform_random_action()
  602. # Réinitialiser le dernier temps d'activité
  603. self.last_activity = now
  604. await asyncio.sleep(60) # Vérifier toutes les minutes
  605. except Exception as e:
  606. logger.error(f"Erreur dans la tâche de surveillance d'inactivité: {e}")
  607. await asyncio.sleep(60)
  608. async def perform_random_action(self):
  609. """Effectue l'action aléatoire de réagir à l'activité d'un membre."""
  610. guild = self.get_guild(self.guild_id) # Assurez-vous que self.guild_id est défini
  611. if not guild:
  612. logger.error("Guild non trouvée.")
  613. return
  614. # Obtenir les membres avec le rôle spécifique
  615. specific_role = get(guild.roles, name=SPECIFIC_ROLE_NAME)
  616. if not specific_role:
  617. logger.error(f"Rôle '{SPECIFIC_ROLE_NAME}' non trouvé dans la guild.")
  618. return
  619. active_members = [member for member in guild.members if specific_role in member.roles and member.activity]
  620. if active_members:
  621. # Sélectionner un membre aléatoire
  622. selected_member = random.choice(active_members)
  623. activity = selected_member.activity
  624. # Récupérer les informations d'activité
  625. # Vérifier si l'activité est de type Spotify
  626. if isinstance(activity, discord.Spotify):
  627. activity_details = (
  628. f"Spotify - {activity.title} by {', '.join(activity.artists)}"
  629. f" from the album {activity.album}"
  630. )
  631. else:
  632. # Pour d'autres types d'activités
  633. activity_details = f"{activity.type.name} - {activity.name}" if activity else "Aucune activité spécifique."
  634. # Préparer le message à envoyer à OpenAI
  635. messages = [
  636. {"role": "system", "content": PERSONALITY_PROMPT},
  637. {"role": "user", "content": f"L'utilisateur {selected_member.mention} est actuellement actif: {activity_details}. Réagis à ce que l'utilisateur est en train de faire en t'adressant à lui et en le citant."}
  638. ]
  639. reply = await call_openai_model(
  640. model="gpt-4o-mini",
  641. messages=messages,
  642. max_tokens=4096,
  643. temperature=1.0
  644. )
  645. if reply:
  646. channel = self.get_channel(chatgpt_channel_id)
  647. if channel:
  648. await channel.send(reply)
  649. self.db_manager.save_message('assistant', reply)
  650. conversation_history.append({
  651. "role": "assistant",
  652. "content": reply
  653. })
  654. logger.info(f"Message aléatoire posté par le bot.")
  655. else:
  656. logger.warning("OpenAI n'a pas généré de réponse pour l'activité du membre.")
  657. else:
  658. # Aucun membre actif, envoyer un message de boredom
  659. messages = [
  660. {"role": "system", "content": PERSONALITY_PROMPT},
  661. {"role": "user", "content": "Personne ne fait quoi que ce soit et on s'ennuie ici. Génère un message approprié avec ta personnalité."}
  662. ]
  663. reply = await call_openai_model(
  664. model="gpt-4o-mini",
  665. messages=messages,
  666. max_tokens=4096,
  667. temperature=1.0
  668. )
  669. if reply:
  670. channel = self.get_channel(chatgpt_channel_id)
  671. if channel:
  672. await channel.send(reply)
  673. self.db_manager.save_message('assistant', reply)
  674. conversation_history.append({
  675. "role": "assistant",
  676. "content": reply
  677. })
  678. logger.info(f"Message d'ennui posté par le bot.")
  679. else:
  680. logger.warning("OpenAI n'a pas généré de réponse pour l'état d'ennui.")
  681. # Actualiser le délai aléatoire
  682. self.random_message_delay = random.randint(180, 360)
  683. logger.info(f"`random_message_delay` mis à jour à {self.random_message_delay} minutes.")
  684. async def process_reminders(self):
  685. """Tâche en arrière-plan pour vérifier et envoyer les rappels."""
  686. await self.wait_until_ready()
  687. while not self.is_closed():
  688. try:
  689. now = datetime.now(pytz.timezone('Europe/Paris')) # Utiliser le même fuseau horaire
  690. reminders = self.db_manager.get_due_reminders(now.strftime('%Y-%m-%d %H:%M:%S'))
  691. for reminder in reminders:
  692. try:
  693. user = await self.fetch_user(int(reminder['user_id']))
  694. except discord.NotFound:
  695. logger.error(f"Utilisateur avec l'ID {reminder['user_id']} non trouvé.")
  696. continue
  697. except discord.HTTPException as e:
  698. logger.error(f"Erreur lors de la récupération de l'utilisateur {reminder['user_id']}: {e}")
  699. continue
  700. try:
  701. channel = await self.fetch_channel(int(reminder['channel_id']))
  702. except discord.NotFound:
  703. logger.error(f"Canal avec l'ID {reminder['channel_id']} non trouvé.")
  704. continue
  705. except discord.HTTPException as e:
  706. logger.error(f"Erreur lors de la récupération du canal {reminder['channel_id']}: {e}")
  707. continue
  708. if channel and user:
  709. personalized_content = await self.get_personalized_reminder(reminder['content'], user)
  710. try:
  711. reminder_message = f"{user.mention} 🕒 Rappel : {personalized_content}"
  712. await channel.send(reminder_message)
  713. logger.info(f"Rappel envoyé à {user} dans le canal {channel}.")
  714. self.db_manager.save_message('assistant', reminder_message)
  715. conversation_history.append({
  716. "role": "assistant",
  717. "content": reminder_message
  718. })
  719. except discord.Forbidden:
  720. logger.error(f"Permissions insuffisantes pour envoyer des messages dans le canal {channel}.")
  721. except discord.HTTPException as e:
  722. logger.error(f"Erreur lors de l'envoi du message dans le canal {channel}: {e}")
  723. else:
  724. logger.warning(f"Canal ou utilisateur introuvable pour le rappel ID {reminder['id']}.")
  725. # Supprimer le rappel après envoi
  726. self.db_manager.delete_reminder(reminder['id'])
  727. await asyncio.sleep(60) # Vérifier toutes les minutes
  728. except Exception as e:
  729. logger.error(f"Erreur dans la tâche de rappels: {e}")
  730. await asyncio.sleep(60)
  731. async def process_messages(self):
  732. """Tâche en arrière-plan pour traiter les messages séquentiellement."""
  733. while True:
  734. message = await self.message_queue.get()
  735. try:
  736. await self.handle_message(message)
  737. except Exception as e:
  738. logger.error(f"Erreur lors du traitement du message : {e}")
  739. try:
  740. await message.channel.send("Une erreur est survenue lors du traitement de votre message.")
  741. except Exception as send_error:
  742. logger.error(f"Erreur lors de l'envoi du message d'erreur : {send_error}")
  743. finally:
  744. self.message_queue.task_done()
  745. async def handle_message(self, message):
  746. """Fonction pour traiter un seul message."""
  747. global conversation_history, last_analysis_index, messages_since_last_analysis
  748. user_text = message.content.strip()
  749. # Traiter les pièces jointes
  750. image_data = None
  751. file_content = None
  752. attachment_filename = None
  753. allowed_extensions = ['.txt', '.py', '.html', '.css', '.js']
  754. if message.attachments:
  755. for attachment in message.attachments:
  756. if any(attachment.filename.lower().endswith(ext) for ext in allowed_extensions):
  757. file_content = await read_text_file(attachment)
  758. attachment_filename = attachment.filename
  759. break
  760. elif attachment.content_type and attachment.content_type.startswith('image/'):
  761. image_data = await encode_image_from_attachment(attachment, mode='high')
  762. break
  763. # Traitement des images
  764. if image_data:
  765. has_user_text = has_text(user_text)
  766. user_text_to_use = user_text if has_user_text else None
  767. temp_msg = await message.channel.send(f"*{BOT_NAME} observe l'image...*")
  768. try:
  769. # Analyser l'image avec GPT-4o
  770. analysis = await call_gpt4o_for_image_analysis(image_data, user_text=user_text_to_use)
  771. if analysis:
  772. # Ajouter l'analyse à l'historique
  773. analysis_message = {
  774. "role": "system",
  775. "content": f"__IMAGE_ANALYSIS__:{analysis}"
  776. }
  777. await add_to_conversation_history(self.db_manager, analysis_message)
  778. # Générer une réponse basée sur l'analyse
  779. reply = await call_gpt4o_mini_with_analysis(analysis, message.author.name, user_text, has_user_text)
  780. if reply:
  781. await temp_msg.delete()
  782. await message.channel.send(reply)
  783. # Construire et ajouter les messages à l'historique
  784. user_message_text = f"{user_text} (a posté une image.)" if has_user_text else (
  785. "Une image a été postée, mais elle n'est pas disponible pour analyse directe. Veuillez vous baser uniquement sur l'analyse fournie."
  786. )
  787. user_message = {
  788. "role": "user",
  789. "content": f"{message.author.name} dit : {user_message_text}"
  790. }
  791. assistant_message = {
  792. "role": "assistant",
  793. "content": reply
  794. }
  795. await add_to_conversation_history(self.db_manager, user_message)
  796. await add_to_conversation_history(self.db_manager, assistant_message)
  797. else:
  798. await temp_msg.delete()
  799. await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
  800. else:
  801. await temp_msg.delete()
  802. await message.channel.send("Désolé, je n'ai pas pu analyser l'image.")
  803. except Exception as e:
  804. await temp_msg.delete()
  805. await message.channel.send("Une erreur est survenue lors du traitement de l'image.")
  806. logger.error(f"Erreur lors du traitement de l'image: {e}")
  807. return # Ne pas continuer le traitement après une image
  808. # Ajouter le contenu du fichier au texte de l'utilisateur si un fichier est présent
  809. if file_content:
  810. user_text += f"\nContenu du fichier {attachment_filename}:\n{file_content}"
  811. # Vérifier si le texte n'est pas vide
  812. if not has_text(user_text):
  813. return # Ne pas appeler l'API si le texte est vide
  814. async with message.channel.typing():
  815. try:
  816. # Appeler l'API OpenAI pour le texte
  817. reply = await call_openai_api(user_text, message.author.name)
  818. if reply:
  819. # Diviser le message en plusieurs parties si nécessaire
  820. message_parts = split_message(reply)
  821. for part in message_parts:
  822. await message.channel.send(part)
  823. # Construire et ajouter les messages à l'historique
  824. user_message = {
  825. "role": "user",
  826. "content": f"{message.author.name} dit : {user_text}"
  827. }
  828. assistant_message = {
  829. "role": "assistant",
  830. "content": reply
  831. }
  832. await add_to_conversation_history(self.db_manager, user_message)
  833. await add_to_conversation_history(self.db_manager, assistant_message)
  834. else:
  835. await message.channel.send("Désolé, je n'ai pas pu générer une réponse.")
  836. except Exception as e:
  837. await message.channel.send("Une erreur est survenue lors de la génération de la réponse.")
  838. logger.error(f"Erreur lors du traitement du texte: {e}")
  839. # ============================
  840. # Commandes Slash via Cogs
  841. # ============================
  842. class AdminCommands(commands.Cog):
  843. """Cog pour les commandes administratives."""
  844. def __init__(self, bot: commands.Bot, db_manager):
  845. self.bot = bot
  846. self.db_manager = db_manager
  847. @app_commands.command(name="reset_history", description="Réinitialise l'historique des conversations.")
  848. @app_commands.checks.has_permissions(administrator=True)
  849. @admin_command
  850. async def reset_history(self, interaction: discord.Interaction):
  851. """Réinitialise l'historique des conversations."""
  852. global conversation_history
  853. conversation_history = []
  854. self.db_manager.reset_history()
  855. await interaction.response.send_message("✅ L'historique des conversations a été réinitialisé.")
  856. @reset_history.error
  857. async def reset_history_error(self, interaction: discord.Interaction, error):
  858. """Gère les erreurs de la commande reset_history."""
  859. if isinstance(error, app_commands.CheckFailure):
  860. await interaction.response.send_message("❌ Vous n'avez pas la permission d'utiliser cette commande.")
  861. else:
  862. logger.error(f"Erreur lors de l'exécution de la commande reset_history: {error}")
  863. await interaction.response.send_message("Une erreur est survenue lors de l'exécution de la commande.")
  864. class ReminderCommands(commands.Cog):
  865. """Cog pour les commandes de rappel."""
  866. def __init__(self, bot: commands.Bot, db_manager: DatabaseManager):
  867. self.bot = bot
  868. self.db_manager = db_manager
  869. @app_commands.command(name="rappel", description="Créer un rappel")
  870. @app_commands.describe(date="Date du rappel (DD/MM/YYYY)")
  871. @app_commands.describe(time="Heure du rappel (HH:MM, 24h)")
  872. @app_commands.describe(content="Contenu du rappel")
  873. async def rappel(self, interaction: discord.Interaction, date: str, time: str, content: str):
  874. """Commande pour créer un rappel."""
  875. user = interaction.user
  876. channel = interaction.channel
  877. # Valider et parser la date et l'heure
  878. try:
  879. remind_datetime_str = f"{date} {time}"
  880. remind_datetime = datetime.strptime(remind_datetime_str, "%d/%m/%Y %H:%M")
  881. # Vous pouvez ajuster le fuseau horaire selon vos besoins
  882. tz = pytz.timezone('Europe/Paris') # Exemple de fuseau horaire
  883. remind_datetime = tz.localize(remind_datetime)
  884. now = datetime.now(tz)
  885. if remind_datetime <= now:
  886. await interaction.response.send_message("❌ La date et l'heure doivent être dans le futur.", ephemeral=True)
  887. return
  888. except ValueError:
  889. await interaction.response.send_message("❌ Format de date ou d'heure invalide. Utilisez DD/MM/YYYY pour la date et HH:MM pour l'heure.", ephemeral=True)
  890. return
  891. # Ajouter le rappel à la base de données
  892. self.db_manager.add_reminder(
  893. user_id=str(user.id),
  894. channel_id=str(channel.id),
  895. remind_at=remind_datetime.strftime('%Y-%m-%d %H:%M:%S'),
  896. content=content
  897. )
  898. # Créer un embed pour la confirmation
  899. embed = discord.Embed(
  900. title="Rappel Créé ✅",
  901. description=(
  902. f"**Date et Heure** : {remind_datetime.strftime('%d/%m/%Y %H:%M')}\n"
  903. f"**Contenu** : {content}"
  904. ),
  905. color=0x00ff00, # Vert
  906. timestamp=datetime.now(timezone.utc)
  907. )
  908. embed.set_footer(text=f"Créé par {user}", icon_url=user.display_avatar.url if user.avatar else user.default_avatar.url)
  909. # Envoyer l'embed de confirmation
  910. await interaction.response.send_message(embed=embed)
  911. @rappel.error
  912. async def rappel_error(self, interaction: discord.Interaction, error):
  913. """Gère les erreurs de la commande rappel."""
  914. logger.error(f"Erreur lors de l'exécution de la commande rappel: {error}")
  915. await interaction.response.send_message("❌ Une erreur est survenue lors de la création du rappel.")
  916. @app_commands.command(name="mes_rappels", description="Voir tous vos rappels enregistrés à venir.")
  917. async def mes_rappels(self, interaction: discord.Interaction):
  918. """Commande pour voir tous les rappels de l'utilisateur."""
  919. user = interaction.user
  920. reminders = self.db_manager.get_user_reminders(str(user.id))
  921. if not reminders:
  922. await interaction.response.send_message(
  923. "🕒 Vous n'avez aucun rappel enregistré à venir.",
  924. ephemeral=True
  925. )
  926. return
  927. # Créer l'embed
  928. embed = discord.Embed(
  929. title="📋 Vos Rappels à Venir",
  930. description=f"Voici la liste de vos rappels enregistrés :",
  931. color=0x00ff00, # Vert
  932. timestamp=datetime.now(timezone.utc)
  933. )
  934. embed.set_footer(text=f"Demandé par {user}", icon_url=user.display_avatar.url if user.avatar else user.default_avatar.url)
  935. # Ajouter chaque rappel comme un champ dans l'embed
  936. for reminder in reminders:
  937. remind_at = reminder['remind_at']
  938. remind_at_formatted = remind_at.strftime('%d/%m/%Y %H:%M')
  939. embed.add_field(
  940. name=f"ID {reminder['id']} - {remind_at_formatted}",
  941. value=reminder['content'],
  942. inline=False
  943. )
  944. await interaction.response.send_message(embed=embed)
  945. @app_commands.command(name="supprimer_rappel", description="Supprime un de vos rappels à venir en utilisant son ID.")
  946. @app_commands.describe(id="L'ID du rappel à supprimer")
  947. async def supprimer_rappel(self, interaction: discord.Interaction, id: int):
  948. """Commande pour supprimer un rappel spécifique."""
  949. user = interaction.user
  950. reminder_id = id
  951. # Récupérer le rappel par ID
  952. reminder = self.db_manager.get_reminder_by_id(reminder_id)
  953. if not reminder:
  954. await interaction.response.send_message(
  955. f"❌ Aucun rappel trouvé avec l'ID `{reminder_id}`.",
  956. ephemeral=True
  957. )
  958. return
  959. # Vérifier si le rappel appartient à l'utilisateur
  960. if reminder['user_id'] != str(user.id):
  961. await interaction.response.send_message(
  962. "❌ Vous ne pouvez supprimer que vos propres rappels.",
  963. ephemeral=True
  964. )
  965. return
  966. # Supprimer le rappel
  967. self.db_manager.delete_reminder(reminder_id)
  968. # Confirmer la suppression à l'utilisateur
  969. embed = discord.Embed(
  970. title="Rappel Supprimé ✅",
  971. description=f"Le rappel avec l'ID `{reminder_id}` et le contenu \"{reminder['content']}\" a été supprimé avec succès.",
  972. color=0xff0000, # Rouge
  973. timestamp=datetime.now(timezone.utc)
  974. )
  975. embed.set_footer(text=f"Supprimé par {user}", icon_url=user.display_avatar.url if user.avatar else user.default_avatar.url)
  976. await interaction.response.send_message(embed=embed)
  977. @supprimer_rappel.error
  978. async def supprimer_rappel_error(self, interaction: discord.Interaction, error):
  979. """Gère les erreurs de la commande supprimer_rappel."""
  980. logger.error(f"Erreur lors de l'exécution de la commande supprimer_rappel: {error}")
  981. await interaction.response.send_message("❌ Une erreur est survenue lors de la suppression du rappel.", ephemeral=True)
  982. class HelpCommands(commands.Cog):
  983. """Cog pour la commande /help."""
  984. def __init__(self, bot: commands.Bot):
  985. self.bot = bot
  986. @app_commands.command(name="help", description="Liste toutes les commandes disponibles.")
  987. async def help(self, interaction: discord.Interaction):
  988. """Commande /help qui liste toutes les commandes disponibles dans un embed."""
  989. general_commands = []
  990. admin_commands = []
  991. # Parcourir toutes les commandes de l'arbre de commandes du bot
  992. for command in self.bot.tree.get_commands():
  993. # Ignorer la commande /help elle-même pour éviter l'auto-inclusion
  994. if command.name == "help":
  995. continue
  996. # Déterminer si la commande est réservée aux administrateurs en vérifiant l'attribut personnalisé
  997. is_admin = getattr(command.callback, 'is_admin', False)
  998. # Ajouter la commande à la liste appropriée
  999. if is_admin:
  1000. admin_commands.append((command.name, command.description))
  1001. else:
  1002. general_commands.append((command.name, command.description))
  1003. # Créer l'embed
  1004. embed = discord.Embed(
  1005. title="📚 Liste des Commandes",
  1006. description="Voici la liste des commandes disponibles :",
  1007. color=0x00ff00
  1008. )
  1009. if general_commands:
  1010. general_desc = "\n".join([f"`/{name}` - {desc}" for name, desc in general_commands])
  1011. embed.add_field(name="Commandes Générales", value=general_desc, inline=False)
  1012. if admin_commands:
  1013. admin_desc = "\n".join([f"`/{name}` - {desc} *(Admin)*" for name, desc in admin_commands])
  1014. embed.add_field(name="Commandes Administratives", value=admin_desc, inline=False)
  1015. embed.set_footer(text=f"Demandé par {interaction.user}", icon_url=interaction.user.display_avatar.url if interaction.user.avatar else interaction.user.default_avatar.url)
  1016. # Envoyer l'embed en réponse
  1017. await interaction.response.send_message(embed=embed)
  1018. async def setup(bot: commands.Bot):
  1019. await bot.add_cog(HelpCommands(bot))
  1020. # ============================
  1021. # Démarrage du Bot Discord
  1022. # ============================
  1023. def main():
  1024. db_manager = DatabaseManager()
  1025. if not db_manager.connection:
  1026. logger.error("Le bot ne peut pas démarrer sans connexion à la base de données.")
  1027. return
  1028. db_manager.load_conversation_history()
  1029. # Initialiser le bot avec le préfixe "!" et les intents définis
  1030. bot = MyDiscordBot(command_prefix="!", db_manager=db_manager, intents=intents)
  1031. # Démarrer le bot
  1032. try:
  1033. bot.run(DISCORD_TOKEN)
  1034. except Exception as e:
  1035. logger.error(f"Erreur lors du démarrage du bot Discord: {e}")
  1036. finally:
  1037. db_manager.close_connection()
  1038. if __name__ == "__main__":
  1039. main()