Update Bot (j'ai plus le repo sur GitHub)

Qui c'est la conne qui a delete le repo sur GitHub? C'EST MOIIIII
This commit is contained in:
2026-02-09 14:36:26 +01:00
parent eab4419e12
commit ad2014b7b2
586 changed files with 58986 additions and 25205 deletions

View File

@@ -1,65 +1,54 @@
const os = require('os');
const fs = require('fs');
const cpuInfo = os.cpus()[0].model; // modèle du CPU
const cpuCores = os.cpus().length; // nombre de coeurs
const totalRAM = (os.totalmem() / (1024 ** 3)).toFixed(2); // en Go
const freeRAM = (os.freemem() / (1024 ** 3)).toFixed(2); // en Go
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { formatUptime } = require('../../utils/helpers');
const { colors } = require('../../utils/constants');
function getOS() {
const platform = os.platform(); // ex: win32, linux
const release = os.release(); // version du kernel / OS
const platform = os.platform();
const release = os.release();
switch (platform) {
case "win32":
return `Windows ${release}`;
case "darwin":
return `macOS ${release}`;
case "linux":
case 'win32': return `Windows ${release}`;
case 'darwin': return `macOS ${release}`;
case 'linux':
try {
const data = fs.readFileSync("/etc/os-release", "utf8");
const data = fs.readFileSync('/etc/os-release', 'utf8');
const match = data.match(/PRETTY_NAME="(.+)"/);
if (match) return `${match[1]} (kernel ${release})`;
} catch {
return `Linux ${release}`;
}
default:
return `${platform} ${release}`;
} catch { return `Linux ${release}`; }
default: return `${platform} ${release}`;
}
}
const uptime = os.uptime(); // en secondes
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
return `${days}j ${hours}h ${minutes}m ${secs}s`;
}
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
module.exports = {
category: 'dev',
data: new SlashCommandBuilder()
.setName('infra')
.setDescription('Voir linfrastructure du bot'),
.setDescription('Voir l\'infrastructure du bot')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
const cpuInfo = os.cpus()[0].model;
const cpuCores = os.cpus().length;
const totalRAM = (os.totalmem() / (1024 ** 3)).toFixed(2);
const freeRAM = (os.freemem() / (1024 ** 3)).toFixed(2);
const usedRAM = (totalRAM - freeRAM).toFixed(2);
const ramUsagePercent = ((usedRAM / totalRAM) * 100).toFixed(1);
const uptime = os.uptime();
const embed = new EmbedBuilder()
.setTitle('Infrastructure du bot')
.setColor('Blue')
.setTitle('🖥️ Infrastructure du Bot')
.setColor(colors.info)
.addFields(
{ name: 'CPU', value: `${cpuInfo} (${cpuCores} coeurs)`, inline: true },
{ name: 'RAM', value: `${freeRAM} Go libres / ${totalRAM} Go totaux`, inline: true },
{ name: 'OS', value: getOS(), inline: true },
{ name: 'Uptime', value: formatUptime(uptime), inline: true }
{ name: '💻 CPU', value: `${cpuInfo}\n${cpuCores} cœurs`, inline: true },
{ name: '💾 RAM', value: `${usedRAM} Go / ${totalRAM} Go\n${ramUsagePercent}% utilisée`, inline: true },
{ name: '🖥️ OS', value: getOS(), inline: true },
{ name: '⏱️ Uptime Système', value: formatUptime(uptime), inline: true },
{ name: '📊 Node.js', value: process.version, inline: true },
{ name: '📦 Architecture', value: os.arch(), inline: true }
)
.setFooter({ text: interaction.client.user.username, iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
}
};
},
};

View File

@@ -1,37 +1,40 @@
const { SlashCommandBuilder, PermissionFlagsBits } = require('discord.js');
const { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
module.exports = {
category: 'dev',
data: new SlashCommandBuilder()
.setName('reload')
.setDescription('Reloads a command.')
.setDescription('Recharge une commande.')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator)
.addStringOption(option =>
option.setName('command')
.setDescription('The command to reload.')
.setDescription('La commande à recharger.')
.setRequired(true)),
async execute(interaction) {
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
return interaction.reply({ content: '❌ Only administrators can use this command.', embeds: [embed] });
return interaction.reply({ content: '❌ Seuls les administrateurs peuvent utiliser cette commande.', flags: MessageFlags.Ephemeral });
}
const commandName = interaction.options.getString('command', true).toLowerCase();
const command = interaction.client.commands.get(commandName);
if (!command) {
return interaction.reply({ content: `There is no command with name \`${commandName}\`!`, embeds: [embed] });
return interaction.reply({ content: `❌ Aucune commande nommée \`${commandName}\` n'a été trouvée !`, flags: MessageFlags.Ephemeral });
}
delete require.cache[require.resolve(`../${command.category}/${command.data.name}.js`)];
try {
const newCommand = require(`../${command.category}/${command.data.name}.js`);
// Supprimer du cache
const commandPath = `../${command.category}/${command.data.name}.js`;
delete require.cache[require.resolve(commandPath)];
// Recharger
const newCommand = require(commandPath);
interaction.client.commands.set(newCommand.data.name, newCommand);
await interaction.reply({ content: `✅ Command \`${newCommand.data.name}\` was reloaded!`, });
await interaction.reply({ content: `✅ Commande \`${newCommand.data.name}\` rechargée avec succès !`, flags: MessageFlags.Ephemeral });
} catch (error) {
console.error(error);
await interaction.reply({ content: `❌ Error while reloading \`${command.data.name}\`:\n\`${error.message}\``, embeds: [embed] });
await interaction.reply({ content: `❌ Erreur lors du rechargement de \`${command.data.name}\`:\n\`${error.message}\``, flags: MessageFlags.Ephemeral });
}
},
};
};

View File

