The begining
This commit is contained in:
commit
dee311e709
71 changed files with 8349 additions and 0 deletions
153
commandes/Backups_project/blacklist.py
Executable file
153
commandes/Backups_project/blacklist.py
Executable file
|
|
@ -0,0 +1,153 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
|
||||
# --------- Config ---------
|
||||
BLACKLIST_FILE = "data/blacklist.json"
|
||||
AGENTS_FILE = "data/agents.json"
|
||||
TICKETS_FILE = "data/tickets_blacklist.json"
|
||||
MAIN_GUILD_ID = 1369669999345537145 # ID du serveur principal
|
||||
|
||||
# --------- Utils JSON ---------
|
||||
def load_json(path, default):
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as f:
|
||||
json.dump(default, f, indent=4)
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_json(path, data):
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
# Initialisation fichiers
|
||||
blacklist = load_json(BLACKLIST_FILE, {})
|
||||
tickets = load_json(TICKETS_FILE, {})
|
||||
|
||||
# --------- Cog ---------
|
||||
class Blacklist(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
def is_agent(self, user_id: int):
|
||||
agents = load_json(AGENTS_FILE, {"agents": []})
|
||||
return str(user_id) in agents["agents"]
|
||||
|
||||
# ---------- Slash Commands ----------
|
||||
@app_commands.command(name="blacklist", description="Blacklist un utilisateur")
|
||||
@app_commands.describe(user="Utilisateur à blacklister", reason="Raison")
|
||||
async def blacklist_slash(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"):
|
||||
if not self.is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
blacklist[str(user.id)] = {"reason": reason}
|
||||
save_json(BLACKLIST_FILE, blacklist)
|
||||
|
||||
try:
|
||||
view = TicketButton(user.id)
|
||||
await user.send(f"🚫 Tu as été blacklisté.\n**Raison :** {reason}", view=view)
|
||||
except:
|
||||
return await interaction.response.send_message("Impossible d’envoyer un MP à l’utilisateur.", ephemeral=True)
|
||||
|
||||
await interaction.response.send_message(f"✅ {user.mention} a été ajouté à la blacklist.", ephemeral=True)
|
||||
|
||||
@app_commands.command(name="unblacklist", description="Retire un utilisateur de la blacklist")
|
||||
@app_commands.describe(user="Utilisateur à retirer")
|
||||
async def unblacklist_slash(self, interaction: discord.Interaction, user: discord.User):
|
||||
if not self.is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
if str(user.id) in blacklist:
|
||||
del blacklist[str(user.id)]
|
||||
save_json(BLACKLIST_FILE, blacklist)
|
||||
await interaction.response.send_message(f"✅ {user.mention} a été retiré de la blacklist.", ephemeral=True)
|
||||
else:
|
||||
await interaction.response.send_message("❌ Cet utilisateur n’est pas blacklisté.", ephemeral=True)
|
||||
|
||||
# ---------- Listener pour nouveau MP ou notification ----------
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
if str(member.id) in blacklist:
|
||||
try:
|
||||
view = TicketButton(member.id)
|
||||
await member.send(
|
||||
"🚫 Tu es blacklisté de ce serveur affilié. "
|
||||
"Tu peux demander plus d’informations via le serveur principal.",
|
||||
view=view
|
||||
)
|
||||
except:
|
||||
pass
|
||||
|
||||
# --------- Boutons ----------
|
||||
class TicketButton(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=None)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="📩 Ouvrir une demande de déblacklist", style=discord.ButtonStyle.danger)
|
||||
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user.id != self.user_id:
|
||||
return await interaction.response.send_message("❌ Ce bouton ne t’appartient pas.", ephemeral=True)
|
||||
|
||||
if str(self.user_id) in tickets:
|
||||
return await interaction.response.send_message("❌ Tu as déjà ouvert une demande.", ephemeral=True)
|
||||
|
||||
# Création du ticket (en MP avec le bot)
|
||||
channel = await interaction.user.create_dm()
|
||||
|
||||
# Sauvegarde
|
||||
tickets[str(self.user_id)] = {"dm_channel_id": channel.id}
|
||||
save_json(TICKETS_FILE, tickets)
|
||||
|
||||
view = CloseTicketView(self.user_id)
|
||||
await channel.send(
|
||||
f"📩 Ticket ouvert par {interaction.user.mention}\n"
|
||||
"Un agent Omega Kube va bientôt te répondre.",
|
||||
view=view
|
||||
)
|
||||
|
||||
await interaction.response.send_message("✅ Ton ticket a été créé en MP avec le bot.", ephemeral=True)
|
||||
|
||||
class CloseTicketView(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=None)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger)
|
||||
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
agents = load_json(AGENTS_FILE, {"agents": []})
|
||||
if str(interaction.user.id) not in agents["agents"]:
|
||||
return await interaction.response.send_message("⛔ Seuls les agents Omega Kube peuvent fermer un ticket.", ephemeral=True)
|
||||
|
||||
view = ConfirmCloseView(self.user_id)
|
||||
await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True)
|
||||
|
||||
class ConfirmCloseView(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=30)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success)
|
||||
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
agents = load_json(AGENTS_FILE, {"agents": []})
|
||||
if str(interaction.user.id) not in agents["agents"]:
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
if str(self.user_id) in tickets:
|
||||
del tickets[str(self.user_id)]
|
||||
save_json(TICKETS_FILE, tickets)
|
||||
|
||||
await interaction.channel.send("✅ Ticket fermé.")
|
||||
await interaction.channel.delete()
|
||||
|
||||
@discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True)
|
||||
|
||||
# --------- Setup Cog ----------
|
||||
async def setup(bot: commands.Bot):
|
||||
cog = Blacklist(bot)
|
||||
await bot.add_cog(cog)
|
||||
await bot.tree.sync(guild=discord.Object(id=MAIN_GUILD_ID))
|
||||
2
commandes/__init__.py
Executable file
2
commandes/__init__.py
Executable file
|
|
@ -0,0 +1,2 @@
|
|||
# Dossier des commandes pour Kuby
|
||||
# Ce dossier contient toutes les commandes et cogs du bot
|
||||
276
commandes/backup.py
Executable file
276
commandes/backup.py
Executable file
|
|
@ -0,0 +1,276 @@
|
|||
# commandes/backup.py
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, List
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import app_commands
|
||||
|
||||
BACKUPS_ROOT = "data/security_backups" # dossier de sortie des sauvegardes
|
||||
|
||||
|
||||
def ensure_dir(path: str) -> None:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
|
||||
def human_dt() -> str:
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
|
||||
|
||||
def progress_bar(pct: float, width: int = 20) -> str:
|
||||
pct = max(0.0, min(1.0, pct))
|
||||
filled = int(round(width * pct))
|
||||
return f"[{'█'*filled}{'─'*(width-filled)}] {int(pct*100)}%"
|
||||
|
||||
|
||||
def estimate_duration_seconds(
|
||||
nb_roles: int,
|
||||
nb_categories: int,
|
||||
nb_text_channels: int,
|
||||
nb_members: int,
|
||||
save_messages: bool,
|
||||
message_limit_per_channel: int,
|
||||
) -> int:
|
||||
meta_cost = nb_roles * 0.02 + nb_categories * 0.03 + nb_text_channels * 0.05 + nb_members * 0.01
|
||||
if save_messages:
|
||||
pages_per_chan = max(1, (message_limit_per_channel + 99) // 100)
|
||||
msg_cost = nb_text_channels * pages_per_chan * 0.12
|
||||
else:
|
||||
msg_cost = 0.0
|
||||
base_overhead = 3.0
|
||||
return int(meta_cost + msg_cost + base_overhead)
|
||||
|
||||
|
||||
def serialize_overwrites(channel: discord.abc.GuildChannel) -> List[Dict[str, Any]]:
|
||||
serialized: List[Dict[str, Any]] = []
|
||||
for target, perms in channel.overwrites.items():
|
||||
target_type = "role" if isinstance(target, discord.Role) else "member"
|
||||
serialized.append({
|
||||
"target_type": target_type,
|
||||
"target_id": target.id,
|
||||
"permissions": perms._values
|
||||
})
|
||||
return serialized
|
||||
|
||||
|
||||
class Backup(commands.Cog):
|
||||
"""Sauvegarde de serveur avec estimation & progression."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
backup_group = app_commands.Group(name="backup", description="Outils de sauvegarde du serveur")
|
||||
|
||||
@backup_group.command(name="create", description="Créer une sauvegarde du serveur")
|
||||
@app_commands.describe(
|
||||
save_messages="Inclure l'export des messages",
|
||||
privacy_notice="Afficher un avertissement public si messages sauvegardés",
|
||||
message_limit_per_channel="Nombre max de messages à exporter par salon (0 = tous)"
|
||||
)
|
||||
async def backup_create(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
save_messages: bool = True,
|
||||
privacy_notice: bool = True,
|
||||
message_limit_per_channel: int = 10000
|
||||
):
|
||||
guild = interaction.guild
|
||||
assert guild is not None, "Cette commande doit être utilisée dans un serveur."
|
||||
|
||||
# Vérification de la whitelist
|
||||
whitelist_cog = self.bot.get_cog("whitelist")
|
||||
if not whitelist_cog:
|
||||
await interaction.response.send_message("❌ Cog Whitelist introuvable.", ephemeral=True)
|
||||
return
|
||||
if not whitelist_cog.is_whitelisted(guild.id, interaction.user.id):
|
||||
await interaction.response.send_message(
|
||||
"❌ Vous n'êtes pas dans la Whitelist.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Déferre l'interaction
|
||||
await interaction.response.defer(ephemeral=True, thinking=True)
|
||||
|
||||
# Avertissement de confidentialité
|
||||
if privacy_notice and save_messages:
|
||||
try:
|
||||
await interaction.channel.send(
|
||||
embed=discord.Embed(
|
||||
title="📢 Information de confidentialité (transparence)",
|
||||
description=(
|
||||
"Une **sauvegarde du serveur** est en cours et **inclus les messages**.\n"
|
||||
"Vous êtes dans l'obligation de prévenir les membres."
|
||||
),
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Préparation chemins
|
||||
ts = human_dt()
|
||||
base_dir = os.path.join(BACKUPS_ROOT, f"{guild.id}_{ts}")
|
||||
ensure_dir(base_dir)
|
||||
ensure_dir(os.path.join(base_dir, "messages"))
|
||||
|
||||
# Estimation
|
||||
nb_roles = len(guild.roles)
|
||||
nb_categories = len(guild.categories)
|
||||
nb_text_channels = len(guild.text_channels)
|
||||
nb_members = guild.member_count or len(guild.members)
|
||||
|
||||
if message_limit_per_channel < 0:
|
||||
message_limit_per_channel = 0
|
||||
|
||||
estimation_s = estimate_duration_seconds(
|
||||
nb_roles, nb_categories, nb_text_channels, nb_members, save_messages,
|
||||
message_limit_per_channel if message_limit_per_channel else 100000
|
||||
)
|
||||
|
||||
prog_embed = discord.Embed(
|
||||
title="🗄️ Sauvegarde en cours",
|
||||
description=(
|
||||
f"**Serveur :** {guild.name} (`{guild.id}`)\n"
|
||||
f"**Estimation :** ~{estimation_s} secondes\n"
|
||||
f"**Options** : messages={'oui' if save_messages else 'non'} • limite/chan={message_limit_per_channel or 'tous'}"
|
||||
),
|
||||
color=discord.Color.blurple()
|
||||
)
|
||||
prog_embed.add_field(name="Progression", value=progress_bar(0.0), inline=False)
|
||||
progress_msg = await interaction.followup.send(embed=prog_embed, ephemeral=False)
|
||||
|
||||
# ================= EXPORT =================
|
||||
weights = {"roles":0.08,"categories":0.12,"channels":0.20,"members":0.10,"messages":0.10 if save_messages else 0.0,"zip":0.10}
|
||||
done = 0.0
|
||||
async def bump(step_key:str, extra_pct:float=0.0,note:str=""):
|
||||
nonlocal done
|
||||
done += weights.get(step_key,0.0)+extra_pct
|
||||
done = min(done,0.999)
|
||||
prog_embed.set_field_at(0,name="Progression",value=progress_bar(done),inline=False)
|
||||
if note:
|
||||
prog_embed.set_footer(text=note)
|
||||
await progress_msg.edit(embed=prog_embed)
|
||||
|
||||
# --- RÔLES ---
|
||||
roles_dump = [{"id": r.id,"name": r.name,"color": r.color.value,"hoist": r.hoist,"mentionable": r.mentionable,"permissions": r.permissions.value,"position": r.position} for r in guild.roles]
|
||||
with open(os.path.join(base_dir,"roles.json"),"w",encoding="utf-8") as f:
|
||||
json.dump(roles_dump,f,indent=2,ensure_ascii=False)
|
||||
await bump("roles", note="Rôles exportés")
|
||||
|
||||
# --- CATÉGORIES ---
|
||||
cats_dump = [{"id": c.id,"name": c.name,"position": c.position,"nsfw": getattr(c,"nsfw",False),"overwrites": serialize_overwrites(c)} for c in guild.categories]
|
||||
with open(os.path.join(base_dir,"categories.json"),"w",encoding="utf-8") as f:
|
||||
json.dump(cats_dump,f,indent=2,ensure_ascii=False)
|
||||
await bump("categories", note="Catégories exportées")
|
||||
|
||||
|
||||
# --- SALONS ---
|
||||
chans_dump=[]
|
||||
for ch in guild.channels:
|
||||
base={"id":ch.id,"name":ch.name,"type":str(ch.type),"position":ch.position,"category_id":ch.category.id if getattr(ch,"category",None) else None,"overwrites":serialize_overwrites(ch)}
|
||||
if isinstance(ch,discord.TextChannel):
|
||||
base.update({"topic":ch.topic,"nsfw":ch.nsfw,"slowmode_delay":ch.slowmode_delay,"default_auto_archive_duration":ch.default_auto_archive_duration})
|
||||
elif isinstance(ch,discord.VoiceChannel):
|
||||
base.update({"bitrate":ch.bitrate,"user_limit":ch.user_limit,"rtc_region":str(ch.rtc_region) if ch.rtc_region else None})
|
||||
elif isinstance(ch,discord.ForumChannel):
|
||||
base.update({"nsfw":ch.nsfw,"default_auto_archive_duration":ch.default_auto_archive_duration})
|
||||
chans_dump.append(base)
|
||||
with open(os.path.join(base_dir,"channels.json"),"w",encoding="utf-8") as f:
|
||||
json.dump(chans_dump,f,indent=2,ensure_ascii=False)
|
||||
await bump("channels", note="Salons exportés")
|
||||
|
||||
|
||||
# --- MEMBRES ---
|
||||
members_dump=[]
|
||||
for m in guild.members:
|
||||
members_dump.append({"id":m.id,"name":str(m),"nick":m.nick,"roles":[r.id for r in m.roles if r!=guild.default_role],"bot":m.bot,"joined_at":m.joined_at.isoformat() if m.joined_at else None})
|
||||
with open(os.path.join(base_dir,"members.json"),"w",encoding="utf-8") as f:
|
||||
json.dump(members_dump,f,indent=2,ensure_ascii=False)
|
||||
await bump("members", note="Membres exportés")
|
||||
|
||||
# --- MESSAGES ---
|
||||
if save_messages:
|
||||
text_chans=list(guild.text_channels)
|
||||
total_tc=len(text_chans)
|
||||
per_chan_weight=weights["messages"]/max(1,total_tc)
|
||||
exported_total=0
|
||||
for idx,tc in enumerate(text_chans,start=1):
|
||||
file_path=os.path.join(base_dir,"messages",f"{tc.id}.jsonl")
|
||||
count=0
|
||||
with open(file_path,"w",encoding="utf-8") as f:
|
||||
try:
|
||||
async for msg in tc.history(limit=message_limit_per_channel if message_limit_per_channel>0 else None, oldest_first=True):
|
||||
data={"id":msg.id,"channel_id":msg.channel.id,"author_id":msg.author.id,"author_bot":msg.author.bot,"created_at":msg.created_at.isoformat(),"content":msg.content,"attachments":[a.url for a in msg.attachments],"embeds":[e.to_dict() for e in msg.embeds],"reference":{"message_id":getattr(msg.reference,"message_id",None),"channel_id":getattr(msg.reference,"channel_id",None)} if msg.reference else None}
|
||||
f.write(json.dumps(data,ensure_ascii=False)+"\n")
|
||||
count+=1
|
||||
except discord.Forbidden: pass
|
||||
except Exception: pass
|
||||
exported_total+=count
|
||||
done+=per_chan_weight
|
||||
prog_embed.set_field_at(0,name="Progression",value=progress_bar(min(done,0.999)),inline=False)
|
||||
prog_embed.set_footer(text=f"Messages exportés : {exported_total} | Salon {idx}/{total_tc}")
|
||||
await progress_msg.edit(embed=prog_embed)
|
||||
|
||||
# --- ZIP FINAL ---
|
||||
zip_path=os.path.join(BACKUPS_ROOT,f"{guild.id}_{ts}.zip")
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path,"w",compression=zipfile.ZIP_DEFLATED,compresslevel=6) as zf:
|
||||
for root,_,files in os.walk(base_dir):
|
||||
for name in files:
|
||||
full=os.path.join(root,name)
|
||||
rel=os.path.relpath(full,base_dir)
|
||||
zf.write(full,rel)
|
||||
except Exception as exc:
|
||||
prog_embed.color=discord.Color.red()
|
||||
prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False)
|
||||
prog_embed.set_footer(text=f"Erreur lors de la création du ZIP : {exc}")
|
||||
await progress_msg.edit(embed=prog_embed)
|
||||
await interaction.followup.send("❌ Erreur lors de la compression ZIP. Si l'erreur persiste veuillez ouvrir un ticket sur Omega Kube", ephemeral=True)
|
||||
return
|
||||
|
||||
# Finalisation
|
||||
prog_embed.color=discord.Color.green()
|
||||
prog_embed.set_field_at(0,name="Progression",value=progress_bar(1.0),inline=False)
|
||||
prog_embed.add_field(name="Archive",value=f"`{zip_path}`",inline=False)
|
||||
prog_embed.set_footer(text="Sauvegarde terminée ✅")
|
||||
await progress_msg.edit(embed=prog_embed)
|
||||
await interaction.followup.send("🎉 Sauvegarde terminée. Un message de progression avec le chemin de l’archive a été publié.", ephemeral=True)
|
||||
|
||||
@backup_group.command(name="list", description="Lister les archives de sauvegarde disponibles")
|
||||
async def backup_list(self, interaction: discord.Interaction):
|
||||
guild = interaction.guild
|
||||
assert guild is not None, "Cette commande doit être utilisée dans un serveur."
|
||||
|
||||
whitelist_cog = self.bot.get_cog("whitelist")
|
||||
if not whitelist_cog:
|
||||
await interaction.response.send_message("❌ Cog Whitelist introuvable.", ephemeral=True)
|
||||
return
|
||||
if not whitelist_cog.is_whitelisted(guild.id, interaction.user.id):
|
||||
await interaction.response.send_message(
|
||||
"❌ Vous n'êtes pas autorisé à utiliser cette commande.", ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
await interaction.response.defer(ephemeral=True, thinking=True)
|
||||
ensure_dir(BACKUPS_ROOT)
|
||||
files = [f for f in os.listdir(BACKUPS_ROOT) if f.endswith(".zip")]
|
||||
files.sort(reverse=True)
|
||||
|
||||
if not files:
|
||||
await interaction.followup.send("Aucune archive trouvée dans `backups/`.", ephemeral=True)
|
||||
return
|
||||
|
||||
desc="\n".join(f"• `{name}`" for name in files[:20])
|
||||
embed=discord.Embed(title="📦 Archives disponibles",description=desc,color=discord.Color.blurple())
|
||||
if len(files)>20:
|
||||
embed.set_footer(text=f"... et {len(files)-20} de plus")
|
||||
await interaction.followup.send(embed=embed,ephemeral=True)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Backup(bot))
|
||||
204
commandes/backup_restore.py
Executable file
204
commandes/backup_restore.py
Executable file
|
|
@ -0,0 +1,204 @@
|
|||
# commandes/backup_restore.py
|
||||
import os
|
||||
import json
|
||||
import zipfile
|
||||
import tempfile
|
||||
import shutil
|
||||
from typing import List
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
BACKUPS_ROOT = "data/security_backups"
|
||||
|
||||
def ensure_dir(path: str):
|
||||
os.makedirs(path, exist_ok=True)
|
||||
|
||||
class BackupRestore(commands.Cog):
|
||||
"""Restauration granulaire d'une sauvegarde."""
|
||||
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
async def check_whitelisted(self, guild_id: int, user_id: int) -> bool:
|
||||
"""Vérifie si un utilisateur est whitelisté."""
|
||||
# Recherche dynamique d’un cog ayant une méthode is_whitelisted
|
||||
for cog in self.bot.cogs.values():
|
||||
if hasattr(cog, "is_whitelisted"):
|
||||
return cog.is_whitelisted(guild_id, user_id)
|
||||
raise RuntimeError("Cog Whitelist non chargé ou mal nommé !")
|
||||
|
||||
async def list_backup_elements(self, backup_path: str) -> List[str]:
|
||||
"""Liste les éléments exportables dans une archive ou dossier de backup."""
|
||||
elements = []
|
||||
if os.path.isdir(backup_path):
|
||||
for f in os.listdir(backup_path):
|
||||
if f.endswith(".json") or os.path.isdir(os.path.join(backup_path, f)):
|
||||
elements.append(f.replace(".json", ""))
|
||||
elif os.path.isfile(backup_path) and backup_path.endswith(".zip"):
|
||||
with zipfile.ZipFile(backup_path, "r") as zf:
|
||||
for f in zf.namelist():
|
||||
base = f.split("/")[0]
|
||||
if base and base not in elements:
|
||||
elements.append(base)
|
||||
return elements
|
||||
|
||||
async def element_autocomplete(self, interaction: discord.Interaction, current: str):
|
||||
"""Complétion automatique pour l'option 'element'."""
|
||||
guild = interaction.guild
|
||||
if guild is None:
|
||||
return []
|
||||
|
||||
ensure_dir(BACKUPS_ROOT)
|
||||
files = [f for f in os.listdir(BACKUPS_ROOT) if f.startswith(str(guild.id))]
|
||||
files.sort(reverse=True)
|
||||
|
||||
if not files:
|
||||
return []
|
||||
|
||||
last_backup = files[0]
|
||||
backup_path = os.path.join(BACKUPS_ROOT, last_backup)
|
||||
elements = await self.list_backup_elements(backup_path)
|
||||
|
||||
return [app_commands.Choice(name=e, value=e) for e in elements if current.lower() in e.lower()][:25]
|
||||
|
||||
async def restore_roles(self, guild: discord.Guild, backup_path: str):
|
||||
"""Restaure les rôles à partir du backup."""
|
||||
roles_file = os.path.join(backup_path, "roles.json")
|
||||
if os.path.isfile(roles_file):
|
||||
with open(roles_file, "r", encoding="utf-8") as f:
|
||||
roles_data = json.load(f)
|
||||
for r in roles_data:
|
||||
try:
|
||||
await guild.create_role(
|
||||
name=r["name"],
|
||||
permissions=discord.Permissions(r["permissions"]),
|
||||
color=discord.Color(r.get("color", 0)),
|
||||
hoist=r.get("hoist", False),
|
||||
mentionable=r.get("mentionable", False),
|
||||
reason="Restauration backup"
|
||||
)
|
||||
except discord.Forbidden:
|
||||
continue
|
||||
|
||||
async def restore_channels(self, guild: discord.Guild, backup_path: str):
|
||||
"""Restaure les channels à partir du backup."""
|
||||
channels_file = os.path.join(backup_path, "channels.json")
|
||||
if os.path.isfile(channels_file):
|
||||
with open(channels_file, "r", encoding="utf-8") as f:
|
||||
channels_data = json.load(f)
|
||||
for c in channels_data:
|
||||
try:
|
||||
if c["type"] == 0: # text
|
||||
await guild.create_text_channel(
|
||||
name=c["name"],
|
||||
category=guild.get_channel(c.get("category_id")),
|
||||
topic=c.get("topic", ""),
|
||||
nsfw=c.get("nsfw", False),
|
||||
reason="Restauration backup"
|
||||
)
|
||||
elif c["type"] == 2: # voice
|
||||
await guild.create_voice_channel(
|
||||
name=c["name"],
|
||||
category=guild.get_channel(c.get("category_id")),
|
||||
bitrate=c.get("bitrate", 64000),
|
||||
user_limit=c.get("user_limit", 0),
|
||||
reason="Restauration backup"
|
||||
)
|
||||
except discord.Forbidden:
|
||||
continue
|
||||
|
||||
async def restore_messages(self, channel: discord.TextChannel, backup_path: str):
|
||||
"""Restaure les messages à partir du backup (solution ZIP incluse)."""
|
||||
if backup_path.endswith(".zip"):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
with zipfile.ZipFile(backup_path, "r") as zip_ref:
|
||||
zip_ref.extractall(temp_dir)
|
||||
messages_file = os.path.join(temp_dir, "messages.json")
|
||||
if not os.path.exists(messages_file):
|
||||
return
|
||||
with open(messages_file, "r", encoding="utf-8") as f:
|
||||
messages = json.load(f)
|
||||
for msg in messages:
|
||||
await channel.send(msg["content"])
|
||||
finally:
|
||||
shutil.rmtree(temp_dir)
|
||||
else:
|
||||
messages_file = os.path.join(backup_path, "messages.json")
|
||||
if not os.path.exists(messages_file):
|
||||
return
|
||||
with open(messages_file, "r", encoding="utf-8") as f:
|
||||
messages = json.load(f)
|
||||
for msg in messages:
|
||||
await channel.send(msg["content"])
|
||||
|
||||
@app_commands.command(name="granulaire", description="Restaure un élément précis d'une sauvegarde")
|
||||
@app_commands.describe(
|
||||
backup="Nom ou ID de la sauvegarde",
|
||||
element="Élément à restaurer (roles, channels, messages...)"
|
||||
)
|
||||
@app_commands.autocomplete(element=element_autocomplete)
|
||||
async def restore_granulaire(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
backup: str,
|
||||
element: str
|
||||
):
|
||||
guild = interaction.guild
|
||||
if guild is None:
|
||||
await interaction.response.send_message("Cette commande doit être utilisée dans un serveur.", ephemeral=True)
|
||||
return
|
||||
|
||||
if not await self.check_whitelisted(guild.id, interaction.user.id):
|
||||
await interaction.response.send_message(
|
||||
"❌ Vous n'êtes pas autorisé à utiliser cette commande.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
backup_path = os.path.join(BACKUPS_ROOT, backup)
|
||||
if not os.path.exists(backup_path):
|
||||
await interaction.response.send_message(f"❌ La sauvegarde `{backup}` est introuvable.", ephemeral=True)
|
||||
return
|
||||
|
||||
await interaction.response.defer(ephemeral=True, thinking=True)
|
||||
|
||||
available_elements = await self.list_backup_elements(backup_path)
|
||||
if element not in available_elements:
|
||||
await interaction.followup.send(
|
||||
f"❌ Élément `{element}` introuvable dans la sauvegarde.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Détection si c’est un zip pour extraire temporairement
|
||||
temp_path = backup_path
|
||||
if backup_path.endswith(".zip"):
|
||||
temp_path = tempfile.mkdtemp()
|
||||
with zipfile.ZipFile(backup_path, "r") as zip_ref:
|
||||
zip_ref.extractall(temp_path)
|
||||
|
||||
try:
|
||||
if element == "roles":
|
||||
await self.restore_roles(guild, temp_path)
|
||||
elif element == "channels":
|
||||
await self.restore_channels(guild, temp_path)
|
||||
elif element == "messages":
|
||||
# Restaure messages dans tous les channels textuels ?
|
||||
for ch in guild.text_channels:
|
||||
await self.restore_messages(ch, temp_path)
|
||||
else:
|
||||
await interaction.followup.send(f"⚠️ Élément `{element}` non géré par la restauration automatique.", ephemeral=True)
|
||||
return
|
||||
finally:
|
||||
if backup_path.endswith(".zip"):
|
||||
shutil.rmtree(temp_path)
|
||||
|
||||
await interaction.followup.send(
|
||||
f"✅ Restauration granulaire de `{element}` depuis la sauvegarde `{backup}` terminée.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(BackupRestore(bot))
|
||||
279
commandes/blacklist.py
Executable file
279
commandes/blacklist.py
Executable file
|
|
@ -0,0 +1,279 @@
|
|||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
from discord import app_commands
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
BLACKLIST_FILE = "data/blacklist.json"
|
||||
AGENTS_FILE = "data/agents.json"
|
||||
TICKETS_FILE = "data/tickets_blacklist.json"
|
||||
MAIN_GUILD_ID = 1369669999345537145 # ID serveur principal
|
||||
TICKET_CATEGORY_ID = 1369669999677145104 # Catégorie tickets
|
||||
|
||||
# --------- Utils JSON ---------
|
||||
def load_json(path, default):
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as f:
|
||||
json.dump(default, f, indent=4)
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_json(path, data):
|
||||
with open(path, "w") as f:
|
||||
json.dump(data, f, indent=4)
|
||||
|
||||
# --------- Cog ---------
|
||||
class Blacklist(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.check_ban_expiry.start() # boucle auto pour bannir après 7j
|
||||
|
||||
def cog_unload(self):
|
||||
self.check_ban_expiry.cancel()
|
||||
|
||||
def is_agent(self, user_id: int):
|
||||
agents = load_json(AGENTS_FILE, {"agents": []})
|
||||
return str(user_id) in agents["agents"]
|
||||
|
||||
@app_commands.command(name="blacklist", description="Blacklist un utilisateur")
|
||||
async def blacklist(self, interaction: discord.Interaction, user: discord.User, reason: str = "Aucune raison fournie"):
|
||||
if not self.is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
blacklist = load_json(BLACKLIST_FILE, {})
|
||||
blacklist[str(user.id)] = {
|
||||
"reason": reason,
|
||||
"timestamp": datetime.utcnow().isoformat()
|
||||
}
|
||||
save_json(BLACKLIST_FILE, blacklist)
|
||||
|
||||
# Préparer l'embed pro
|
||||
embed = discord.Embed(
|
||||
title="🚫 Mise en blacklist - Réseau Omega Kube",
|
||||
description=(
|
||||
f"Bonjour {user.mention},\n\n"
|
||||
f"Vous avez été **blacklisté** du **Réseau Omega Kube**.\n\n"
|
||||
f"**Raison :** {reason}\n\n"
|
||||
"⚠️ Vous êtes banni de tous nos serveurs partenaires. "
|
||||
"Cependant, vous restez présent sur le **serveur principal**, "
|
||||
"mais tu ne peux pas interagir avec la communauté.\n\n"
|
||||
"📩 Vous disposez de **7 jours** pour contester cette décision en ouvrant un ticket via le bouton ci-dessous.\n\n"
|
||||
"⏳ Passé ce délai, vous serez **définitivement banni de tout le réseau**, sans autre recours possible."
|
||||
),
|
||||
color=discord.Color.red()
|
||||
)
|
||||
embed.set_footer(text="Omega Kube - Nixfix06 & Mgstudios")
|
||||
|
||||
view = TicketButton(user.id)
|
||||
|
||||
# Envoyer MP avant action
|
||||
try:
|
||||
await user.send(embed=embed, view=view)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Bannir de tous les serveurs sauf principal
|
||||
for guild in self.bot.guilds:
|
||||
if guild.id == MAIN_GUILD_ID:
|
||||
member = guild.get_member(user.id)
|
||||
if member:
|
||||
try:
|
||||
# Supprime la permission d'écrire partout
|
||||
for channel in guild.channels:
|
||||
overwrite = channel.overwrites_for(member)
|
||||
overwrite.send_messages = False
|
||||
overwrite.add_reactions = False
|
||||
await channel.set_permissions(member, overwrite=overwrite)
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
member = guild.get_member(user.id)
|
||||
if member:
|
||||
try:
|
||||
await member.ban(reason=f"Blacklisté : {reason}")
|
||||
except:
|
||||
pass
|
||||
|
||||
await interaction.response.send_message(
|
||||
f"✅ {user.mention} blacklisté, banni des partenaires et restreint sur le serveur principal.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
@app_commands.command(name="unblacklist", description="Retirer un utilisateur de la blacklist")
|
||||
async def unblacklist(self, interaction: discord.Interaction, user: discord.User):
|
||||
if not self.is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
blacklist = load_json(BLACKLIST_FILE, {})
|
||||
if str(user.id) in blacklist:
|
||||
del blacklist[str(user.id)]
|
||||
save_json(BLACKLIST_FILE, blacklist)
|
||||
await interaction.response.send_message(f"✅ {user.mention} retiré de la blacklist.", ephemeral=True)
|
||||
else:
|
||||
await interaction.response.send_message("❌ Cet utilisateur n’est pas blacklisté.", ephemeral=True)
|
||||
|
||||
@tasks.loop(hours=1)
|
||||
async def check_ban_expiry(self):
|
||||
"""Boucle qui vérifie si 7 jours sont écoulés → ban total."""
|
||||
blacklist = load_json(BLACKLIST_FILE, {})
|
||||
to_remove = []
|
||||
for user_id, data in blacklist.items():
|
||||
try:
|
||||
ts = datetime.fromisoformat(data["timestamp"])
|
||||
if datetime.utcnow() > ts + timedelta(days=7):
|
||||
# Ban définitif partout
|
||||
user = await self.bot.fetch_user(int(user_id))
|
||||
for guild in self.bot.guilds:
|
||||
member = guild.get_member(int(user_id))
|
||||
if member:
|
||||
try:
|
||||
await member.ban(reason="Blacklist définitive (7j écoulés)")
|
||||
except:
|
||||
pass
|
||||
to_remove.append(user_id)
|
||||
except:
|
||||
continue
|
||||
|
||||
for uid in to_remove:
|
||||
del blacklist[uid]
|
||||
|
||||
save_json(BLACKLIST_FILE, blacklist)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message):
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
tickets = load_json(TICKETS_FILE, {})
|
||||
|
||||
if isinstance(message.channel, discord.DMChannel):
|
||||
user_id = str(message.author.id)
|
||||
if user_id in tickets:
|
||||
guild = self.bot.get_guild(MAIN_GUILD_ID)
|
||||
ticket_channel = guild.get_channel(tickets[user_id]["channel_id"])
|
||||
if ticket_channel:
|
||||
embed = discord.Embed(
|
||||
description=message.content,
|
||||
color=discord.Color.blue()
|
||||
).set_author(name=message.author, icon_url=message.author.display_avatar.url)
|
||||
await ticket_channel.send(embed=embed)
|
||||
|
||||
else:
|
||||
for user_id, data in tickets.items():
|
||||
if message.channel.id == data["channel_id"] and self.is_agent(message.author.id):
|
||||
user = await self.bot.fetch_user(int(user_id))
|
||||
try:
|
||||
embed = discord.Embed(
|
||||
description=message.content,
|
||||
color=discord.Color.green()
|
||||
).set_author(name=f"Agent {message.author}", icon_url=message.author.display_avatar.url)
|
||||
await user.send(embed=embed)
|
||||
except:
|
||||
pass
|
||||
|
||||
# --------- Boutons ---------
|
||||
class TicketButton(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=None)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="📩 Ouvrir une demande de révision", style=discord.ButtonStyle.primary)
|
||||
async def open_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user.id != self.user_id:
|
||||
return await interaction.response.send_message("❌ Ce bouton ne t’appartient pas.", ephemeral=True)
|
||||
|
||||
tickets = load_json(TICKETS_FILE, {})
|
||||
if str(self.user_id) in tickets:
|
||||
return await interaction.response.send_message("❌ Tu as déjà un ticket ouvert.", ephemeral=True)
|
||||
|
||||
guild = interaction.client.get_guild(MAIN_GUILD_ID)
|
||||
category = guild.get_channel(TICKET_CATEGORY_ID)
|
||||
|
||||
ticket_channel = await guild.create_text_channel(
|
||||
name=f"ticket-{self.user_id}",
|
||||
category=category,
|
||||
topic=f"Ticket de l'utilisateur {self.user_id}"
|
||||
)
|
||||
|
||||
tickets[str(self.user_id)] = {"channel_id": ticket_channel.id}
|
||||
save_json(TICKETS_FILE, tickets)
|
||||
|
||||
view_close = CloseTicketView(self.user_id)
|
||||
embed = discord.Embed(
|
||||
title="📩 Ticket ouvert",
|
||||
description=f"Un utilisateur blacklisté (<@{self.user_id}>) a ouvert une demande de révision.\n\nUn agent Omega Kube prendra en charge ce dossier.",
|
||||
color=discord.Color.blurple()
|
||||
)
|
||||
await ticket_channel.send(embed=embed, view=view_close)
|
||||
|
||||
await interaction.response.send_message("✅ Ton ticket a bien été créé.", ephemeral=True)
|
||||
|
||||
button.disabled = True
|
||||
self.stop()
|
||||
|
||||
class CloseTicketView(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=None)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="❌ Fermer le ticket", style=discord.ButtonStyle.danger)
|
||||
async def close_ticket(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Seuls les agents peuvent fermer ce ticket.", ephemeral=True)
|
||||
|
||||
view = ConfirmCloseView(self.user_id)
|
||||
await interaction.response.send_message("⚠️ Veux-tu vraiment fermer ce ticket ?", view=view, ephemeral=True)
|
||||
|
||||
class ConfirmCloseView(discord.ui.View):
|
||||
def __init__(self, user_id: int):
|
||||
super().__init__(timeout=30)
|
||||
self.user_id = user_id
|
||||
|
||||
@discord.ui.button(label="✅ Oui", style=discord.ButtonStyle.success)
|
||||
async def confirm(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if not interaction.client.get_cog("Blacklist").is_agent(interaction.user.id):
|
||||
return await interaction.response.send_message("⛔ Tu n’as pas la permission.", ephemeral=True)
|
||||
|
||||
tickets = load_json(TICKETS_FILE, {})
|
||||
if str(self.user_id) in tickets:
|
||||
channel_id = tickets[str(self.user_id)]["channel_id"]
|
||||
guild = interaction.guild
|
||||
channel = guild.get_channel(channel_id)
|
||||
if channel:
|
||||
await channel.delete()
|
||||
|
||||
del tickets[str(self.user_id)]
|
||||
save_json(TICKETS_FILE, tickets)
|
||||
|
||||
user = await interaction.client.fetch_user(int(self.user_id))
|
||||
try:
|
||||
embed = discord.Embed(
|
||||
title="🔒 Ticket fermé",
|
||||
description="Ton ticket a été examiné et fermé.\n\n⚠️ Après analyse, la sanction est confirmée et tu restes blacklisté de l’ensemble du réseau.",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
await user.send(embed=embed)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
|
||||
for guild in interaction.client.guilds:
|
||||
member = guild.get_member(self.user_id)
|
||||
if member:
|
||||
try:
|
||||
await member.ban(reason="Ticket blacklist fermé")
|
||||
except:
|
||||
pass
|
||||
|
||||
await interaction.response.send_message("✅ Ticket fermé avec succès.", ephemeral=True)
|
||||
|
||||
@discord.ui.button(label="❌ Non", style=discord.ButtonStyle.secondary)
|
||||
async def cancel(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
await interaction.response.send_message("❌ Fermeture annulée.", ephemeral=True)
|
||||
|
||||
# --------- Setup ---------
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Blacklist(bot))
|
||||
17
commandes/fun.py
Executable file
17
commandes/fun.py
Executable file
|
|
@ -0,0 +1,17 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import random
|
||||
|
||||
class FunCommands(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.emojis = ['😀', '😂', '😎', '😍', '😡', '🤖', '👻', '🎉', '🔥', '🌈', '🍕', '🍔', '🍟', '🚀', '🌟']
|
||||
|
||||
@app_commands.command(name="fun", description="Envoie 10 emojis fun aléatoires")
|
||||
async def fun(self, interaction: discord.Interaction):
|
||||
chosen_emojis = random.sample(self.emojis, 10)
|
||||
await interaction.response.send_message(" ".join(chosen_emojis))
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(FunCommands(bot))
|
||||
109
commandes/goodbye.py
Normal file
109
commandes/goodbye.py
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import app_commands
|
||||
import sqlite3
|
||||
import aiohttp
|
||||
import io
|
||||
import os
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from datetime import datetime, timezone
|
||||
|
||||
class Goodbye(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
|
||||
|
||||
def humanize_time_delta(self, date):
|
||||
if date is None: return "date inconnue"
|
||||
if date.tzinfo is None: date = date.replace(tzinfo=timezone.utc)
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = now - date
|
||||
total_days = delta.total_seconds() / (3600 * 24)
|
||||
rounded_days = round(total_days)
|
||||
|
||||
years = rounded_days // 365
|
||||
months = rounded_days // 30
|
||||
weeks = rounded_days // 7
|
||||
days = rounded_days
|
||||
|
||||
if years > 0: return f"il y a {years} {'an' if years == 1 else 'ans'}"
|
||||
elif months > 0: return f"il y a {months} mois"
|
||||
elif weeks > 0: return f"il y a {weeks} {'semaine' if weeks == 1 else 'semaines'}"
|
||||
elif days > 0: return f"il y a {days} {'jour' if days == 1 else 'jours'}"
|
||||
elif delta.seconds // 3600 > 0:
|
||||
hours = delta.seconds // 3600
|
||||
return f"il y a {hours} {'heure' if hours == 1 else 'heures'}"
|
||||
elif (delta.seconds % 3600) // 60 > 0:
|
||||
minutes = (delta.seconds % 3600) // 60
|
||||
return f"il y a {minutes} {'minute' if minutes == 1 else 'minutes'}"
|
||||
else:
|
||||
seconds = delta.seconds % 60
|
||||
return f"il y a {seconds} {'seconde' if seconds <= 1 else 'secondes'}"
|
||||
|
||||
async def create_goodbye_image(self, username, avatar_url):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
base_dir = os.path.abspath(os.path.join(current_dir, ".."))
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(avatar_url) as resp:
|
||||
avatar_bytes = await resp.read()
|
||||
|
||||
background = Image.open(os.path.join(base_dir, 'background_template_goodbye.png'))
|
||||
draw = ImageDraw.Draw(background)
|
||||
|
||||
font_large = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 70)
|
||||
font_small = ImageFont.truetype(os.path.join(base_dir, 'custom_font_small.otf'), 50)
|
||||
font_large_omega = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 55)
|
||||
|
||||
draw.text((325, 70), "A Bientot", font=font_large, fill=(255,255,255))
|
||||
draw.text((325, 135), f"{username} chez", font=font_small, fill=(255,255,255))
|
||||
draw.text((325, 195), "OMEGA KUBE", font=font_large_omega, fill=(255,255,255))
|
||||
|
||||
avatar = Image.open(io.BytesIO(avatar_bytes)).convert("RGBA").resize((224, 224), Image.LANCZOS)
|
||||
mask = Image.new('L', avatar.size, 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
mask_draw.ellipse((12, 12, 212, 212), fill=255)
|
||||
background.paste(avatar, (40, 40), mask)
|
||||
|
||||
img_byte_arr = io.BytesIO()
|
||||
background.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
return img_byte_arr
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_remove(self, member):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT goodbye_channel FROM server_configs WHERE server_id = ?', (member.guild.id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if result and result[0]:
|
||||
channel = member.guild.get_channel(result[0])
|
||||
if channel:
|
||||
img = await self.create_goodbye_image(member.name, member.avatar.url if member.avatar else member.default_avatar.url)
|
||||
joined_message = self.humanize_time_delta(member.joined_at)
|
||||
|
||||
embed = discord.Embed(title=f"{member} viens de nous quitter.",
|
||||
description="Nous espérons vous revoir bientôt <a:rain:1317973615714504806>",
|
||||
color=discord.Color.dark_magenta())
|
||||
embed.set_footer(text=f"Avait rejoint {joined_message}")
|
||||
embed.set_image(url="attachment://goodbye.png")
|
||||
await channel.send(embed=embed, file=discord.File(img, filename="goodbye.png"))
|
||||
|
||||
@app_commands.command(name="setgoodbye", description="Définit le salon d'aurevoir")
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
async def set_goodbye(self, interaction: discord.Interaction, channel: discord.TextChannel):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''INSERT INTO server_configs (server_id, goodbye_channel)
|
||||
VALUES (?, ?) ON CONFLICT(server_id)
|
||||
DO UPDATE SET goodbye_channel=excluded.goodbye_channel''',
|
||||
(interaction.guild_id, channel.id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await interaction.response.send_message(f"Salon d'aurevoir défini sur {channel.mention}", ephemeral=True)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Goodbye(bot))
|
||||
60
commandes/modules_security/antiraid.py
Normal file
60
commandes/modules_security/antiraid.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
|
||||
def is_whitelisted(guild_id, user_id):
|
||||
if not os.path.exists(WHITELIST_FILE): return False
|
||||
try:
|
||||
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return user_id in data.get(str(guild_id), [])
|
||||
except: return False
|
||||
|
||||
class AntiRaid(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.join_cache = {}
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
if member.bot: return
|
||||
|
||||
# 1. WHITELIST
|
||||
if is_whitelisted(member.guild.id, member.id): return
|
||||
|
||||
# 2. LECTURE DYNAMIQUE
|
||||
if not os.path.exists(SETTINGS_FILE): return
|
||||
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
|
||||
settings = json.load(f).get(str(member.guild.id), {})
|
||||
|
||||
if not settings.get("anti_raid"): return
|
||||
|
||||
guild_id = member.guild.id
|
||||
now = datetime.now()
|
||||
|
||||
if guild_id not in self.join_cache: self.join_cache[guild_id] = []
|
||||
self.join_cache[guild_id].append(now)
|
||||
self.join_cache[guild_id] = [t for t in self.join_cache[guild_id] if (now - t).seconds < 10]
|
||||
|
||||
if len(self.join_cache[guild_id]) > settings.get("raid_threshold", 3):
|
||||
await self.handle_raid(member.guild, settings)
|
||||
|
||||
async def handle_raid(self, guild, settings):
|
||||
if settings.get("auto_ban"):
|
||||
# Logique d'auto-ban des derniers membres ici
|
||||
pass
|
||||
|
||||
log_channel_id = settings.get("log_channel_id")
|
||||
if log_channel_id:
|
||||
channel = guild.get_channel(int(log_channel_id))
|
||||
if channel:
|
||||
embed = discord.Embed(title="🚨 ALERTE RAID", description=f"Vague de {len(self.join_cache[guild.id])} membres !", color=0xFF0000)
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(AntiRaid(bot))
|
||||
155
commandes/modules_security/antispam.py
Normal file
155
commandes/modules_security/antispam.py
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
|
||||
class AntiSpam(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Compteurs pour le Spam
|
||||
self.message_counts = {}
|
||||
self.spam_warnings = {}
|
||||
|
||||
# Compteurs pour les Liens
|
||||
self.link_warnings = {}
|
||||
|
||||
def is_whitelisted(self, guild_id, user_id):
|
||||
"""Vérifie si l'utilisateur est protégé (Whitelist ou Owner)"""
|
||||
# 1. Le propriétaire est toujours whitelist
|
||||
guild = self.bot.get_guild(guild_id)
|
||||
if guild and user_id == guild.owner_id:
|
||||
return True
|
||||
|
||||
# 2. Vérification fichier Whitelist
|
||||
if not os.path.exists(WHITELIST_FILE): return False
|
||||
try:
|
||||
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return user_id in data.get(str(guild_id), [])
|
||||
except: return False
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message):
|
||||
if message.author.bot or not message.guild: return
|
||||
|
||||
# --- 1. IMMUNITÉ TOTALE ---
|
||||
# Si Whitelisté, on ignore tout (Spam et Liens)
|
||||
if self.is_whitelisted(message.guild.id, message.author.id):
|
||||
return
|
||||
|
||||
# --- 2. CHARGEMENT CONFIG ---
|
||||
if not os.path.exists(SETTINGS_FILE): return
|
||||
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
|
||||
settings = json.load(f).get(str(message.guild.id), {})
|
||||
|
||||
# =================================================================
|
||||
# MODULE ANTI-LINK (3 Avertissements -> Mute)
|
||||
# =================================================================
|
||||
if settings.get("anti_link"):
|
||||
# Liste des déclencheurs interdits
|
||||
forbidden = ["discord.gg/", "http://", "https://", "www."]
|
||||
|
||||
# On vérifie si le message contient un lien interdit
|
||||
if any(trigger in message.content.lower() for trigger in forbidden):
|
||||
# Si l'auteur a la permission de gérer les messages, on laisse passer (optionnel, sinon retirer cette ligne)
|
||||
if not message.author.guild_permissions.manage_messages:
|
||||
await self.handle_link_violation(message)
|
||||
return # On arrête là, pas besoin de vérifier le spam si le message est supprimé
|
||||
|
||||
# =================================================================
|
||||
# MODULE ANTI-SPAM (3 Avertissements -> Mute)
|
||||
# =================================================================
|
||||
if settings.get("anti_spam"):
|
||||
user_id = message.author.id
|
||||
now = datetime.now().timestamp()
|
||||
|
||||
if user_id not in self.message_counts:
|
||||
self.message_counts[user_id] = []
|
||||
|
||||
self.message_counts[user_id].append(now)
|
||||
# Fenêtre de temps : 5 secondes
|
||||
self.message_counts[user_id] = [t for t in self.message_counts[user_id] if now - t < 5]
|
||||
|
||||
# Seuil dynamique (défaut: 5)
|
||||
limit = settings.get("spam_threshold", 5)
|
||||
|
||||
if len(self.message_counts[user_id]) > limit:
|
||||
await self.handle_spam_violation(message)
|
||||
|
||||
|
||||
# --- GESTION DES VIOLATIONS LIENS ---
|
||||
async def handle_link_violation(self, message):
|
||||
try:
|
||||
await message.delete()
|
||||
except:
|
||||
pass # Message déjà supprimé ou manque de perms
|
||||
|
||||
user_id = message.author.id
|
||||
warnings = self.link_warnings.get(user_id, 0) + 1
|
||||
self.link_warnings[user_id] = warnings
|
||||
|
||||
if warnings <= 3:
|
||||
embed = discord.Embed(
|
||||
title="🔗 Liens Interdits",
|
||||
description=f"{message.author.mention}, les liens ne sont pas autorisés.\n**Avertissement : {warnings}/3**",
|
||||
color=0xFFA500
|
||||
)
|
||||
await message.channel.send(embed=embed, delete_after=5)
|
||||
else:
|
||||
await self.sanction_user(message, "Publication de liens interdits")
|
||||
self.link_warnings[user_id] = 0 # Reset après sanction
|
||||
|
||||
# --- GESTION DES VIOLATIONS SPAM ---
|
||||
async def handle_spam_violation(self, message):
|
||||
user_id = message.author.id
|
||||
|
||||
# Nettoyage visuel du spam
|
||||
try:
|
||||
await message.channel.purge(limit=5, check=lambda m: m.author == message.author)
|
||||
except: pass
|
||||
|
||||
warnings = self.spam_warnings.get(user_id, 0) + 1
|
||||
self.spam_warnings[user_id] = warnings
|
||||
|
||||
# Reset le compteur de messages pour éviter le spam d'avertissements
|
||||
self.message_counts[user_id] = []
|
||||
|
||||
if warnings <= 3:
|
||||
embed = discord.Embed(
|
||||
title="🛡️ Anti-Spam",
|
||||
description=f"{message.author.mention}, veuillez cesser de spammer.\n**Avertissement : {warnings}/3**",
|
||||
color=0xFFA500
|
||||
)
|
||||
await message.channel.send(embed=embed, delete_after=5)
|
||||
else:
|
||||
await self.sanction_user(message, "Spam excessif")
|
||||
self.spam_warnings[user_id] = 0 # Reset après sanction
|
||||
|
||||
# --- SANCTION COMMUNE (TIMEOUT 5 MIN) ---
|
||||
async def sanction_user(self, message, reason):
|
||||
try:
|
||||
# Durée : 5 minutes
|
||||
duration = discord.utils.utcnow() + asyncio.timedelta(minutes=5)
|
||||
|
||||
# Application du Timeout (Exclusion temporaire)
|
||||
await message.author.timeout(duration, reason=f"Kuby Security: {reason} (3 Warns)")
|
||||
|
||||
embed = discord.Embed(
|
||||
title="⛔ Exclusion Temporaire",
|
||||
description=f"**Utilisateur :** {message.author.mention}\n**Durée :** 5 minutes\n**Raison :** {reason} (Limite atteinte).",
|
||||
color=0xFF0000
|
||||
)
|
||||
await message.channel.send(embed=embed)
|
||||
|
||||
except discord.Forbidden:
|
||||
await message.channel.send("⚠️ Je n'ai pas la permission d'exclure ce membre (Vérifiez ma hiérarchie).")
|
||||
except Exception as e:
|
||||
print(f"Erreur Sanction: {e}")
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(AntiSpam(bot))
|
||||
62
commandes/modules_security/logs_manager.py
Normal file
62
commandes/modules_security/logs_manager.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
|
||||
def get_log_channel(guild, settings):
|
||||
channel_id = settings.get("log_channel_id")
|
||||
if channel_id:
|
||||
return guild.get_channel(int(channel_id))
|
||||
return None
|
||||
|
||||
class LogsManager(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message_delete(self, message):
|
||||
if message.author.bot: return
|
||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(message.guild.id), {})
|
||||
|
||||
log_chan = get_log_channel(message.guild, settings)
|
||||
if log_chan:
|
||||
embed = discord.Embed(
|
||||
title="🗑️ Message Supprimé",
|
||||
description=f"**Auteur :** {message.author.mention}\n**Salon :** {message.channel.mention}",
|
||||
color=0xFFA500,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
embed.add_field(name="Contenu", value=message.content or "*(Image ou Embed)*")
|
||||
await log_chan.send(embed=embed)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_remove(self, member):
|
||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(member.guild.id), {})
|
||||
log_chan = get_log_channel(member.guild, settings)
|
||||
if log_chan:
|
||||
embed = discord.Embed(
|
||||
title="📤 Départ d'un membre",
|
||||
description=f"{member.mention} ({member.id}) a quitté le serveur.",
|
||||
color=0xFF0000,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
await log_chan.send(embed=embed)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_ban(self, guild, user):
|
||||
settings = dict(json.load(open(SETTINGS_FILE, "r"))).get(str(guild.id), {})
|
||||
log_chan = get_log_channel(guild, settings)
|
||||
if log_chan:
|
||||
embed = discord.Embed(
|
||||
title="🔨 Bannissement",
|
||||
description=f"**Utilisateur :** {user.mention} ({user.id}) a été banni.",
|
||||
color=0x000000,
|
||||
timestamp=datetime.utcnow()
|
||||
)
|
||||
await log_chan.send(embed=embed)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(LogsManager(bot))
|
||||
51
commandes/no_link.py
Executable file
51
commandes/no_link.py
Executable file
|
|
@ -0,0 +1,51 @@
|
|||
# utils/no_link.py
|
||||
import re
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
|
||||
# Fichier JSON de configuration security
|
||||
SECURITY_FILE = "security.json"
|
||||
|
||||
def load_guild_security(guild_id: int) -> dict:
|
||||
"""Retourne la configuration d'un serveur."""
|
||||
if not os.path.isfile(SECURITY_FILE):
|
||||
return {}
|
||||
with open(SECURITY_FILE, "r") as f:
|
||||
data = json.load(f)
|
||||
return data.get(str(guild_id), {})
|
||||
|
||||
# Regex pour détecter les liens
|
||||
URL_REGEX = re.compile(r"(?i)\b((?:https?://|www\.)[^\s<>]+)\b")
|
||||
|
||||
class NoLink(commands.Cog):
|
||||
"""Supprime les messages contenant un lien si l'option anti_link est activée pour le serveur."""
|
||||
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message) -> None:
|
||||
if message.author.bot or not message.guild:
|
||||
return
|
||||
|
||||
guild_settings = load_guild_security(message.guild.id)
|
||||
if not guild_settings.get("anti_link", False):
|
||||
return # Option désactivée → ne rien faire
|
||||
|
||||
if URL_REGEX.search(message.content):
|
||||
try:
|
||||
await message.delete()
|
||||
await message.channel.send(
|
||||
f"{message.author.mention} ⚠️ Les liens sont interdits sur ce serveur.",
|
||||
delete_after=5
|
||||
)
|
||||
except discord.Forbidden:
|
||||
print(f"Impossible de supprimer le message de {message.author} (permissions manquantes).")
|
||||
except Exception as exc:
|
||||
print(f"Erreur lors de la suppression du message : {exc}")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(NoLink(bot))
|
||||
82
commandes/scan.py
Executable file
82
commandes/scan.py
Executable file
|
|
@ -0,0 +1,82 @@
|
|||
# scan.py
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import hashlib
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
|
||||
MALWARE_DB = "malware.json"
|
||||
SUSPICIOUS_EXTENSIONS = [".exe", ".scr", ".bat", ".js", ".vbs", ".cmd"]
|
||||
MALWARE_PATTERNS = [
|
||||
rb"powershell\s+-nop",
|
||||
rb"cmd\s+/c",
|
||||
rb"eval\(",
|
||||
rb"base64_decode\(",
|
||||
rb"CreateObject\(\"WScript\.Shell\"\)"
|
||||
]
|
||||
|
||||
def load_malware_db():
|
||||
if not os.path.isfile(MALWARE_DB):
|
||||
with open(MALWARE_DB, "w") as f:
|
||||
json.dump([], f)
|
||||
return []
|
||||
with open(MALWARE_DB, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def get_file_hash(file_path):
|
||||
sha256_hash = hashlib.sha256()
|
||||
with open(file_path, "rb") as f:
|
||||
for byte_block in iter(lambda: f.read(4096), b""):
|
||||
sha256_hash.update(byte_block)
|
||||
return sha256_hash.hexdigest()
|
||||
|
||||
def scan_file(file_path):
|
||||
if not os.path.isfile(file_path):
|
||||
return {"error": "Fichier introuvable."}
|
||||
|
||||
file_hash = get_file_hash(file_path)
|
||||
malware_db = load_malware_db()
|
||||
|
||||
if file_hash in malware_db:
|
||||
return {"hash": file_hash, "malicious": True, "reason": "Hash connu dans la base malveillante."}
|
||||
|
||||
_, ext = os.path.splitext(file_path)
|
||||
if ext.lower() in SUSPICIOUS_EXTENSIONS:
|
||||
return {"hash": file_hash, "malicious": True, "reason": f"Extension suspecte : {ext}"}
|
||||
|
||||
with open(file_path, "rb") as f:
|
||||
content = f.read()
|
||||
for pattern in MALWARE_PATTERNS:
|
||||
if re.search(pattern, content):
|
||||
return {"hash": file_hash, "malicious": True, "reason": f"Pattern suspect détecté : {pattern.decode(errors='ignore')}"}
|
||||
|
||||
return {"hash": file_hash, "malicious": False, "reason": "Fichier sain."}
|
||||
|
||||
|
||||
class Scan(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@app_commands.command(name="scan", description="Scanner un fichier pour détecter un malware.")
|
||||
async def scan(self, interaction: discord.Interaction, file: discord.Attachment):
|
||||
await interaction.response.defer() # Permet de prendre un peu de temps pour le scan
|
||||
|
||||
# Téléchargement temporaire du fichier
|
||||
tmp_path = f"tmp_{file.filename}"
|
||||
await file.save(tmp_path)
|
||||
|
||||
result = scan_file(tmp_path)
|
||||
os.remove(tmp_path) # Supprime le fichier temporaire
|
||||
|
||||
if result.get("error"):
|
||||
await interaction.followup.send(f"❌ Erreur : {result['error']}")
|
||||
elif result.get("malicious"):
|
||||
await interaction.followup.send(f"⚠️ Fichier malveillant détecté !\nRaison : {result['reason']}\nHash : `{result['hash']}`")
|
||||
else:
|
||||
await interaction.followup.send(f"✅ Fichier sain.\nHash : `{result['hash']}`")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(Scan(bot))
|
||||
161
commandes/security.py
Executable file
161
commandes/security.py
Executable file
|
|
@ -0,0 +1,161 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
|
||||
# Chemins des fichiers
|
||||
SETTINGS_FILE = "data/security_settings.json"
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
|
||||
# --- UTILITAIRES ---
|
||||
def load_settings(guild_id):
|
||||
if not os.path.exists(SETTINGS_FILE): return {}
|
||||
try:
|
||||
with open(SETTINGS_FILE, "r", encoding='utf-8') as f:
|
||||
return json.load(f).get(str(guild_id), {})
|
||||
except: return {}
|
||||
|
||||
def save_settings(guild_id, settings):
|
||||
data = {}
|
||||
if os.path.exists(SETTINGS_FILE):
|
||||
try:
|
||||
with open(SETTINGS_FILE, "r", encoding='utf-8') as f: data = json.load(f)
|
||||
except: pass
|
||||
data[str(guild_id)] = settings
|
||||
os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
|
||||
with open(SETTINGS_FILE, "w", encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=4, ensure_ascii=False)
|
||||
|
||||
def is_whitelisted(guild_id, user_id):
|
||||
if not os.path.exists(WHITELIST_FILE): return False
|
||||
try:
|
||||
with open(WHITELIST_FILE, "r", encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
return user_id in data.get(str(guild_id), [])
|
||||
except: return False
|
||||
|
||||
# --- MODAL DE RÉGLAGE DU SEUIL ---
|
||||
class ThresholdModal(discord.ui.Modal, title="Réglage du Seuil Anti-Spam"):
|
||||
threshold_input = discord.ui.TextInput(
|
||||
label="Messages max autorisés (en 5 secondes)",
|
||||
placeholder="Par défaut : 5",
|
||||
min_length=1,
|
||||
max_length=2,
|
||||
required=True
|
||||
)
|
||||
|
||||
def __init__(self, guild_id, settings, view):
|
||||
super().__init__()
|
||||
self.guild_id = guild_id
|
||||
self.settings = settings
|
||||
self.parent_view = view
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
if not self.threshold_input.value.isdigit():
|
||||
return await interaction.response.send_message("❌ Erreur : Veuillez entrer un nombre entier.", ephemeral=True)
|
||||
|
||||
new_val = int(self.threshold_input.value)
|
||||
self.settings["spam_threshold"] = new_val
|
||||
save_settings(self.guild_id, self.settings)
|
||||
|
||||
await interaction.response.send_message(f"✅ Le seuil a été réglé sur **{new_val}** messages.", ephemeral=True)
|
||||
|
||||
# --- VUE DE GESTION ---
|
||||
class SecurityView(discord.ui.View):
|
||||
def __init__(self, interaction, settings, category=None):
|
||||
super().__init__(timeout=None)
|
||||
self.interaction = interaction
|
||||
self.guild = interaction.guild
|
||||
self.settings = settings
|
||||
self.category = category
|
||||
self.build_view()
|
||||
|
||||
def build_view(self):
|
||||
self.clear_items()
|
||||
|
||||
if self.category is None:
|
||||
self.add_item(CategorySelect())
|
||||
else:
|
||||
config_map = {
|
||||
"raid": [("anti_raid", "🛡️ Anti-Raid"), ("auto_ban", "🔨 Auto-Ban")],
|
||||
"spam": [("anti_spam", "🛡️ Anti-Spam"), ("anti_link", "🔗 Anti-Link"), ("anti_mention", "👥 Anti-Mention")],
|
||||
"nuke": [("anti_channel_delete", "📁 Anti-Salon"), ("anti_role_create", "🎭 Anti-Rôle")],
|
||||
"logs": [("server_logs", "📋 Activer Logs")]
|
||||
}
|
||||
|
||||
for key, label in config_map.get(self.category, []):
|
||||
is_on = self.settings.get(key, False)
|
||||
style = discord.ButtonStyle.green if is_on else discord.ButtonStyle.red
|
||||
status = "✅" if is_on else "❌"
|
||||
btn = discord.ui.Button(label=f"{label} [{status}]", style=style, custom_id=key)
|
||||
btn.callback = self.toggle_setting
|
||||
self.add_item(btn)
|
||||
|
||||
# BOUTON SPÉCIAL : RÉGLAGE SEUIL (uniquement dans spam)
|
||||
if self.category == "spam":
|
||||
btn_threshold = discord.ui.Button(label=f"🔢 Seuil: {self.settings.get('spam_threshold', 5)}", style=discord.ButtonStyle.blurple)
|
||||
btn_threshold.callback = self.open_threshold_modal
|
||||
self.add_item(btn_threshold)
|
||||
|
||||
back_btn = discord.ui.Button(label="⬅️ Retour", style=discord.ButtonStyle.gray)
|
||||
back_btn.callback = self.back_to_main
|
||||
self.add_item(back_btn)
|
||||
|
||||
async def toggle_setting(self, interaction: discord.Interaction):
|
||||
key = interaction.data['custom_id']
|
||||
self.settings[key] = not self.settings.get(key, False)
|
||||
save_settings(self.guild.id, self.settings)
|
||||
self.build_view()
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
async def open_threshold_modal(self, interaction: discord.Interaction):
|
||||
await interaction.response.send_modal(ThresholdModal(self.guild.id, self.settings, self))
|
||||
|
||||
async def back_to_main(self, interaction: discord.Interaction):
|
||||
self.category = None
|
||||
self.build_view()
|
||||
embed = self.create_main_embed()
|
||||
await interaction.response.edit_message(embed=embed, view=self)
|
||||
|
||||
def create_main_embed(self):
|
||||
embed = discord.Embed(title="🛡️ Centre de Sécurité Kuby", description="Interface modulaire de protection.", color=0x2b2d31)
|
||||
if self.guild.icon: embed.set_thumbnail(url=self.guild.icon.url)
|
||||
embed.add_field(name="Propriétaire", value=self.guild.owner.mention)
|
||||
return embed
|
||||
|
||||
class CategorySelect(discord.ui.Select):
|
||||
def __init__(self):
|
||||
options = [
|
||||
discord.SelectOption(label="Anti-Raid", value="raid", emoji="⚔️"),
|
||||
discord.SelectOption(label="Anti-Spam", value="spam", emoji="🛡️"),
|
||||
discord.SelectOption(label="Anti-Nuke", value="nuke", emoji="☢️"),
|
||||
discord.SelectOption(label="Logs", value="logs", emoji="📋")
|
||||
]
|
||||
super().__init__(placeholder="Sélectionnez un module...", options=options)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction):
|
||||
view: SecurityView = self.view
|
||||
view.category = self.values[0]
|
||||
view.build_view()
|
||||
embed = discord.Embed(title=f"⚙️ Configuration : {view.category.upper()}", color=0x2b2d31)
|
||||
await interaction.response.edit_message(embed=embed, view=view)
|
||||
|
||||
# --- COG ---
|
||||
class Security(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@app_commands.command(name="security", description="Ouvrir le panneau Kuby")
|
||||
async def security(self, interaction: discord.Interaction):
|
||||
if not (interaction.user.guild_permissions.administrator or
|
||||
interaction.user.id == interaction.guild.owner_id or
|
||||
is_whitelisted(interaction.guild.id, interaction.user.id)):
|
||||
return await interaction.response.send_message("❌ Accès refusé.", ephemeral=True)
|
||||
|
||||
settings = load_settings(interaction.guild.id)
|
||||
view = SecurityView(interaction, settings)
|
||||
await interaction.response.send_message(embed=view.create_main_embed(), view=view)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Security(bot))
|
||||
28
commandes/sendbotmessages.py
Executable file
28
commandes/sendbotmessages.py
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
class SentBotMessages(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
|
||||
@app_commands.command(name="sentbotmessages", description="Le bot envoie un message dans un salon choisi.")
|
||||
@app_commands.describe(
|
||||
message="Le message à envoyer",
|
||||
salon="Le salon où envoyer le message"
|
||||
)
|
||||
async def sentbotmessages(self, interaction: discord.Interaction, message: str, salon: discord.TextChannel):
|
||||
# Vérifie si l'utilisateur est admin
|
||||
if not interaction.user.guild_permissions.administrator:
|
||||
await interaction.response.send_message("⛔ Tu n'as pas la permission d'utiliser cette commande.", ephemeral=True)
|
||||
return
|
||||
|
||||
try:
|
||||
await salon.send(message)
|
||||
await interaction.response.send_message(f"✅ Message envoyé dans {salon.mention}.", ephemeral=True)
|
||||
except Exception as e:
|
||||
await interaction.response.send_message(f"❌ Une erreur est survenue : {e}", ephemeral=True)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(SentBotMessages(bot))
|
||||
|
||||
114
commandes/snipe.py
Executable file
114
commandes/snipe.py
Executable file
|
|
@ -0,0 +1,114 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from datetime import datetime
|
||||
from collections import deque
|
||||
|
||||
class SnipeCommands(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Dictionary to store deleted messages per channel
|
||||
# Key: channel_id, Value: deque of deleted messages (max 10 per channel)
|
||||
self.deleted_messages = {}
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message_delete(self, message):
|
||||
"""Track deleted messages for the snipe command"""
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
channel_id = message.channel.id
|
||||
|
||||
# Initialize deque for this channel if it doesn't exist
|
||||
if channel_id not in self.deleted_messages:
|
||||
self.deleted_messages[channel_id] = deque(maxlen=10)
|
||||
|
||||
# Store message details
|
||||
message_data = {
|
||||
'content': message.content,
|
||||
'author': message.author,
|
||||
'timestamp': datetime.now(),
|
||||
'attachments': [attachment.url for attachment in message.attachments],
|
||||
'embeds': len(message.embeds)
|
||||
}
|
||||
|
||||
self.deleted_messages[channel_id].appendleft(message_data)
|
||||
|
||||
@app_commands.command(name="snipe", description="Voir les derniers messages supprimés d'un salon")
|
||||
@app_commands.describe(nombre="Nombre de messages à afficher (1-10, par défaut: 1)")
|
||||
async def snipe(self, interaction: discord.Interaction, nombre: int = 1):
|
||||
"""Retrieve deleted messages from the current channel"""
|
||||
channel_id = interaction.channel.id
|
||||
|
||||
# Validate the number parameter
|
||||
if nombre < 1 or nombre > 10:
|
||||
await interaction.response.send_message(
|
||||
"❌ Le nombre doit être entre 1 et 10.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Check if we have any deleted messages for this channel
|
||||
if channel_id not in self.deleted_messages or not self.deleted_messages[channel_id]:
|
||||
await interaction.response.send_message(
|
||||
"❌ Aucun message supprimé trouvé dans ce salon.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Get the requested number of messages
|
||||
messages = list(self.deleted_messages[channel_id])[:nombre]
|
||||
|
||||
# Create embed for displaying deleted messages
|
||||
embed = discord.Embed(
|
||||
title=f"🗑️ Messages supprimés ({len(messages)} sur {len(self.deleted_messages[channel_id])} disponibles)",
|
||||
color=discord.Color.red()
|
||||
)
|
||||
|
||||
for i, msg_data in enumerate(messages, 1):
|
||||
author = msg_data['author']
|
||||
content = msg_data['content'] if msg_data['content'] else "*Message sans texte*"
|
||||
timestamp = msg_data['timestamp'].strftime("%H:%M:%S")
|
||||
|
||||
# Truncate long messages
|
||||
if len(content) > 1000:
|
||||
content = content[:997] + "..."
|
||||
|
||||
field_name = f"#{i} - {author.display_name} ({timestamp})"
|
||||
field_value = content
|
||||
|
||||
# Add attachment info if present
|
||||
if msg_data['attachments']:
|
||||
field_value += f"\n📎 {len(msg_data['attachments'])} pièce(s) jointe(s)"
|
||||
if msg_data['embeds'] > 0:
|
||||
field_value += f"\n📊 {msg_data['embeds']} embed(s)"
|
||||
|
||||
embed.add_field(
|
||||
name=field_name,
|
||||
value=field_value,
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.set_footer(text=f"Canal: #{interaction.channel.name}")
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="snipeclear", description="Effacer l'historique des messages supprimés pour ce salon")
|
||||
async def snipeclear(self, interaction: discord.Interaction):
|
||||
"""Clear the deleted messages history for the current channel"""
|
||||
channel_id = interaction.channel.id
|
||||
|
||||
if channel_id in self.deleted_messages:
|
||||
self.deleted_messages[channel_id].clear()
|
||||
await interaction.response.send_message(
|
||||
"✅ Historique des messages supprimés effacé pour ce salon.",
|
||||
ephemeral=True
|
||||
)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
"❌ Aucun historique à effacer pour ce salon.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(SnipeCommands(bot))
|
||||
207
commandes/ticket.py
Normal file
207
commandes/ticket.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import discord
|
||||
from discord.ext import commands, tasks
|
||||
from discord.ui import View, Button, Modal, TextInput
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
|
||||
# --- Fonctions de gestion de données ---
|
||||
|
||||
def load_config():
|
||||
"""Charge la configuration des tickets"""
|
||||
path = "data/tickets/config.json"
|
||||
if not os.path.exists("data/tickets"):
|
||||
os.makedirs("data/tickets")
|
||||
if not os.path.exists(path):
|
||||
with open(path, "w") as f:
|
||||
json.dump({}, f, indent=4)
|
||||
with open(path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
def save_config(config):
|
||||
"""Sauvegarde la configuration des tickets"""
|
||||
path = "data/tickets/config.json"
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
json.dump(config, f, indent=4, ensure_ascii=False)
|
||||
|
||||
# --- Vues (Interactions) ---
|
||||
|
||||
class TicketView(View):
|
||||
def __init__(self, bot, config):
|
||||
super().__init__(timeout=None)
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
|
||||
@discord.ui.button(label="Créer un ticket", style=discord.ButtonStyle.blurple, custom_id="create_ticket")
|
||||
async def create_ticket(self, interaction: discord.Interaction, button: Button):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
guild_id = str(interaction.guild_id)
|
||||
|
||||
if guild_id not in self.config:
|
||||
return await interaction.followup.send("Le système n'est pas configuré.", ephemeral=True)
|
||||
|
||||
guild_config = self.config[guild_id]
|
||||
|
||||
# Vérifier si l'utilisateur a déjà un ticket
|
||||
for channel in interaction.guild.text_channels:
|
||||
if channel.name.startswith(f"ticket-{interaction.user.id}"):
|
||||
return await interaction.followup.send(f"Vous avez déjà un ticket : {channel.mention}", ephemeral=True)
|
||||
|
||||
# Configuration des permissions
|
||||
category = interaction.guild.get_channel(int(guild_config["category_id"]))
|
||||
overwrites = {
|
||||
interaction.guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
||||
interaction.user: discord.PermissionOverwrite(read_messages=True, send_messages=True, attach_files=True),
|
||||
interaction.guild.me: discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
}
|
||||
|
||||
for role_id in guild_config["staff_roles"]:
|
||||
role = interaction.guild.get_role(int(role_id))
|
||||
if role:
|
||||
overwrites[role] = discord.PermissionOverwrite(read_messages=True, send_messages=True)
|
||||
|
||||
# Création du salon
|
||||
channel_name = f"ticket-{interaction.user.id}-{datetime.now().strftime('%m%d%H%M')}"
|
||||
ticket_channel = await interaction.guild.create_text_channel(
|
||||
name=channel_name,
|
||||
category=category,
|
||||
overwrites=overwrites,
|
||||
topic=f"Ticket de {interaction.user} ({interaction.user.id})"
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Nouveau Ticket",
|
||||
description=guild_config["ticket_message"],
|
||||
color=discord.Color.blue(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
embed.set_author(name=interaction.user.display_name, icon_url=interaction.user.display_avatar.url)
|
||||
|
||||
await ticket_channel.send(embed=embed, view=TicketCloseView(self.bot, self.config))
|
||||
|
||||
# Logs
|
||||
if guild_config.get("log_channel"):
|
||||
log_ch = self.bot.get_channel(int(guild_config["log_channel"]))
|
||||
if log_ch:
|
||||
log_embed = discord.Embed(title="Ticket Créé", color=discord.Color.green())
|
||||
log_embed.add_field(name="Auteur", value=interaction.user.mention)
|
||||
log_embed.add_field(name="Salon", value=ticket_channel.mention)
|
||||
await log_ch.send(embed=log_embed)
|
||||
|
||||
await interaction.followup.send(f"Ticket créé : {ticket_channel.mention}", ephemeral=True)
|
||||
|
||||
class TicketCloseView(View):
|
||||
def __init__(self, bot, config):
|
||||
super().__init__(timeout=None)
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
|
||||
@discord.ui.button(label="Fermer le ticket", style=discord.ButtonStyle.red, custom_id="close_ticket")
|
||||
async def close_ticket(self, interaction: discord.Interaction, button: Button):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
guild_config = self.config.get(str(interaction.guild_id))
|
||||
|
||||
# Modifier permissions (bloquer l'écriture)
|
||||
overwrites = interaction.channel.overwrites
|
||||
for target in overwrites:
|
||||
if isinstance(target, discord.Member) and target != interaction.guild.me:
|
||||
overwrites[target] = discord.PermissionOverwrite(read_messages=True, send_messages=False)
|
||||
|
||||
await interaction.channel.edit(name=f"closed-{interaction.channel.name}", overwrites=overwrites)
|
||||
|
||||
embed = discord.Embed(title="Ticket Fermé", description=f"Fermé par {interaction.user.mention}", color=discord.Color.red())
|
||||
await interaction.channel.send(embed=embed, view=TicketReopenView(self.bot, self.config))
|
||||
await interaction.followup.send("Ticket fermé.", ephemeral=True)
|
||||
|
||||
class TicketReopenView(View):
|
||||
def __init__(self, bot, config):
|
||||
super().__init__(timeout=None)
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
|
||||
@discord.ui.button(label="Rouvrir", style=discord.ButtonStyle.green, custom_id="reopen_ticket")
|
||||
async def reopen(self, interaction: discord.Interaction, button: Button):
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
new_name = interaction.channel.name.replace("closed-", "")
|
||||
await interaction.channel.edit(name=new_name)
|
||||
await interaction.channel.send(f"Ticket rouvert par {interaction.user.mention}", view=TicketCloseView(self.bot, self.config))
|
||||
|
||||
@discord.ui.button(label="Supprimer", style=discord.ButtonStyle.grey, custom_id="delete_ticket")
|
||||
async def delete(self, interaction: discord.Interaction, button: Button):
|
||||
await interaction.response.send_message("Suppression dans 3s...")
|
||||
await asyncio.sleep(3)
|
||||
await interaction.channel.delete()
|
||||
|
||||
# --- Modale de Configuration ---
|
||||
|
||||
class TicketConfigModal(Modal):
|
||||
def __init__(self, bot, config, guild_id):
|
||||
super().__init__(title="Configuration Tickets")
|
||||
self.bot = bot
|
||||
self.config = config
|
||||
self.guild_id = str(guild_id)
|
||||
|
||||
self.category_id = TextInput(label="ID Catégorie", placeholder="Ex: 123456789", required=True)
|
||||
self.staff_roles = TextInput(label="IDs Rôles Staff (virgule)", placeholder="ID1, ID2", required=True)
|
||||
self.log_channel = TextInput(label="ID Salon Logs", required=False)
|
||||
self.ticket_msg = TextInput(label="Message Ticket", style=discord.TextStyle.paragraph, required=True)
|
||||
self.welcome_msg = TextInput(label="Message Panel", required=True)
|
||||
|
||||
for item in [self.category_id, self.staff_roles, self.log_channel, self.ticket_msg, self.welcome_msg]:
|
||||
self.add_item(item)
|
||||
|
||||
async def on_submit(self, interaction: discord.Interaction):
|
||||
role_ids = [r.strip() for r in self.staff_roles.value.split(",")]
|
||||
self.config[self.guild_id] = {
|
||||
"category_id": self.category_id.value,
|
||||
"staff_roles": role_ids,
|
||||
"log_channel": self.log_channel.value,
|
||||
"ticket_message": self.ticket_msg.value,
|
||||
"welcome_message": self.welcome_msg.value
|
||||
}
|
||||
save_config(self.config)
|
||||
await interaction.response.send_message("Configuration sauvegardée !", ephemeral=True)
|
||||
|
||||
# --- Le Cog Principal ---
|
||||
|
||||
class Ticket(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.config = load_config()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
# Enregistrer les vues persistantes (pour qu'elles marchent après reboot)
|
||||
self.bot.add_view(TicketView(self.bot, self.config))
|
||||
self.bot.add_view(TicketCloseView(self.bot, self.config))
|
||||
self.bot.add_view(TicketReopenView(self.bot, self.config))
|
||||
print("Module Ticket prêt")
|
||||
|
||||
@commands.hybrid_group(name="ticket")
|
||||
@commands.has_permissions(administrator=True)
|
||||
async def ticket(self, ctx):
|
||||
if ctx.invoked_subcommand is None:
|
||||
await ctx.send("Utilisez `/ticket setup` ou `/ticket panel`", ephemeral=True)
|
||||
|
||||
@ticket.command(name="setup")
|
||||
async def setup_cmd(self, ctx):
|
||||
await ctx.interaction.response.send_modal(TicketConfigModal(self.bot, self.config, ctx.guild.id))
|
||||
|
||||
@ticket.command(name="panel")
|
||||
async def panel_cmd(self, ctx, channel: discord.TextChannel = None):
|
||||
guild_id = str(ctx.guild.id)
|
||||
if guild_id not in self.config:
|
||||
return await ctx.send("Configurez d'abord le système via `/ticket setup`.")
|
||||
|
||||
target = channel or ctx.channel
|
||||
embed = discord.Embed(
|
||||
title="Support",
|
||||
description=self.config[guild_id]["welcome_message"],
|
||||
color=discord.Color.blue()
|
||||
)
|
||||
await target.send(embed=embed, view=TicketView(self.bot, self.config))
|
||||
await ctx.send("Panel envoyé !", ephemeral=True)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Ticket(bot))
|
||||
116
commandes/welcome.py
Normal file
116
commandes/welcome.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import app_commands
|
||||
import sqlite3
|
||||
import aiohttp
|
||||
import io
|
||||
import os
|
||||
import logging
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
class Welcome(commands.Cog):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
# Base de données à la racine
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), "..", "config.db")
|
||||
self._init_db()
|
||||
|
||||
def _init_db(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS server_configs (
|
||||
server_id INTEGER PRIMARY KEY,
|
||||
welcome_channel INTEGER,
|
||||
goodbye_channel INTEGER,
|
||||
welcome_dm_message TEXT
|
||||
)
|
||||
''')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
async def create_welcome_image(self, username, avatar_url):
|
||||
current_dir = os.path.dirname(__file__)
|
||||
base_dir = os.path.abspath(os.path.join(current_dir, ".."))
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(avatar_url) as resp:
|
||||
avatar_bytes = await resp.read()
|
||||
|
||||
background = Image.open(os.path.join(base_dir, 'background_template.png'))
|
||||
draw = ImageDraw.Draw(background)
|
||||
|
||||
font_large = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 70)
|
||||
font_small = ImageFont.truetype(os.path.join(base_dir, 'custom_font_small.otf'), 50)
|
||||
font_large_omega = ImageFont.truetype(os.path.join(base_dir, 'custom_font_large.otf'), 55)
|
||||
|
||||
draw.text((325, 70), "Bienvenue", font=font_large, fill=(255,255,255))
|
||||
draw.text((325, 135), f"{username} chez", font=font_small, fill=(255,255,255))
|
||||
draw.text((325, 195), "OMEGA KUBE", font=font_large_omega, fill=(255,255,255))
|
||||
|
||||
avatar = Image.open(io.BytesIO(avatar_bytes)).convert("RGBA").resize((224, 224), Image.LANCZOS)
|
||||
mask = Image.new('L', avatar.size, 0)
|
||||
mask_draw = ImageDraw.Draw(mask)
|
||||
mask_draw.ellipse((12, 12, 212, 212), fill=255)
|
||||
background.paste(avatar, (40, 40), mask)
|
||||
|
||||
img_byte_arr = io.BytesIO()
|
||||
background.save(img_byte_arr, format='PNG')
|
||||
img_byte_arr.seek(0)
|
||||
return img_byte_arr
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur image: {e}")
|
||||
return None
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
# On récupère 2 colonnes : index 0 et 1
|
||||
cursor.execute('SELECT welcome_channel, welcome_dm_message FROM server_configs WHERE server_id = ?', (member.guild.id,))
|
||||
result = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
# 1. Gestion du DM (Index 1)
|
||||
welcome_dm_text = result[1] if result and result[1] else "Bienvenue {username} sur le serveur {servername} ! 🎉\n\nNous sommes ravis de t'accueillir parmi nous."
|
||||
|
||||
try:
|
||||
formatted_message = welcome_dm_text.format(
|
||||
username=member.name,
|
||||
servername=member.guild.name,
|
||||
member_count=member.guild.member_count
|
||||
)
|
||||
await member.send(formatted_message)
|
||||
await member.send("https://i.giphy.com/media/v1.Y2lkPTc5MGI3NjExNXV1NW02NTk0bGF2ZHcyMGFna2F6amRrbGpobGpsMHowOW5xa2E5eCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/ZtFuW4rsUbfrRzPauJ/giphy.gif")
|
||||
except Exception as e:
|
||||
logging.warning(f"Impossible d'envoyer le DM à {member.name}: {e}")
|
||||
|
||||
# 2. Gestion du salon de bienvenue (Index 0)
|
||||
if result and result[0]:
|
||||
channel = member.guild.get_channel(result[0])
|
||||
if channel:
|
||||
img = await self.create_welcome_image(member.name, member.avatar.url if member.avatar else member.default_avatar.url)
|
||||
if img:
|
||||
embed = discord.Embed(title=f"Merci d'accueillir {member} 🤝",
|
||||
description="Bienvenue dans le serveur Omega Kube !\nSi vous cherchez des RolePlay de qualité,\nVous êtes au bon endroit. <a:sip:1316891821858619452>",
|
||||
color=discord.Color.dark_purple())
|
||||
embed.set_image(url="attachment://welcome.png")
|
||||
await channel.send(content=member.mention, embed=embed, file=discord.File(img, filename="welcome.png"))
|
||||
|
||||
@app_commands.command(name="setwelcome", description="Définit le salon et le message DM de bienvenue")
|
||||
@app_commands.checks.has_permissions(administrator=True)
|
||||
async def set_welcome(self, interaction: discord.Interaction, channel: discord.TextChannel, message_dm: str = None):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''INSERT INTO server_configs (server_id, welcome_channel, welcome_dm_message)
|
||||
VALUES (?, ?, ?) ON CONFLICT(server_id)
|
||||
DO UPDATE SET welcome_channel=excluded.welcome_channel,
|
||||
welcome_dm_message=COALESCE(excluded.welcome_dm_message, welcome_dm_message)''',
|
||||
(interaction.guild_id, channel.id, message_dm))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
await interaction.response.send_message("Configuration de bienvenue enregistrée !", ephemeral=True)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Welcome(bot))
|
||||
98
commandes/whitelist.py
Executable file
98
commandes/whitelist.py
Executable file
|
|
@ -0,0 +1,98 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
import json
|
||||
import os
|
||||
from typing import List
|
||||
|
||||
WHITELIST_FILE = "data/whitelist.json"
|
||||
|
||||
class Whitelist(commands.GroupCog, name="whitelist"):
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.whitelist = self.load_whitelist()
|
||||
|
||||
def load_whitelist(self):
|
||||
if not os.path.exists(WHITELIST_FILE):
|
||||
return {}
|
||||
try:
|
||||
with open(WHITELIST_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
def save_whitelist(self):
|
||||
with open(WHITELIST_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(self.whitelist, f, ensure_ascii=False, indent=4)
|
||||
|
||||
def _get_guild_whitelist(self, guild_id: int) -> List[str]:
|
||||
return self.whitelist.get(str(guild_id), [])
|
||||
|
||||
def _set_guild_whitelist(self, guild_id: int, users: List[str]):
|
||||
self.whitelist[str(guild_id)] = users
|
||||
self.save_whitelist()
|
||||
|
||||
# ✅ Ajouter un utilisateur
|
||||
@app_commands.command(name="ajouter", description="Ajoute un utilisateur à la whitelist")
|
||||
@app_commands.describe(utilisateur="L'utilisateur à ajouter")
|
||||
async def ajouter(self, interaction: discord.Interaction, utilisateur: discord.User):
|
||||
guild_id = interaction.guild.id
|
||||
users = self._get_guild_whitelist(guild_id)
|
||||
user_id = str(utilisateur.id)
|
||||
|
||||
if user_id in users:
|
||||
await interaction.response.send_message(f"🔒 {utilisateur.mention} est déjà dans la whitelist.", ephemeral=True)
|
||||
return
|
||||
|
||||
users.append(user_id)
|
||||
self._set_guild_whitelist(guild_id, users)
|
||||
await interaction.response.send_message(f"✅ {utilisateur.mention} a été ajouté à la whitelist.", ephemeral=True)
|
||||
|
||||
# ❌ Retirer un utilisateur
|
||||
@app_commands.command(name="retirer", description="Retire un utilisateur de la whitelist")
|
||||
@app_commands.describe(utilisateur="L'utilisateur à retirer")
|
||||
async def retirer(self, interaction: discord.Interaction, utilisateur: discord.User):
|
||||
guild_id = interaction.guild.id
|
||||
users = self._get_guild_whitelist(guild_id)
|
||||
user_id = str(utilisateur.id)
|
||||
|
||||
if user_id not in users:
|
||||
await interaction.response.send_message(f"⚠️ {utilisateur.mention} n'est pas dans la whitelist.", ephemeral=True)
|
||||
return
|
||||
|
||||
users.remove(user_id)
|
||||
self._set_guild_whitelist(guild_id, users)
|
||||
await interaction.response.send_message(f"✅ {utilisateur.mention} a été retiré de la whitelist.", ephemeral=True)
|
||||
|
||||
# 📋 Voir la whitelist
|
||||
@app_commands.command(name="liste", description="Affiche les utilisateurs de la whitelist")
|
||||
async def liste(self, interaction: discord.Interaction):
|
||||
guild_id = interaction.guild.id
|
||||
users = self._get_guild_whitelist(guild_id)
|
||||
|
||||
if not users:
|
||||
await interaction.response.send_message("📭 La whitelist est vide pour ce serveur.", ephemeral=True)
|
||||
return
|
||||
|
||||
membres = []
|
||||
for user_id in users:
|
||||
try:
|
||||
utilisateur = await self.bot.fetch_user(int(user_id))
|
||||
membres.append(f"- {utilisateur.mention} ({utilisateur.name}#{utilisateur.discriminator})")
|
||||
except discord.NotFound:
|
||||
membres.append(f"- <@{user_id}> (Utilisateur introuvable)")
|
||||
|
||||
texte = "\n".join(membres)
|
||||
await interaction.response.send_message(f"📃 **Utilisateurs whitelistés pour ce serveur :**\n{texte}", ephemeral=True)
|
||||
|
||||
def is_whitelisted(self, guild_id: int, user_id: int) -> bool:
|
||||
"""Check if a user is whitelisted for a specific guild."""
|
||||
return str(user_id) in self._get_guild_whitelist(guild_id)
|
||||
|
||||
def get_whitelist_users(self, guild_id: int) -> List[str]:
|
||||
"""Get all whitelisted user IDs for a specific guild."""
|
||||
return self._get_guild_whitelist(guild_id).copy()
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(Whitelist(bot))
|
||||
256
commandes/whitelist_monitor.py
Executable file
256
commandes/whitelist_monitor.py
Executable file
|
|
@ -0,0 +1,256 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ui import View, Button
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
class WhitelistMonitor(commands.Cog):
|
||||
"""Monitors whitelist members for role changes and server leave events."""
|
||||
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.whitelist_file = "data/whitelist.json"
|
||||
self.big_permissions = [
|
||||
discord.Permissions.administrator,
|
||||
discord.Permissions.manage_guild,
|
||||
discord.Permissions.manage_roles,
|
||||
discord.Permissions.manage_channels,
|
||||
discord.Permissions.kick_members,
|
||||
discord.Permissions.ban_members,
|
||||
discord.Permissions.manage_messages,
|
||||
discord.Permissions.moderate_members,
|
||||
discord.Permissions.manage_nicknames,
|
||||
discord.Permissions.manage_emojis,
|
||||
discord.Permissions.manage_webhooks,
|
||||
discord.Permissions.view_audit_log
|
||||
]
|
||||
|
||||
def load_whitelist(self) -> List[str]:
|
||||
"""Load whitelist from JSON file."""
|
||||
if not os.path.exists(self.whitelist_file):
|
||||
return []
|
||||
try:
|
||||
with open(self.whitelist_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, FileNotFoundError):
|
||||
return []
|
||||
|
||||
def save_whitelist(self, whitelist: List[str]):
|
||||
"""Save whitelist to JSON file."""
|
||||
with open(self.whitelist_file, "w", encoding="utf-8") as f:
|
||||
json.dump(whitelist, f, ensure_ascii=False, indent=4)
|
||||
|
||||
def is_whitelisted(self, user_id: int) -> bool:
|
||||
"""Check if user is whitelisted."""
|
||||
whitelist = self.load_whitelist()
|
||||
return str(user_id) in whitelist
|
||||
|
||||
def remove_from_whitelist(self, user_id: int) -> bool:
|
||||
"""Remove user from whitelist."""
|
||||
whitelist = self.load_whitelist()
|
||||
user_str = str(user_id)
|
||||
if user_str in whitelist:
|
||||
whitelist.remove(user_str)
|
||||
self.save_whitelist(whitelist)
|
||||
return True
|
||||
return False
|
||||
|
||||
def has_big_permissions(self, role: discord.Role) -> bool:
|
||||
"""Check if role has significant permissions."""
|
||||
permissions = role.permissions
|
||||
for perm in self.big_permissions:
|
||||
if permissions.administrator or getattr(permissions, perm.name, False):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def send_whitelist_notification(self, member: discord.Member, removed_roles: List[discord.Role]):
|
||||
"""Send DM notification to user about whitelist member losing roles."""
|
||||
try:
|
||||
# Find guild administrators to notify
|
||||
admins = [m for m in member.guild.members if m.guild_permissions.administrator and not m.bot]
|
||||
if not admins:
|
||||
return
|
||||
|
||||
# Create embed
|
||||
embed = discord.Embed(
|
||||
title="⚠️ Whitelist Alert",
|
||||
description=f"Un membre whitelisté a perdu des rôles importants dans **{member.guild.name}**.",
|
||||
color=discord.Color.orange(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="👤 Membre concerné",
|
||||
value=f"{member.mention} ({member.display_name})",
|
||||
inline=False
|
||||
)
|
||||
|
||||
if removed_roles:
|
||||
roles_text = "\n".join([f"• {role.name}" for role in removed_roles])
|
||||
embed.add_field(
|
||||
name="🗑️ Rôles retirés",
|
||||
value=roles_text,
|
||||
inline=False
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="📋 Statut whitelist",
|
||||
value="✅ Toujours whitelisté",
|
||||
inline=True
|
||||
)
|
||||
|
||||
# Create view with removal button
|
||||
view = WhitelistRemovalView(member.id, member.guild.id)
|
||||
|
||||
# Send to all admins
|
||||
for admin in admins:
|
||||
try:
|
||||
await admin.send(embed=embed, view=view)
|
||||
except discord.Forbidden:
|
||||
continue # User has DMs disabled
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error sending whitelist notification: {e}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||
"""Monitor role changes for whitelist members."""
|
||||
if before.roles == after.roles:
|
||||
return
|
||||
|
||||
if not self.is_whitelisted(after.id):
|
||||
return
|
||||
|
||||
# Find removed roles
|
||||
removed_roles = [role for role in before.roles if role not in after.roles]
|
||||
|
||||
# Check if any removed roles had big permissions
|
||||
important_removed = [role for role in removed_roles if self.has_big_permissions(role)]
|
||||
|
||||
if important_removed:
|
||||
await self.send_whitelist_notification(after, important_removed)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_remove(self, member: discord.Member):
|
||||
"""Auto-remove from whitelist when member leaves server."""
|
||||
if self.is_whitelisted(member.id):
|
||||
removed = self.remove_from_whitelist(member.id)
|
||||
if removed:
|
||||
# Log the removal
|
||||
try:
|
||||
log_channel_id = self.get_log_channel(member.guild.id)
|
||||
if log_channel_id:
|
||||
log_channel = member.guild.get_channel(log_channel_id)
|
||||
if log_channel:
|
||||
embed = discord.Embed(
|
||||
title="🗑️ Auto-Whitelist Removal",
|
||||
description=f"{member.mention} a été retiré automatiquement de la whitelist après avoir quitté le serveur.",
|
||||
color=discord.Color.red(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
await log_channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Error logging whitelist removal: {e}")
|
||||
|
||||
def get_log_channel(self, guild_id: int) -> Optional[int]:
|
||||
"""Get log channel ID from security settings."""
|
||||
try:
|
||||
security_file = "data/security_settings.json"
|
||||
if os.path.exists(security_file):
|
||||
with open(security_file, "r") as f:
|
||||
settings = json.load(f)
|
||||
return settings.get(str(guild_id), {}).get("log_channel_id")
|
||||
except:
|
||||
pass
|
||||
return None
|
||||
|
||||
class WhitelistRemovalView(View):
|
||||
"""View with button to remove user from whitelist."""
|
||||
|
||||
def __init__(self, user_id: int, guild_id: int):
|
||||
super().__init__(timeout=3600) # 1 hour timeout
|
||||
self.user_id = user_id
|
||||
self.guild_id = guild_id
|
||||
|
||||
@discord.ui.button(
|
||||
label="Retirer de la whitelist",
|
||||
style=discord.ButtonStyle.red,
|
||||
emoji="🗑️"
|
||||
)
|
||||
async def remove_whitelist(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
button: Button
|
||||
):
|
||||
"""Remove user from whitelist when button is clicked."""
|
||||
try:
|
||||
# Get the cog instance
|
||||
cog = self.bot.get_cog("WhitelistMonitor")
|
||||
if not cog:
|
||||
await interaction.response.send_message(
|
||||
"❌ Erreur: Système de whitelist non disponible.",
|
||||
ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Remove from whitelist
|
||||
removed = cog.remove_from_whitelist(self.user_id)
|
||||
|
||||
if removed:
|
||||
# Get user info
|
||||
try:
|
||||
user = await self.bot.fetch_user(self.user_id)
|
||||
user_mention = user.mention
|
||||
except:
|
||||
user_mention = f"<@{self.user_id}>"
|
||||
|
||||
# Update button
|
||||
button.disabled = True
|
||||
button.label = "✅ Retiré"
|
||||
button.style = discord.ButtonStyle.green
|
||||
|
||||
await interaction.response.edit_message(view=self)
|
||||
|
||||
# Send confirmation
|
||||
embed = discord.Embed(
|
||||
title="✅ Retrait effectué",
|
||||
description=f"{user_mention} a été retiré de la whitelist.",
|
||||
color=discord.Color.green()
|
||||
)
|
||||
await interaction.followup.send(embed=embed, ephemeral=True)
|
||||
|
||||
# Log the removal
|
||||
try:
|
||||
guild = self.bot.get_guild(self.guild_id)
|
||||
if guild:
|
||||
log_channel_id = cog.get_log_channel(self.guild_id)
|
||||
if log_channel_id:
|
||||
log_channel = guild.get_channel(log_channel_id)
|
||||
if log_channel:
|
||||
embed = discord.Embed(
|
||||
title="🗑️ Whitelist Manual Removal",
|
||||
description=f"{user_mention} a été retiré manuellement de la whitelist.",
|
||||
color=discord.Color.green(),
|
||||
timestamp=datetime.now()
|
||||
)
|
||||
embed.set_footer(text=f"Retiré par {interaction.user.display_name}")
|
||||
await log_channel.send(embed=embed)
|
||||
except Exception as e:
|
||||
print(f"Error logging manual removal: {e}")
|
||||
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
"ℹ️ L'utilisateur n'était pas dans la whitelist.",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
await interaction.response.send_message(
|
||||
f"❌ Erreur lors du retrait: {str(e)}",
|
||||
ephemeral=True
|
||||
)
|
||||
|
||||
async def setup(bot):
|
||||
await bot.add_cog(WhitelistMonitor(bot))
|
||||
Loading…
Add table
Add a link
Reference in a new issue