feat: full feature suite — welcome, modmail, security, staff, avis, rules, links, shop, custom
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
2026-10-02 19:45:46 +02:00
|
|
|
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*",
|
2026-10-04 20:07:10 +02:00
|
|
|
color=0x2F3136,
|
feat: full feature suite — welcome, modmail, security, staff, avis, rules, links, shop, custom
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
2026-10-02 19:45:46 +02:00
|
|
|
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))
|