@@ -0,0 +1,119 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors } = require('../../utils/constants');
const { sendLog } = require('../../utils/helpers');
module.exports = {
category: 'info',
data: new SlashCommandBuilder()
.setName('adminprofil')
.setDescription('Administrer les profils utilisateurs (Admin uniquement)')
.addSubcommand(subcommand =>
subcommand
.setName('reset')
.setDescription('Réinitialiser le profil d\'un utilisateur')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur dont tu veux réinitialiser le profil')
.setRequired(true))
.addStringOption(option =>
option.setName('type')
.setDescription('Ce qui doit être réinitialisé')
.setRequired(true)
.addChoices(
{ name: 'Signature', value: 'signature' },
{ name: 'Tout le profil', value: 'all' }
))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de la réinitialisation')
.setRequired(false))),
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
return interaction.editReply({
content: '❌ Tu n\'as pas la permission d\'utiliser cette commande. (Administrateur requis)'
});
}
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'reset') {
const target = interaction.options.getUser('user');
const resetType = interaction.options.getString('type');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
try {
const [profiles] = await db.query(
'SELECT * FROM user_profiles WHERE userId = ? AND guildId = ?',
[target.id, interaction.guild.id]
);
if (profiles.length === 0) {
return interaction.editReply({
content: `${target.tag} n'a pas de profil à réinitialiser.`
});
}
if (resetType === 'signature') {
await db.query(
'UPDATE user_profiles SET signature = NULL, updatedAt = ? WHERE userId = ? AND guildId = ?',
[Date.now(), target.id, interaction.guild.id]
);
const embed = new EmbedBuilder()
.setTitle('✅ Signature Réinitialisée')
.setColor(colors.success)
.setDescription(`La signature de ${target.toString()} a été réinitialisée.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.tag}`, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} else if (resetType === 'all') {
await db.query(
'DELETE FROM user_profiles WHERE userId = ? AND guildId = ?',
[target.id, interaction.guild.id]
);
const embed = new EmbedBuilder()
.setTitle('✅ Profil Réinitialisé')
.setColor(colors.success)
.setDescription(`Le profil de ${target.toString()} a été complètement réinitialisé.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.tag}`, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
const logEmbed = new EmbedBuilder()
.setTitle('🔄 Profil Réinitialisé (Admin)')
.setColor(colors.warning)
.setDescription(`Le profil de ${target.toString()} a été réinitialisé.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.tag} (${target.id})`, inline: true },
{ name: '⚙️ Admin', value: `${interaction.user.tag} (${interaction.user.id})`, inline: true },
{ name: '🔄 Type', value: resetType === 'signature' ? 'Signature' : 'Tout le profil', inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: 'France Femboy Bot • Administration' })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur adminprofil:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
},
};

43
commands/info/embed.js Normal file
View File

@@ -0,0 +1,43 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
module.exports = {
category: 'info',
data: new SlashCommandBuilder()
.setName('embed')
.setDescription('Créer un embed personnalisé.')
.addStringOption(option =>
option.setName('title')
.setDescription('Titre de l\'embed')
.setRequired(true))
.addStringOption(option =>
option.setName('description')
.setDescription('Description de l\'embed')
.setRequired(true))
.addStringOption(option =>
option.setName('color')
.setDescription('Couleur de l\'embed (hex, ex: #FF0000)')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction) {
const title = interaction.options.getString('title');
const description = interaction.options.getString('description');
const colorInput = interaction.options.getString('color');
let color = 0x5865F2;
if (colorInput) {
const hexMatch = colorInput.match(/^#?([0-9A-Fa-f]{6})$/);
if (hexMatch) {
color = parseInt(hexMatch[1], 16);
}
}
const embed = new EmbedBuilder()
.setTitle(title)
.setDescription(description)
.setColor(color)
.setFooter({ text: `Créé par ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View File

@@ -1,4 +1,6 @@
const {SlashCommandBuilder, EmbedBuilder} = require("discord.js");
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { formatUptime } = require('../../utils/helpers');
const { colors } = require('../../utils/constants');
module.exports = {
category: 'info',
@@ -6,42 +8,55 @@ module.exports = {
.setName('info')
.setDescription('Obtenir les informations du bot'),
async execute(interaction) {
const uptime = formatUptime(process.uptime());
const embed = new EmbedBuilder()
.setTitle('📝 Informations du Bot')
.setColor('DarkVividPink')
.addFields(
{
name:'👩‍💻 Développeuse Principale',
value:'<@361526553940721684>',
inline: true
},
{
name: '🧠 Langages et environnement',
value: [
'• **Langage :** <:javascript:1425179797692092506> JavaScript (ECMAScript 2024)',
'• **Backend :** <:nodejs:1425179878252089435> Node.js v22.20.0',
'• **Librairies :** <:discordjs:1425179852536938670> Discord.js v14.22',
'• **IDE :** <:webstorm:1429190717066055841> JetBrains WebStorm 2025.2.3',
].join('\n'),
inline: false
},
{
name: '💡 Date de création',
value: '20 Août 2025'
},
{
name: '⏱️ Uptime',
value: `${Math.floor(process.uptime()/3600)}h ${Math.floor(process.uptime()%3600/60)}m`,
inline: true
}
)
.setFooter({
text: 'Fembot • Made with love by Syxpi 💞',
.setAuthor({
name: `Système d'Information • ${interaction.client.user.username}`,
iconURL: interaction.client.user.displayAvatarURL()
})
.setColor(colors.info)
.setThumbnail(interaction.client.user.displayAvatarURL({dynamic: true, size: 256}))
.setDescription('Voici les détails techniques et statistiques concernant mon fonctionnement actuel.')
.addFields(
{
name: '👑 Propriétaire',
value: `> <@361526553940721684> (Lumi)`,
inline: true
},
{
name: '⌛ En ligne depuis',
value: `> \`${uptime}\``,
inline: true
},
{
name: '🛠️ Stack Technique',
value: [
'```yml',
'Langage: JavaScript (ES2024)',
'Runtime: Node.js v25.2.1',
'Library: Discord.js v14.22.1',
'IDE: JetBrains WebStorm',
'```'
].join('\n'),
inline: false
},
{
name: '📈 Statistiques Globales',
value: [
`• **Guildes:** \`${interaction.client.guilds.cache.size}\``,
`• **Membres:** \`${interaction.client.guilds.cache.reduce((acc, guild) => acc + guild.memberCount, 0)}\``,
`• **Commandes:** \`${interaction.client.commands.size}\``,
].join('\n'),
inline: true
}
)
.setFooter({
text: `France Femboy Bot • Développé avec passion par Lumi 💞`,
iconURL: interaction.client.user.displayAvatarURL()
})
.setTimestamp();
await interaction.reply({ embeds: [embed] });
await interaction.reply({embeds: [embed]});
},
};
};

334
commands/info/profil.js Normal file
View File

@@ -0,0 +1,334 @@
const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { getUserXP, getUserRank } = require('../../functions/xp/xp.js');
const { colors } = require('../../utils/constants');
const BADGE_EMOJIS = {
'staff': '👨‍💼',
'partner': '<:discord_partener:1437046788916904097>',
'hypesquad': '<:hypesquad_balance:1437046792922464337>',
'hypesquad_bravery': '<:hypesquad_bravery:1437046793899872467>',
'hypesquad_brilliance': '<:hypesquad_brilliance:1437046794868756550>',
'hypesquad_balance': '<:hypesquad_balance:1437046792922464337>',
'hypesquad_events': '<:hypesquad_events:1437046795778920448>',
'early_supporter': '<:early_supporter:1437046790087250071>',
'bug_hunter_level_1': '🐛',
'bug_hunter_level_2': '🐛',
'verified_developer': '✅',
'active_developer': '<:active_developer:1437049369936531587>',
'certified_moderator': '🛡️',
'bot_http_interactions': '🤖',
'quests': '<:quests:1437046809464934492>',
'slash_commands': '<:slash_commands:1437046810458984553>',
'orbs': '<:orbs:1437046808185540628>',
'nitro': '<:nitro:1437046796810715136>',
'nitro_1_month': '<:nitro_1_month:1437046798685442241>',
'nitro_3_months': '<:nitro_3_months:1437046801550278757>',
'nitro_6_months': '<:nitro_6_months:1437046805102723143>',
'nitro_1_year': '<:nitro_1_year:1437046799515783198>',
'nitro_2_years': '<:nitro_2_years:1437046800522416138>',
'nitro_3_years': '<:nitro_3_years:1437046802519035924>',
'nitro_5_years': '<:nitro_5_years:1437046803747967016>',
'nitro_6_years': '<:nitro_6_years:1437046806747021514>',
'boost': '<:boost:1437046778083016745>',
'boost_2_months': '<:boost_2_months:1437046780322779266>',
'boost_3_months': '<:boost_3_months:1437046781375414383>',
'boost_6_months': '<:boost_6_months:1437046782247829616>',
'boost_9_months': '<:boost_9_months:1437046783070044200>',
'boost_12_months': '<:boost_12_months:1437046784445644930>',
'boost_15_months': '<:boost_15_months:1437046785402081281>',
'boost_18_months': '<:boost_18_months:1437046786643722342>',
'boost_24_months': '<:boost_24_months:1437046787667136612>',
};
function getDiscordBadges(user, member) {
const badgeEmojis = [];
if (user.flags) {
try {
const flagsArray = user.flags.toArray();
const flagToBadgeMap = {
'Staff': 'staff', 'Partner': 'partner', 'HypeSquad': 'hypesquad',
'HypeSquadOnlineHouse1': 'hypesquad_bravery', 'HypeSquadOnlineHouse2': 'hypesquad_brilliance', 'HypeSquadOnlineHouse3': 'hypesquad_balance',
'HypeSquadEvents': 'hypesquad_events', 'BugHunterLevel1': 'bug_hunter_level_1', 'BugHunterLevel2': 'bug_hunter_level_2',
'PremiumEarlySupporter': 'early_supporter', 'VerifiedDeveloper': 'verified_developer', 'ActiveDeveloper': 'active_developer',
'CertifiedModerator': 'certified_moderator', 'BotHTTPInteractions': 'bot_http_interactions',
'Quests': 'quests', 'QuestsEarly': 'quests', 'SlashCommands': 'slash_commands',
};
for (const flag of flagsArray) {
const flagLower = flag.toLowerCase();
const badgeKey = flagToBadgeMap[flag];
if (badgeKey && BADGE_EMOJIS[badgeKey]) {
badgeEmojis.push(BADGE_EMOJIS[badgeKey]);
continue;
}
if (flagLower.includes('nitro')) {
if (flagLower.match(/6.*year|year.*6|emerald/)) badgeEmojis.push(BADGE_EMOJIS.nitro_6_years);
else if (flagLower.match(/5.*year|year.*5/)) badgeEmojis.push(BADGE_EMOJIS.nitro_5_years);
else if (flagLower.match(/3.*year|year.*3/)) badgeEmojis.push(BADGE_EMOJIS.nitro_3_years);
else if (flagLower.match(/2.*year|year.*2/)) badgeEmojis.push(BADGE_EMOJIS.nitro_2_years);
else if (flagLower.match(/1.*year|year.*1/) && !flagLower.includes('month')) badgeEmojis.push(BADGE_EMOJIS.nitro_1_year);
else if (flagLower.match(/6.*month|month.*6/)) badgeEmojis.push(BADGE_EMOJIS.nitro_6_months);
else if (flagLower.match(/3.*month|month.*3/)) badgeEmojis.push(BADGE_EMOJIS.nitro_3_months);
else if (flagLower.match(/1.*month|month.*1/)) badgeEmojis.push(BADGE_EMOJIS.nitro_1_month);
else badgeEmojis.push(BADGE_EMOJIS.nitro);
}
else if (flagLower.includes('boost') && !flagLower.includes('server')) {
if (flagLower.match(/24|two.*four|twenty.*four/i)) badgeEmojis.push(BADGE_EMOJIS.boost_24_months);
else if (flagLower.match(/18|eighteen/i)) badgeEmojis.push(BADGE_EMOJIS.boost_18_months);
else if (flagLower.match(/15|fifteen/i)) badgeEmojis.push(BADGE_EMOJIS.boost_15_months);
else if (flagLower.match(/12|twelve|one.*year|1.*year/) && !flagLower.match(/18|15|24|128|112/i)) badgeEmojis.push(BADGE_EMOJIS.boost_12_months);
else if (flagLower.match(/9|nine/) && !flagLower.match(/19|29|90|99/i)) badgeEmojis.push(BADGE_EMOJIS.boost_9_months);
else if (flagLower.match(/6|six/) && !flagLower.match(/16|26|60|66|68|69/i)) badgeEmojis.push(BADGE_EMOJIS.boost_6_months);
else if (flagLower.match(/3|three/) && !flagLower.match(/13|23|30|33|34|35|36|37|38|39/i)) badgeEmojis.push(BADGE_EMOJIS.boost_3_months);
else if (flagLower.match(/2|two/) && !flagLower.match(/12|20|21|22|23|24|25|26|27|28|29/i)) badgeEmojis.push(BADGE_EMOJIS.boost_2_months);
else badgeEmojis.push(BADGE_EMOJIS.boost);
}
else if (flagLower.includes('orb')) badgeEmojis.push(BADGE_EMOJIS.orbs);
else if (flagLower.includes('quest')) badgeEmojis.push(BADGE_EMOJIS.quests);
else if (flagLower.includes('slash')) badgeEmojis.push(BADGE_EMOJIS.slash_commands);
}
} catch (err) { console.warn('Erreur flags:', err.message); }
}
const hasNitroBadge = badgeEmojis.some(e => typeof e === 'string' && e.includes('nitro'));
const hasPremiumType = user.premiumType && user.premiumType !== 0;
const hasBanner = user.banner;
const hasAccentColor = user.accentColor;
const hasAnimatedAvatar = user.avatar && user.avatar.startsWith('a_');
const hasNitro = hasPremiumType || hasBanner || (hasAnimatedAvatar && hasAccentColor);
if (!hasNitroBadge && hasNitro) badgeEmojis.push(BADGE_EMOJIS.nitro);
if (member && member.premiumSince) {
const hasBoostBadge = badgeEmojis.some(e => typeof e === 'string' && e.includes('boost'));
if (!hasBoostBadge) {
const boostDurationMs = Date.now() - member.premiumSince.getTime();
const boostDurationMonths = Math.floor(boostDurationMs / (1000 * 60 * 60 * 24 * 30));
let boostBadge = BADGE_EMOJIS.boost;
if (boostDurationMonths >= 24) boostBadge = BADGE_EMOJIS.boost_24_months;
else if (boostDurationMonths >= 18) boostBadge = BADGE_EMOJIS.boost_18_months;
else if (boostDurationMonths >= 15) boostBadge = BADGE_EMOJIS.boost_15_months;
else if (boostDurationMonths >= 12) boostBadge = BADGE_EMOJIS.boost_12_months;
else if (boostDurationMonths >= 9) boostBadge = BADGE_EMOJIS.boost_9_months;
else if (boostDurationMonths >= 6) boostBadge = BADGE_EMOJIS.boost_6_months;
else if (boostDurationMonths >= 3) boostBadge = BADGE_EMOJIS.boost_3_months;
else if (boostDurationMonths >= 2) boostBadge = BADGE_EMOJIS.boost_2_months;
badgeEmojis.push(boostBadge);
}
}
return [...new Set(badgeEmojis)];
}
function getStatusFrench(presence) {
if (!presence || !presence.status) return { text: 'Hors ligne', emoji: '⚫' };
switch (presence.status) {
case 'online': return { text: 'En ligne', emoji: '🟢' };
case 'idle': return { text: 'Absent', emoji: '🟡' };
case 'dnd': return { text: 'Ne pas déranger', emoji: '🔴' };
default: return { text: 'Hors ligne', emoji: '⚫' };
}
}
module.exports = {
category: 'info',
data: new SlashCommandBuilder()
.setName('profil')
.setDescription('Afficher le profil d\'un utilisateur')
.addSubcommand(subcommand =>
subcommand.setName('view').setDescription('Voir le profil d\'un utilisateur')
.addUserOption(option => option.setName('user').setDescription('L\'utilisateur dont tu veux voir le profil').setRequired(false)))
.addSubcommand(subcommand =>
subcommand.setName('set').setDescription('Modifier ton profil')
.addStringOption(option => option.setName('signature').setDescription('Ta signature personnalisée (max 200 caractères)').setRequired(false).setMaxLength(200))
.addStringOption(option => option.setName('birthday').setDescription('Ta date d\'anniversaire (JJ/MM/AAAA)').setRequired(false))
.addStringOption(option => option.setName('color').setDescription('Couleur de ton profil (Hex: #RRGGBB)').setRequired(false))
.addStringOption(option => option.setName('gender').setDescription('Ton genre/pronoms').setRequired(false).setMaxLength(50))
.addStringOption(option => option.setName('location').setDescription('Ta localisation').setRequired(false).setMaxLength(100)))
.addSubcommand(subcommand =>
subcommand.setName('reset').setDescription('Réinitialiser ton profil')),
async execute(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'view') {
await interaction.deferReply();
const target = interaction.options.getUser('user') || interaction.user;
const fetchedUser = await interaction.client.users.fetch(target.id, { force: true, cache: false }).catch(() => target);
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur ce serveur.' });
try {
const userXP = await getUserXP(target.id, interaction.guild.id);
const [profiles] = await db.query('SELECT * FROM user_profiles WHERE userId = ? AND guildId = ?', [target.id, interaction.guild.id]);
const profile = profiles[0] || null;
const badgeEmojis = getDiscordBadges(fetchedUser, member);
const badgesText = badgeEmojis.length > 0 ? badgeEmojis.join(' ') : 'Aucun badge';
const status = getStatusFrench(member.presence);
const accountAge = Math.floor((Date.now() - target.createdTimestamp) / (1000 * 60 * 60 * 24));
const accountAgeYears = Math.floor(accountAge / 365);
const accountAgeMonths = Math.floor((accountAge % 365) / 30);
const accountAgeDays = accountAge % 30;
let accountAgeText = accountAgeYears > 0 ? `${accountAgeYears} an${accountAgeYears > 1 ? 's' : ''}` : '';
if (accountAgeMonths > 0) accountAgeText += (accountAgeText ? ' ' : '') + `${accountAgeMonths} mois`;
if (!accountAgeText) accountAgeText = `${accountAgeDays} jour${accountAgeDays > 1 ? 's' : ''}`;
const creationDate = `<t:${Math.floor(target.createdTimestamp / 1000)}:D>`;
const voiceTimeMinutes = userXP?.totalVoiceTime || 0;
const voiceTimeText = `${Math.floor(voiceTimeMinutes / 60)}h ${voiceTimeMinutes % 60}min`;
const rank = userXP ? await getUserRank(target.id, interaction.guild.id) : 0;
const [bumps] = await db.query('SELECT COUNT(*) as count FROM bumps WHERE userId = ? AND guildId = ?', [target.id, interaction.guild.id]);
const [welcomeLogs] = await db.query('SELECT COUNT(*) as count FROM xp_logs WHERE userId = ? AND guildId = ? AND source = ?', [target.id, interaction.guild.id, 'welcome']);
const [totalMessages] = await db.query('SELECT SUM(totalMessages) as total FROM user_xp WHERE userId = ?', [target.id]);
const embedColor = profile?.color && /^#[0-9A-F]{6}$/i.test(profile.color) ? profile.color : colors.primary;
const embed = new EmbedBuilder()
.setAuthor({ name: `Profil de ${target.username}`, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setColor(embedColor)
.setThumbnail(target.displayAvatarURL({ dynamic: true, size: 256 }))
.setDescription(profile?.signature ? `\`\`\`${profile.signature}\`\`\`` : '*Aucune signature définie.*')
.addFields(
{ name: '👤 Informations', value: `• **Identifiant:** \`${target.id}\`\n• **Badges:** ${badgesText}\n• **Âge Compte:** \`${accountAgeText}\`\n• **Statut:** ${status.emoji} \`${status.text}\``, inline: false },
{ name: '📊 Statistiques XP', value: `• **Niveau:** \`${userXP?.level || 0}\`\n• **XP Total:** \`${userXP?.xp || 0}\`\n• **Rang:** \`#${rank}\``, inline: true },
{ name: '🎙️ Activité Vocal', value: `• **Temps:** \`${voiceTimeText}\`\n• **Bumps:** \`${bumps[0]?.count || 0}\`\n• **Messages:** \`${totalMessages[0]?.total || userXP?.totalMessages || 0}\``, inline: true },
{ name: '🤝 Engagement', value: `• **Bienvenues:** \`${welcomeLogs[0]?.count || 0}\`\n• **Création:** ${creationDate}`, inline: false }
)
.setFooter({ text: `Demandé par ${interaction.user.tag}`, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp();
const personalInfos = [];
if (profile?.gender) personalInfos.push(`• **Genre/Pronoms:** ${profile.gender}`);
if (profile?.location) personalInfos.push(`• **Localisation:** ${profile.location}`);
if (profile?.birthday) {
const birthday = new Date(profile.birthday);
const today = new Date();
let age = today.getFullYear() - birthday.getFullYear();
const monthDiff = today.getMonth() - birthday.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthday.getDate())) age--;
personalInfos.push(`• **Anniversaire:** ${birthday.toLocaleDateString('fr-FR')} (${age} ans)`);
}
if (personalInfos.length > 0) {
embed.addFields({ name: '📝 Infos Personnelles', value: personalInfos.join('\n'), inline: false });
}
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur profil:', err);
await interaction.editReply({ content: '❌ Erreur lors de l\'affichage du profil.' });
}
} else if (subcommand === 'set') {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const signature = interaction.options.getString('signature');
const birthdayInput = interaction.options.getString('birthday');
const color = interaction.options.getString('color');
const gender = interaction.options.getString('gender');
const location = interaction.options.getString('location');
if (!signature && !birthdayInput && !color && !gender && !location) {
return interaction.editReply({ content: '❌ Tu dois modifier au moins un élément.' });
}
try {
let birthdayDate = null;
if (birthdayInput) {
const parts = birthdayInput.split('/');
if (parts.length === 3) {
const day = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1;
const year = parseInt(parts[2], 10);
const currentYear = new Date().getFullYear();
if (year < 1900 || year > currentYear) {
return interaction.editReply({ content: '❌ Année invalide. Doit être entre 1900 et aujourd\'hui.' });
}
const date = new Date(year, month, day);
if (date.getFullYear() === year && date.getMonth() === month && date.getDate() === day) {
// Vérifier si la date est dans le futur
if (date > new Date()) {
return interaction.editReply({ content: '❌ Tu ne peux pas être né dans le futur !' });
}
birthdayDate = `${year}-${month + 1}-${day}`;
} else {
return interaction.editReply({ content: '❌ Date invalide. Format: JJ/MM/AAAA' });
}
} else {
return interaction.editReply({ content: '❌ Format de date invalide. Utilise JJ/MM/AAAA' });
}
}
if (color && !/^#[0-9A-F]{6}$/i.test(color)) {
return interaction.editReply({ content: '❌ Couleur invalide. Utilise le format Hex (ex: #FF0000).' });
}
const [profiles] = await db.query('SELECT * FROM user_profiles WHERE userId = ? AND guildId = ?', [interaction.user.id, interaction.guild.id]);
if (profiles.length > 0) {
const updates = [];
const params = [];
if (signature) { updates.push('signature = ?'); params.push(signature); }
if (birthdayDate) { updates.push('birthday = ?'); params.push(birthdayDate); }
if (color) { updates.push('color = ?'); params.push(color); }
if (gender) { updates.push('gender = ?'); params.push(gender); }
if (location) { updates.push('location = ?'); params.push(location); }
updates.push('updatedAt = ?');
params.push(Date.now());
params.push(interaction.user.id);
params.push(interaction.guild.id);
await db.query(`UPDATE user_profiles SET ${updates.join(', ')} WHERE userId = ? AND guildId = ?`, params);
} else {
await db.query(
'INSERT INTO user_profiles (userId, guildId, signature, birthday, color, gender, location, updatedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[
interaction.user.id,
interaction.guild.id,
signature || null,
birthdayDate || null,
color || null,
gender || null,
location || null,
Date.now()
]
);
}
const embed = new EmbedBuilder().setTitle('✅ Profil Modifié').setColor(colors.success).setDescription('Ton profil a été mis à jour avec succès !').setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur set profil:', err);
await interaction.editReply({ content: '❌ Erreur lors de la modification du profil.' });
}
} else if (subcommand === 'reset') {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
await db.query('DELETE FROM user_profiles WHERE userId = ? AND guildId = ?', [interaction.user.id, interaction.guild.id]);
const embed = new EmbedBuilder().setTitle('✅ Profil Réinitialisé').setColor(colors.success).setDescription('Ton profil a été entièrement réinitialisé.').setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur reset profil:', err);
await interaction.editReply({ content: '❌ Erreur lors de la réinitialisation du profil.' });
}
}
},
};

View File

@@ -0,0 +1,53 @@
const { SlashCommandBuilder, EmbedBuilder, GuildVerificationLevel } = require('discord.js');
const { colors, emojis } = require('../../utils/constants');
const verificationLevels = {
[GuildVerificationLevel.None]: 'Aucune',
[GuildVerificationLevel.Low]: 'Faible',
[GuildVerificationLevel.Medium]: 'Moyenne',
[GuildVerificationLevel.High]: 'Élevée',
[GuildVerificationLevel.VeryHigh]: 'Très Élevée'
};
module.exports = {
category: 'info',
data: new SlashCommandBuilder()
.setName('serverinfo')
.setDescription('Affiche les informations du serveur.'),
async execute(interaction) {
const guild = interaction.guild;
const owner = await guild.fetchOwner();
const embed = new EmbedBuilder()
.setAuthor({
name: guild.name,
iconURL: guild.iconURL({ dynamic: true }) || undefined
})
.setTitle(`${emojis.server} Informations du Serveur`)
.setColor(colors.info)
.setThumbnail(guild.iconURL({ dynamic: true, size: 256 }) || null)
.addFields(
{ name: '👑 Propriétaire', value: `${owner.user.toString()}\n\`${owner.user.tag}\``, inline: true },
{ name: `${emojis.id} ID`, value: `\`${guild.id}\``, inline: true },
{ name: `${emojis.calendar} Créé le`, value: `<t:${Math.floor(guild.createdTimestamp / 1000)}:F>`, inline: true },
{ name: '👥 Membres', value: `\`${guild.memberCount.toLocaleString()}\``, inline: true },
{ name: '💬 Canaux', value: `\`${guild.channels.cache.size}\``, inline: true },
{ name: '😀 Emojis', value: `\`${guild.emojis.cache.size}\``, inline: true },
{ name: '🎭 Rôles', value: `\`${guild.roles.cache.size}\``, inline: true },
{ name: '✅ Vérification', value: `\`${verificationLevels[guild.verificationLevel] || 'Inconnue'}\``, inline: true },
{ name: '🔒 Sécurité', value: guild.mfaLevel === 1 ? '`Élevé (2FA)`' : '`Normal`', inline: true }
)
.setFooter({ text: `${guild.name}${interaction.client.user.username}`, iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
if (guild.description) {
embed.setDescription(guild.description);
}
if (guild.banner) {
embed.setImage(guild.bannerURL({ dynamic: true, size: 1024 }));
}
await interaction.reply({ embeds: [embed] });
},
};

70
commands/info/userinfo.js Normal file
View File

@@ -0,0 +1,70 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { colors, emojis } = require('../../utils/constants');
function getStatusFrench(presence) {
if (!presence || !presence.status) return { text: 'Hors ligne', emoji: '⚫' };
switch (presence.status) {
case 'online': return { text: 'En ligne', emoji: '🟢' };
case 'idle': return { text: 'Absent', emoji: '🟡' };
case 'dnd': return { text: 'Ne pas déranger', emoji: '🔴' };
default: return { text: 'Hors ligne', emoji: '⚫' };
}
}
module.exports = {
category: 'info',
data: new SlashCommandBuilder()
.setName('userinfo')
.setDescription('Affiche les informations d\'un utilisateur.')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur dont tu veux voir les informations')
.setRequired(false)),
async execute(interaction) {
const target = interaction.options.getUser('user') || interaction.user;
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
const embed = new EmbedBuilder()
.setAuthor({
name: target.displayName,
iconURL: target.displayAvatarURL({ dynamic: true })
})
.setTitle(`${emojis.user} Informations Utilisateur`)
.setColor(colors.info)
.setThumbnail(target.displayAvatarURL({ dynamic: true, size: 256 }))
.addFields(
{ name: `${emojis.id} ID`, value: `\`${target.id}\``, inline: true },
{ name: `${emojis.calendar} Compte créé`, value: `<t:${Math.floor(target.createdTimestamp / 1000)}:F>`, inline: true },
{ name: '🤖 Bot', value: target.bot ? '`Oui`' : '`Non`', inline: true }
)
.setFooter({ text: `${interaction.guild.name}${interaction.client.user.username}`, iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
if (member) {
const status = getStatusFrench(member.presence);
const roles = member.roles.cache.filter(r => r.id !== interaction.guild.id);
const rolesDisplay = roles.size > 0 ? roles.sort((a, b) => b.position - a.position).map(r => r.toString()).slice(0, 10).join(', ') : '`Aucun`';
embed.addFields(
{ name: '📥 A rejoint le', value: `<t:${Math.floor(member.joinedTimestamp / 1000)}:F>`, inline: true },
{ name: '⏰ Statut', value: `${status.emoji} \`${status.text}\``, inline: true },
{ name: '👑 Rôle le plus élevé', value: member.roles.highest.toString(), inline: true },
{ name: '🎭 Rôles', value: rolesDisplay, inline: false }
);
if (member.nickname) {
embed.addFields({ name: '📝 Surnom', value: `\`${member.nickname}\``, inline: true });
}
if (roles.size > 10) {
embed.addFields({ name: '📊 Total de rôles', value: `\`${roles.size}\``, inline: true });
}
}
if (target.banner) {
embed.setImage(target.bannerURL({ dynamic: true, size: 1024 }));
}
await interaction.reply({ embeds: [embed] });
},
};

View File

@@ -1,121 +1,119 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } = require('discord.js');
const db = require('../../functions/database/db.js'); // instance mysql2/promise
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { parseDuration, formatDuration, sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Select a member and ban them.')
.setDescription('Bannir un membre du serveur.')
.addUserOption(option =>
option.setName('target')
.setDescription('The member to ban')
.setDescription('Le membre à bannir')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Reason')
.setDescription('Raison du bannissement')
.setRequired(false))
.addStringOption(option =>
option.setName('duration')
.setDescription('Ban duration (ex: 1h, 2d, leave empty for permanent)')
.setDescription('Durée du ban (ex: 1h, 2d, laisser vide pour permanent)')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction) {
const target = interaction.options.getUser('target');
const reason = interaction.options.getString('reason') || 'No reason provided';
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
const durationInput = interaction.options.getString('duration');
await interaction.deferReply()
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: 'No user specified!' });
await interaction.guild.bans.create(target.id, { reason: `Banned by ${interaction.user.tag}: ${reason}` });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (member) {
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.guild.ownerId !== interaction.user.id) {
return interaction.editReply({ content: '❌ Tu ne peux pas bannir cet utilisateur car il a un rôle supérieur ou égal au tien.' });
}
if (member.id === interaction.guild.ownerId) {
return interaction.editReply({ content: '❌ Tu ne peux pas bannir le propriétaire du serveur.' });
}
if (!member.bannable) {
return interaction.editReply({ content: '❌ Je ne peux pas bannir cet utilisateur (rôle supérieur au mien ?).' });
}
}
let type = 'Permanent';
let unbanDate = null;
let durationText = 'Permanent';
if (durationInput) {
const regex = /^(\d+)(s|m|h|d|mo|y)$/;
const match = durationInput.match(regex);
if (match) {
const value = parseInt(match[1]);
const unit = match[2];
let multiplier = 1000;
if (unit === 's') multiplier *= 60 / 60;
if (unit === 'm') multiplier *= 60;
if (unit === 'h') multiplier *= 60 * 60;
if (unit === 'd') multiplier *= 60 * 60 * 24;
if (unit === 'mo') multiplier *= 60 * 60 * 24 * 30;
if (unit === 'y') multiplier *= 60 * 60 * 24 * 365;
unbanDate = Date.now() + value * multiplier;
const durationMs = parseDuration(durationInput);
if (durationMs) {
unbanDate = Date.now() + durationMs;
type = 'Temporary';
durationText = formatDuration(durationMs);
} else {
return interaction.editReply({ content: '❌ Format de durée invalide (ex: 1h, 2d).' });
}
}
// juste avant la création de l'embed
let durationText = type;
if (type === 'Temporary' && unbanDate) {
const initialDurationMs = unbanDate - Date.now() + (Date.now() - Date.now()); // ça reste l'unbanDate - timestamp original
function formatInitialDuration(ms) {
const seconds = Math.floor(ms / 1000) % 60;
const minutes = Math.floor(ms / (1000 * 60)) % 60;
const hours = Math.floor(ms / (1000 * 60 * 60)) % 24;
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
const months = Math.floor(ms / (1000 * 60 * 60 * 24 * 30));
const years = Math.floor(ms / (1000 * 60 * 60 * 24 * 365));
if (years) return `${years} Année${years > 1 ? 's' : ''}`;
if (months) return `${months} Mois`;
if (days) return `${days} Jour${days > 1 ? 's' : ''}`;
if (hours) return `${hours} Heure${hours > 1 ? 's' : ''}`;
if (minutes) return `${minutes} Minute${minutes > 1 ? 's' : ''}`;
if (seconds) return `${seconds} Seconde${seconds > 1 ? 's' : ''}`;
return 'Instantané';
}
durationText = formatInitialDuration(initialDurationMs);
}
try {
// Bannissement
interaction.guild.bans.create(target.id, { reason: `Banned by ${interaction.user.tag}: ${reason}` });
await interaction.guild.bans.create(target.id, { reason: `Banni par ${interaction.user.tag}: ${reason}` });
// Stockage dans MySQL (bans)
await db.query(
`INSERT INTO bans (userId, reason, modId, timestamp, type, unbanDate, guildId)
VALUES (?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE reason=VALUES(reason), modId=VALUES(modId), timestamp=VALUES(timestamp), type=VALUES(type), unbanDate=VALUES(unbanDate)`,
ON DUPLICATE KEY UPDATE reason=VALUES(reason), modId=VALUES(modId), timestamp=VALUES(timestamp), type=VALUES(type), unbanDate=VALUES(unbanDate)`,
[target.id, reason, interaction.user.id, Date.now(), type, unbanDate, interaction.guild.id]
);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Bannissement', reason, type, interaction.guild.id, Date.now()]
);
await interaction.editReply(`✅ Successfully banned ${target.tag} (${type})`);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.ban} Utilisateur Banni`)
.setColor(colors.ban)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été banni du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '⏱️ Durée', value: `\`${durationText}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true },
{ name: '🏷️ Type', value: `\`${type}\``, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
// Embed log
const logChannel = interaction.guild.channels.cache.find(ch => ch.name === 'logs');
if (logChannel) {
const embed = new EmbedBuilder()
.setTitle('User Banned')
.setColor('Red')
.addFields(
{ name: 'Banned User', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: 'Banned By', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: 'Reason', value: reason, inline: false },
{ name: 'Duration', value: durationText, inline: true },
{ name: 'Date', value: new Date().toLocaleString(), inline: true },
{ name: 'Guild', value: interaction.guild.name, inline: true }
)
.setTimestamp();
logChannel.send({ embeds: [embed] });
}
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.ban} Bannissement`)
.setColor(colors.ban)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été banni du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '⏱️ Durée', value: `\`${durationText}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error(err);
await interaction.editReply({ content: `Failed to ban ${target.tag}` });
console.error('Erreur ban:', err);
await interaction.editReply({ content: `Erreur: ${err.message}` });
}
}
};
},
};

View File

@@ -1,56 +1,69 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('casier')
.setDescription('Voir toutes les sanctions dun membre.')
.setDescription('Voir toutes les sanctions d\'un membre.')
.addUserOption(option =>
option.setName('membre')
.setDescription('Le membre dont tu veux voir le casier')
.setRequired(true)),
async execute(interaction) {
await interaction.deferReply();
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const member = interaction.options.getUser('membre');
try {
const [rows] = await db.query(
'SELECT * FROM logs WHERE userId = ? ORDER BY timestamp DESC',
[member.id]
'SELECT * FROM logs WHERE userId = ? AND guildId = ? ORDER BY timestamp DESC',
[member.id, interaction.guild.id]
);
if (!rows.length) return interaction.editReply(`${member.tag} na aucune sanction.`);
const sanctions = rows.filter(row =>
row.action !== 'Modification de sanction' &&
row.action !== 'Révocation de sanction' &&
row.action !== 'Modification de mute'
);
if (!sanctions.length) {
return interaction.editReply({ content: `${member.tag} n'a aucune sanction sur ce serveur.` });
}
// Découpe les sanctions en pages de 25
const pages = [];
for (let i = 0; i < rows.length; i += 10) {
pages.push(rows.slice(i, i + 10));
for (let i = 0; i < sanctions.length; i += 10) {
pages.push(sanctions.slice(i, i + 10));
}
const embeds = pages.map((page, pageIndex) => {
const embed = new EmbedBuilder()
.setTitle(`Casier de ${member.tag} - Page ${pageIndex + 1}/${pages.length}`)
.setColor('Orange')
.setAuthor({ name: member.displayName, iconURL: member.displayAvatarURL({ dynamic: true }) })
.setTitle(`📋 Casier Judiciaire - Page ${pageIndex + 1}/${pages.length}`)
.setColor(colors.warning)
.setThumbnail(member.displayAvatarURL({ dynamic: true, size: 256 }))
.setDescription(`**${member.toString()}** • \`${member.tag}\``)
.setFooter({ text: `Total: ${sanctions.length} sanction(s) • ${interaction.guild.name}` })
.setTimestamp();
page.forEach((row, index) => {
const date = new Date(row.timestamp).toLocaleString();
const date = `<t:${Math.floor(row.timestamp / 1000)}:F>`;
const actionCapitalized = row.action.charAt(0).toUpperCase() + row.action.slice(1);
const action = row.action || 'N/A';
const guildName = row.guildId
? interaction.client.guilds.cache.get(row.guildId)?.name || 'Unknown Server'
: 'Unknown Server';
? interaction.client.guilds.cache.get(row.guildId)?.name || 'Serveur inconnu'
: 'Serveur inconnu';
const displayNumber = index + 1 + pageIndex * 10;
embed.addFields({
name: `#${index + 1 + pageIndex * 10} - ${actionCapitalized}`,
value: `**Modérateur:** <@${row.modId}> (${row.modTag || 'N/A'})\n` +
`**Raison:** ${row.reason || 'N/A'}\n` +
`**Serveur:** ${guildName}\n` +
name: `#${displayNumber} (ID: \`${row.id}\`) - ${actionCapitalized}`,
value: `**Modérateur:** <@${row.modId}> (\`${row.modTag || 'N/A'}\`)\n` +
`**Raison:** ${row.reason || '`N/A`'}\n` +
`**Serveur:** \`${guildName}\`\n` +
`**Date:** ${date}\n` +
`**Type:** ${action}`,
`**Type:** \`${row.type || 'N/A'}\``,
inline: false
});
});
@@ -58,28 +71,36 @@ module.exports = {
return embed;
});
// Composants pour naviguer
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId('prev').setLabel('⬅️').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('next').setLabel('➡️').setStyle(ButtonStyle.Primary)
new ButtonBuilder().setCustomId('prev').setLabel('⬅️ Précédent').setStyle(ButtonStyle.Primary).setDisabled(pages.length === 1),
new ButtonBuilder().setCustomId('next').setLabel('Suivant ➡️').setStyle(ButtonStyle.Primary).setDisabled(pages.length === 1)
);
let currentPage = 0;
const message = await interaction.editReply({ embeds: [embeds[currentPage]], components: [row] });
const message = await interaction.editReply({ embeds: [embeds[currentPage]], components: pages.length > 1 ? [row] : [] });
// Collecteur de boutons
const collector = message.createMessageComponentCollector({ time: 60_000 });
collector.on('collect', i => {
if (i.user.id !== interaction.user.id) return i.reply({ content: 'Ce nest pas ton menu !', embeds: [embed] });
if (i.customId === 'next') currentPage = (currentPage + 1) % embeds.length;
if (i.customId === 'prev') currentPage = (currentPage - 1 + embeds.length) % embeds.length;
collector.on('collect', async i => {
if (i.user.id !== interaction.user.id) {
return i.reply({ content: '❌ Ce n\'est pas ton menu !', flags: MessageFlags.Ephemeral });
}
if (i.customId === 'next') {
currentPage = (currentPage + 1) % embeds.length;
}
if (i.customId === 'prev') {
currentPage = (currentPage - 1 + embeds.length) % embeds.length;
}
i.update({ embeds: [embeds[currentPage]] });
await i.update({ embeds: [embeds[currentPage]] });
});
collector.on('end', () => {
message.edit({ components: [] }).catch(() => null);
});
} catch (err) {
console.error(err);
await interaction.editReply('❌ Une erreur est survenue lors de la récupération du casier.');
console.error('Erreur casier:', err);
await interaction.editReply({ content: '❌ Une erreur est survenue lors de la récupération du casier.' });
}
},
};
};

View File

@@ -0,0 +1,149 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { parseDuration, formatDuration, sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('changemute')
.setDescription('Changer le temps ou la raison d\'un mute.')
.addUserOption(option =>
option.setName('user')
.setDescription('Le membre concerné')
.setRequired(true))
.addStringOption(option =>
option.setName('time')
.setDescription('Nouvelle durée du mute (ex: 5m, 1h, 1d)')
.setRequired(false))
.addStringOption(option =>
option.setName('reason')
.setDescription('Nouvelle raison du mute')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('user');
const newTime = interaction.options.getString('time');
const newReason = interaction.options.getString('reason');
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (!newTime && !newReason) return interaction.editReply({ content: '❌ Tu dois spécifier au moins une nouvelle durée ou une nouvelle raison.' });
const [muteRows] = await db.query(
'SELECT * FROM mutes WHERE userId = ? AND guildId = ?',
[target.id, interaction.guild.id]
);
if (!muteRows.length && (!member.communicationDisabledUntilTimestamp || member.communicationDisabledUntilTimestamp < Date.now())) {
return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas muté.' });
}
const currentMute = muteRows[0];
const oldReason = currentMute?.reason || 'N/A';
const oldUnmuteDate = currentMute?.unmuteDate || null;
try {
let newUnmuteDate = oldUnmuteDate;
let newDurationMs = null;
let durationText = 'Non modifié';
if (newTime) {
newDurationMs = parseDuration(newTime);
if (!newDurationMs) return interaction.editReply({ content: '❌ Durée invalide. Utilise un format comme: 5m, 1h, 2d (max 28 jours).' });
const maxDuration = 28 * 24 * 60 * 60 * 1000;
if (newDurationMs > maxDuration) return interaction.editReply({ content: '❌ La durée maximum est de 28 jours.' });
newUnmuteDate = Date.now() + newDurationMs;
durationText = formatDuration(newDurationMs);
await member.timeout(newDurationMs, `Mute modifié par ${interaction.user.tag}: ${newReason || oldReason}`);
}
const finalReason = newReason || oldReason;
const type = 'Temporary';
if (currentMute) {
await db.query(
'UPDATE mutes SET reason = ?, modId = ?, modTag = ?, timestamp = ?, unmuteDate = ?, type = ? WHERE userId = ? AND guildId = ?',
[finalReason, interaction.user.id, interaction.user.tag, Date.now(), newUnmuteDate, type, target.id, interaction.guild.id]
);
} else {
await db.query(
`INSERT INTO mutes (userId, guildId, reason, modId, modTag, timestamp, unmuteDate, type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, interaction.guild.id, finalReason, interaction.user.id, interaction.user.tag, Date.now(), newUnmuteDate, type]
);
}
const changeLog = [];
if (newTime) changeLog.push(`Durée: ${formatDuration(newDurationMs)}`);
if (newReason) changeLog.push(`Raison: ${oldReason}${newReason}`);
const changeReason = changeLog.join(', ');
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Modification de mute', changeReason, type, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Mute Modifié`)
.setColor(colors.success)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '🔧 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '⏰ Nouvelle durée', value: durationText, inline: true },
{ name: '📝 Ancienne raison', value: oldReason, inline: false },
{ name: '📝 Nouvelle raison', value: finalReason, inline: false }
)
.setFooter({ text: `ID: ${target.id}` })
.setTimestamp();
if (newUnmuteDate) {
embed.addFields({ name: '📅 Démute le', value: `<t:${Math.floor(newUnmuteDate / 1000)}:F>`, inline: true });
}
await interaction.editReply({ embeds: [embed] });
try {
const dmEmbed = new EmbedBuilder()
.setTitle(`${emojis.info} Ton mute a été modifié`)
.setColor(colors.info)
.setDescription(`Ton mute sur **${interaction.guild.name}** a été modifié`)
.addFields(
{ name: '⏰ Durée', value: durationText, inline: true },
{ name: '📝 Raison', value: finalReason, inline: false }
)
.setFooter({ text: interaction.guild.name, iconURL: interaction.guild.iconURL({ dynamic: true }) })
.setTimestamp();
await target.send({ embeds: [dmEmbed] });
} catch { /* DMs fermés */ }
const logEmbed = new EmbedBuilder()
.setTitle(`${emojis.success} Modification de Mute`)
.setColor(colors.success)
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '🔧 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Modifications', value: changeReason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id} | Serveur: ${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur changemute:', err);
await interaction.editReply({ content: `❌ Échec de la modification du mute: ${err.message}` });
}
},
};

View File

@@ -0,0 +1,114 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('changesanction')
.setDescription('Changer la raison d\'une sanction.')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur concerné')
.setRequired(true))
.addIntegerOption(option =>
option.setName('sanction_id')
.setDescription('L\'ID DB de la sanction (visible dans /casier, ex: ID: 123)')
.setRequired(true))
.addStringOption(option =>
option.setName('new_reason')
.setDescription('La nouvelle raison')
.setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('user');
const sanctionId = interaction.options.getInteger('sanction_id');
const newReason = interaction.options.getString('new_reason');
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const [sanctionRows] = await db.query(
'SELECT * FROM logs WHERE id = ? AND userId = ? AND guildId = ?',
[sanctionId, target.id, interaction.guild.id]
);
if (!sanctionRows.length) {
const [allUserLogs] = await db.query(
'SELECT id, action, guildId FROM logs WHERE id = ? AND userId = ?',
[sanctionId, target.id]
);
if (allUserLogs.length > 0) {
const log = allUserLogs[0];
const guildName = interaction.client.guilds.cache.get(log.guildId)?.name || 'Serveur inconnu';
return interaction.editReply({
content: `❌ La sanction avec l'ID ${sanctionId} existe, mais elle appartient au serveur "${guildName}" (guildId: ${log.guildId}), pas à "${interaction.guild.name}" (guildId: ${interaction.guild.id}).`
});
}
return interaction.editReply({
content: `❌ Aucune sanction trouvée avec l'ID ${sanctionId} pour ${target.tag}.\n\n💡 **Astuce:** Utilise l'ID DB visible dans \`/casier\` (ex: \`#1 (ID: 123)\` → utilise \`123\`).`
});
}
const sanction = sanctionRows[0];
if (sanction.action === 'Modification de sanction' ||
sanction.action === 'Révocation de sanction' ||
sanction.action === 'Modification de mute') {
return interaction.editReply({ content: `❌ Cette entrée est une modification, pas une sanction. Utilise l'ID d'une vraie sanction.` });
}
const oldReason = sanction.reason;
await db.query(
'UPDATE logs SET reason = ?, modId = ?, modTag = ? WHERE id = ?',
[newReason, interaction.user.id, interaction.user.tag, sanction.id]
);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Modification de sanction', `Ancienne raison: ${oldReason} → Nouvelle raison: ${newReason}`, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Sanction Modifiée`)
.setColor(colors.success)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '🔧 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Ancienne raison', value: oldReason || 'N/A', inline: false },
{ name: '📝 Nouvelle raison', value: newReason, inline: false },
{ name: '🆔 ID Sanction', value: `${sanction.id}`, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setTitle(`${emojis.success} Modification de Sanction`)
.setColor(colors.success)
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '🔧 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Ancienne raison', value: oldReason || 'N/A', inline: false },
{ name: '📝 Nouvelle raison', value: newReason, inline: false },
{ name: '🆔 ID Sanction', value: `${sanction.id}`, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id} | Serveur: ${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur changesanction:', err);
await interaction.editReply({ content: `❌ Échec de la modification de la sanction: ${err.message}` });
}
},
};

