Initialisation du repository de Beta
This commit is contained in:
commit
14985f6dbb
9469 changed files with 1903273 additions and 0 deletions
BIN
commandes/autres/__pycache__/config.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/config.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/config.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/logger.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/logger.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/logger.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/logger.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/ping.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/ping.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/ping.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/ping.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/send_message.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/send_message.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/send_message.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/send_message.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/setgoodbyechannel.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/setgoodbyechannel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/setgoodbyechannel.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/setgoodbyechannel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/setmuterole.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/setmuterole.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/setwelcomechannel.cpython-311.pyc
Normal file
BIN
commandes/autres/__pycache__/setwelcomechannel.cpython-311.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/setwelcomechannel.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/setwelcomechannel.cpython-312.pyc
Normal file
Binary file not shown.
BIN
commandes/autres/__pycache__/utils.cpython-312.pyc
Normal file
BIN
commandes/autres/__pycache__/utils.cpython-312.pyc
Normal file
Binary file not shown.
91
commandes/autres/config.py
Normal file
91
commandes/autres/config.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import os
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_file: str = 'config.json'):
|
||||
self.config_file = config_file
|
||||
self.data: Dict[str, Any] = {}
|
||||
self.load_config()
|
||||
|
||||
def load_config(self):
|
||||
"""Load configuration from file if it exists."""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
self.data = json.load(f)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
self.data = {}
|
||||
else:
|
||||
self.data = {}
|
||||
|
||||
def save_config(self):
|
||||
"""Save configuration to file if dirty."""
|
||||
if self.dirty:
|
||||
try:
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.data, f, indent=4, ensure_ascii=False)
|
||||
self.dirty = False
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la sauvegarde de la configuration: {e}")
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""Get a configuration value."""
|
||||
return self.data.get(key, default)
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""Set a configuration value and save."""
|
||||
self.data[key] = value
|
||||
self.save_config()
|
||||
|
||||
def get_welcome_channel(self, guild_id: int) -> Optional[int]:
|
||||
"""Get welcome channel ID for a guild."""
|
||||
return self.get('welcome_channels', {}).get(str(guild_id))
|
||||
|
||||
def set_welcome_channel(self, guild_id: int, channel_id: int):
|
||||
"""Set welcome channel for a guild."""
|
||||
welcome_channels = self.get('welcome_channels', {})
|
||||
welcome_channels[str(guild_id)] = channel_id
|
||||
self.set('welcome_channels', welcome_channels)
|
||||
|
||||
def get_goodbye_channel(self, guild_id: int) -> Optional[int]:
|
||||
"""Get goodbye channel ID for a guild."""
|
||||
return self.get('goodbye_channels', {}).get(str(guild_id))
|
||||
|
||||
def set_goodbye_channel(self, guild_id: int, channel_id: int):
|
||||
"""Set goodbye channel for a guild."""
|
||||
goodbye_channels = self.get('goodbye_channels', {})
|
||||
goodbye_channels[str(guild_id)] = channel_id
|
||||
self.set('goodbye_channels', goodbye_channels)
|
||||
|
||||
def get_mute_role(self, guild_id: int) -> Optional[str]:
|
||||
"""Get mute role name for a guild."""
|
||||
return self.get('mute_roles', {}).get(str(guild_id))
|
||||
|
||||
def set_mute_role(self, guild_id: int, role_name: str):
|
||||
"""Set mute role for a guild."""
|
||||
mute_roles = self.get('mute_roles', {})
|
||||
mute_roles[str(guild_id)] = role_name
|
||||
self.set('mute_roles', mute_roles)
|
||||
self.save_config() # Force save for critical config
|
||||
|
||||
def get_allowed_words(self) -> list:
|
||||
"""Get list of allowed words."""
|
||||
return self.get('allowed_words', [])
|
||||
|
||||
def set_allowed_words(self, words: list):
|
||||
"""Set allowed words."""
|
||||
self.set('allowed_words', words)
|
||||
|
||||
def get_mod_report_channel(self, guild_id: int) -> Optional[int]:
|
||||
"""Get mod report channel ID for a guild."""
|
||||
return self.get('mod_report_channels', {}).get(str(guild_id))
|
||||
|
||||
def set_mod_report_channel(self, guild_id: int, channel_id: int):
|
||||
"""Set mod report channel for a guild."""
|
||||
mod_channels = self.get('mod_report_channels', {})
|
||||
mod_channels[str(guild_id)] = channel_id
|
||||
self.set('mod_report_channels', mod_channels)
|
||||
|
||||
# Global config instance
|
||||
config = Config()
|
||||
65
commandes/autres/help.py
Normal file
65
commandes/autres/help.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import discord
|
||||
import os
|
||||
|
||||
HELP_TEXT = """
|
||||
**Le Superviseur** - Bot de modération Discord
|
||||
|
||||
**Commandes générales:**
|
||||
• `@Le Superviseur {message}` - Parler avec l'IA (répondez à ses messages aussi)
|
||||
• `!help` - Afficher cette aide
|
||||
• `!status` - Afficher le statut du bot
|
||||
|
||||
**Modération:**
|
||||
• `!kick user [raison]` - Expulser un utilisateur
|
||||
• `!ban user [raison]` - Bannir un utilisateur
|
||||
• `!unban user` - Débannir un utilisateur
|
||||
• `!warn user [raison]` - Avertir un utilisateur
|
||||
• `!list_warnings` - Lister les avertissements
|
||||
• `!mute user duration` - Muter temporairement
|
||||
• `!timeout user duration` - Timeout temporaire
|
||||
• `!purge amount` - Supprimer des messages
|
||||
|
||||
**Gestion des salons:**
|
||||
• `!create_channel name1 [name2 name3 ...] [category type]` - Créer plusieurs salons (type: textuel/vocal, optionnel)
|
||||
• `!delete_channel channel1 [channel2 ...]` - Supprimer plusieurs salons
|
||||
• `!modify_channel channel new_name [new_category] [topic] [user_limit]` - Modifier un salon par session (pour multi, répétez la commande)
|
||||
• `!move_channel channel1 new_category [channel2 new_category ...]` - Déplacer plusieurs salons vers une nouvelle catégorie
|
||||
• `!rename_channel channel1 new_name [channel2 new_name ...]` - Renommer plusieurs salons
|
||||
|
||||
**Gestion des catégories:**
|
||||
• `!create_category name1 [name2 ...]` - Créer plusieurs catégories
|
||||
• `!delete_category category1 [category2 ...]` - Supprimer plusieurs catégories
|
||||
• `!modify_category category new_name` - Modifier une catégorie par commande (pour multi, répétez)
|
||||
• `!move_category category1 position [category2 position ...]` - Déplacer plusieurs catégories
|
||||
• `!rename_category category1 new_name [category2 new_name ...]` - Renommer plusieurs catégories
|
||||
|
||||
**Gestion des rôles:**
|
||||
• `!create_role name [color] [permissions]` - Créer un rôle par commande (pour multi, répétez)
|
||||
• `!delete_role role1 [role2 ...]` - Supprimer plusieurs rôles
|
||||
• `!modify_role role new_name [new_color] [new_permissions]` - Modifier un rôle par commande (pour multi, répétez)
|
||||
• `!add_role_to_user user role1 [role2 ...]` - Donner plusieurs rôles à un utilisateur
|
||||
• `!remove_role_from_user user role1 [role2 ...]` - Retirer plusieurs rôles d'un utilisateur
|
||||
• `!move_role role1 position [role2 position ...]` - Déplacer plusieurs rôles
|
||||
• `!rename_role role1 new_name [role2 new_name ...]` - Renommer plusieurs rôles
|
||||
|
||||
**Configuration:**
|
||||
• `!set_welcome_channel channel` - Canal de bienvenue
|
||||
• `!set_goodbye_channel channel` - Canal d'au revoir
|
||||
• `!set_mute_role role` - Rôle muet
|
||||
|
||||
**Outils:**
|
||||
• `!ping` - Vérifier latence
|
||||
|
||||
**Note:** Les commandes supportent maintenant plusieurs cibles à la fois quand applicable. Remplacez `user`, `channel`, etc. par des mentions ou IDs. Les [ ] indiquent paramètres optionnels. Pour créer/modifier multiples salons/rôles avec options différentes, utilisez des commandes séparées ou l'IA.
|
||||
|
||||
Le bot utilise l'IA Ollama pour des réponses intelligentes et peut gérer les salons, rôles et modération efficacement.
|
||||
"""
|
||||
|
||||
async def execute(bot, params, message):
|
||||
embed = discord.Embed(
|
||||
title="Aide du Superviseur",
|
||||
description=HELP_TEXT,
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
embed.set_footer(text="Utilisez les mentions pour référencer utilisateurs/salons/rôles")
|
||||
await message.channel.send(embed=embed)
|
||||
54
commandes/autres/logger.py
Normal file
54
commandes/autres/logger.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import logging
|
||||
import json
|
||||
from datetime import datetime
|
||||
from .config import config
|
||||
|
||||
class ActionLogger:
|
||||
def __init__(self, log_file: str = 'actions.log'):
|
||||
self.log_file = log_file
|
||||
self.logger = logging.getLogger('ActionLogger')
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
# File handler
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(logging.INFO)
|
||||
|
||||
# Formatter
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
# Add handler if not already added
|
||||
if not self.logger.handlers:
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
def log_action(self, action: str, user: str, guild: str, params: dict, success: bool, error: str = None):
|
||||
"""Log an action performed by the bot."""
|
||||
log_entry = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'action': action,
|
||||
'user': user,
|
||||
'guild': guild,
|
||||
'params': params,
|
||||
'success': success,
|
||||
'error': error
|
||||
}
|
||||
|
||||
message = f"ACTION: {json.dumps(log_entry, ensure_ascii=False)}"
|
||||
if success:
|
||||
self.logger.info(message)
|
||||
else:
|
||||
self.logger.error(message)
|
||||
|
||||
def log_message(self, level: str, message: str):
|
||||
"""Log a general message."""
|
||||
if level.casefold() == 'info':
|
||||
self.logger.info(message)
|
||||
elif level.casefold() == 'warning':
|
||||
self.logger.warning(message)
|
||||
elif level.casefold() == 'error':
|
||||
self.logger.error(message)
|
||||
elif level.casefold() == 'debug':
|
||||
self.logger.debug(message)
|
||||
|
||||
# Global logger instance
|
||||
action_logger = ActionLogger()
|
||||
9
commandes/autres/ping.py
Normal file
9
commandes/autres/ping.py
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import discord
|
||||
import time
|
||||
|
||||
async def execute(bot, params, message):
|
||||
start = time.perf_counter()
|
||||
temp_msg = await message.channel.send("Pong!")
|
||||
end = time.perf_counter()
|
||||
latency = round((end - start) * 1000) # ms
|
||||
await temp_msg.edit(content=f"Pong! Latence : {latency}ms\nLatence API : {round(bot.latency * 1000)}ms")
|
||||
95
commandes/autres/send_message.py
Normal file
95
commandes/autres/send_message.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import discord
|
||||
|
||||
async def execute(bot, params, message):
|
||||
channel_name = params.get('channel_name')
|
||||
dm_user_mention = params.get('dm_user')
|
||||
content = params.get('message', '')
|
||||
|
||||
if not content.strip():
|
||||
raise Exception("Le message ne peut pas être vide.")
|
||||
|
||||
guild = message.guild
|
||||
sent_count = 0
|
||||
|
||||
if channel_name:
|
||||
# Send to channel
|
||||
# 1. Exact match
|
||||
channel = discord.utils.get(guild.channels, name=channel_name)
|
||||
|
||||
# 2. Fuzzy match if not found (case insensitive, or partial match)
|
||||
if not channel:
|
||||
# Case insensitive
|
||||
channel = discord.utils.find(lambda c: c.name.lower() == channel_name.lower(), guild.channels)
|
||||
|
||||
if not channel:
|
||||
# Partial match (ignoring emojis/special chars often helps)
|
||||
# Normalize: remove non-alphanumeric chars for comparison
|
||||
def normalize(s):
|
||||
return "".join(c for c in s if c.isalnum()).lower()
|
||||
|
||||
target_norm = normalize(channel_name)
|
||||
if target_norm:
|
||||
channel = discord.utils.find(lambda c: normalize(c.name) == target_norm, guild.channels)
|
||||
|
||||
if not channel:
|
||||
# Last resort: contains
|
||||
channel = discord.utils.find(lambda c: channel_name.lower() in c.name.lower(), guild.channels)
|
||||
|
||||
if not channel or not isinstance(channel, discord.TextChannel):
|
||||
raise Exception(f"Salon '{channel_name}' introuvable.")
|
||||
|
||||
# Check permissions
|
||||
if not channel.permissions_for(guild.me).send_messages:
|
||||
raise Exception(f"Le bot n'a pas les permissions pour envoyer des messages dans #{channel.name}.")
|
||||
await bot.messaging.send_message_with_limit(channel, content)
|
||||
sent_count += 1
|
||||
|
||||
if dm_user_mention:
|
||||
# Send DM to user
|
||||
# Parse mention or ID or display name
|
||||
user = None
|
||||
if dm_user_mention.startswith('<@') and dm_user_mention.endswith('>'):
|
||||
# Extract ID from mention
|
||||
user_id_str = dm_user_mention.split('@')[-1].rstrip('>')
|
||||
if user_id_str.isdigit():
|
||||
user_id = int(user_id_str)
|
||||
user = guild.get_member(user_id)
|
||||
if not user:
|
||||
try:
|
||||
user = await guild.fetch_member(user_id)
|
||||
except discord.NotFound:
|
||||
raise Exception(f"Utilisateur introuvable via mention: {dm_user_mention}")
|
||||
elif dm_user_mention.isdigit():
|
||||
user_id = int(dm_user_mention)
|
||||
user = guild.get_member(user_id)
|
||||
if not user:
|
||||
try:
|
||||
user = await guild.fetch_member(user_id)
|
||||
except discord.NotFound:
|
||||
raise Exception(f"Utilisateur introuvable via ID: {dm_user_mention}")
|
||||
else:
|
||||
# Try to find by display name or username
|
||||
user = discord.utils.find(lambda m: m.display_name == dm_user_mention or m.name == dm_user_mention, guild.members)
|
||||
if not user:
|
||||
# Try partial match (case insensitive)
|
||||
dm_user_lower = dm_user_mention.lower()
|
||||
user = discord.utils.find(lambda m: dm_user_lower in m.display_name.lower() or dm_user_lower in m.name.lower(), guild.members)
|
||||
if not user:
|
||||
raise Exception(f"Utilisateur introuvable via nom: {dm_user_mention}")
|
||||
|
||||
try:
|
||||
await user.send(content)
|
||||
sent_count += 1
|
||||
except discord.Forbidden:
|
||||
raise Exception(f"Impossible d'envoyer un DM à {dm_user_mention}. L'utilisateur a peut-être bloqué le bot.")
|
||||
|
||||
if sent_count == 0:
|
||||
raise Exception("Au moins un destinataire (channel_name ou dm_user) doit être spécifié.")
|
||||
|
||||
# If both were specified and successful
|
||||
if channel_name and dm_user_mention and sent_count == 2:
|
||||
await message.channel.send(f"Message envoyé dans #{channel_name} et en DM à {dm_user_mention}.")
|
||||
elif channel_name:
|
||||
await message.channel.send(f"Message envoyé dans #{channel_name}.")
|
||||
elif dm_user_mention:
|
||||
await message.channel.send(f"Message envoyé en DM à {dm_user_mention}.")
|
||||
12
commandes/autres/setgoodbyechannel.py
Normal file
12
commandes/autres/setgoodbyechannel.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import discord
|
||||
from .config import config
|
||||
|
||||
async def execute(bot, params, message):
|
||||
channel_name = params.get('channel_name')
|
||||
guild = message.guild
|
||||
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
|
||||
if not channel:
|
||||
await message.channel.send(f"Salon '{channel_name}' introuvable.")
|
||||
return
|
||||
config.set_goodbye_channel(guild.id, channel.id)
|
||||
await message.channel.send(f"Salon d'au revoir défini sur {channel.mention}.")
|
||||
14
commandes/autres/setmuterole.py
Normal file
14
commandes/autres/setmuterole.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import discord
|
||||
from .config import config
|
||||
|
||||
async def execute(bot, params, message):
|
||||
role_name = params.get('role_name')
|
||||
if not role_name:
|
||||
await message.channel.send("Veuillez spécifier un nom de rôle.")
|
||||
return
|
||||
role = discord.utils.find(lambda r: r.name.casefold() == role_name.casefold(), message.guild.roles)
|
||||
if not role:
|
||||
await message.channel.send(f"Rôle '{role_name}' introuvable.")
|
||||
return
|
||||
config.set_mute_role(message.guild.id, role.name)
|
||||
await message.channel.send(f"Rôle mute défini sur {role.mention}.")
|
||||
12
commandes/autres/setwelcomechannel.py
Normal file
12
commandes/autres/setwelcomechannel.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import discord
|
||||
from .config import config
|
||||
|
||||
async def execute(bot, params, message):
|
||||
channel_name = params.get('channel_name')
|
||||
guild = message.guild
|
||||
channel = discord.utils.find(lambda c: c.name.casefold() == channel_name.casefold(), guild.channels)
|
||||
if not channel:
|
||||
await message.channel.send(f"Salon '{channel_name}' introuvable.")
|
||||
return
|
||||
config.set_welcome_channel(guild.id, channel.id)
|
||||
await message.channel.send(f"Salon de bienvenue défini sur {channel.mention}.")
|
||||
46
commandes/autres/status.py
Normal file
46
commandes/autres/status.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import discord
|
||||
from datetime import timedelta
|
||||
import time
|
||||
import psutil
|
||||
import os
|
||||
|
||||
async def execute(bot, params, message):
|
||||
embed = discord.Embed(
|
||||
title="Statut du Superviseur",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
|
||||
# Bot info
|
||||
embed.add_field(
|
||||
name="🤖 Bot",
|
||||
value=f"Connecté en tant que {bot.user.mention}\n"
|
||||
f"Serveur principal : {bot.guild_id}\n"
|
||||
f"Latence : {round(bot.latency * 1000)}ms",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# AI/Memory stats
|
||||
metrics = bot.get_metrics()
|
||||
embed.add_field(
|
||||
name="🧠 IA & Mémoire",
|
||||
value=f"Requêtes LLM : {metrics.get('total_llm_requests', 0)}\n"
|
||||
f"Succès : {metrics.get('total_llm_success', 0)}\n"
|
||||
f"Erreurs : {metrics.get('total_llm_errors', 0)}\n"
|
||||
f"Temps moyen : {round(metrics.get('total_llm_time', 0) / max(metrics.get('total_llm_requests', 1), 1), 2)}s",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# System stats
|
||||
process = psutil.Process(os.getpid())
|
||||
memory = process.memory_info().rss / 1024 / 1024 # MB
|
||||
uptime = timedelta(seconds=time.time() - process.create_time())
|
||||
embed.add_field(
|
||||
name="💻 Système",
|
||||
value=f"Mémoire : {round(memory, 2)} MB\n"
|
||||
f"Disponible : {round(psutil.virtual_memory().available / 1024 / 1024, 0)} MB\n"
|
||||
f"Disponible pour bot : {round(psutil.virtual_memory().available / 1024 / 1024 / 1024, 2)} GB",
|
||||
inline=True
|
||||
)
|
||||
|
||||
embed.set_footer(text=f"Uptime: {uptime}")
|
||||
await message.channel.send(embed=embed)
|
||||
34
commandes/autres/utils.py
Normal file
34
commandes/autres/utils.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
"""
|
||||
Utilities for command parsing and common operations
|
||||
"""
|
||||
|
||||
import discord
|
||||
|
||||
async def parse_user_mention(bot, target_mention, message):
|
||||
"""
|
||||
Parse a user mention, username, or ID to get the user ID and member object.
|
||||
Returns (user_id, member) or (None, None) if not found.
|
||||
"""
|
||||
if target_mention.startswith('<@') and target_mention.endswith('>'):
|
||||
user_id = int(target_mention[2:-1]) if not target_mention.startswith('<@!') else int(target_mention[3:-1])
|
||||
elif target_mention.startswith('@'):
|
||||
# Handle @username
|
||||
username = target_mention[1:].lower()
|
||||
member = discord.utils.find(lambda m: username in m.name.lower() or username in m.display_name.lower(), message.guild.members)
|
||||
if not member:
|
||||
return None, None
|
||||
user_id = member.id
|
||||
else:
|
||||
try:
|
||||
user_id = int(target_mention)
|
||||
except ValueError:
|
||||
return None, None
|
||||
|
||||
member = message.guild.get_member(user_id)
|
||||
if not member:
|
||||
try:
|
||||
user = await bot.fetch_user(user_id)
|
||||
return user_id, None # User exists but not in guild
|
||||
except discord.NotFound:
|
||||
return None, None
|
||||
return user_id, member
|
||||
Loading…
Add table
Add a link
Reference in a new issue