const { Events, MessageFlags, Message } = require('discord.js') const { mClient } = require('../index') require('dotenv').config() module.exports = { name: Events.InteractionCreate, once: false, async execute(interaction) { if (interaction.isChatInputCommand()) { commandHandler(interaction) } if (interaction.isButton()) { buttonHandler(interaction) } if (interaction.isMentionableSelectMenu()) { selectMenuHandler(interaction) } if (interaction.isUserSelectMenu()) { selectMenuHandler(interaction) } if (interaction.isModalSubmit()) { modalSubmitHandler(interaction) } } } async function commandHandler(interaction) { const command = interaction.client.commands.get(interaction.commandName); if (!command) { console.error(`No command matching ${interaction.commandName} was found.`); return; } try { await command.execute(interaction); } catch (error) { console.error(error); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } else { await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } } } async function buttonHandler(interaction) { const button = interaction.client.buttons.get(interaction.customId) if (!button) { return }// console.error(`No button registered matching ${interaction.customId}.`) } try { await button.execute(interaction); } catch (error) { console.error(error); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } else { await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } } } async function selectMenuHandler(interaction) { const selectMenu = interaction.client.selectMenus.get(interaction.customId) if (!selectMenu) { return console.error(`No Select Menu registered matching ${interaction.customId}.`) } try { await selectMenu.execute(interaction); } catch (error) { console.error(error); if (interaction.replied || interaction.deferred) { await interaction.followUp({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } else { await interaction.reply({ content: 'There was an error while executing this command!', flags: MessageFlags.Ephemeral }); } } } function getParticipantWeight(participant, now = Date.now()) { // Never selected before -> strongly favor them if (!participant.lastExpertAt) { return 6; } const lastExpertAt = new Date(participant.lastExpertAt).getTime(); // Invalid date safety if (Number.isNaN(lastExpertAt)) { return 6; } const daysSinceExpert = Math.max( 0, (now - lastExpertAt) / (24 * 60 * 60 * 1000) ); /* * Square-root weighting prevents very old participants * from completely dominating the random selection. * * Examples: * 1 day -> ~1.41 * 7 days -> ~2.83 * 30 days -> ~5.57 */ return Math.sqrt(daysSinceExpert + 1); } function weightedRandom(participants) { if (!participants.length) { return null; } const weightedParticipants = participants.map(participant => ({ participant, weight: getParticipantWeight(participant) })); const totalWeight = weightedParticipants.reduce( (total, item) => total + item.weight, 0 ); let random = Math.random() * totalWeight; for (const item of weightedParticipants) { random -= item.weight; if (random <= 0) { return item.participant; } } // Floating-point safety fallback return weightedParticipants[weightedParticipants.length - 1].participant; } async function selectWgeParticipant( participateColl, { excludeUserId = null } = {} ) { const filter = { status: true }; if (excludeUserId) { filter.userID = { $ne: excludeUserId }; } const participants = await participateColl .find(filter) .toArray(); return weightedRandom(participants); } async function modalSubmitHandler(interaction) { if (interaction.customId !== 'wgeBaseModal') { return; } const db = mClient.db('neo-db'); const wgeColl = db.collection('timer_wge'); const participateColl = db.collection('items_wge'); const historyWgeColl = db.collection('history_wge'); const assigningUser = interaction.user; const selectedUserId = interaction.components[0].component.values[0]; const topic = interaction.fields.getTextInputValue( 'wgeTopicInput' ); const defaultAssignedTime = 7 * 24 * 60 * 60 * 1000; // Get current running WGE so the previous expert // cannot immediately be selected again. const prevWge = await wgeColl.findOne({ debug: false, state: 'running' }); let assignedUser; let selectedParticipant; // --------------------------------- // RANDOM / WEIGHTED ASSIGNMENT // --------------------------------- if (selectedUserId === process.env.D_ID) { selectedParticipant = await selectWgeParticipant( participateColl, { excludeUserId: prevWge?.assignedUser } ); if (!selectedParticipant) { return interaction.reply({ content: 'There are no eligible participants available.', flags: MessageFlags.Ephemeral }); } const member = await interaction.guild.members .fetch(selectedParticipant.userID) .catch(() => null); if (!member) { return interaction.reply({ content: 'The selected participant could not be found in the server.', flags: MessageFlags.Ephemeral }); } assignedUser = member.user; } // --------------------------------- // MANUAL ASSIGNMENT // --------------------------------- else { selectedParticipant = await participateColl.findOne({ userID: selectedUserId, status: true }); if (!selectedParticipant) { return interaction.reply({ content: `<@${selectedUserId}> is not participating!`, flags: MessageFlags.Ephemeral }); } if ( prevWge && selectedUserId === prevWge.assignedUser ) { return interaction.reply({ content: `<@${selectedUserId}> was the previous expert and cannot be selected again this round.`, flags: MessageFlags.Ephemeral }); } assignedUser = { id: selectedUserId }; } // --------------------------------- // ARCHIVE PREVIOUS WGE // --------------------------------- if (prevWge) { await historyWgeColl.insertOne({ assignedUser: prevWge.assignedUser, assigningUser: prevWge.assigningUser, timeRemaining: prevWge.timeRemaining, topic: prevWge.topic, debug: prevWge.debug, createdAt: prevWge.createdAt }); console.log({ message: 'Saved previous WGE to history_wge', data: prevWge }); await wgeColl.deleteOne({ _id: prevWge._id }); } const assignedAt = new Date(); // --------------------------------- // UPDATE PARTICIPANT STATISTICS // --------------------------------- await participateColl.updateOne( { _id: selectedParticipant._id }, { $set: { lastExpertAt: assignedAt }, $inc: { expertCount: 1 } } ); // --------------------------------- // CREATE NEW WGE // --------------------------------- await wgeColl.insertOne({ assignedUser: assignedUser.id, assigningUser: assigningUser.id, startedAt: Date.now(), timeRemaining: defaultAssignedTime, state: 'running', notifications: { half: false, day24: false, complete: false }, topic, debug: false, createdAt: assignedAt }); return interaction.reply({ content: `<@${assignedUser.id}> is the next expert! Their topic is **${topic}**` }); }