View File

@@ -0,0 +1,74 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('clear')
.setDescription('Supprimer plusieurs messages d\'un coup.')
.addIntegerOption(option =>
option.setName('amount')
.setDescription('Nombre de messages à supprimer (1-100)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100))
.addUserOption(option =>
option.setName('target')
.setDescription('Supprimer uniquement les messages de cet utilisateur')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction) {
const amount = interaction.options.getInteger('amount');
const target = interaction.options.getUser('target');
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.channel.isTextBased()) {
return interaction.editReply({ content: '❌ Cette commande ne peut être utilisée que dans un canal texte.' });
}
try {
const messages = await interaction.channel.messages.fetch({ limit: amount });
let messagesToDelete = messages;
if (target) {
messagesToDelete = messages.filter(msg => msg.author.id === target.id);
}
const filteredMessages = messagesToDelete.filter(msg => {
const msgAge = Date.now() - msg.createdTimestamp;
return msgAge < 14 * 24 * 60 * 60 * 1000;
});
if (filteredMessages.size === 0) {
return interaction.editReply({ content: '❌ Aucun message à supprimer (les messages de plus de 14 jours ne peuvent pas être supprimés).' });
}
await interaction.channel.bulkDelete(filteredMessages, true);
const embed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.success} Messages Supprimés`)
.setColor(colors.success)
.setDescription(`**${filteredMessages.size}** message(s) supprimé(s)${target ? ` de ${target.toString()}` : ''}.`)
.addFields(
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📊 Messages supprimés', value: `\`${filteredMessages.size}\``, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `${interaction.guild.name}${interaction.channel.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur clear:', err);
if (err.code === 50034) {
await interaction.editReply({ content: '❌ Les messages de plus de 14 jours ne peuvent pas être supprimés.' });
} else {
await interaction.editReply({ content: `❌ Échec de la suppression: ${err.message}` });
}
}
},
};

View File

@@ -0,0 +1,89 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('kick')
.setDescription('Expulser un membre du serveur.')
.addUserOption(option =>
option.setName('target')
.setDescription('Le membre à expulser')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de l\'expulsion')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.KickMembers),
async execute(interaction) {
const target = interaction.options.getUser('target');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (member.id === interaction.guild.ownerId) return interaction.editReply({ content: '❌ Tu ne peux pas expulser le propriétaire du serveur.' });
if (member.id === interaction.user.id) return interaction.editReply({ content: '❌ Tu ne peux pas t\'expulser toi-même.' });
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.guild.ownerId !== interaction.user.id) {
return interaction.editReply({ content: '❌ Tu ne peux pas expulser cet utilisateur car il a un rôle supérieur ou égal au tien.' });
}
if (!member.kickable) {
return interaction.editReply({ content: '❌ Je n\'ai pas les permissions nécessaires pour expulser cet utilisateur.' });
}
try {
await member.kick(`Expulsé par ${interaction.user.tag}: ${reason}`);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Expulsion', reason, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.kick} Utilisateur Expulsé`)
.setColor(colors.kick)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été expulsé du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.kick} Expulsion`)
.setColor(colors.kick)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été expulsé du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur kick:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};

113
commands/moderation/mute.js Normal file
View File

@@ -0,0 +1,113 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { parseDuration, formatDuration, sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('mute')
.setDescription('Muter un membre du serveur.')
.addUserOption(option =>
option.setName('user')
.setDescription('Le membre à muter')
.setRequired(true))
.addStringOption(option =>
option.setName('duration')
.setDescription('Durée du mute (ex: 5m, 1h, 1d, max 28j)')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison du mute')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('user');
const durationInput = interaction.options.getString('duration');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (member.id === interaction.guild.ownerId) return interaction.editReply({ content: '❌ Tu ne peux pas muter le propriétaire du serveur.' });
if (member.id === interaction.user.id) return interaction.editReply({ content: '❌ Tu ne peux pas te muter toi-même.' });
if (member.roles.highest.position >= interaction.member.roles.highest.position && interaction.guild.ownerId !== interaction.user.id) {
return interaction.editReply({ content: '❌ Tu ne peux pas muter cet utilisateur car il a un rôle supérieur ou égal au tien.' });
}
if (!member.moderatable) {
return interaction.editReply({ content: '❌ Je n\'ai pas les permissions nécessaires pour muter cet utilisateur.' });
}
const durationMs = parseDuration(durationInput);
if (!durationMs) return interaction.editReply({ content: '❌ Durée invalide. Utilise un format comme: 5m, 1h, 2d (max 28 jours).' });
const maxDuration = 28 * 24 * 60 * 60 * 1000;
if (durationMs > maxDuration) return interaction.editReply({ content: '❌ La durée maximum est de 28 jours.' });
const unmuteDate = Date.now() + durationMs;
const type = 'Temporary'; // Timeout est toujours temporaire
try {
await member.timeout(durationMs, `Muté par ${interaction.user.tag}: ${reason}`);
await db.query(
`INSERT INTO mutes (userId, guildId, reason, modId, modTag, timestamp, unmuteDate, type)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE reason=VALUES(reason), modId=VALUES(modId), modTag=VALUES(modTag), timestamp=VALUES(timestamp), unmuteDate=VALUES(unmuteDate), type=VALUES(type)`,
[target.id, interaction.guild.id, reason, interaction.user.id, interaction.user.tag, Date.now(), unmuteDate, type]
);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Mute', reason, type, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.mute} Utilisateur Muté`)
.setColor(colors.mute)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été muté.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '⏰ Durée', value: `\`${formatDuration(durationMs)}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Jusqu\'à', value: `<t:${Math.floor(unmuteDate / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.mute} Mute`)
.setColor(colors.mute)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été muté.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '⏰ Durée', value: `\`${formatDuration(durationMs)}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Jusqu\'à', value: `<t:${Math.floor(unmuteDate / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur mute:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};

View File

@@ -0,0 +1,97 @@
const { SlashCommandBuilder, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('mysanctions')
.setDescription('Voir tes propres sanctions sur ce serveur.'),
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const userId = interaction.user.id;
try {
const [rows] = await db.query(
'SELECT * FROM logs WHERE userId = ? AND guildId = ? ORDER BY timestamp DESC',
[userId, interaction.guild.id]
);
const sanctions = rows.filter(row =>
row.action !== 'Modification de sanction' &&
row.action !== 'Révocation de sanction' &&
row.action !== 'Modification de mute'
);
if (!sanctions.length) {
return interaction.editReply({ content: '✅ Tu n\'as aucune sanction sur ce serveur.' });
}
const pages = [];
for (let i = 0; i < sanctions.length; i += 10) {
pages.push(sanctions.slice(i, i + 10));
}
const embeds = pages.map((page, pageIndex) => {
const embed = new EmbedBuilder()
.setTitle(`📋 Mes Sanctions - Page ${pageIndex + 1}/${pages.length}`)
.setColor(colors.warning)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.setFooter({ text: `Total: ${sanctions.length} sanction(s)` })
.setTimestamp();
page.forEach((row, index) => {
const date = `<t:${Math.floor(row.timestamp / 1000)}:F>`;
const actionCapitalized = row.action.charAt(0).toUpperCase() + row.action.slice(1);
embed.addFields({
name: `#${index + 1 + pageIndex * 10} - ${actionCapitalized}`,
value: `**Modérateur:** <@${row.modId}> (${row.modTag || 'N/A'})\n` +
`**Raison:** ${row.reason || 'N/A'}\n` +
`**Date:** ${date}\n` +
`**Type:** ${row.type || 'N/A'}`,
inline: false
});
});
return embed;
});
if (pages.length === 1) {
return interaction.editReply({ embeds: [embeds[0]] });
}
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId('prev').setLabel('⬅️ Précédent').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId('next').setLabel('Suivant ➡️').setStyle(ButtonStyle.Primary)
);
let currentPage = 0;
const message = await interaction.editReply({ embeds: [embeds[currentPage]], components: [row] });
const collector = message.createMessageComponentCollector({ time: 60_000 });
collector.on('collect', async i => {
if (i.user.id !== interaction.user.id) {
return i.reply({ content: '❌ Ce n\'est pas ton menu !', flags: MessageFlags.Ephemeral });
}
if (i.customId === 'next') {
currentPage = (currentPage + 1) % embeds.length;
}
if (i.customId === 'prev') {
currentPage = (currentPage - 1 + embeds.length) % embeds.length;
}
await i.update({ embeds: [embeds[currentPage]] });
});
collector.on('end', () => {
message.edit({ components: [] }).catch(() => null);
});
} catch (err) {
console.error('Erreur mysanctions:', err);
await interaction.editReply({ content: '❌ Une erreur est survenue lors de la récupération de tes sanctions.' });
}
},
};

