Compare commits
No commits in common. "3ea90d7078ea8c1c6c28ca594812e0125341352b" and "33b93039488128082353bd08abd6d9cb163cf69b" have entirely different histories.
3ea90d7078
...
33b9303948
13 changed files with 16 additions and 925 deletions
|
|
@ -3,7 +3,7 @@
|
||||||
#
|
#
|
||||||
# Declencheur : push sur `main`.
|
# Declencheur : push sur `main`.
|
||||||
# Runner : forgejo-runner self-hosted (label omhk-deploy, sur .22).
|
# 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).
|
# (/home/discord/Bots/VisionApp, bind-mount .:/app).
|
||||||
#
|
#
|
||||||
# Pipeline : syntaxe -> rsync -> deploy -> health check -> rollback auto.
|
# Pipeline : syntaxe -> rsync -> deploy -> health check -> rollback auto.
|
||||||
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
chmod 700 ~/.ssh
|
chmod 700 ~/.ssh
|
||||||
printf '%s\n' "$VISION_SSH_KEY" > ~/.ssh/id_vision
|
printf '%s\n' "$VISION_SSH_KEY" > ~/.ssh/id_vision
|
||||||
chmod 600 ~/.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
|
shell: bash
|
||||||
|
|
||||||
# ═══ TEST SYNTAXE (sur .22, rapide) ══════════════════════════════
|
# ═══ TEST SYNTAXE (sur .22, rapide) ══════════════════════════════
|
||||||
|
|
@ -89,13 +89,13 @@ jobs:
|
||||||
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
|
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
|
||||||
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
|
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
|
||||||
--exclude '.vscode/' --exclude '.idea/' \
|
--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
|
shell: bash
|
||||||
|
|
||||||
# ═══ DEPLOY ═══════════════════════════════════════════════════════
|
# ═══ DEPLOY ═══════════════════════════════════════════════════════
|
||||||
- name: Deploiement du conteneur (docker compose, cote pavillon80)
|
- name: Deploiement du conteneur (docker compose, cote .117)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd repo
|
cd repo
|
||||||
|
|
@ -105,8 +105,8 @@ jobs:
|
||||||
if echo "$CHANGED" | grep -qE '(^|/)(requirements\.txt|Dockerfile)$'; then
|
if echo "$CHANGED" | grep -qE '(^|/)(requirements\.txt|Dockerfile)$'; then
|
||||||
REBUILD="true"
|
REBUILD="true"
|
||||||
fi
|
fi
|
||||||
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \
|
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
|
||||||
discord@pavillon80.changeip.org \
|
discord@192.168.1.117 \
|
||||||
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${COMMIT}' '${REBUILD}'"
|
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${COMMIT}' '${REBUILD}'"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
|
@ -118,7 +118,7 @@ jobs:
|
||||||
echo "Attente du demarrage du bot..."
|
echo "Attente du demarrage du bot..."
|
||||||
for i in $(seq 1 10); do
|
for i in $(seq 1 10); do
|
||||||
sleep 3
|
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
|
if echo "$STATUS" | grep -q '"status":"ok"'; then
|
||||||
echo "Health check OK (tentative $i)"
|
echo "Health check OK (tentative $i)"
|
||||||
exit 0
|
exit 0
|
||||||
|
|
@ -147,11 +147,11 @@ jobs:
|
||||||
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
|
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
|
||||||
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
|
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
|
||||||
--exclude '.vscode/' --exclude '.idea/' \
|
--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/
|
||||||
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \
|
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
|
||||||
discord@pavillon80.changeip.org \
|
discord@192.168.1.117 \
|
||||||
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${PREV_COMMIT}' false"
|
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${PREV_COMMIT}' false"
|
||||||
echo "ROLLBACK termine"
|
echo "ROLLBACK termine"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -160,7 +160,7 @@ jobs:
|
||||||
- name: Verification finale
|
- name: Verification finale
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts -p 444 \
|
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
|
||||||
discord@pavillon80.changeip.org \
|
discord@192.168.1.117 \
|
||||||
"docker ps --filter name=vision-bot --format '{{.Names}} {{.Status}}' && curl -s -m 5 http://localhost:1890/health && echo"
|
"docker ps --filter name=vision-bot --format '{{.Names}} {{.Status}}' && curl -s -m 5 http://localhost:1890/health && echo"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
|
||||||
21
bot.py
21
bot.py
|
|
@ -25,25 +25,8 @@ class VisionBot(commands.Bot):
|
||||||
|
|
||||||
async def setup_hook(self):
|
async def setup_hook(self):
|
||||||
logger.info("Chargement des cogs...")
|
logger.info("Chargement des cogs...")
|
||||||
extensions = [
|
await self.load_extension("commandes.hello")
|
||||||
"commandes.hello",
|
await self.load_extension("tickets.cog")
|
||||||
"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}")
|
|
||||||
|
|
||||||
self.tree.on_error = self.on_app_command_error
|
self.tree.on_error = self.on_app_command_error
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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, {})
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
|
|
@ -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))
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue