diff --git a/commands/slash/applications/wge.js b/commands/slash/applications/wge.js index eb0e44a..412cb36 100644 --- a/commands/slash/applications/wge.js +++ b/commands/slash/applications/wge.js @@ -1,6 +1,18 @@ const { ModalBuilder, UserSelectMenuBuilder, TextInputBuilder, EmbedBuilder, TimestampStyles, time } = require("@discordjs/builders"); const { SlashCommandBuilder, LabelBuilder, TextInputStyle, MessageFlags } = require("discord.js"); const { mClient } = require("../../.."); +require('dotenv').config() + +function getRemaining(timer) { + if (timer.state !== 'running') { + return timer.timeRemaining; + } + + return Math.max( + 0, + timer.timeRemaining - (Date.now() - timer.startedAt) + ); +} function ordinal(n) { const s = ["th", "st", "nd", "rd"]; @@ -15,7 +27,10 @@ async function wgeStatus(interaction) { const historyColl = db.collection('history_wge'); const results = await wgeColl.findOne({ - "debug": false + $and: [ + { debug: false }, + { state: 'running' } + ] }) if (!results) { return interaction.editReply({ @@ -52,6 +67,8 @@ async function wgeStart(interaction) { .setCustomId('wgeUserInput') .setMaxValues(1) .setRequired(true) + .addDefaultUsers(process.env.D_ID) + // DefaultUser is randomizer const wgeTopicInput = new TextInputBuilder() .setCustomId('wgeTopicInput') @@ -75,23 +92,152 @@ async function wgeStart(interaction) { } async function wgeStop(interaction) { + const db = mClient.db('neo-db'); + const wgeColl = db.collection('timer_wge'); -} + const timer = await wgeColl.findOne({ + debug: false + }); -async function wgePause(interaction) { + if (!timer) { + return interaction.reply({ + content: 'Timer not found.', + flags: MessageFlags.Ephemeral + }); + } + // freeze remaining time + const remaining = getRemaining(timer); + + await wgeColl.updateOne( + { _id: timer._id }, + { + $set: { + timeRemaining: remaining, + startedAt: null, + state: 'stopped' + } + } + ); + + await interaction.reply({ + content: `🛑 Timer stopped for **${timer.topic}**` + }); } async function wgeResume(interaction) { + const db = mClient.db('neo-db'); + const wgeColl = db.collection('timer_wge'); + const timer = await wgeColl.findOne({ + debug: false + }); + + if (!timer) { + return interaction.reply({ + content: 'Timer not found.', + ephemeral: true + }); + } + + // freeze remaining time + const remaining = getRemaining(timer); + + await wgeColl.updateOne( + { _id: timer._id }, + { + $set: { + timeRemaining: remaining, + startedAt: Date.now(), + state: 'running' + } + } + ); + + await interaction.reply({ + content: `🟢 Timer resumed for **${timer.topic}**` + }); } async function wgeForfeit(interaction) { + // Reset Timer, Reassign Randomly + const db = mClient.db('neo-db') + const wgeColl = db.collection('timer_wge') + // Randomizer Function + const participateColl = db.collection('items_wge') + const randomParticipant = await participateColl.aggregate([ + { $match: { status: true } }, + { $sample: { size: 1 } } + ]).toArray() + + if (!randomParticipant.length) { return interaction.reply({ content: 'No one else is participating!' }) } + + random = await interaction.guild.members.fetch(randomParticipant[0].userID) + wgeAssignedUser = random.user + + const wge = await wgeColl.findOne({ + $and: [ + { debug: false }, + { state: 'running'} + ] + }) + + await wgeColl.deleteOne({ + _id: wge._id + }) + + const wgeDefaultAssignedTime = 7 * 24 * 60 * 60 * 1000; // 7 days in ms + + interaction.reply({ + content: `${interaction.user} has forfeited! <@${wgeAssignedUser.id}> is the next expert! Their topic is **${wge.topic}**` + }) + + await wgeColl.insertOne({ + assignedUser: wgeAssignedUser.id, + assigningUser: wge.assigningUser, + + startedAt: Date.now(), + timeRemaining: wgeDefaultAssignedTime, + + state: 'running', + + notifications: { + half: false, + day24: false, + complete: false + }, + + topic: wge.topic, + debug: false, + + createdAt: new Date() + }); } async function wgeHistory(interaction) { + //FIXME! +} +async function wgeParticipate(interaction) { + const status = interaction.options.getBoolean('status') + const user = interaction.user + const db = mClient.db('neo-db') + const coll = db.collection('items_wge') + + // Choice was false, ergo not participating + await coll.findOneAndUpdate({ + userID: user.id + }, { + $set: { + userID: user.id, + status: status + } + }, { + upsert: true + }) + + interaction.reply(`You are ${status ? '' : 'no longer '}participating!`) } module.exports = { @@ -112,20 +258,13 @@ module.exports = { .setDescription('start a new wge session') ) - // Stop the current Session, No Reassignment + // Stop the current Session Timer .addSubcommand(s => s .setName('stop') .setDescription('stop the current wge session') ) - // Halt the current Session Timer - .addSubcommand(s => - s - .setName('pause') - .setDescription('pause the current wge session timer') - ) - // Resume the current Session Timer .addSubcommand(s => s @@ -146,6 +285,17 @@ module.exports = { .setName('history') .setDescription('display the previous wge sessions') ) + + // List the previous WGE Sessions + .addSubcommand(s => + s + .setName('participate') + .setDescription('do you want to play a game?') + .addBooleanOption(o => + o.setName('status') + .setDescription('choose') + ) + ) , async execute(interaction) { switch (interaction.options._subcommand) { @@ -171,6 +321,9 @@ module.exports = { case 'history': wgeHistory(interaction) break; + case 'participate': + wgeParticipate(interaction) + break; default: break; } diff --git a/events/ClientReady.js b/events/ClientReady.js index 242a3dd..8816b09 100644 --- a/events/ClientReady.js +++ b/events/ClientReady.js @@ -50,7 +50,7 @@ module.exports = { if (dbEntry && isToday(dbEntry.day, dbEntry.month)) { // Today is their birthday - await member.roles.add(birthdayRoleID).catch(err => + await member.roles.add(birthdayRoleID).catch(err => console.error(`Failed to add birthday role to ${member.user.tag}:`, err) ); console.log(`Added birthday role to ${member.user.tag}`); @@ -58,7 +58,7 @@ module.exports = { } else { // Default fallback: remove birthday role if present if (member.roles.cache.has(birthdayRoleID)) { - await member.roles.remove(birthdayRoleID).catch(err => + await member.roles.remove(birthdayRoleID).catch(err => console.error(`Failed to remove birthday role from ${member.user.tag}:`, err) ); console.log(`Removed birthday role from ${member.user.tag} (not today or not in DB)`); @@ -68,5 +68,15 @@ module.exports = { }; await handleBirthdays(); + + // First check after restart + console.log('Checking Timer...') + client.emit('timerCheck'); + + // Check every minute + setInterval(() => { + console.log('Checking Timer...') + client.emit('timerCheck'); + }, 60_000); } }; \ No newline at end of file diff --git a/events/InteractionCreate.js b/events/InteractionCreate.js index 9864f9e..01e6592 100644 --- a/events/InteractionCreate.js +++ b/events/InteractionCreate.js @@ -1,5 +1,7 @@ const { Events, MessageFlags } = require('discord.js') const { mClient } = require('../index') +require('dotenv').config() + module.exports = { name: Events.InteractionCreate, once: false, @@ -59,27 +61,50 @@ async function selectMenuHandler(interaction) { } async function modalSubmitHandler(interaction) { if (interaction.customId === 'wgeBaseModal') { - await interaction.reply({ - content: 'Your submission was received successfully!' - }); + const db = mClient.db('neo-db') + const wgeColl = db.collection('timer_wge') // Get the data entered by the user - const wgeAssignedUser = interaction.components[0].component.users.first(); + var wgeAssignedUser = interaction.components[0].component.users.first(); const wgeAssigningUser = interaction.user + if(wgeAssignedUser.id === process.env.D_ID){ + // Randomizer Function + const participateColl = db.collection('items_wge') + const randomParticipant = await participateColl.aggregate([ + { $match: { status: true}}, + { $sample: { size: 1}} + ]).toArray() + + if(!randomParticipant){return interaction.followUp({content: 'No User is participating!'})} + + random = await interaction.guild.members.fetch(randomParticipant[0].userID) + wgeAssignedUser = random.user + } const wgeAssignedTopic = interaction.fields.getTextInputValue('wgeTopicInput'); const wgeDefaultAssignedTime = 7 * 24 * 60 * 60 * 1000; // 7 days in ms - const db = mClient.db('neo-db') - const wgeColl = db.collection('timer_wge') + await wgeColl.insertOne({ + assignedUser: wgeAssignedUser.id, + assigningUser: wgeAssigningUser.id, - wgeColl.insertOne({ - "assignedUser": wgeAssignedUser.id, - "assigningUser": wgeAssigningUser.id, - "timeRemaining": wgeDefaultAssignedTime, - "createdAt": new Date(), - "topic": wgeAssignedTopic, - "debug" :false - }) + startedAt: Date.now(), + timeRemaining: wgeDefaultAssignedTime, + + state: 'running', + + notifications: { + half: false, + day24: false, + complete: false + }, + + topic: wgeAssignedTopic, + debug: false, + + createdAt: new Date() + }); + + return interaction.reply({content: `<@${wgeAssignedUser.id}> is the next expert! Their topic is **${wgeAssignedTopic}**`}) } } \ No newline at end of file diff --git a/events/timer24h.js b/events/timer24h.js new file mode 100644 index 0000000..5c60d37 --- /dev/null +++ b/events/timer24h.js @@ -0,0 +1,27 @@ +async function getMember(client, userId) { + const guild = client.guilds.cache.get(process.env.D_GuildID); + if (!guild) return null; + + // cache first (fast path) + let member = guild.members.cache.get(userId); + + // fallback (safe path) + if (!member) { + member = await guild.members.fetch(userId).catch(() => null); + } + + return member; +} + +module.exports = { + name: 'timer24h', + + async execute(timer, client) { + const member = await getMember(client, timer.assignedUser); + if (!member) return; + + await member.send( + `⚠️ Less than 24 hours remaining for: **${timer.topic}**` + ).catch(() => {}); + } +}; \ No newline at end of file diff --git a/events/timerCheck.js b/events/timerCheck.js new file mode 100644 index 0000000..055ff6f --- /dev/null +++ b/events/timerCheck.js @@ -0,0 +1,76 @@ +const { mClient } = require('../index'); +function getRemaining(timer) { + if (timer.state !== 'running') { + return timer.timeRemaining; + } + + return Math.max( + 0, + timer.timeRemaining - (Date.now() - timer.startedAt) + ); +} + +module.exports = { + name: 'timerCheck', + + async execute(client) { + const db = mClient.db('neo-db'); + const wgeColl = db.collection('timer_wge'); + + const timers = await wgeColl.find({ + state: { $ne: 'stopped' } + }).toArray(); + + const now = Date.now(); + + for (const timer of timers) { + + const remaining = getRemaining(timer); + + // ------------------------- + // HALF TIME (3.5 days left) + // ------------------------- + if ( + !timer.notifications?.half && + remaining <= 3.5 * 24 * 60 * 60 * 1000 + ) { + await wgeColl.updateOne( + { _id: timer._id }, + { $set: { "notifications.half": true } } + ); + + client.emit('timerHalf', timer); + } + + // ------------------------- + // 24H WARNING + // ------------------------- + if ( + !timer.notifications?.day24 && + remaining <= 24 * 60 * 60 * 1000 + ) { + await wgeColl.updateOne( + { _id: timer._id }, + { $set: { "notifications.day24": true } } + ); + + client.emit('timer24h', timer); + } + + // ------------------------- + // COMPLETION + // ------------------------- + if ( + !timer.notifications?.complete && + remaining <= 0 + ) { + await wgeColl.updateOne( + { _id: timer._id }, + { $set: { "notifications.complete": true } } + ); + + client.emit('timerComplete', timer); + } + } + } +}; \ No newline at end of file diff --git a/events/timerComplete.js b/events/timerComplete.js new file mode 100644 index 0000000..f8c1101 --- /dev/null +++ b/events/timerComplete.js @@ -0,0 +1,95 @@ +const { mClient } = require(".."); + +async function getMember(client, userId) { + const guild = client.guilds.cache.get(process.env.D_GuildID); + if (!guild) return null; + + // cache first (fast path) + let member = guild.members.cache.get(userId); + + // fallback (safe path) + if (!member) { + member = await guild.members.fetch(userId).catch(() => null); + } + + return member; +} + +module.exports = { + name: 'timerComplete', + + async execute(timer, client) { + const member = await getMember(client, timer.assignedUser); + if (!member) return; + + await member.send( + `❌ Timer ran out: **${timer.topic}** ...Re-assigning randomly...` + ).catch(() => { }); + + // Reset Timer, Reassign Randomly + const db = mClient.db('neo-db') + const wgeColl = db.collection('timer_wge') + + // Randomizer Function + const participateColl = db.collection('items_wge') + const randomParticipant = await participateColl.aggregate([ + { $match: { status: true } }, + { $sample: { size: 1 } } + ]).toArray() + + if (!randomParticipant.length) { return interaction.reply({ content: 'No one else is participating!' }) } + + const wge = await wgeColl.findOne({ + $and: [ + { debug: false }, + { state: 'running' } + ] + }) + + const channelColl = db.collection('config_channels') + const guild = await client.guilds.cache.get(process.env.D_GuildID); + const wgeChannel = await channelColl.findOne({ + $and: [ + { purpose: 'wge' }, + { guildID: process.env.D_GuildID } + ] + }) + + const channel = await client.channels.cache.get(wgeChannel.channelID) + + random = await guild.members.fetch(randomParticipant[0].userID) + wgeAssignedUser = random.user + + await wgeColl.deleteOne({ + _id: wge._id + }) + + const wgeDefaultAssignedTime = 7 * 24 * 60 * 60 * 1000; // 7 days in ms + + channel.send({ + content: `The Timer for <@${wge.assignedUser}> has run out! Assigning randomly... \r\n<@${wgeAssignedUser.id}> is the next expert! Their topic still is **${wge.topic}**` + }) + + await wgeColl.insertOne({ + assignedUser: wgeAssignedUser.id, + assigningUser: wge.assigningUser, + + startedAt: Date.now(), + timeRemaining: wgeDefaultAssignedTime, + + state: 'running', + + notifications: { + half: false, + day24: false, + complete: false + }, + + topic: wge.topic, + debug: false, + + createdAt: new Date() + }); + + } +}; \ No newline at end of file diff --git a/events/timerHalf.js b/events/timerHalf.js new file mode 100644 index 0000000..0c3eaa8 --- /dev/null +++ b/events/timerHalf.js @@ -0,0 +1,27 @@ +async function getMember(client, userId) { + const guild = client.guilds.cache.get(process.env.D_GuildID); + if (!guild) return null; + + // cache first (fast path) + let member = guild.members.cache.get(userId); + + // fallback (safe path) + if (!member) { + member = await guild.members.fetch(userId).catch(() => null); + } + + return member; +} + +module.exports = { + name: 'timerHalf', + + async execute(timer, client) { + const member = await getMember(client, timer.assignedUser); + if (!member) return; + + await member.send( + `⏳ Half time reached for: **${timer.topic}**` + ).catch(() => {}); + } +}; \ No newline at end of file