View File

@@ -0,0 +1,103 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('purge')
.setDescription('Supprimer les messages d\'un utilisateur dans un canal.')
.addUserOption(option =>
option.setName('target')
.setDescription('L\'utilisateur dont supprimer les messages')
.setRequired(true))
.addIntegerOption(option =>
option.setName('amount')
.setDescription('Nombre de messages à vérifier (1-100)')
.setRequired(true)
.setMinValue(1)
.setMaxValue(100))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de la purge')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction) {
const target = interaction.options.getUser('target');
const amount = interaction.options.getInteger('amount');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.channel.isTextBased()) {
return interaction.editReply({ content: '❌ Cette commande ne peut être utilisée que dans un canal texte.' });
}
try {
const messages = await interaction.channel.messages.fetch({ limit: amount });
const messagesToDelete = messages.filter(msg => msg.author.id === target.id);
const filteredMessages = messagesToDelete.filter(msg => {
const msgAge = Date.now() - msg.createdTimestamp;
return msgAge < 14 * 24 * 60 * 60 * 1000;
});
if (filteredMessages.size === 0) {
return interaction.editReply({ content: `❌ Aucun message à supprimer de ${target.tag} (les messages de plus de 14 jours ne peuvent pas être supprimés).` });
}
await interaction.channel.bulkDelete(filteredMessages, true);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Purge', reason, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.success} Messages Supprimés`)
.setColor(colors.success)
.setDescription(`**${filteredMessages.size}** message(s) de ${target.toString()} supprimé(s).`)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📊 Messages supprimés', value: `\`${filteredMessages.size}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.success} Purge`)
.setColor(colors.success)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`**${filteredMessages.size}** message(s) de ${target.toString()} supprimé(s).`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📊 Messages supprimés', value: `\`${filteredMessages.size}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur purge:', err);
if (err.code === 50034) {
await interaction.editReply({ content: '❌ Les messages de plus de 14 jours ne peuvent pas être supprimés.' });
} else {
await interaction.editReply({ content: `❌ Échec de la purge: ${err.message}` });
}
}
},
};

