Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions src/commands/Utility/close.js
Original file line number Diff line number Diff line change
@@ -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);
}
};
2 changes: 1 addition & 1 deletion src/config/bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
Expand Down
112 changes: 87 additions & 25 deletions src/events/messageCreate.js
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@





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';
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) {
Expand All @@ -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 {
Expand All @@ -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;
Expand All @@ -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}`
Expand All @@ -113,5 +177,3 @@ async function handleLeveling(message, client) {
logger.error('Error handling leveling for message:', error);
}
}


11 changes: 11 additions & 0 deletions src/interactions/buttons/modmail_no.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
name: 'modmail_no',

async execute(interaction) {
await interaction.update({
content: 'Ticket geannuleerd.',
embeds: [],
components: []
});
}
};
70 changes: 70 additions & 0 deletions src/interactions/buttons/modmail_yes.js
Original file line number Diff line number Diff line change
@@ -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: []
});
}
};