diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml index 24f603c..d07ca77 100644 --- a/.forgejo/workflows/deploy.yml +++ b/.forgejo/workflows/deploy.yml @@ -3,7 +3,7 @@ # # Declencheur : push sur `main`. # Runner : forgejo-runner self-hosted (label omhk-deploy, sur .22). -# Cible : pavillon80.changeip.org:444 -> conteneur Docker `vision-bot` +# Cible : 192.168.1.117 -> conteneur Docker `vision-bot` # (/home/discord/Bots/VisionApp, bind-mount .:/app). # # Pipeline : syntaxe -> rsync -> deploy -> health check -> rollback auto. @@ -42,7 +42,7 @@ jobs: chmod 700 ~/.ssh printf '%s\n' "$VISION_SSH_KEY" > ~/.ssh/id_vision chmod 600 ~/.ssh/id_vision - ssh-keyscan -H -p 444 pavillon80.changeip.org >> ~/.ssh/known_hosts 2>/dev/null + ssh-keyscan -H 192.168.1.117 >> ~/.ssh/known_hosts 2>/dev/null shell: bash # ═══ TEST SYNTAXE (sur .22, rapide) ══════════════════════════════ @@ -89,13 +89,13 @@ jobs: --exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \ --exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \ --exclude '.vscode/' --exclude '.idea/' \ - -e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444" \ + -e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts" \ . \ - discord@pavillon80.changeip.org:/home/discord/Bots/VisionApp/ + discord@192.168.1.117:/home/discord/Bots/VisionApp/ shell: bash # ═══ DEPLOY ═══════════════════════════════════════════════════════ - - name: Deploiement du conteneur (docker compose, cote pavillon80) + - name: Deploiement du conteneur (docker compose, cote .117) run: | set -euo pipefail cd repo @@ -105,8 +105,8 @@ jobs: if echo "$CHANGED" | grep -qE '(^|/)(requirements\.txt|Dockerfile)$'; then REBUILD="true" fi - ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \ - discord@pavillon80.changeip.org \ + ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \ + discord@192.168.1.117 \ "bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${COMMIT}' '${REBUILD}'" shell: bash @@ -118,7 +118,7 @@ jobs: echo "Attente du demarrage du bot..." for i in $(seq 1 10); do sleep 3 - STATUS=$(curl -s -m 5 http://pavillon80.changeip.org:1890/health 2>/dev/null || echo "") + STATUS=$(curl -s -m 5 http://192.168.1.117:1890/health 2>/dev/null || echo "") if echo "$STATUS" | grep -q '"status":"ok"'; then echo "Health check OK (tentative $i)" exit 0 @@ -147,11 +147,11 @@ jobs: --exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \ --exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \ --exclude '.vscode/' --exclude '.idea/' \ - -e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444" \ + -e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts" \ . \ - discord@pavillon80.changeip.org:/home/discord/Bots/VisionApp/ - ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \ - discord@pavillon80.changeip.org \ + discord@192.168.1.117:/home/discord/Bots/VisionApp/ + ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \ + discord@192.168.1.117 \ "bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${PREV_COMMIT}' false" echo "ROLLBACK termine" exit 1 @@ -160,7 +160,7 @@ jobs: - name: Verification finale run: | set -euo pipefail - ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \ - discord@pavillon80.changeip.org \ + ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \ + discord@192.168.1.117 \ "docker ps --filter name=vision-bot --format '{{.Names}} {{.Status}}' && curl -s -m 5 http://localhost:1890/health && echo" shell: bash diff --git a/bot.py b/bot.py index d3c3a2d..a7e7b58 100644 --- a/bot.py +++ b/bot.py @@ -25,25 +25,8 @@ class VisionBot(commands.Bot): async def setup_hook(self): logger.info("Chargement des cogs...") - extensions = [ - "commandes.hello", - "tickets.cog", - "features.welcome", - "features.modmail", - "features.security", - "features.staff", - "features.avis", - "features.rules", - "features.links", - "features.shop", - "features.custom", - ] - for ext in extensions: - try: - await self.load_extension(ext) - logger.info(f" ✓ {ext}") - except Exception as e: - logger.error(f" ✗ {ext}: {e}") + await self.load_extension("commandes.hello") + await self.load_extension("tickets.cog") self.tree.on_error = self.on_app_command_error diff --git a/features/__init__.py b/features/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/features/avis.py b/features/avis.py deleted file mode 100644 index eecef8d..0000000 --- a/features/avis.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - - -class AvisModal(discord.ui.Modal, title="Donner mon avis"): - note = discord.ui.TextInput(label="Note (1-5)", max_length=1, placeholder="5") - commentaire = discord.ui.TextInput( - label="Commentaire", - style=discord.TextStyle.paragraph, - required=False, - max_length=1000, - ) - - def __init__(self, config: dict): - super().__init__() - self.config = config - - async def on_submit(self, interaction: discord.Interaction): - try: - note = max(1, min(5, int(self.note.value))) - except ValueError: - await interaction.response.send_message("Note invalide (1-5).", ephemeral=True) - return - - guild = interaction.guild or self.bot.get_guild(self.config["guild_id"]) - if not guild: - await interaction.response.send_message("Serveur introuvable.", ephemeral=True) - return - - channel = guild.get_channel(self.config["channel_id"]) - if not channel: - await interaction.response.send_message("Salon d'avis introuvable.", ephemeral=True) - return - - stars = "⭐" * note - embed = discord.Embed( - title="Avis", - description=f"**Note :** {stars} ({note}/5)\n**Auteur :** {interaction.user.mention}\n\n{self.commentaire.value or 'Aucun commentaire'}", - color=discord.Color.gold(), - ) - embed.set_thumbnail(url=interaction.user.display_avatar.url) - await channel.send(embed=embed) - await interaction.response.send_message("Merci pour ton avis !", ephemeral=True) - - -class Avis(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command(name="avis_setup", description="Configurer le système d'avis") - @app_commands.describe(channel="Salon où envoyer les avis") - @app_commands.checks.has_permissions(manage_guild=True) - async def avis_setup(self, interaction: discord.Interaction, channel: discord.TextChannel): - shared.set(interaction.guild_id, "avis", {"channel_id": channel.id, "enabled": True}) - await interaction.response.send_message(f"Les avis seront envoyés dans {channel.mention}", ephemeral=True) - - @app_commands.command(name="avis", description="Donner un avis sur le serveur") - async def avis(self, interaction: discord.Interaction): - cfg = shared.section(interaction.guild_id, "avis") - if not cfg.get("enabled"): - await interaction.response.send_message("Le système d'avis n'est pas activé.", ephemeral=True) - return - await interaction.response.send_modal(AvisModal(cfg)) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Avis(bot)) diff --git a/features/custom.py b/features/custom.py deleted file mode 100644 index 55c7310..0000000 --- a/features/custom.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -logger = logging.getLogger("vision") - - -class CustomMessages(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command(name="botstatus", description="Changer le statut du bot") - @app_commands.describe(text="Texte du statut", type_type="Type: playing, watching, listening") - @app_commands.checks.has_permissions(manage_guild=True) - async def botstatus(self, interaction: discord.Interaction, text: str, type_type: str = "playing"): - activities = { - "playing": discord.ActivityType.playing, - "watching": discord.ActivityType.watching, - "listening": discord.ActivityType.listening, - "streaming": discord.ActivityType.streaming, - } - activity_type = activities.get(type_type.lower(), discord.ActivityType.playing) - activity = discord.Activity(type=activity_type, name=text) - await self.bot.change_presence(activity=activity) - await interaction.response.send_message(f"Statut changé : **{text}**", ephemeral=True) - - @app_commands.command(name="custom_send", description="Envoyer un message custom") - @app_commands.describe(channel="Canal", message="Contenu", title="Titre de l'embed") - @app_commands.checks.has_permissions(manage_guild=True) - async def custom_send( - self, interaction: discord.Interaction, channel: discord.TextChannel, message: str, title: str = "" - ): - if title: - embed = discord.Embed(title=title, description=message, color=discord.Color.blurple()) - await channel.send(embed=embed) - else: - await channel.send(message) - await interaction.response.send_message(f"Message envoyé dans {channel.mention}", ephemeral=True) - - @app_commands.command(name="embed", description="Envoyer un embed personnalisé") - @app_commands.describe( - channel="Canal", title="Titre", description="Description", color="Couleur (hex)", image_url="Image" - ) - @app_commands.checks.has_permissions(manage_guild=True) - async def embed( - self, - interaction: discord.Interaction, - channel: discord.TextChannel, - title: str, - description: str, - color: str = "#5865F2", - image_url: str = "", - ): - try: - color_int = int(color.lstrip("#"), 16) - except ValueError: - color_int = 0x5865F2 - - embed = discord.Embed(title=title, description=description, color=color_int) - if image_url: - embed.set_image(url=image_url) - embed.set_footer(text=f"Envoyé par {interaction.user}") - - await channel.send(embed=embed) - await interaction.response.send_message(f"Embed envoyé dans {channel.mention}", ephemeral=True) - - -async def setup(bot: commands.Bot): - await bot.add_cog(CustomMessages(bot)) diff --git a/features/links.py b/features/links.py deleted file mode 100644 index a20eace..0000000 --- a/features/links.py +++ /dev/null @@ -1,60 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - -DEFAULT_LINKS = { - "Site web": "https://example.com", - "Discord": "https://discord.gg/example", - "YouTube": "https://youtube.com/@example", -} - - -class Links(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command(name="links", description="Voir les liens du serveur") - async def links(self, interaction: discord.Interaction): - cfg = shared.section(interaction.guild_id, "links") - links = cfg.get("links", DEFAULT_LINKS) - - container = discord.ui.Container() - lines = [f"[{name}]({url})" for name, url in links.items()] - container.add_item(discord.ui.TextDisplay("## Liens du serveur\n" + "\n".join(lines))) - view = discord.ui.LayoutView() - view.add_item(container) - await interaction.response.send_message(view=view) - - @app_commands.command(name="links_setup", description="Configurer les liens") - @app_commands.describe(name="Nom du lien", url="URL du lien") - @app_commands.checks.has_permissions(manage_guild=True) - async def links_setup(self, interaction: discord.Interaction, name: str, url: str): - cfg = shared.get(interaction.guild_id, "links") - links = cfg.get("links", {}) - links[name] = url - cfg["links"] = links - shared.set(interaction.guild_id, "links", cfg) - await interaction.response.send_message(f"Lien **{name}** ajouté.", ephemeral=True) - - @app_commands.command(name="links_remove", description="Supprimer un lien") - @app_commands.describe(name="Nom du lien") - @app_commands.checks.has_permissions(manage_guild=True) - async def links_remove(self, interaction: discord.Interaction, name: str): - cfg = shared.get(interaction.guild_id, "links") - links = cfg.get("links", {}) - links.pop(name, None) - cfg["links"] = links - shared.set(interaction.guild_id, "links", cfg) - await interaction.response.send_message(f"Lien **{name}** supprimé.", ephemeral=True) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Links(bot)) diff --git a/features/modmail.py b/features/modmail.py deleted file mode 100644 index 992f2fd..0000000 --- a/features/modmail.py +++ /dev/null @@ -1,125 +0,0 @@ -from __future__ import annotations - -import contextlib -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - -_open_tickets: dict[int, int] = {} - - -class ModMail(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @commands.Cog.listener() - async def on_message(self, message: discord.Message): - if message.author.bot or message.guild: - return - - cfg = shared.get(0, "modmail") - if not cfg.get("enabled"): - return - - guild_id = cfg.get("guild_id") - if not guild_id: - return - - guild = self.bot.get_guild(guild_id) - if not guild: - return - - existing = _open_tickets.get(message.author.id) - if existing: - channel = guild.get_channel(existing) - if channel: - embed = discord.Embed( - description=message.content or "*Pièce jointe*", - color=discord.Color.blurple(), - timestamp=message.created_at, - ) - embed.set_author(name=str(message.author), icon_url=message.author.display_avatar.url) - await channel.send(embed=embed) - for att in message.attachments: - await channel.send(file=await discord.File(await att.to_file(), filename=att.filename)) - return - - category_id = cfg.get("category_id") - staff_role_id = cfg.get("staff_role_id") - category = guild.get_channel(category_id) if category_id else None - - overwrites = { - guild.default_role: discord.PermissionOverwrite(view_channel=False), - guild.me: discord.PermissionOverwrite( - view_channel=True, - send_messages=True, - read_message_history=True, - attach_files=True, - manage_channels=True, - ), - message.author: discord.PermissionOverwrite( - view_channel=True, send_messages=True, read_message_history=True - ), - } - if staff_role_id: - role = guild.get_role(staff_role_id) - if role: - overwrites[role] = discord.PermissionOverwrite( - view_channel=True, send_messages=True, read_message_history=True, attach_files=True - ) - - channel = await guild.create_text_channel( - f"modmail-{message.author.name}", - category=category, - overwrites=overwrites, - topic=f"Modmail de {message.author} ({message.author.id})", - ) - - _open_tickets[message.author.id] = channel.id - - container = discord.ui.Container() - container.add_item( - discord.ui.TextDisplay( - f"## Modmail\n**De :** {message.author.mention}\n**Raison :** {message.content[:500] if message.content else 'Pièce jointe'}" - ) - ) - view = discord.ui.LayoutView() - view.add_item(container) - - mention = message.author.mention - if staff_role_id: - mention += f" <@&{staff_role_id}>" - await channel.send(content=mention, view=view) - - with contextlib.suppress(discord.HTTPException): - await message.author.send("Ton message a été envoyé au staff, réponses ici.") - - @app_commands.command(name="modmail_setup", description="Configurer le système de modmail") - @app_commands.describe(category="Category pour les tickets", staff_role="Rôle staff") - @app_commands.checks.has_permissions(manage_guild=True) - async def modmail_setup( - self, interaction: discord.Interaction, category: discord.CategoryChannel, staff_role: discord.Role - ): - shared.set( - 0, - "modmail", - { - "enabled": True, - "guild_id": interaction.guild_id, - "category_id": category.id, - "staff_role_id": staff_role.id, - }, - ) - await interaction.response.send_message( - f"Modmail configuré : catégorie {category.mention}, staff {staff_role.mention}", ephemeral=True - ) - - -async def setup(bot: commands.Bot): - await bot.add_cog(ModMail(bot)) diff --git a/features/rules.py b/features/rules.py deleted file mode 100644 index 5b9b4e5..0000000 --- a/features/rules.py +++ /dev/null @@ -1,91 +0,0 @@ -from __future__ import annotations - -import contextlib -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - -DEFAULT_RULES = [ - "Respectez tous les membres du serveur.", - "Pas de spam ni de flood.", - "Pas de contenu NSFW ou gore.", - "Pas de publicité sans autorisation.", - "Utilisez les canaux appropriés.", - "Suivez les instructions du staff.", -] - - -class RulesAcceptButton(discord.ui.Button): - def __init__(self): - super().__init__( - label="J'accepte le règlement", - style=discord.ButtonStyle.success, - custom_id="rules_accept", - ) - - async def callback(self, interaction: discord.Interaction): - cfg = shared.section(interaction.guild_id, "rules") - role_id = cfg.get("role_id") - if role_id: - role = interaction.guild.get_role(role_id) - if role: - with contextlib.suppress(discord.HTTPException): - await interaction.user.add_roles(role, reason="Règlement accepté") - await interaction.response.send_message("Merci d'avoir accepté le règlement !", ephemeral=True) - - -class RulesView(discord.ui.LayoutView): - def __init__(self): - super().__init__(timeout=None) - container = discord.ui.Container() - container.add_item(discord.ui.ActionRow(RulesAcceptButton())) - self.add_item(container) - - -class Rules(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self.bot.add_view(RulesView()) - - @app_commands.command(name="rules_setup", description="Configurer et envoyer le règlement") - @app_commands.describe(channel="Canal du règlement", role="Rôle donné après acceptation") - @app_commands.checks.has_permissions(manage_guild=True) - async def rules_setup(self, interaction: discord.Interaction, channel: discord.TextChannel, role: discord.Role): - cfg = shared.get(interaction.guild_id, "rules") - cfg["channel_id"] = channel.id - cfg["role_id"] = role.id - shared.set(interaction.guild_id, "rules", cfg) - - rules = cfg.get("rules", DEFAULT_RULES) - rules_text = "\n".join(f"{i + 1}. {r}" for i, r in enumerate(rules)) - - container = discord.ui.Container() - container.add_item(discord.ui.TextDisplay(f"## Règlement du serveur\n\n{rules_text}")) - container.add_item(discord.ui.Separator()) - container.add_item(discord.ui.ActionRow(RulesAcceptButton())) - - view = discord.ui.LayoutView() - view.add_item(container) - await channel.send(view=view) - - await interaction.response.send_message(f"Règlement publié dans {channel.mention}", ephemeral=True) - - @app_commands.command(name="rules_set", description="Modifier les règles") - @app_commands.describe(rule1="Règle 1", rule2="Règle 2", rule3="Règle 3") - @app_commands.checks.has_permissions(manage_guild=True) - async def rules_set(self, interaction: discord.Interaction, rule1: str, rule2: str = "", rule3: str = ""): - rules = [r for r in [rule1, rule2, rule3] if r] - cfg = shared.get(interaction.guild_id, "rules") - cfg["rules"] = rules - shared.set(interaction.guild_id, "rules", cfg) - await interaction.response.send_message(f"{len(rules)} règles mises à jour.", ephemeral=True) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Rules(bot)) diff --git a/features/security.py b/features/security.py deleted file mode 100644 index 4d4570c..0000000 --- a/features/security.py +++ /dev/null @@ -1,104 +0,0 @@ -from __future__ import annotations - -import logging -from datetime import datetime, timedelta, timezone - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - -_raids: dict[int, list[tuple[datetime, str]]] = {} - - -class Security(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - cfg = shared.section(member.guild.id, "security") - if not cfg.get("antiraid"): - return - - now = datetime.now(timezone.utc) - raids = _raids.setdefault(member.guild.id, []) - raids.append((now, str(member.id))) - cutoff = now - timedelta(seconds=cfg.get("antiraid_window", 10)) - _raids[member.guild.id] = [(t, uid) for t, uid in raids if t > cutoff] - - threshold = cfg.get("antiraid_threshold", 5) - if len(_raids[member.guild.id]) >= threshold: - try: - await member.guild.edit(verification_level=discord.VerificationLevel.highest) - embed = discord.Embed( - title="🛡️ Alerte Antiraid", - description=f"**{len(_raids[member.guild.id])}** membres en {cfg.get('antiraid_window', 10)}s.\n" - f"**Verification_level** augmenté automatiquement.", - color=discord.Color.red(), - ) - if cfg.get("log_channel_id"): - ch = member.guild.get_channel(cfg["log_channel_id"]) - if ch: - await ch.send(embed=embed) - except discord.HTTPException: - pass - _raids[member.guild.id] = [] - - @commands.Cog.listener() - async def on_member_ban(self, guild: discord.Guild, user: discord.User | discord.Member): - cfg = shared.section(guild.id, "security") - if not cfg.get("ban_protection"): - return - embed = discord.Embed( - title="🔨 Protection Ban", - description=f"**{user}** a été banni de **{guild.name}**", - color=discord.Color.orange(), - ) - if cfg.get("log_channel_id"): - ch = guild.get_channel(cfg["log_channel_id"]) - if ch: - await ch.send(embed=embed) - - @commands.Cog.listener() - async def on_member_remove(self, member: discord.Member): - cfg = shared.section(member.guild.id, "security") - if not cfg.get("join_leave_log"): - return - embed = discord.Embed( - title="📥 Départ", - description=f"**{member}** a quitté **{member.guild.name}**", - color=discord.Color.grey(), - timestamp=datetime.now(timezone.utc), - ) - if cfg.get("log_channel_id"): - ch = member.guild.get_channel(cfg["log_channel_id"]) - if ch: - await ch.send(embed=embed) - - @app_commands.command(name="security_setup", description="Configurer la sécurité") - @app_commands.describe(log_channel="Canal de logs", antiraid_threshold="Seuil antiraid (membres/10s)") - @app_commands.checks.has_permissions(manage_guild=True) - async def security_setup( - self, interaction: discord.Interaction, log_channel: discord.TextChannel, antiraid_threshold: int = 5 - ): - cfg = shared.get(interaction.guild_id, "security") - cfg["enabled"] = True - cfg["log_channel_id"] = log_channel.id - cfg["antiraid"] = True - cfg["antiraid_threshold"] = antiraid_threshold - cfg["antiraid_window"] = 10 - cfg["ban_protection"] = True - cfg["join_leave_log"] = True - shared.set(interaction.guild_id, "security", cfg) - await interaction.response.send_message( - f"Sécurité activée. Logs dans {log_channel.mention}. Seuil antiraid : {antiraid_threshold} membres / 10s.", - ephemeral=True, - ) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Security(bot)) diff --git a/features/shared.py b/features/shared.py deleted file mode 100644 index 485879b..0000000 --- a/features/shared.py +++ /dev/null @@ -1,58 +0,0 @@ -from __future__ import annotations - -import json -import threading -from pathlib import Path - -from config import DATA_DIR - -FEATURES_DIR = DATA_DIR / "features" -_LOCK = threading.Lock() -_cache: dict = {} - - -def _ensure(): - FEATURES_DIR.mkdir(parents=True, exist_ok=True) - - -def _path(guild_id: int) -> Path: - return FEATURES_DIR / f"{guild_id}.json" - - -def get(guild_id: int, key: str = "") -> dict: - with _LOCK: - _ensure() - if guild_id not in _cache: - f = _path(guild_id) - if f.exists(): - with open(f, "r", encoding="utf-8") as fh: - _cache[guild_id] = json.load(fh) - else: - _cache[guild_id] = {} - data = _cache[guild_id] - return data.get(key, {}) if key else data - - -def set(guild_id: int, key: str, value) -> None: - with _LOCK: - _ensure() - if guild_id not in _cache: - f = _path(guild_id) - if f.exists(): - with open(f, "r", encoding="utf-8") as fh: - _cache[guild_id] = json.load(fh) - else: - _cache[guild_id] = {} - _cache[guild_id][key] = value - _save(guild_id) - - -def _save(guild_id: int) -> None: - _ensure() - with open(_path(guild_id), "w", encoding="utf-8") as fh: - json.dump(_cache[guild_id], fh, indent=2, ensure_ascii=False) - - -def section(guild_id: int, name: str) -> dict: - data = get(guild_id) - return data.get(name, {}) diff --git a/features/shop.py b/features/shop.py deleted file mode 100644 index ae60a33..0000000 --- a/features/shop.py +++ /dev/null @@ -1,62 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - - -class Shop(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command(name="shop_setup", description="Configurer les annonces boutique") - @app_commands.describe(channel="Canal des annonces boutique") - @app_commands.checks.has_permissions(manage_guild=True) - async def shop_setup(self, interaction: discord.Interaction, channel: discord.TextChannel): - shared.set(interaction.guild_id, "shop", {"channel_id": channel.id, "enabled": True}) - await interaction.response.send_message(f"Annonces boutique dans {channel.mention}", ephemeral=True) - - @app_commands.command(name="shop_announce", description="Annoncer un nouveau produit") - @app_commands.describe(name="Nom du produit", description="Description", price="Prix", image_url="URL de l'image") - @app_commands.checks.has_permissions(manage_guild=True) - async def shop_announce( - self, - interaction: discord.Interaction, - name: str, - description: str, - price: str = "", - image_url: str = "", - ): - cfg = shared.section(interaction.guild_id, "shop") - if not cfg.get("enabled"): - await interaction.response.send_message("Boutique non configurée.", ephemeral=True) - return - - channel = interaction.guild.get_channel(cfg["channel_id"]) - if not channel: - await interaction.response.send_message("Canal boutique introuvable.", ephemeral=True) - return - - embed = discord.Embed( - title=f"🆕 Nouveau produit — {name}", - description=description, - color=discord.Color.green(), - ) - if price: - embed.add_field(name="💰 Prix", value=price, inline=True) - if image_url: - embed.set_image(url=image_url) - - embed.set_footer(text=f"Annoncé par {interaction.user}") - await channel.send(embed=embed) - await interaction.response.send_message(f"Annonce envoyée dans {channel.mention}", ephemeral=True) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Shop(bot)) diff --git a/features/staff.py b/features/staff.py deleted file mode 100644 index f0aafd5..0000000 --- a/features/staff.py +++ /dev/null @@ -1,107 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - -STAFF_RANKS = ["Membre", "Trial", "Modérateur", "Super Modérateur", "Admin", "Fondateur"] - - -class Staff(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - - @app_commands.command(name="staff_setup", description="Configurer le système de staff") - @app_commands.describe(staff_role="Rôle principal du staff") - @app_commands.checks.has_permissions(manage_guild=True) - async def staff_setup(self, interaction: discord.Interaction, staff_role: discord.Role): - cfg = shared.get(interaction.guild_id, "staff") - cfg["enabled"] = True - cfg["staff_role_id"] = staff_role.id - cfg["ranks"] = { - "Membre": None, - "Trial": None, - "Modérateur": None, - "Super Modérateur": None, - "Admin": None, - "Fondateur": None, - } - shared.set(interaction.guild_id, "staff", cfg) - await interaction.response.send_message( - f"Système de staff configuré. Rôle principal : {staff_role.mention}", ephemeral=True - ) - - @app_commands.command(name="staff_promote", description="Promouvoir un membre du staff") - @app_commands.describe(member="Membre", rank="Rang") - @app_commands.checks.has_permissions(manage_roles=True) - async def staff_promote(self, interaction: discord.Interaction, member: discord.Member, rank: str): - cfg = shared.section(interaction.guild_id, "staff") - ranks = cfg.get("ranks", {}) - if rank not in ranks: - await interaction.response.send_message(f"Rang inconnu. Rangs : {', '.join(STAFF_RANKS)}", ephemeral=True) - return - - if rank not in [r for r in STAFF_RANKS]: - await interaction.response.send_message("Rang invalide.", ephemeral=True) - return - - staff = cfg.get("staff", []) - entry = next((s for s in staff if s["user_id"] == member.id), None) - if entry: - entry["rank"] = rank - else: - staff.append({"user_id": member.id, "username": str(member), "rank": rank}) - cfg["staff"] = staff - shared.set(interaction.guild_id, "staff", cfg) - - await interaction.response.send_message(f"**{member}** est maintenant **{rank}**.", ephemeral=True) - - @app_commands.command(name="staff_demote", description="Rétrograder un membre du staff") - @app_commands.describe(member="Membre") - @app_commands.checks.has_permissions(manage_roles=True) - async def staff_demote(self, interaction: discord.Interaction, member: discord.Member): - cfg = shared.get(interaction.guild_id, "staff") - staff = cfg.get("staff", []) - idx = next((i for i, s in enumerate(staff) if s["user_id"] == member.id), None) - if idx is None: - await interaction.response.send_message(f"**{member}** n'est pas dans le staff.", ephemeral=True) - return - - current = staff[idx]["rank"] - rank_idx = STAFF_RANKS.index(current) if current in STAFF_RANKS else 0 - new_rank = STAFF_RANKS[rank_idx - 1] if rank_idx > 0 else "Membre" - staff[idx]["rank"] = new_rank - cfg["staff"] = staff - shared.set(interaction.guild_id, "staff", cfg) - await interaction.response.send_message(f"**{member}** est maintenant **{new_rank}**.", ephemeral=True) - - @app_commands.command(name="staff_list", description="Voir la liste du staff") - async def staff_list(self, interaction: discord.Interaction): - cfg = shared.section(interaction.guild_id, "staff") - staff = cfg.get("staff", []) - if not staff: - await interaction.response.send_message("Aucun membre du staff.", ephemeral=True) - return - - lines = [] - for s in sorted( - staff, key=lambda x: STAFF_RANKS.index(x["rank"]) if x["rank"] in STAFF_RANKS else 0, reverse=True - ): - lines.append(f"**{s['rank']}** — <@{s['user_id']}>") - - embed = discord.Embed( - title="Équipe Staff", - description="\n".join(lines), - color=discord.Color.blurple(), - ) - await interaction.response.send_message(embed=embed) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Staff(bot)) diff --git a/features/welcome.py b/features/welcome.py deleted file mode 100644 index 76eaaa0..0000000 --- a/features/welcome.py +++ /dev/null @@ -1,136 +0,0 @@ -from __future__ import annotations - -import logging - -import discord -from discord import app_commands -from discord.ext import commands - -from features import shared - -logger = logging.getLogger("vision") - - -class Welcome(commands.Cog): - def __init__(self, bot: commands.Bot): - self.bot = bot - self._invites: dict[int, dict[str, int]] = {} - - @commands.Cog.listener() - async def on_ready(self): - await self._cache_invites() - - @commands.Cog.listener() - async def on_guild_join(self, guild: discord.Guild): - await self._cache_invites_for(guild) - - @commands.Cog.listener() - async def on_member_join(self, member: discord.Member): - cfg = shared.section(member.guild.id, "welcome") - if not cfg.get("enabled"): - return - - channel_id = cfg.get("channel_id") - if not channel_id: - return - channel = member.guild.get_channel(channel_id) - if not channel: - return - - inviter = await self._find_inviter(member) - - text = cfg.get("message", "**{user}** a rejoint **{guild}** ! ({member_count} membres)") - text = text.replace("{user}", member.mention).replace("{guild}", member.guild.name) - text = text.replace("{member_count}", str(member.guild.member_count)) - text = text.replace("{username}", member.name) - - embed = discord.Embed(description=text, color=discord.Color.blurple()) - embed.set_thumbnail(url=member.display_avatar.url) - - if inviter: - inviter_data = cfg.get("invites", {}).get(str(inviter.id), 0) - embed.add_field(name="Invité par", value=f"{inviter.mention} ({inviter_data + 1} invites)") - - await channel.send(embed=embed) - - if cfg.get("dm_message"): - try: - dm_text = cfg["dm_message"].replace("{guild}", member.guild.name) - await member.send(dm_text) - except discord.HTTPException: - pass - - await self._update_invite_count(member.guild, inviter) - - async def _find_inviter(self, member: discord.Member) -> discord.Member | None: - try: - invites = await member.guild.invites() - except discord.HTTPException: - return None - - cached = self._invites.get(member.guild.id, {}) - for inv in invites: - if cached.get(inv.code, 0) < inv.uses: - self._invites.setdefault(member.guild.id, {})[inv.code] = inv.uses - return inv.inviter - - self._cache_invites_for(member.guild) - return None - - async def _update_invite_count(self, guild: discord.Guild, inviter: discord.Member | None): - if not inviter: - return - cfg = shared.get(guild.id, "welcome") - invites = cfg.get("invites", {}) - key = str(inviter.id) - invites[key] = invites.get(key, 0) + 1 - shared.set(guild.id, "welcome", {**cfg, "invites": invites}) - - async def _cache_invites(self): - for guild in self.bot.guilds: - await self._cache_invites_for(guild) - - async def _cache_invites_for(self, guild: discord.Guild): - try: - invites = await guild.invites() - self._invites[guild.id] = {inv.code: inv.uses for inv in invites} - except discord.HTTPException: - self._invites[guild.id] = {} - - @app_commands.command(name="welcome_setup", description="Configurer le message d'arrivée") - @app_commands.describe(channel="Canal d'arrivée", message="Message (utilise {user}, {guild}, {member_count})") - @app_commands.checks.has_permissions(manage_guild=True) - async def welcome_setup(self, interaction: discord.Interaction, channel: discord.TextChannel, message: str = ""): - cfg = shared.get(interaction.guild_id, "welcome") - cfg["enabled"] = True - cfg["channel_id"] = channel.id - if message: - cfg["message"] = message - shared.set(interaction.guild_id, "welcome", cfg) - await interaction.response.send_message(f"Message d'arrivée configuré dans {channel.mention}", ephemeral=True) - - @app_commands.command(name="welcome_disable", description="Désactiver le message d'arrivée") - @app_commands.checks.has_permissions(manage_guild=True) - async def welcome_disable(self, interaction: discord.Interaction): - cfg = shared.get(interaction.guild_id, "welcome") - cfg["enabled"] = False - shared.set(interaction.guild_id, "welcome", cfg) - await interaction.response.send_message("Message d'arrivée désactivé.", ephemeral=True) - - @app_commands.command(name="invites", description="Voir le nombre d'invitations d'un membre") - @app_commands.describe(member="Membre à vérifier") - async def invites(self, interaction: discord.Interaction, member: discord.Member): - cfg = shared.get(interaction.guild_id, "welcome") - invites = cfg.get("invites", {}) - count = invites.get(str(member.id), 0) - embed = discord.Embed( - title=f"Invitations de {member}", - description=f"**{count}** invitations", - color=discord.Color.blurple(), - ) - embed.set_thumbnail(url=member.display_avatar.url) - await interaction.response.send_message(embed=embed) - - -async def setup(bot: commands.Bot): - await bot.add_cog(Welcome(bot))