View File

@@ -0,0 +1,101 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('revokesanction')
.setDescription('Révoquer une sanction (Haut gradés, Co-Fonda/Fonda uniquement).')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur concerné')
.setRequired(true))
.addIntegerOption(option =>
option.setName('sanction_id')
.setDescription('L\'ID DB de la sanction à révoquer (visible dans /casier, ex: ID: 123)')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de la révocation')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
async execute(interaction) {
const target = interaction.options.getUser('user');
const sanctionId = interaction.options.getInteger('sanction_id');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator) && interaction.user.id !== interaction.guild.ownerId) {
return interaction.editReply({ content: '❌ Seuls les administrateurs et le propriétaire du serveur peuvent révoquer une sanction.' });
}
try {
const [sanctionRows] = await db.query(
'SELECT * FROM logs WHERE id = ? AND userId = ? AND guildId = ?',
[sanctionId, target.id, interaction.guild.id]
);
if (!sanctionRows.length) {
return interaction.editReply({
content: `❌ Aucune sanction trouvée avec l'ID ${sanctionId} pour ${target.tag}.\n\n💡 **Astuce:** Utilise l'ID DB visible dans \`/casier\` (ex: \`#1 (ID: 123)\` → utilise \`123\`).`
});
}
const sanction = sanctionRows[0];
if (sanction.action === 'Modification de sanction' ||
sanction.action === 'Révocation de sanction' ||
sanction.action === 'Modification de mute') {
return interaction.editReply({ content: `❌ Cette entrée est une modification, pas une sanction. Utilise l'ID d'une vraie sanction.` });
}
await db.query(
'UPDATE logs SET reason = CONCAT(reason, " [RÉVOQUÉE]") WHERE id = ?',
[sanction.id]
);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Révocation de sanction', `Sanction #${sanction.id} révoquée. Raison: ${reason}`, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Sanction Révoquée`)
.setColor(colors.success)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '👑 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Raison de révocation', value: reason, inline: false },
{ name: '🆔 ID Sanction révoquée', value: `${sanction.id}`, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setTitle(`${emojis.success} Révocation de Sanction`)
.setColor(colors.success)
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '👑 Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Raison de révocation', value: reason, inline: false },
{ name: '🆔 ID Sanction révoquée', value: `${sanction.id}`, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id} | Serveur: ${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur revokesanction:', err);
await interaction.editReply({ content: `❌ Échec de la révocation de la sanction: ${err.message}` });
}
},
};

View File

