diff --git a/.env.example b/.env.example index 4f08ac030..3889acad8 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,10 @@ CLIENT_ID=your_discord_client_id_here GUILD_ID=your_discord_guild_id_here OWNER_IDS=your_discord_id_here (optional) +# ModMail Configuration +MODMAIL_CATEGORY_ID=your_modmail_category_channel_id_here +MODMAIL_STAFF_ROLE_ID=your_staff_role_id_here + # Bot Runtime Configuration NODE_ENV=production LOG_LEVEL=warn diff --git a/src/commands/Utility/close.js b/src/commands/Utility/close.js new file mode 100644 index 000000000..1c7dcdc15 --- /dev/null +++ b/src/commands/Utility/close.js @@ -0,0 +1,33 @@ +import { SlashCommandBuilder } from 'discord.js'; + +export default { + data: new SlashCommandBuilder() + .setName('close') + .setDescription('Sluit een ModMail ticket'), + + async execute(interaction) { + + if (!interaction.channel.topic) { + return interaction.reply({ + content: 'Dit is geen ticket.', + ephemeral: true + }); + } + + const user = await interaction.client.users.fetch( + interaction.channel.topic + ); + + await user.send( + '🔒 Je ticket is gesloten.' + ).catch(() => {}); + + await interaction.reply( + '✅ Ticket gesloten.' + ); + + setTimeout(() => { + interaction.channel.delete().catch(() => {}); + }, 3000); + } +}; diff --git a/src/config/bot.js b/src/config/bot.js index 36e588cd4..136546383 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -25,7 +25,7 @@ export const botConfig = { activities: [ { // Text users will see (example: "Playing /help | Titan Bot"). - name: "Made with ❤️", + name: "© Thijs's Bot shop", // Activity type number (0 = Playing). type: 0, }, diff --git a/src/events/messageCreate.js b/src/events/messageCreate.js index b7f9841dc..3c0bbd951 100644 --- a/src/events/messageCreate.js +++ b/src/events/messageCreate.js @@ -1,9 +1,10 @@ - - - - - -import { Events } from 'discord.js'; +import { + Events, + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder +} from 'discord.js'; import { logger } from '../utils/logger.js'; import { getLevelingConfig, getUserLevelData } from '../services/leveling.js'; import { addXp } from '../services/xpSystem.js'; @@ -11,13 +12,21 @@ import { checkRateLimit } from '../utils/rateLimiter.js'; const MESSAGE_XP_RATE_LIMIT_ATTEMPTS = 12; const MESSAGE_XP_RATE_LIMIT_WINDOW_MS = 10000; +const MODMAIL_CATEGORY_ID = process.env.MODMAIL_CATEGORY_ID || '1498650880604639272'; export default { name: Events.MessageCreate, async execute(message, client) { try { - - if (message.author.bot || !message.guild) return; + if (message.author.bot) return; + + if (!message.guild) { + await handleModmailDm(message, client); + return; + } + + const handledStaffReply = await handleModmailStaffReply(message, client); + if (handledStaffReply) return; await handleLeveling(message, client); } catch (error) { @@ -26,12 +35,78 @@ export default { } }; +async function handleModmailDm(message, client) { + const guild = await getConfiguredGuild(client); + if (!guild) { + logger.warn('Modmail DM received, but GUILD_ID is not configured or the guild is unavailable.'); + return; + } + const ticket = guild.channels.cache.find( + channel => channel.parentId === MODMAIL_CATEGORY_ID && channel.topic === message.author.id + ); + if (ticket) { + await ticket.send({ + content: `User ${message.author.tag}: ${message.content || '(no text)'}`, + files: [...message.attachments.values()].map(attachment => attachment.url) + }); + return; + } + const embed = new EmbedBuilder() + .setTitle('ModMail') + .setDescription('Weet je zeker dat je een ticket wilt openen?'); + + const row = new ActionRowBuilder() + .addComponents( + new ButtonBuilder() + .setCustomId(`modmail_yes:${message.author.id}`) + .setLabel('Ja') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`modmail_no:${message.author.id}`) + .setLabel('Nee') + .setStyle(ButtonStyle.Danger) + ); + + await message.author.send({ + embeds: [embed], + components: [row] + }); +} +async function handleModmailStaffReply(message, client) { + if ( + message.channel?.parentId !== MODMAIL_CATEGORY_ID || + !message.channel?.topic + ) { + return false; + } + const user = await client.users.fetch(message.channel.topic).catch(() => null); + if (!user) { + await message.reply('I could not find the user for this modmail ticket.'); + return true; + } + + await user.send({ + content: `Staff: ${message.content || '(no text)'}`, + files: [...message.attachments.values()].map(attachment => attachment.url) + }).catch(async () => { + await message.reply('I could not DM this user. Their DMs may be closed.'); + }); + + return true; +} + +async function getConfiguredGuild(client) { + const guildId = client.config?.bot?.guildId; + if (!guildId) return null; + return client.guilds.cache.get(guildId) + || await client.guilds.fetch(guildId).catch(() => null); +} async function handleLeveling(message, client) { try { @@ -42,17 +117,15 @@ async function handleLeveling(message, client) { } const levelingConfig = await getLevelingConfig(client, message.guild.id); - + if (!levelingConfig?.enabled) { return; } - if (levelingConfig.ignoredChannels?.includes(message.channel.id)) { return; } - if (levelingConfig.ignoredRoles?.length > 0) { const member = await message.guild.members.fetch(message.author.id).catch(() => { return null; @@ -62,48 +135,39 @@ async function handleLeveling(message, client) { } } - if (levelingConfig.blacklistedUsers?.includes(message.author.id)) { return; } - if (!message.content || message.content.trim().length === 0) { return; } const userData = await getUserLevelData(client, message.guild.id, message.author.id); - - + const cooldownTime = levelingConfig.xpCooldown || 60; const now = Date.now(); const timeSinceLastMessage = now - (userData.lastMessage || 0); - - + if (timeSinceLastMessage < cooldownTime * 1000) { return; } - const minXP = levelingConfig.xpRange?.min || levelingConfig.xpPerMessage?.min || 15; const maxXP = levelingConfig.xpRange?.max || levelingConfig.xpPerMessage?.max || 25; - const safeMinXP = Math.max(1, minXP); const safeMaxXP = Math.max(safeMinXP, maxXP); - const xpToGive = Math.floor(Math.random() * (safeMaxXP - safeMinXP + 1)) + safeMinXP; - let finalXP = xpToGive; if (levelingConfig.xpMultiplier && levelingConfig.xpMultiplier > 1) { finalXP = Math.floor(finalXP * levelingConfig.xpMultiplier); } - const result = await addXp(client, message.guild, message.member, finalXP); - + if (result.success && result.leveledUp) { logger.info( `${message.author.tag} leveled up to level ${result.level} in ${message.guild.name}` @@ -113,5 +177,3 @@ async function handleLeveling(message, client) { logger.error('Error handling leveling for message:', error); } } - - diff --git a/src/interactions/buttons/modmail_no.js b/src/interactions/buttons/modmail_no.js new file mode 100644 index 000000000..d0fb2b5db --- /dev/null +++ b/src/interactions/buttons/modmail_no.js @@ -0,0 +1,11 @@ +export default { + name: 'modmail_no', + + async execute(interaction) { + await interaction.update({ + content: 'Ticket geannuleerd.', + embeds: [], + components: [] + }); + } +}; diff --git a/src/interactions/buttons/modmail_yes.js b/src/interactions/buttons/modmail_yes.js new file mode 100644 index 000000000..72133e4bd --- /dev/null +++ b/src/interactions/buttons/modmail_yes.js @@ -0,0 +1,70 @@ +import { + ChannelType, + PermissionFlagsBits +} from 'discord.js'; + +const CATEGORY_ID = process.env.MODMAIL_CATEGORY_ID || '1498650880604639272'; +const STAFF_ROLE_ID = process.env.MODMAIL_STAFF_ROLE_ID || '1498650876473249818'; + +export default { + name: 'modmail_yes', + + async execute(interaction, client, args) { + const userId = args[0]; + if (!userId || userId !== interaction.user.id) { + return interaction.reply({ + content: 'Deze modmail knop is niet voor jou.', + ephemeral: true + }); + } + + const guild = client.guilds.cache.get(client.config.bot.guildId) + || await client.guilds.fetch(client.config.bot.guildId).catch(() => null); + + if (!guild) { + return interaction.reply({ + content: 'Ik kan de support server niet vinden. Controleer GUILD_ID.', + ephemeral: true + }); + } + + const existing = guild.channels.cache.find( + channel => channel.parentId === CATEGORY_ID && channel.topic === userId + ); + + if (existing) { + return interaction.reply({ + content: 'Je hebt al een open ticket.', + ephemeral: true + }); + } + + const channel = await guild.channels.create({ + name: `ticket-${userId}`, + type: ChannelType.GuildText, + parent: CATEGORY_ID, + topic: userId, + permissionOverwrites: [ + { + id: guild.id, + deny: [PermissionFlagsBits.ViewChannel] + }, + { + id: STAFF_ROLE_ID, + allow: [ + PermissionFlagsBits.ViewChannel, + PermissionFlagsBits.SendMessages + ] + } + ] + }); + + await channel.send(`Nieuw ModMail ticket van <@${userId}>`); + + await interaction.update({ + content: 'Ticket geopend.', + embeds: [], + components: [] + }); + } +};