feat: full feature suite — welcome, modmail, security, staff, avis, rules, links, shop, custom
All checks were successful
ci / ci (push) Successful in 1m1s
All checks were successful
ci / ci (push) Successful in 1m1s
features/shared.py: per-guild JSON config store features/welcome.py: on_member_join message + invite tracking + /invites features/modmail.py: DM-based tickets with staff relay features/security.py: antiraid (join rate detection), ban protection, join/leave log features/staff.py: staff ranks, promote/demote, /staff_list features/avis.py: /avis modal -> embed in configured channel features/rules.py: /rules_setup with accept button + role assignment features/links.py: /links panel, /links_setup, /links_remove features/shop.py: /shop_announce new product embeds features/custom.py: /botstatus, /custom_send, /embed
This commit is contained in:
parent
33b9303948
commit
5d9baee102
12 changed files with 911 additions and 2 deletions
21
bot.py
21
bot.py
|
|
@ -25,8 +25,25 @@ class VisionBot(commands.Bot):
|
|||
|
||||
async def setup_hook(self):
|
||||
logger.info("Chargement des cogs...")
|
||||
await self.load_extension("commandes.hello")
|
||||
await self.load_extension("tickets.cog")
|
||||
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}")
|
||||
|
||||
self.tree.on_error = self.on_app_command_error
|
||||
|
||||
|
|
|
|||
0
features/__init__.py
Normal file
0
features/__init__.py
Normal file
76
features/avis.py
Normal file
76
features/avis.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
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))
|
||||
73
features/custom.py
Normal file
73
features/custom.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
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))
|
||||
60
features/links.py
Normal file
60
features/links.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
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))
|
||||
125
features/modmail.py
Normal file
125
features/modmail.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
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))
|
||||
91
features/rules.py
Normal file
91
features/rules.py
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
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))
|
||||
104
features/security.py
Normal file
104
features/security.py
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
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))
|
||||
58
features/shared.py
Normal file
58
features/shared.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
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, {})
|
||||
62
features/shop.py
Normal file
62
features/shop.py
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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))
|
||||
107
features/staff.py
Normal file
107
features/staff.py
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
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))
|
||||
136
features/welcome.py
Normal file
136
features/welcome.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
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