@@ -1,19 +1,21 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits } = require('discord.js');
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('unban')
.setDescription('Unban a user by their ID or tag.')
.setDescription('bannir un utilisateur par son ID ou son tag.')
.addStringOption(option =>
option.setName('user')
.setDescription('The ID or tag (Username#1234) of the user to unban')
.setDescription('L\'ID ou le tag (Username#1234) de l\'utilisateur à débannir')
.setRequired(true))
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers),
async execute(interaction) {
await interaction.deferReply();
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const input = interaction.options.getString('user').trim();
@@ -22,44 +24,57 @@ module.exports = {
const bannedUser = bannedUsers.find(b =>
b.user.id === input || b.user.tag.toLowerCase() === input.toLowerCase()
);
if (!bannedUser) return interaction.editReply('❌ This user is not banned.');
// Récupération depuis MySQL
const [rows] = await db.query('SELECT * FROM bans WHERE userId = ?', [bannedUser.user.id]);
const banRecord = rows[0];
if (!bannedUser) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas banni.' });
const reason = `Unbanned by ${interaction.user.tag}`;
const reason = `banni par ${interaction.user.tag}`;
await interaction.guild.members.unban(bannedUser.user.id, reason);
if (banRecord) await db.query('DELETE FROM bans WHERE userId = ?', [bannedUser.user.id]);
await db.query('DELETE FROM bans WHERE userId = ? AND guildId = ?', [bannedUser.user.id, interaction.guild.id]);
await interaction.editReply(`✅ Successfully unbanned ${bannedUser.user.tag}`);
const embed = new EmbedBuilder()
.setAuthor({ name: bannedUser.user.displayName, iconURL: bannedUser.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.unban} Utilisateur Débanni`)
.setColor(colors.unban)
.setThumbnail(bannedUser.user.displayAvatarURL({ dynamic: true }))
.setDescription(`${bannedUser.user.toString()} a été débanni du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${bannedUser.user.toString()}\n\`${bannedUser.user.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${bannedUser.user.id}${interaction.guild.name}` })
.setTimestamp();
const logChannel = interaction.guild.channels.cache.find(ch => ch.name === 'logs');
if (logChannel) {
const embed = new EmbedBuilder()
.setTitle('User Unbanned')
.setColor('Green')
.addFields(
{ name: 'Membre Banni', value: `<@${bannedUser.user.id}> (${bannedUser.user.tag})`, inline: true },
{ name: 'Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: 'Date', value: new Date().toLocaleString(), inline: true },
{ name: 'Guild', value: interaction.guild.name, inline: true }
)
.setTimestamp();
logChannel.send({ embeds: [embed] });
}
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.unban} Débannissement`)
.setColor(colors.unban)
.setThumbnail(bannedUser.user.displayAvatarURL({ dynamic: true }))
.setDescription(`${bannedUser.user.toString()} a été débanni du serveur.`)
.addFields(
{ name: '👤 Utilisateur', value: `${bannedUser.user.toString()}\n\`${bannedUser.user.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${bannedUser.user.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
// Log dans la DB avec guildId
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[bannedUser.user.id, bannedUser.user.tag, interaction.user.id, interaction.user.tag, 'unban', reason, null, interaction.guild.id, Date.now()]
[bannedUser.user.id, bannedUser.user.tag, interaction.user.id, interaction.user.tag, 'bannissement', reason, null, interaction.guild.id, Date.now()]
);
} catch (err) {
console.error(err);
await interaction.editReply('❌ Failed to unban the user.');
console.error('Erreur unban:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
};
},
};

View File

@@ -0,0 +1,86 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('unmute')
.setDescription('Démuter un membre du serveur.')
.addUserOption(option =>
option.setName('user')
.setDescription('Le membre à démuter')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison du démute')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('user');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (!member.communicationDisabledUntilTimestamp || member.communicationDisabledUntilTimestamp < Date.now()) {
return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas muté.' });
}
try {
await member.timeout(null, `Démuté par ${interaction.user.tag}: ${reason}`);
await db.query('DELETE FROM mutes WHERE userId = ? AND guildId = ?', [target.id, interaction.guild.id]);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Unmute', reason, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.unmute} Utilisateur Démuté`)
.setColor(colors.unmute)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été démuté.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.unmute} Unmute`)
.setColor(colors.unmute)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a été démuté.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur unmute:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};

View File

@@ -0,0 +1,78 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('untimeout')
.setDescription('Retirer le timeout d\'un membre.')
.addUserOption(option =>
option.setName('target')
.setDescription('Le membre à dé-timeout')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de la levée du timeout')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('target');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (!member.communicationDisabledUntilTimestamp || member.communicationDisabledUntilTimestamp < Date.now()) {
return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas en timeout.' });
}
try {
await member.timeout(null, `Timeout retiré par ${interaction.user.tag}: ${reason}`);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Timeout retiré', reason, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Timeout Retiré`)
.setColor(colors.success)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '✅ Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
const logEmbed = new EmbedBuilder()
.setTitle(`${emojis.success} Timeout Retiré`)
.setColor(colors.success)
.addFields(
{ name: '👤 Utilisateur', value: `<@${target.id}> (${target.tag})`, inline: true },
{ name: '✅ Modérateur', value: `<@${interaction.user.id}> (${interaction.user.tag})`, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id} | Serveur: ${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur untimeout:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};

101
commands/moderation/warn.js Normal file
View File

@@ -0,0 +1,101 @@
const { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { sendLog } = require('../../utils/helpers');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'moderation',
data: new SlashCommandBuilder()
.setName('warn')
.setDescription('Avertir un membre.')
.addUserOption(option =>
option.setName('target')
.setDescription('Le membre à avertir')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de l\'avertissement')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers),
async execute(interaction) {
const target = interaction.options.getUser('target');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
const member = await interaction.guild.members.fetch(target.id).catch(() => null);
if (!member) return interaction.editReply({ content: '❌ Cet utilisateur n\'est pas sur le serveur.' });
if (member.id === interaction.user.id) return interaction.editReply({ content: '❌ Tu ne peux pas t\'avertir toi-même.' });
try {
const [warnRows] = await db.query(
'SELECT COUNT(*) as count FROM logs WHERE userId = ? AND guildId = ? AND action = ?',
[target.id, interaction.guild.id, 'Avertissement']
);
const warnCount = warnRows[0].count + 1;
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[target.id, target.tag, interaction.user.id, interaction.user.tag, 'Avertissement', reason, null, interaction.guild.id, Date.now()]
);
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.warn} Utilisateur Averti`)
.setColor(colors.warn)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a reçu un avertissement.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '🔢 Nombre d\'avertissements', value: `\`${warnCount}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
try {
const dmEmbed = new EmbedBuilder()
.setTitle(`${emojis.warn} Tu as reçu un avertissement`)
.setColor(colors.warn)
.setDescription(`Tu as été averti sur **${interaction.guild.name}**`)
.addFields(
{ name: '📝 Raison', value: reason, inline: false },
{ name: '🔢 Nombre d\'avertissements', value: `${warnCount}`, inline: true }
)
.setFooter({ text: interaction.guild.name, iconURL: interaction.guild.iconURL({ dynamic: true }) })
.setTimestamp();
await target.send({ embeds: [dmEmbed] });
} catch { /* DMs fermés */ }
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.warn} Avertissement`)
.setColor(colors.warn)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.setDescription(`${target.toString()} a reçu un avertissement.`)
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '🔢 Nombre d\'avertissements', value: `\`${warnCount}\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
} catch (err) {
console.error('Erreur warn:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};

673
commands/ticket/ticket.js Normal file
View File

@@ -0,0 +1,673 @@
const {
SlashCommandBuilder,
PermissionFlagsBits,
EmbedBuilder,
ChannelType,
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
MessageFlags
} = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors } = require('../../utils/constants');
const fs = require('fs');
const path = require('path');
// Questions pré-définies pour les candidatures
const CANDIDATURE_QUESTIONS = [
'Pourquoi souhaites-tu rejoindre le staff ?',
'Quelles sont tes disponibilités ?',
'As-tu déjà de l\'expérience en modération ?',
'Comment gérerais-tu un conflit entre membres ?',
'Qu\'est-ce qui te motive à aider la communauté ?'
];
// Mapping des types de tickets vers des préfixes d'ID
const TICKET_TYPE_PREFIXES = {
'Support': 'SUPP',
'Plainte': 'PLNT',
'Plainte Staff': 'PLST',
'Candidature': 'CAND',
'Problème Technique': 'TECH'
};
function generateTicketId(type = 'Support') {
const prefix = TICKET_TYPE_PREFIXES[type] || 'TICK';
const timestamp = Date.now().toString(36).toUpperCase();
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
return `${prefix}-${timestamp}-${random}`;
}
async function getCandidatureResponses(ticketId) {
const [responses] = await db.query(
'SELECT * FROM candidature_responses WHERE ticketId = ? ORDER BY questionNumber ASC',
[ticketId]
);
return responses;
}
function generateTranscriptHTML(ticket, messages, candidatureResponses, useRelativeCss = false) {
const date = new Date(ticket.createdAt);
const dateStr = date.toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' });
const closedDateStr = ticket.closedAt ? new Date(ticket.closedAt).toLocaleDateString('fr-FR', { year: 'numeric', month: '2-digit', day: '2-digit' }) : null;
const typeEmojis = {
'Support': '💬',
'Plainte': '📢',
'Plainte Staff': '⚠️',
'Candidature': '📝',
'Problème Technique': '🔧'
};
const statusColors = {
'Ouvert': '#10b981',
'Fermé': '#ef4444',
'En attente': '#f59e0b'
};
let messagesHTML = '';
if (messages.length === 0) {
messagesHTML = '<p class="no-messages">Aucun message enregistré.</p>';
} else {
messagesHTML = messages.map(msg => {
const msgDate = new Date(msg.timestamp).toLocaleString('fr-FR');
const attachmentsHTML = msg.attachments
? `<div class="attachments">📎 Pièces jointes: ${msg.attachments.split(', ').map(url => `<a href="${url}" target="_blank">${url}</a>`).join(', ')}</div>`
: '';
return `
<div class="message">
<div class="message-header">
<span class="message-author">${escapeHtml(msg.userTag)}</span>
<span class="message-date">${msgDate}</span>
</div>
<div class="message-content">${escapeHtml(msg.content || '*Message vide*').replace(/\n/g, '<br>')}</div>
${attachmentsHTML}
</div>
`;
}).join('');
}
let candidatureHTML = '';
if (candidatureResponses && candidatureResponses.length > 0) {
candidatureHTML = `
<section class="candidature-section">
<h2>📝 Réponses aux Questions de Candidature</h2>
${candidatureResponses.map(response => `
<div class="question-item">
<div class="question-label">Question ${response.questionNumber}</div>
<div class="question-text">${escapeHtml(response.question)}</div>
<div class="response">${escapeHtml(response.response || '*Pas de réponse*').replace(/\n/g, '<br>')}</div>
</div>
`).join('')}
</section>
`;
}
// Déterminer l'URL du CSS
let cssUrl;
if (useRelativeCss) {
cssUrl = '../../transcript.css';
} else {
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
cssUrl = `https://${webUrl}/static/transcript.css`;
}
return `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Transcription - ${ticket.ticketId}</title>
<link rel="stylesheet" href="${cssUrl}">
</head>
<body>
<div class="container">
<div class="header">
<h1>${typeEmojis[ticket.type] || '🎫'} Transcription du Ticket</h1>
<div class="ticket-id">${ticket.ticketId}</div>
</div>
<div class="info-section">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Type</div>
<div class="info-value">${typeEmojis[ticket.type] || '🎫'} ${ticket.type}</div>
</div>
<div class="info-item">
<div class="info-label">Créé par</div>
<div class="info-value">${escapeHtml(ticket.userTag)}</div>
</div>
<div class="info-item">
<div class="info-label">Créé le</div>
<div class="info-value">${dateStr}</div>
</div>
<div class="info-item">
<div class="info-label">Statut</div>
<div class="info-value">
<span class="status-badge" style="background: ${statusColors[ticket.status]}; color: white;">
${ticket.status}
</span>
</div>
</div>
${closedDateStr ? `
<div class="info-item">
<div class="info-label">Fermé le</div>
<div class="info-value">${closedDateStr}</div>
</div>
` : ''}
</div>
</div>
<div class="messages-section">
<h2>💬 Messages</h2>
${messagesHTML}
</div>
${candidatureHTML}
<div class="footer">
<p>Transcription générée le ${new Date().toLocaleString('fr-FR')}</p>
<p>Femboy Croissant Bot • • Système de Tickets</p>
</div>
</div>
</body>
</html>`;
}
function escapeHtml(text) {
if (!text) return '';
const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
return text.replace(/[&<>"']/g, m => map[m]);
}
const commandModule = {
category: 'ticket',
data: new SlashCommandBuilder()
.setName('ticket')
.setDescription('Gérer les tickets')
.addSubcommand(subcommand => subcommand.setName('setup').setDescription('Configurer (Admin)').addChannelOption(o => o.setName('channel').setDescription('Salon')))
.addSubcommand(subcommand => subcommand.setName('close').setDescription('Fermer un ticket').addStringOption(o => o.setName('reason').setDescription('Raison')))
.addSubcommand(subcommand => subcommand.setName('transcript').setDescription('Générer transcript'))
.addSubcommand(subcommand => subcommand.setName('reopen').setDescription('Rouvrir un ticket'))
.addSubcommand(subcommand => subcommand.setName('add').setDescription('Ajouter utilisateur').addUserOption(o => o.setName('user').setDescription('Utilisateur').setRequired(true)))
.addSubcommand(subcommand => subcommand.setName('claim').setDescription('Réclamer un ticket')),
async execute(interaction) {
const subcommand = interaction.options.getSubcommand();
if (subcommand === 'setup') await handleSetup(interaction);
else if (subcommand === 'close') await handleClose(interaction);
else if (subcommand === 'transcript') await handleTranscript(interaction);
else if (subcommand === 'reopen') await handleReopen(interaction);
else if (subcommand === 'add') await handleAdd(interaction);
else if (subcommand === 'claim') await handleClaim(interaction);
},
};
async function handleCreate(interaction, ticketType) {
// Si l'interaction est un ModalSubmit, on a déjà répondu dans interactionCreate ? Non, on va defer ici.
// Attention : handleCreate est appelé après le ModalSubmit.
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const type = ticketType || 'Support';
// Récupérer le sujet s'il vient d'un modal
let subject = 'Aucun sujet précisé';
if (interaction.isModalSubmit() && interaction.fields) {
try {
subject = interaction.fields.getTextInputValue('ticket_subject');
} catch (e) { /* Pas de champ subject (ex: commande slash sans modal) */ }
}
try {
const [existingTickets] = await db.query(
'SELECT * FROM tickets WHERE userId = ? AND guildId = ? AND status = ?',
[interaction.user.id, interaction.guild.id, 'Ouvert']
);
if (existingTickets.length > 0) {
const existingTicket = existingTickets[0];
const channel = interaction.guild.channels.cache.get(existingTicket.channelId);
return interaction.editReply({
content: `❌ Tu as déjà un ticket ouvert : ${channel ? channel.toString() : `ID: ${existingTicket.ticketId}`}`
});
}
let ticketCategory = interaction.guild.channels.cache.find(
ch => ch.type === ChannelType.GuildCategory && ch.name.toLowerCase().includes('ticket')
);
if (!ticketCategory) {
try {
ticketCategory = await interaction.guild.channels.create({
name: '🎫 Tickets',
type: ChannelType.GuildCategory,
permissionOverwrites: [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels, PermissionFlagsBits.SendMessages] },
],
});
} catch (err) { ticketCategory = null; }
}
const ticketId = generateTicketId(type);
const cleanUsername = interaction.user.username.toLowerCase().replace(/[^a-z0-9-]/g, '').substring(0, 20);
const typeName = type.toLowerCase().replace(/\s+/g, '-').substring(0, 20);
const channelName = `${typeName}-${cleanUsername}`.substring(0, 100);
let channel;
// Permissions initiales : Le user voit le salon
const userPerms = [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.AttachFiles] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages, PermissionFlagsBits.ManageMessages, PermissionFlagsBits.EmbedLinks, PermissionFlagsBits.AttachFiles] },
];
try {
channel = await interaction.guild.channels.create({
name: channelName,
type: ChannelType.GuildText,
parent: ticketCategory?.id,
permissionOverwrites: userPerms,
});
} catch (createErr) {
channel = await interaction.guild.channels.create({
name: channelName,
type: ChannelType.GuildText,
permissionOverwrites: userPerms,
});
}
const modRoles = interaction.guild.roles.cache.filter(role => role.permissions.has(PermissionFlagsBits.ManageMessages) && !role.managed);
for (const role of modRoles.values()) {
await channel.permissionOverwrites.edit(role.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true });
}
await db.query(
`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, type, 'Ouvert', Date.now()]
);
const typeEmojis = { 'Support': '💬', 'Plainte': '📢', 'Plainte Staff': '⚠️', 'Candidature': '📝', 'Problème Technique': '🔧' };
const welcomeEmbed = new EmbedBuilder()
.setColor(colors.success)
.setTitle(`${typeEmojis[type] || '🎫'} Ticket Ouvert : ${type}`)
.setDescription(`👋 Bonjour ${interaction.user.toString()} !\n\nUn membre de l'équipe va prendre en charge votre demande.`)
.addFields(
{ name: '📋 Sujet de la demande', value: `\`\`\`${subject}\`\`\``, inline: false },
{ name: '🆔 ID', value: `\`${ticketId}\``, inline: true },
{ name: '📅 Date', value: `<t:${Math.floor(Date.now() / 1000)}:R>`, inline: true }
)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.setFooter({ text: 'France Femboy Bot • Support', iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Réclamer').setStyle(ButtonStyle.Secondary).setEmoji('👋')
);
await channel.send({ content: `${interaction.user} <@&${modRoles.first()?.id || interaction.user.id}>`, embeds: [welcomeEmbed], components: [row] });
await interaction.editReply({ content: `✅ Ticket créé avec succès ! ${channel.toString()}` });
} catch (err) {
console.error('Erreur creation ticket:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
// 1. Point d'entrée pour la fermeture (Bouton ou Slash)
async function handleClose(interaction) {
// 1. Vérification Permissions (User ne doit pas pouvoir fermer)
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
return interaction.reply({ content: '❌ Seuls les membres du staff peuvent fermer un ticket.', flags: MessageFlags.Ephemeral });
}
if (interaction.isButton()) {
const modal = new ModalBuilder().setCustomId('ticket_close_reason_modal').setTitle('Fermeture du Ticket');
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);
return;
}
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
let reason = interaction.options?.getString('reason') || 'Aucune raison fournie';
await processTicketClose(interaction, reason);
}
async function handleCloseModal(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
// Check permission again for security
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) {
return interaction.editReply({ content: '❌ Action non autorisée.' });
}
const reason = interaction.fields.getTextInputValue('reason') || 'Aucune raison fournie';
await processTicketClose(interaction, reason);
}
// 3. Logique commune de fermeture
async function processTicketClose(interaction, reason) {
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (!tickets.length) return interaction.editReply({ content: '❌ Erreur ticket.' });
const ticket = tickets[0];
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Fermé', Date.now(), interaction.user.id, ticket.ticketId]);
// --- GENERATION TRANSCRIPT AUTO ---
try {
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
// Structure dossiers
const date = new Date(ticket.createdAt);
const dateFolder = date.toISOString().split('T')[0];
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
const dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
// Nom de fichier unique avec ID du ticket
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
const filePath = path.join(dateDir, fileName);
fs.writeFileSync(filePath, htmlContent, 'utf8');
// Chemin relatif pour la DB (ex: transcripts/support/2025-01-01/file.html)
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
} catch (transcriptErr) {
console.error('Erreur génération transcript auto:', transcriptErr);
}
// ----------------------------------
const closeEmbed = new EmbedBuilder()
.setTitle('🔒 Ticket Fermé')
.setColor(colors.error)
.setDescription(`Ticket clôturé par ${interaction.user}.`)
.addFields(
{ name: '📝 Raison', value: `\`\`\`${reason}\`\`\``, inline: false },
{ name: '👮 Géré par', value: ticket.claimedBy ? `<@${ticket.claimedBy}>` : '> *Personne*', inline: true }
);
// BOUTONS DE GESTION APRES FERMETURE
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_reopen_${ticket.ticketId}`).setLabel('Rouvrir').setStyle(ButtonStyle.Success).setEmoji('🔓'),
new ButtonBuilder().setCustomId(`ticket_delete_${ticket.ticketId}`).setLabel('Supprimer').setStyle(ButtonStyle.Danger).setEmoji('🗑️')
);
const closeMsg = await interaction.channel.send({ embeds: [closeEmbed], components: [row] });
if (!interaction.channel.name.startsWith('🔒-')) {
await interaction.channel.setName(`🔒-${interaction.channel.name}`).catch(() => {});
}
// VISIBILITE : On retire la vue au membre
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: false });
// Désactivation des ANCIENS boutons (Sauf le nouveau message de fermeture)
try {
const messages = await interaction.channel.messages.fetch({ limit: 20 });
const oldMessages = messages.filter(m => m.author.id === interaction.client.user.id && m.components.length > 0 && m.id !== closeMsg.id);
for (const oldMsg of oldMessages.values()) {
const newRows = oldMsg.components.map(row => {
const newRow = new ActionRowBuilder();
row.components.forEach(c => newRow.addComponents(ButtonBuilder.from(c).setDisabled(true)));
return newRow;
});
await oldMsg.edit({ components: newRows });
}
} catch (e) {}
await interaction.editReply({ content: '✅ Ticket fermé.' });
} catch (err) { await interaction.editReply({ content: `❌ Erreur: ${err.message}` }); }
}
async function handleTranscript(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (tickets.length === 0) return interaction.editReply({ content: '❌ Ce canal n\'est pas un ticket.' });
const ticket = tickets[0];
const [messages] = await db.query('SELECT * FROM ticket_messages WHERE ticketId = ? ORDER BY timestamp', [ticket.ticketId]);
// Structure dossiers
const date = new Date(ticket.createdAt);
const dateFolder = date.toISOString().split('T')[0];
const typeFolder = ticket.type.toLowerCase().replace(/\s+/g, '-');
const cleanUsername = ticket.userTag.replace(/[^a-zA-Z0-9-_]/g, '-').toLowerCase();
const transcriptsBaseDir = path.join(process.cwd(), 'transcripts');
const dateDir = path.join(transcriptsBaseDir, typeFolder, dateFolder);
if (!fs.existsSync(dateDir)) fs.mkdirSync(dateDir, { recursive: true });
// Génération
const candidatureResponses = ticket.type === 'Candidature' ? await getCandidatureResponses(ticket.ticketId) : null;
const htmlContent = generateTranscriptHTML(ticket, messages, candidatureResponses, true);
// Nom de fichier unique avec ID du ticket
const fileName = `${dateFolder}-${cleanUsername}-${ticket.ticketId}.html`;
const filePath = path.join(dateDir, fileName);
fs.writeFileSync(filePath, htmlContent, 'utf8');
const webPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
const webUrl = process.env.TRANSCRIPT_WEB_URL || 'transcript.syxpi.fr';
// Mise à jour DB
const dbPath = `transcripts/${typeFolder}/${dateFolder}/${fileName}`;
await db.query('UPDATE tickets SET transcriptPath = ? WHERE ticketId = ?', [dbPath, ticket.ticketId]);
await interaction.editReply({
content: `✅ **Transcription générée !**\n\n📄 **Fichier local:** \`${fileName}\`\n🌐 **Lien Web:** https://${webUrl}/${webPath}`,
files: [{ attachment: filePath, name: fileName }]
});
} catch (err) {
console.error(err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleReopen(interaction) {
if (interaction.isButton()) {
const modal = new ModalBuilder().setCustomId('ticket_reopen_modal').setTitle('Réouverture du Ticket');
const input = new TextInputBuilder().setCustomId('reason').setLabel('Raison').setStyle(TextInputStyle.Paragraph).setRequired(true);
modal.addComponents(new ActionRowBuilder().addComponents(input));
await interaction.showModal(modal);
return;
}
await handleReopenModal(interaction, "Commande Slash");
}
async function handleReopenModal(interaction, slashReason = null) {
if (!slashReason) await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const reason = slashReason || interaction.fields.getTextInputValue('reason');
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
const ticket = tickets[0];
await db.query('UPDATE tickets SET status = ?, closedAt = NULL, closedBy = NULL WHERE ticketId = ?', ['Ouvert', ticket.ticketId]);
// Restituer la vue
await interaction.channel.permissionOverwrites.edit(ticket.userId, { ViewChannel: true, SendMessages: true });
if (interaction.channel.name.startsWith('🔒-')) {
await interaction.channel.setName(interaction.channel.name.replace('🔒-', ''));
}
const embed = new EmbedBuilder()
.setTitle('🔓 Ticket Réouvert')
.setColor(colors.success)
.setDescription(`Le ticket a été rouvert par ${interaction.user}.`)
.addFields({ name: '📝 Raison', value: `\`\`\`${reason}\`\`\`` });
// IMPORTANT : On remet le bouton FERMER
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticket.ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒')
);
await interaction.channel.send({ embeds: [embed], components: [row] });
// Supprimer le message de fermeture précédent
try {
const messages = await interaction.channel.messages.fetch({ limit: 10 });
const closeMsg = messages.find(m => m.embeds[0]?.title === '🔒 Ticket Fermé');
if (closeMsg) await closeMsg.delete();
} catch (e) {}
const msg = { content: '✅ Ticket rouvert.' };
if (interaction.replied || interaction.deferred) await interaction.editReply(msg);
else await interaction.reply({ ...msg, flags: MessageFlags.Ephemeral });
} catch (err) { console.error(err); }
}
async function handleAdd(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const user = interaction.options.getUser('user');
await interaction.channel.permissionOverwrites.edit(user.id, { ViewChannel: true, SendMessages: true, ReadMessageHistory: true });
const embed = new EmbedBuilder()
.setTitle('👤 Utilisateur Ajouté')
.setColor(colors.info)
.setDescription(`${user.toString()} a été ajouté au ticket par ${interaction.user.toString()}.`)
.setTimestamp();
await interaction.channel.send({ embeds: [embed] });
await interaction.editReply({ content: `${user.tag} ajouté.` });
} catch (err) {
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleClaim(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.member.permissions.has(PermissionFlagsBits.ManageMessages)) return interaction.editReply({ content: '❌ Staff uniquement.' });
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (!tickets.length) return interaction.editReply({ content: '❌ Pas un ticket.' });
const ticket = tickets[0];
if (ticket.claimedBy === interaction.user.id) {
await db.query('UPDATE tickets SET claimedBy = NULL, claimedByTag = NULL WHERE ticketId = ?', [ticket.ticketId]);
await interaction.channel.send({ embeds: [new EmbedBuilder().setColor(colors.info).setDescription(`🛑 **Ticket libéré** par ${interaction.user.toString()}.`)] });
return interaction.editReply({ content: '✅ Ticket libéré.' });
}
if (ticket.claimedBy) return interaction.editReply({ content: `❌ Déjà réclamé par quelqu'un d'autre.` });
await db.query('UPDATE tickets SET claimedBy = ?, claimedByTag = ?, claimedAt = ? WHERE ticketId = ?', [interaction.user.id, interaction.user.tag, Date.now(), ticket.ticketId]);
const embed = new EmbedBuilder()
.setColor(colors.success)
.setTitle('👋 Ticket Pris en Charge')
.setDescription(`Ce ticket est désormais géré par ${interaction.user.toString()}.`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.setTimestamp();
await interaction.channel.send({ embeds: [embed] });
await interaction.editReply({ content: '✅ Ticket réclamé.' });
} catch (err) {
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
}
async function handleDelete(interaction) {
// Suppression directe sans modal
await handleDeleteModal(interaction, "Suppression demandée par l'utilisateur");
}
async function handleDeleteModal(interaction, slashReason = null) {
// Si l'interaction n'a pas été différée ou répondue, on le fait
if (!interaction.deferred && !interaction.replied) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
}
try {
const [tickets] = await db.query('SELECT * FROM tickets WHERE channelId = ?', [interaction.channel.id]);
if (tickets.length) {
await db.query('UPDATE tickets SET status = ?, closedAt = ?, closedBy = ? WHERE ticketId = ?', ['Supprimé', Date.now(), interaction.user.id, tickets[0].ticketId]);
}
await interaction.channel.delete();
} catch (err) { console.error(err); }
}
// Fonction spécifique pour le submit du modal
async function handleCandidatureModalSubmit(interaction) {
const ticketId = generateTicketId('Candidature');
const channelName = `candidature-${interaction.user.username.replace(/[^a-z0-9]/gi, '').substring(0,15)}`;
try {
let category = interaction.guild.channels.cache.find(c => c.type === ChannelType.GuildCategory && c.name.toLowerCase().includes('ticket'));
const channel = await interaction.guild.channels.create({
name: channelName, type: ChannelType.GuildText, parent: category?.id,
permissionOverwrites: [
{ id: interaction.guild.id, deny: [PermissionFlagsBits.ViewChannel] },
{ id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
{ id: interaction.client.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.ManageChannels] }
]
});
await db.query(`INSERT INTO tickets (ticketId, channelId, userId, userTag, guildId, type, status, createdAt) VALUES (?, ?, ?, ?, ?, 'Candidature', 'Ouvert', ?)`, [ticketId, channel.id, interaction.user.id, interaction.user.tag, interaction.guild.id, Date.now()]);
const fields = [];
for (let i = 0; i < Math.min(CANDIDATURE_QUESTIONS.length, 5); i++) {
const val = interaction.fields.getTextInputValue(`cand_q${i+1}`);
// Correction: Utilisation de Date.now() au lieu de new Date() pour le timestamp
await db.query('INSERT INTO candidature_responses (ticketId, questionNumber, question, response, timestamp) VALUES (?, ?, ?, ?, ?)', [ticketId, i+1, CANDIDATURE_QUESTIONS[i], val, Date.now()]);
// Utiliser la question complète comme nom de champ
fields.push({ name: CANDIDATURE_QUESTIONS[i], value: val.substring(0, 1024) });
}
const embed = new EmbedBuilder()
.setTitle('📝 Nouvelle Candidature Staff')
.setColor(colors.warning)
.setDescription(`Candidature soumise par ${interaction.user.toString()} (\`${interaction.user.tag}\`)`)
.setThumbnail(interaction.user.displayAvatarURL({ dynamic: true }))
.addFields(fields)
.addFields(
{ name: '━━━━━━━━━━━━━━━━', value: ' **Informations**' },
{ name: '🆔 ID Ticket', value: `\`${ticketId}\``, inline: true },
{ name: '📅 Soumis le', value: `<t:${Math.floor(Date.now() / 1000)}:F>`, inline: true }
)
.setFooter({ text: 'France Femboy Bot • Recrutement', iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder().setCustomId(`ticket_close_${ticketId}`).setLabel('Fermer').setStyle(ButtonStyle.Danger).setEmoji('🔒'),
new ButtonBuilder().setCustomId(`ticket_claim_${ticketId}`).setLabel('Traiter').setStyle(ButtonStyle.Success).setEmoji('✅')
);
await channel.send({ content: '@here Nouvelle candidature', embeds: [embed], components: [row] });
await interaction.reply({ content: `✅ Candidature envoyée : ${channel.toString()}`, flags: MessageFlags.Ephemeral });
} catch (e) { console.error(e); }
}
// Exports
commandModule.handleCreate = handleCreate;
commandModule.handleClose = handleClose;
commandModule.handleCloseModal = handleCloseModal;
commandModule.handleReopen = handleReopen;
commandModule.handleReopenModal = handleReopenModal;
commandModule.handleDelete = handleDelete;
commandModule.handleDeleteModal = handleDeleteModal;
commandModule.handleAdd = handleAdd;
commandModule.handleClaim = handleClaim;
commandModule.handleTranscript = handleTranscript;
commandModule.handleCandidatureModalSubmit = handleCandidatureModalSubmit;
module.exports = commandModule;
module.exports.CANDIDATURE_QUESTIONS = CANDIDATURE_QUESTIONS;
module.exports.generateTicketId = generateTicketId;

View File

@@ -0,0 +1,31 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
module.exports = {
category: 'utility',
data: new SlashCommandBuilder()
.setName('avatar')
.setDescription('Affiche l\'avatar d\'un utilisateur.')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur dont tu veux voir l\'avatar')
.setRequired(false)),
async execute(interaction) {
const target = interaction.options.getUser('user') || interaction.user;
const { colors, emojis } = require('../../utils/constants');
const embed = new EmbedBuilder()
.setAuthor({
name: `${target.displayName}`,
iconURL: target.displayAvatarURL({ dynamic: true })
})
.setTitle(`${emojis.avatar} Avatar`)
.setColor(colors.utility)
.setImage(target.displayAvatarURL({ dynamic: true, size: 4096 }))
.setDescription(`[Télécharger l'avatar](${target.displayAvatarURL({ dynamic: true, size: 4096 })})`)
.setFooter({ text: `Demandé par ${interaction.user.displayName}${interaction.guild.name}`, iconURL: interaction.user.displayAvatarURL() })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
},
};

View File

@@ -4,20 +4,30 @@ module.exports = {
category: 'utility',
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with API latency and ICMP latency!'),
.setDescription('Affiche la latence du bot.'),
async execute(interaction) {
// --- API latency Discord ---
const apiLatency = Math.round(interaction.client.ws.ping);
await interaction.deferReply();
const sent = await interaction.fetchReply();
// --- Reply final ---
const apiLatency = Math.round(interaction.client.ws.ping);
const roundTripLatency = sent.createdTimestamp - interaction.createdTimestamp;
const { colors, emojis } = require('../../utils/constants');
const embed = new EmbedBuilder()
.setColor('#00FFFF')
.setTitle('🏓 Ping Status')
.setAuthor({
name: `${interaction.client.user.username}`,
iconURL: interaction.client.user.displayAvatarURL()
})
.setTitle(`${emojis.ping} Statut de la Latence`)
.setColor(colors.utility)
.addFields(
{ name: '🌐 API Latency', value: `${apiLatency}ms`, inline: true }
{ name: '🌐 Latence API', value: `\`${apiLatency}ms\``, inline: true },
{ name: '⏱️ Latence Round-Trip', value: `\`${roundTripLatency}ms\``, inline: true }
)
.setFooter({ text: `${interaction.guild.name}${interaction.client.user.username}`, iconURL: interaction.client.user.displayAvatarURL() })
.setTimestamp();
await interaction.reply({ embeds: [embed] });
await interaction.editReply({ embeds: [embed] });
},
};
};

79
commands/xp/bumptime.js Normal file
View File

@@ -0,0 +1,79 @@
const { SlashCommandBuilder, EmbedBuilder, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { colors, emojis } = require('../../utils/constants.js');
module.exports = {
category: 'xp',
data: new SlashCommandBuilder()
.setName('bumptime')
.setDescription('Vérifier le dernier bump et le temps restant avant le prochain'),
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const [bumps] = await db.query(
'SELECT * FROM bumps WHERE guildId = ? ORDER BY bumpTime DESC LIMIT 1',
[interaction.guild.id]
);
if (bumps.length === 0) {
return interaction.editReply({
content: '❌ Aucun bump enregistré pour ce serveur.'
});
}
const lastBump = bumps[0];
const bumpTime = parseInt(lastBump.bumpTime, 10);
const now = Date.now();
const timeSinceBump = now - bumpTime;
const twoHours = 2 * 60 * 60 * 1000;
const timeUntilNextBump = twoHours - timeSinceBump;
const user = await interaction.client.users.fetch(lastBump.userId).catch(() => null);
const userTag = user ? user.tag : `ID: ${lastBump.userId}`;
const embed = new EmbedBuilder()
.setAuthor({
name: interaction.guild.name,
iconURL: interaction.guild.iconURL({ dynamic: true }) || undefined
})
.setTimestamp();
if (timeUntilNextBump > 0) {
const hours = Math.floor(timeUntilNextBump / (60 * 60 * 1000));
const minutes = Math.floor((timeUntilNextBump % (60 * 60 * 1000)) / (60 * 1000));
const seconds = Math.floor((timeUntilNextBump % (60 * 1000)) / 1000);
embed.setTitle('⏳ Bump en Cooldown')
.setColor(colors.warning)
.setDescription(`Le dernier bump a été effectué il y a moins de 2 heures.`)
.addFields(
{ name: '👤 Dernier bump par', value: `\`${userTag}\``, inline: true },
{ name: '⏰ Il y a', value: `\`${Math.floor(timeSinceBump / (60 * 60 * 1000))}h ${Math.floor((timeSinceBump % (60 * 60 * 1000)) / (60 * 1000))}m\``, inline: true },
{ name: '🕐 Temps restant', value: `\`${hours}h ${minutes}m ${seconds}s\``, inline: true },
{ name: '📅 Prochain bump disponible', value: `<t:${Math.floor((bumpTime + twoHours) / 1000)}:R>`, inline: false }
)
.setFooter({ text: 'Utilise /bump (Disboard) pour bump le serveur' });
} else {
embed.setTitle(`${emojis.success} Bump Disponible`)
.setColor(colors.success)
.setDescription(`**Le serveur peut être bumpé maintenant !**`)
.addFields(
{ name: '👤 Dernier bump par', value: `\`${userTag}\``, inline: true },
{ name: '⏰ Il y a', value: `\`${Math.floor(timeSinceBump / (60 * 60 * 1000))}h ${Math.floor((timeSinceBump % (60 * 60 * 1000)) / (60 * 1000))}m\``, inline: true },
{ name: '✅ Statut', value: '**Disponible**', inline: true },
{ name: '💡 Astuce', value: 'Utilise `/bump` (Disboard) pour bump le serveur !', inline: false }
)
.setFooter({ text: 'Utilise /bump (Disboard) pour bump le serveur' });
}
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur bumptime:', err);
await interaction.editReply({
content: '❌ Erreur lors de la vérification du bump.'
});
}
},
};

View File

@@ -0,0 +1,65 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getLeaderboard, getXPProgress } = require('../../functions/xp/xp.js');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'xp',
data: new SlashCommandBuilder()
.setName('leaderboard')
.setDescription('Affiche le classement des niveaux')
.addIntegerOption(option =>
option.setName('limit')
.setDescription('Nombre d\'utilisateurs à afficher (par défaut: 10)')
.setRequired(false)
.setMinValue(1)
.setMaxValue(25)),
async execute(interaction) {
await interaction.deferReply();
const limit = interaction.options.getInteger('limit') || 10;
const leaderboard = await getLeaderboard(interaction.guild.id, limit);
if (leaderboard.length === 0) {
return interaction.editReply({
content: '❌ Aucun utilisateur trouvé dans le classement.'
});
}
const leaderboardData = [];
for (const user of leaderboard) {
try {
const member = await interaction.guild.members.fetch(user.userId).catch(() => null);
if (member) {
leaderboardData.push({
user: member.user,
xp: user.xp,
level: user.level
});
}
} catch (err) { /* Ignorer */ }
}
const medals = ['🥇', '🥈', '🥉'];
const description = leaderboardData.map((data, index) => {
const medal = medals[index] || `\`${index + 1}.\``;
const progress = getXPProgress(data.xp, data.level);
return `${medal} **${data.user.displayName}** • ${emojis.level} Niveau \`${data.level}\`${emojis.xp} \`${data.xp.toLocaleString()} XP\`\`${progress.percentage}%\``;
}).join('\n');
const embed = new EmbedBuilder()
.setAuthor({
name: interaction.guild.name,
iconURL: interaction.guild.iconURL({ dynamic: true }) || undefined
})
.setTitle(`${emojis.leaderboard} Classement des Niveaux`)
.setColor(colors.xp)
.setDescription(description || 'Aucun utilisateur dans le classement')
.setFooter({
text: `Top ${limit}${interaction.guild.name}`,
iconURL: interaction.guild.iconURL({ dynamic: true }) || undefined
})
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
},
};

97
commands/xp/level.js Normal file
View File

@@ -0,0 +1,97 @@
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const { getUserXP, getXPProgress, getXPForNextLevel, getUserRank } = require('../../functions/xp/xp.js');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'xp',
data: new SlashCommandBuilder()
.setName('level')
.setDescription('Affiche ton niveau et ton XP')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur dont tu veux voir le niveau')
.setRequired(false)),
async execute(interaction) {
await interaction.deferReply();
const target = interaction.options.getUser('user') || interaction.user;
const userXP = await getUserXP(target.id, interaction.guild.id);
if (!userXP) {
return interaction.editReply({
content: '❌ Erreur lors de la récupération des données XP.'
});
}
const progress = getXPProgress(userXP.xp, userXP.level);
const xpForNextLevel = getXPForNextLevel(userXP.level);
const rank = await getUserRank(target.id, interaction.guild.id);
const barLength = 20;
let filled = 0;
let empty = barLength;
if (progress.needed > 0 && progress.current >= 0) {
const percentage = Math.min(100, Math.max(0, (progress.current / progress.needed) * 100));
filled = Math.max(0, Math.min(barLength, Math.floor((percentage / 100) * barLength)));
empty = Math.max(0, barLength - filled);
}
const progressBar = '█'.repeat(filled) + '░'.repeat(empty);
// Formatage du temps vocal
const totalMinutes = userXP.totalVoiceTime || 0;
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const voiceTimeStr = `${hours}h ${minutes}m`;
const embed = new EmbedBuilder()
.setAuthor({
name: target.displayName,
iconURL: target.displayAvatarURL({ dynamic: true })
})
.setTitle(`${emojis.level} Niveau ${userXP.level}`)
.setColor(colors.xp)
.setThumbnail(target.displayAvatarURL({ dynamic: true, size: 256 }))
.setDescription(`**${emojis.rank} Rang #${rank}** sur ${interaction.guild.name}`)
.addFields(
{
name: `${emojis.xp} XP Total`,
value: `\`${userXP.xp.toLocaleString()} XP\``,
inline: true
},
{
name: '📈 Progression',
value: `\`${progress.current}/${progress.needed} XP\` (${progress.percentage}%)`,
inline: true
},
{
name: '🎯 Prochain Niveau',
value: `\`${xpForNextLevel} XP\` requis`,
inline: true
},
{
name: '📊 Barre de Progression',
value: `\`\`\`${progressBar}\`\`\``,
inline: false
},
{
name: '💬 Messages',
value: `\`${(userXP.totalMessages || 0).toLocaleString()}\``,
inline: true
},
{
name: '🎤 Temps Vocal',
value: `\`${voiceTimeStr}\``,
inline: true
}
)
.setFooter({
text: `${interaction.guild.name} • Niveau ${userXP.level}`,
iconURL: interaction.guild.iconURL({ dynamic: true }) || undefined
})
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
},
};

116
commands/xp/resetlevel.js Normal file
View File

@@ -0,0 +1,116 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { getUserXP } = require('../../functions/xp/xp.js');
const { colors, emojis } = require('../../utils/constants');
const { sendLog } = require('../../utils/helpers');
module.exports = {
category: 'xp',
data: new SlashCommandBuilder()
.setName('resetlevel')
.setDescription('Réinitialiser le niveau d\'un utilisateur (Admin/Modérateur uniquement)')
.addUserOption(option =>
option.setName('user')
.setDescription('L\'utilisateur dont tu veux réinitialiser le niveau')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison du reset (optionnel)')
.setRequired(false))
.setDefaultMemberPermissions(PermissionFlagsBits.ManageMessages),
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const target = interaction.options.getUser('user');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
if (!target) return interaction.editReply({ content: '❌ Aucun utilisateur spécifié !' });
if (target.id === interaction.user.id) return interaction.editReply({ content: '❌ Tu ne peux pas te reset toi-même.' });
if (target.id === interaction.guild.ownerId) return interaction.editReply({ content: '❌ Tu ne peux pas reset le niveau du propriétaire du serveur.' });
const targetMember = await interaction.guild.members.fetch(target.id).catch(() => null);
if (targetMember) {
if (targetMember.roles.highest.position >= interaction.member.roles.highest.position &&
interaction.guild.ownerId !== interaction.user.id) {
return interaction.editReply({
content: '❌ Tu ne peux pas reset le niveau d\'un utilisateur avec un rôle supérieur ou égal au tien.'
});
}
}
try {
const userXP = await getUserXP(target.id, interaction.guild.id);
if (!userXP || userXP.xp === 0) {
return interaction.editReply({
content: `${target.tag} n'a aucun niveau/XP à réinitialiser.`
});
}
const oldLevel = userXP.level;
const oldXP = userXP.xp;
const oldMessages = userXP.totalMessages || 0;
const oldVoiceTime = userXP.totalVoiceTime || 0;
await db.query('DELETE FROM user_xp WHERE userId = ? AND guildId = ?', [target.id, interaction.guild.id]);
await db.query('DELETE FROM xp_logs WHERE userId = ? AND guildId = ?', [target.id, interaction.guild.id]);
await db.query(
`INSERT INTO logs (userId, userTag, modId, modTag, action, reason, type, guildId, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
target.id,
target.tag,
interaction.user.id,
interaction.user.tag,
'Reset de niveau',
`Niveau ${oldLevel} (${oldXP} XP) réinitialisé. Raison: ${reason}`,
'Permanent',
interaction.guild.id,
Date.now()
]
);
const logEmbed = new EmbedBuilder()
.setAuthor({ name: interaction.user.displayName, iconURL: interaction.user.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.warning} Reset de Niveau`)
.setColor(colors.warning)
.setDescription(`Le niveau de ${target.toString()} a été réinitialisé.`)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `${target.toString()}\n\`${target.tag}\``, inline: true },
{ name: '👮 Modérateur', value: `${interaction.user.toString()}\n\`${interaction.user.tag}\``, inline: true },
{ name: '📊 Niveau avant', value: `\`${oldLevel}\``, inline: true },
{ name: '📈 XP avant', value: `\`${oldXP.toLocaleString()} XP\``, inline: true },
{ name: '💬 Messages', value: `\`${oldMessages.toLocaleString()}\``, inline: true },
{ name: '🎤 Temps vocal', value: `\`${Math.floor(oldVoiceTime / 60)}h\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: `ID: ${target.id}${interaction.guild.name}` })
.setTimestamp();
await sendLog(interaction.guild, { embeds: [logEmbed] });
const embed = new EmbedBuilder()
.setAuthor({ name: target.displayName, iconURL: target.displayAvatarURL({ dynamic: true }) })
.setTitle(`${emojis.success} Niveau Réinitialisé`)
.setColor(colors.success)
.setDescription(`Le niveau de ${target.toString()} a été complètement réinitialisé.`)
.setThumbnail(target.displayAvatarURL({ dynamic: true }))
.addFields(
{ name: '👤 Utilisateur', value: `\`${target.tag}\``, inline: true },
{ name: '📊 Niveau avant', value: `\`${oldLevel}\``, inline: true },
{ name: '📈 XP avant', value: `\`${oldXP.toLocaleString()} XP\``, inline: true },
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: 'Toutes les données XP ont été supprimées de la base de données.' })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} catch (err) {
console.error('Erreur resetlevel:', err);
await interaction.editReply({ content: `❌ Erreur lors du reset de niveau: ${err.message}` });
}
},
};

140
commands/xp/xpexclude.js Normal file
View File

@@ -0,0 +1,140 @@
const { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } = require('discord.js');
const db = require('../../functions/database/db.js');
const { isChannelExcluded } = require('../../functions/xp/xp.js');
const { colors, emojis } = require('../../utils/constants');
module.exports = {
category: 'xp',
data: new SlashCommandBuilder()
.setName('xpexclude')
.setDescription('Gérer les salons exclus de l\'XP (Admin uniquement)')
.addSubcommand(subcommand =>
subcommand
.setName('add')
.setDescription('Exclure un salon de l\'XP')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Le salon à exclure')
.setRequired(true))
.addStringOption(option =>
option.setName('reason')
.setDescription('Raison de l\'exclusion')
.setRequired(false)))
.addSubcommand(subcommand =>
subcommand
.setName('remove')
.setDescription('Retirer un salon de l\'exclusion')
.addChannelOption(option =>
option.setName('channel')
.setDescription('Le salon à retirer')
.setRequired(true)))
.addSubcommand(subcommand =>
subcommand
.setName('list')
.setDescription('Liste des salons exclus')),
async execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!interaction.member.permissions.has(PermissionFlagsBits.Administrator)) {
return interaction.editReply({
content: '❌ Tu n\'as pas la permission d\'utiliser cette commande. (Administrateur requis)'
});
}
const subcommand = interaction.options.getSubcommand();
try {
if (subcommand === 'add') {
const channel = interaction.options.getChannel('channel');
const reason = interaction.options.getString('reason') || 'Aucune raison fournie';
if (!channel.isTextBased()) {
return interaction.editReply({
content: '❌ Le salon doit être un canal texte.'
});
}
const excluded = await isChannelExcluded(channel.id, interaction.guild.id);
if (excluded) {
return interaction.editReply({
content: `❌ Le salon ${channel.toString()} est déjà exclu de l'XP.`
});
}
await db.query(
'INSERT INTO xp_excluded_channels (channelId, guildId, reason, addedBy, addedAt) VALUES (?, ?, ?, ?, ?)',
[channel.id, interaction.guild.id, reason, interaction.user.id, Date.now()]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Salon Exclu`)
.setColor(colors.success)
.setDescription(`Le salon ${channel.toString()} a été exclu de l'XP.`)
.addFields(
{ name: '📝 Raison', value: reason, inline: false }
)
.setFooter({ text: `${interaction.guild.name} • Système XP` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} else if (subcommand === 'remove') {
const channel = interaction.options.getChannel('channel');
const excluded = await isChannelExcluded(channel.id, interaction.guild.id);
if (!excluded) {
return interaction.editReply({
content: `❌ Le salon ${channel.toString()} n'est pas exclu de l'XP.`
});
}
await db.query(
'DELETE FROM xp_excluded_channels WHERE channelId = ? AND guildId = ?',
[channel.id, interaction.guild.id]
);
const embed = new EmbedBuilder()
.setTitle(`${emojis.success} Exclusion Retirée`)
.setColor(colors.success)
.setDescription(`Le salon ${channel.toString()} n'est plus exclu de l'XP.`)
.setFooter({ text: `${interaction.guild.name} • Système XP` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
} else if (subcommand === 'list') {
const [excludedChannels] = await db.query(
'SELECT * FROM xp_excluded_channels WHERE guildId = ?',
[interaction.guild.id]
);
if (excludedChannels.length === 0) {
return interaction.editReply({
content: '✅ Aucun salon n\'est exclu de l\'XP.'
});
}
const description = excludedChannels.map(ch => {
const channel = interaction.guild.channels.cache.get(ch.channelId);
return `${channel ? channel.toString() : `ID: ${ch.channelId}`} - ${ch.reason || 'Aucune raison'}`;
}).join('\n');
const embed = new EmbedBuilder()
.setAuthor({
name: interaction.guild.name,
iconURL: interaction.guild.iconURL({ dynamic: true }) || undefined
})
.setTitle('📋 Salons Exclus de l\'XP')
.setColor(colors.xp)
.setDescription(description || 'Aucun salon exclu')
.setFooter({ text: `${excludedChannels.length} salon(s) exclu(s)` })
.setTimestamp();
await interaction.editReply({ embeds: [embed] });
}
} catch (err) {
console.error('Erreur xpexclude:', err);
await interaction.editReply({ content: `❌ Erreur: ${err.message}` });
}
},
};