From 0ee0b2cea74648f2a4a1c971bc6e4678fe25df34 Mon Sep 17 00:00:00 2001 From: kingomes <83099848+kingomes@users.noreply.github.com> Date: Sat, 16 Aug 2025 03:34:56 -0500 Subject: [PATCH] feat: giveaway improvements (#57) --- package.json | 4 +- src/commands/giveaway.ts | 413 +++++++++++++++------ src/commands/handlers/giveawayDiscard.ts | 21 ++ src/commands/handlers/giveawayEdit.ts | 36 ++ src/commands/handlers/giveawayEditModal.ts | 109 ++++++ src/commands/handlers/giveawayEnd.ts | 83 +++++ src/commands/handlers/giveawayEnter.ts | 61 +++ src/commands/handlers/giveawayLeave.ts | 56 +++ src/events/embedReact.ts | 21 -- src/events/embedRemoveReact.ts | 15 - src/utils/db.ts | 4 +- tsconfig.json | 4 +- 12 files changed, 668 insertions(+), 159 deletions(-) create mode 100644 src/commands/handlers/giveawayDiscard.ts create mode 100644 src/commands/handlers/giveawayEdit.ts create mode 100644 src/commands/handlers/giveawayEditModal.ts create mode 100644 src/commands/handlers/giveawayEnd.ts create mode 100644 src/commands/handlers/giveawayEnter.ts create mode 100644 src/commands/handlers/giveawayLeave.ts delete mode 100644 src/events/embedReact.ts delete mode 100644 src/events/embedRemoveReact.ts diff --git a/package.json b/package.json index 1c11fb9..278a1eb 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "imports": { "#plugins": "./dist/src/plugins/index.js", "#utils": "./dist/src/utils/index.js", - "#constants": "./dist/src/constants.js" + "#constants": "./dist/src/constants.js", + "#db": "./dist/src/utils/db.js", + "#commands/*": "./dist/src/commands/*" }, "scripts": { "test": "echo \"Error: no test specified\" && exit 1", diff --git a/src/commands/giveaway.ts b/src/commands/giveaway.ts index a15315c..a2d502a 100644 --- a/src/commands/giveaway.ts +++ b/src/commands/giveaway.ts @@ -1,8 +1,14 @@ -import { commandModule, CommandType, scheduledTask } from "@sern/handler"; +import { commandModule, CommandType } from "@sern/handler"; import { ownerOnly, publish } from "#plugins"; -import { ApplicationCommandOptionType, EmbedBuilder } from "discord.js"; -import { db } from "../utils/db.js"; -import { add, addDays, addHours, addMinutes, addSeconds } from "date-fns" +import { + ApplicationCommandOptionType, + ButtonBuilder, + ActionRowBuilder, + ButtonStyle, + EmbedBuilder, +} from "discord.js"; +import { db } from "#db"; +import { add } from "date-fns"; import { Timestamp } from "#utils"; export default commandModule({ @@ -14,149 +20,318 @@ export default commandModule({ name: "item", description: "The item that will be given away", type: ApplicationCommandOptionType.String, - required: true + required: true, }, { name: "time", description: "The amount of time that the giveaway will be up", type: ApplicationCommandOptionType.String, - required: true - } + required: true, + autocomplete: true, + command: { + async execute(ctx) { + const focus = ctx.options.getFocused(); + const timeUnits = [ + "seconds", + "second", + "sec", + "secs", + "minutes", + "minute", + "min", + "mins", + "hours", + "hour", + "hr", + "hrs", + "days", + "day", + ]; + + if (!focus) return ctx.respond([]); + + const andUnitMatch = focus.match(/and\s*(\d+)\s*(\w*)$/i); + if (andUnitMatch) { + const num = andUnitMatch[1]; + const partialUnit = andUnitMatch[2]; + const filtered = timeUnits.filter((unit) => + unit.toLowerCase().startsWith(partialUnit.toLowerCase()) + ); + return ctx.respond( + filtered.map((unit) => ({ + name: `${num}${unit.slice(partialUnit.length)}`, + value: `${num}${unit.slice(partialUnit.length)}`, + })) + ); + } + + const andMatch = focus.match(/and\s*(\d+)\s*$/i); + if (andMatch) { + const num = andMatch[1]; + return ctx.respond( + timeUnits.map((unit) => ({ + name: `${num} ${unit}`, + value: `${num} ${unit}`, + })) + ); + } + + if (/^\d+\s*$/.test(focus)) { + return ctx.respond( + timeUnits.map((unit) => ({ + name: `${focus} ${unit}`, + value: `${focus} ${unit}`, + })) + ); + } + + const match = focus.match(/^(\d+)\s*(.*)$/); + if (match) { + const [, num, partialUnit] = match; + const filtered = timeUnits.filter((unit) => + unit.toLowerCase().startsWith(partialUnit.toLowerCase()) + ); + let suggestions = filtered.map((unit) => ({ + name: `${num} ${unit}`, + value: `${num} ${unit}`, + })); + + if ( + filtered.length === 1 && + partialUnit.length > 0 && + filtered[0] === partialUnit.toLowerCase() + ) { + suggestions.push({ + name: `${num} ${filtered[0]} and `, + value: `${num} ${filtered[0]} and `, + }); + } else if ( + filtered.length === 1 && + partialUnit.length > 0 && + filtered[0].startsWith(partialUnit.toLowerCase()) + ) { + suggestions.push({ + name: `${num} ${filtered[0]} and `, + value: `${num} ${filtered[0]} and `, + }); + } + + return ctx.respond(suggestions); + } + + const filtered = timeUnits.filter((unit) => + unit.toLowerCase().includes(focus.toLowerCase()) + ); + return ctx.respond( + filtered.map((unit) => ({ + name: unit, + value: unit, + })) + ); + }, + }, + }, ], execute: async (ctx, { deps }) => { - const item = ctx.options.getString("item") - const timeLeftString = ctx.options.getString("time", true) + const item = ctx.options.getString("item"); + const timeLeftString = ctx.options.getString("time", true); - let timeUnit1 - let timeLeft1 - let timeUnit2 - let timeLeft2 + let timeUnit1; + let timeLeft1; + let timeUnit2; + let timeLeft2; - const [part1, part2] = timeLeftString?.split("and") - timeUnit1 = part1?.split(" ")[1] - timeLeft1 = Number(part1?.split(" ")[0]) + const [part1, part2] = timeLeftString?.split("and"); + timeUnit1 = part1?.split(" ")[1]; + timeLeft1 = Number(part1?.split(" ")[0]); - if (part2) { - const timeLeftStringPart2 = part2.replace(part2.substring(0, 1), "") - timeUnit2 = timeLeftStringPart2?.split(" ")[1] - timeLeft2 = Number(timeLeftStringPart2?.split(" ")[0]) - } + if (part2) { + const timeLeftStringPart2 = part2.replace(part2.substring(0, 1), ""); + timeUnit2 = timeLeftStringPart2?.split(" ")[1]; + timeLeft2 = Number(timeLeftStringPart2?.split(" ")[0]); + } - const startTime = new Date() + const startTime = new Date(); - let endTime: Date + let endTime: Date; - const secondNames = ['seconds', 'second', 'sec', 'secs'] - const minuteNames = ['minutes', 'minute', 'min', 'mins'] - const hourNames = ['hours', 'hour', 'hr', 'hrs'] - const dayNames = ['days', 'day'] + const secondNames = ["seconds", "second", "sec", "secs"]; + const minuteNames = ["minutes", "minute", "min", "mins"]; + const hourNames = ["hours", "hour", "hr", "hrs"]; + const dayNames = ["days", "day"]; - endTime = add(startTime, { - timeUnit1: timeLeft1, - timeUnit2: timeLeft2 - }) + endTime = add(startTime, { + seconds: secondNames.includes(timeUnit1!) + ? timeLeft1 + : secondNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + minutes: minuteNames.includes(timeUnit1!) + ? timeLeft1 + : minuteNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + hours: hourNames.includes(timeUnit1!) + ? timeLeft1 + : hourNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + days: dayNames.includes(timeUnit1!) + ? timeLeft1 + : dayNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + }); - // This if chain uses date-fns to correctly calculate the time allocated to the giveaway based on what the - // user types (seconds, minutes, etc.) + const endTimeStamp: string = ``; + const endTimeStamp2 = new Timestamp(endTime.getTime()).timestamp; - // if the time unit before the "and" is "seconds" or one of the other entries in the secondNames array, add the time entered - // to the startTime and save that in the endTime - if (secondNames.includes(timeUnit1!)) { - endTime = endTime === startTime ? addSeconds(startTime, timeLeft1) : addSeconds(endTime, timeLeft1) - } - // if the time unit after the "and" is "seconds" or one of the other entries in the secondNames array, add the time entered - // to the startTime and save that in the endTime - if (secondNames.includes(timeUnit2!)) { - endTime = endTime === startTime ? addSeconds(startTime, timeLeft2!) : addSeconds(endTime, timeLeft2!) - } - // if the time unit before the "and" is "minutes" or one of the other entries in the minuteNames array, add the time entered - // to the startTime and save that in the endTime - if (minuteNames.includes(timeUnit1!)) { - endTime = endTime === startTime ? addMinutes(startTime, timeLeft1) : addMinutes(endTime, timeLeft1) - } - // if the time unit after the "and" is "minutes" or one of the other entries in the minuteNames array, add the time entered - // to the startTime and save that in the endTime - if (minuteNames.includes(timeUnit2!)) { - endTime = endTime === startTime ? addMinutes(startTime, timeLeft2!) : addMinutes(endTime, timeLeft2!) - } - // if the time unit before the "and" is "hours" or one of the other entries in the hourNames array, add the time entered - // to the startTime and save that in the endTime - if (hourNames.includes(timeUnit1!)) { - endTime = endTime === startTime ? addHours(startTime, timeLeft1) : addHours(endTime, timeLeft1) - } - // if the time unit after the "and" is "hours" or one of the other entries in the hourNames array, add the time entered - // to the startTime and save that in the endTime - if (hourNames.includes(timeUnit2!)) { - endTime = endTime === startTime ? addHours(startTime, timeLeft2!) : addHours(endTime, timeLeft2!) - } - // if the time unit before the "and" is "days" or one of the other entries in the dayNames array, add the time entered - // to the startTime and save that in the endTime - if (dayNames.includes(timeUnit1!)) { - endTime = endTime === startTime ? addDays(startTime, timeLeft1) : addDays(endTime, timeLeft1) - } - // if the time unit after the "and" is "days" or one of the other entries in the dayNames array, add the time entered - // to the startTime and save that in the endTime - if (dayNames.includes(timeUnit2!)) { - endTime = endTime === startTime ? addDays(startTime, timeLeft2!) : addDays(endTime, timeLeft2!) - } - - const endTimeStamp: string = `` - const endTimeStamp2 = new Timestamp(endTime.getTime()).timestamp - - let embed = new EmbedBuilder() + let embed = new EmbedBuilder() .setTitle(`🥳 ${item} giveaway 🥳`) - .setDescription('React to enter the giveaway!') - .addFields( - {name: '\u200b', value: `Hosted by: <@${ctx.userId}>`}, - {name: '\u200b', value: `Ends: ${new Timestamp(Number(endTimeStamp2)).getRelativeTime()} (${endTimeStamp})`} - ) - + .setDescription("Click the button to enter the giveaway!") + .addFields({ + name: "\u200b", + value: `Hosted by: <@${ctx.userId}> + Entries: 0 + Ends: ${new Timestamp(Number(endTimeStamp2)).getRelativeTime()} (${endTimeStamp})`, + }); - await ctx.reply({ + await ctx + .reply({ embeds: [embed], - }).then(embedMessage => { - db.prepare(`INSERT INTO giveaway_message(message_id, host_id) VALUES (?, ?)`).run(embedMessage.id, ctx.userId) + components: [setupRows()], + }) + .then((embedMessage) => { + let giveawayEnded = false; - embedMessage.react("🎉") + const startTimeStamp = new Timestamp(startTime.getTime()).timestamp; - //checks if author reacted to itself - const selfReactionInterval = setInterval(() => { - const userReactions = embedMessage.reactions.cache.filter(reaction => reaction.users.cache.has(ctx.userId)); - - for (const reaction of userReactions.values()) { - reaction.users.remove(ctx.userId); - ctx.interaction.followUp({content: "As the host of the giveaway, you cannot enter it.", ephemeral: true}) - } - }, 1000) + db.prepare( + `INSERT INTO giveaway_message(message_id, start_timestamp, end_time, host_id, item) VALUES (?, ?, ?, ?, ?)` + ).run(embedMessage.id, startTimeStamp, endTime.getTime(), ctx.userId, item); - let intervalTime = endTime.getTime() - startTime.getTime() + // test entries + // db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?, ?)`).run([embedMessage.id, 1]) + // db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?, ?)`).run([embedMessage.id, 2]) + // db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?, ?)`).run([embedMessage.id, 3]) + // db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?, ?)`).run([embedMessage.id, 4]) + // db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?, ?)`).run([embedMessage.id, 5]) - setTimeout(() => { - const stmt = db.prepare(`SELECT * FROM entries WHERE message_id = ?`).all(embedMessage.id) + function endGiveaway() { + const giveawayData = db + .prepare(`SELECT item FROM giveaway_message WHERE message_id = ?`) + .get(embedMessage.id); + const item = giveawayData?.item ?? "Unknown item"; - let winnerIndex = Math.floor(Math.random() * stmt.length) + const stmt = db + .prepare(`SELECT * FROM entries WHERE message_id = ?`) + .all(embedMessage.id); - if (stmt.length > 0 && stmt[winnerIndex].user_id !== ctx.userId) { - const winnerId = stmt[winnerIndex].user_id + const eligible = stmt.filter( + (entry: { user_id: string }) => + entry.user_id !== embedMessage.author.id && + entry.user_id !== ctx.user.id + ); - embedMessage.edit({content: `Congratulations <@${winnerId}> on winning the ${item} giveaway!`, embeds: []}) - } - else if (stmt.length > 1 && stmt[winnerIndex].user_id === ctx.userId) { - while (stmt[winnerIndex].user_id === ctx.userId) { - winnerIndex = Math.floor(Math.random() * stmt.length) + let winnerIndex = Math.floor(Math.random() * eligible.length); + + if (eligible.length > 0 && eligible[winnerIndex].user_id !== ctx.userId) { + const winnerId = stmt[winnerIndex].user_id; + + embedMessage.edit({ + content: `Congratulations <@${winnerId}> on winning the ${item} giveaway! ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } else if ( + eligible.length > 1 && + eligible[winnerIndex].user_id === ctx.userId + ) { + while (eligible[winnerIndex].user_id === ctx.userId) { + winnerIndex = Math.floor(Math.random() * eligible.length); } - const winnerId = stmt[winnerIndex].user_id + const winnerId = eligible[winnerIndex].user_id; - embedMessage.edit({content: `Congratulations <@${winnerId}> on winning the ${item} giveaway!`, embeds: []}) + embedMessage.edit({ + content: `Congratulations <@${winnerId}> on winning the ${item} giveaway! ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } else if ( + (eligible.length === 1 && eligible[winnerIndex].user_id === ctx.userId) || + eligible.length === 0 + ) { + embedMessage.edit({ + content: `Couldn't determine a winner: Not enough eligible users. ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } + } + let interval = setInterval(() => { + const giveaway = db + .prepare( + `SELECT end_time, ended FROM giveaway_message WHERE message_id = ?` + ) + .get(embedMessage.id); + if (!giveaway || giveaway.ended) { + clearInterval(interval); + return; } - else if ((stmt.length === 1 && stmt[winnerIndex].user_id === ctx.userId) || stmt.length === 0) { - embedMessage.edit({content: `Not enough eligible users`, embeds: []}) + const now = Date.now(); + if (now >= giveaway.end_time) { + endGiveaway(); + db.prepare(`DELETE FROM giveaway_message WHERE message_id = ?`).run( + embedMessage.id + ); + db.prepare(`DELETE FROM entries WHERE message_id = ?`).run(embedMessage.id); + clearInterval(interval); } - db.prepare(`DELETE FROM giveaway_message WHERE message_id = ?`).run(embedMessage.id) - db.prepare(`DELETE FROM entries WHERE message_id = ?`).run(embedMessage.id) - clearInterval(selfReactionInterval) - }, intervalTime) - }) - } -}) \ No newline at end of file + }, 1000); + }); + }, +}); + +export function discardRows() { + const discardGiveaway = new ButtonBuilder({ + customId: "discard", + label: "Discard", + style: ButtonStyle.Primary, + }); + + return new ActionRowBuilder().addComponents(discardGiveaway); +} + +export function setupRows() { + const enterGiveaway = new ButtonBuilder({ + customId: "enter", + label: "Enter Giveaway", + style: ButtonStyle.Success, + }); + const leaveGiveaway = new ButtonBuilder({ + customId: "leave", + label: "Leave Giveaway", + style: ButtonStyle.Danger, + }); + const editGiveaway = new ButtonBuilder({ + customId: "edit", + label: "Edit Giveaway", + style: ButtonStyle.Primary, + }); + const endGiveaway = new ButtonBuilder({ + customId: "end", + label: "End Giveaway", + style: ButtonStyle.Secondary, + }); + + return new ActionRowBuilder().addComponents( + enterGiveaway, + leaveGiveaway, + editGiveaway, + endGiveaway + ); +} diff --git a/src/commands/handlers/giveawayDiscard.ts b/src/commands/handlers/giveawayDiscard.ts new file mode 100644 index 0000000..8ba5dbb --- /dev/null +++ b/src/commands/handlers/giveawayDiscard.ts @@ -0,0 +1,21 @@ +import { ownerIDs } from "#constants"; +import { commandModule, CommandType } from "@sern/handler"; +import { db } from "#db"; + +export default commandModule({ + type: CommandType.Button, + name: "discard", + async execute(ctx) { + if (!ownerIDs.includes(ctx.user.id)) + return ctx.reply({ + ephemeral: true, + content: `You cannot discard the giveaway because you are not one of the owners`, + }); + + db.prepare(`DELETE FROM giveaway_message WHERE message_id = ?`).run(ctx.message.id); + db.prepare(`DELETE FROM entries WHERE message_id = ?`).run(ctx.message.id); + + ctx.message.delete(); + await ctx.reply({ ephemeral: true, content: `Giveaway discarded!` }); + }, +}); diff --git a/src/commands/handlers/giveawayEdit.ts b/src/commands/handlers/giveawayEdit.ts new file mode 100644 index 0000000..448e44f --- /dev/null +++ b/src/commands/handlers/giveawayEdit.ts @@ -0,0 +1,36 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { ownerIDs } from "#constants"; +import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from "discord.js"; + +export default commandModule({ + type: CommandType.Button, + name: "edit", + async execute(ctx) { + if (!ownerIDs.includes(ctx.user.id)) + return ctx.reply({ + ephemeral: true, + content: `You cannot edit the giveaway because you are not one of the owners`, + }); + + const modal = new ModalBuilder().setCustomId("giveawayEditModal").setTitle("Edit Giveaway"); + + const itemInput = new TextInputBuilder() + .setCustomId("item") + .setLabel("New Giveaway Item") + .setStyle(TextInputStyle.Short) + .setRequired(false); + + const timeInput = new TextInputBuilder() + .setCustomId("time") + .setLabel("New Time") + .setStyle(TextInputStyle.Short) + .setRequired(false); + + modal.addComponents( + new ActionRowBuilder().addComponents(itemInput), + new ActionRowBuilder().addComponents(timeInput) + ); + + await ctx.showModal(modal); + }, +}); diff --git a/src/commands/handlers/giveawayEditModal.ts b/src/commands/handlers/giveawayEditModal.ts new file mode 100644 index 0000000..9e19267 --- /dev/null +++ b/src/commands/handlers/giveawayEditModal.ts @@ -0,0 +1,109 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { ownerIDs } from "#constants"; +import { db } from "#db"; +import { Timestamp } from "#utils"; +import { add } from "date-fns"; +import { EmbedBuilder } from "discord.js"; + +export default commandModule({ + type: CommandType.Modal, + name: "giveawayEditModal", + async execute(ctx) { + if (!ownerIDs.includes(ctx.user.id)) + return ctx.reply({ + ephemeral: true, + content: `You cannot edit the giveaway because you are not one of the owners`, + }); + + const newItem = ctx.fields.getTextInputValue("item"); + const newTime = ctx.fields.getTextInputValue("time"); + + let timeUnit1; + let timeLeft1; + let timeUnit2; + let timeLeft2; + + const [part1, part2] = newTime?.split("and"); + timeUnit1 = part1?.split(" ")[1]; + timeLeft1 = Number(part1?.split(" ")[0]); + + if (part2) { + const timeLeftStringPart2 = part2.replace(part2.substring(0, 1), ""); + timeUnit2 = timeLeftStringPart2?.split(" ")[1]; + timeLeft2 = Number(timeLeftStringPart2?.split(" ")[0]); + } + + const startTime = new Date(); + + let endTime: Date; + + const secondNames = ["seconds", "second", "sec", "secs"]; + const minuteNames = ["minutes", "minute", "min", "mins"]; + const hourNames = ["hours", "hour", "hr", "hrs"]; + const dayNames = ["days", "day"]; + + endTime = add(startTime, { + seconds: secondNames.includes(timeUnit1!) + ? timeLeft1 + : secondNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + minutes: minuteNames.includes(timeUnit1!) + ? timeLeft1 + : minuteNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + hours: hourNames.includes(timeUnit1!) + ? timeLeft1 + : hourNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + days: dayNames.includes(timeUnit1!) + ? timeLeft1 + : dayNames.includes(timeUnit2!) + ? timeLeft2 + : 0, + }); + if (endTime.getTime() - startTime.getTime() <= 0) + return ctx.reply({ + content: "Please try again with a valid time.", + ephemeral: true, + }); + + const endTimeStamp: string = ``; + const endTimeStamp2 = new Timestamp(endTime.getTime()).timestamp; + + db.prepare(`UPDATE giveaway_message SET item = ? WHERE message_id = ?`).run( + newItem, + ctx.message?.id + ); + db.prepare(`UPDATE giveaway_message SET end_time = ? WHERE message_id = ?`).run( + endTime.getTime(), + ctx.message?.id + ); + + await ctx.reply({ content: "Giveaway updated!", ephemeral: true }); + + const message = await ctx.channel?.messages.fetch(ctx.message!.id); + const giveaway = db + .prepare(`SELECT item, end_time FROM giveaway_message WHERE message_id = ?`) + .get(ctx.message?.id); + + const entryCount = db + .prepare(`SELECT COUNT(*) as count FROM entries WHERE message_id = ?`) + .get(ctx.message!.id).count; + + const newEmbed = EmbedBuilder.from(message!.embeds[0]) + .setTitle(`🥳 ${giveaway.item} giveaway 🥳`) + .spliceFields(0, 1, { + name: "\u200b", + value: `Hosted by: <@${ + message!.interaction?.user.id + }>\nEntries: ${entryCount}\nEnds: ${new Timestamp( + Number(endTimeStamp2) + ).getRelativeTime()} (${endTimeStamp})`, + }); + + await message!.edit({ embeds: [newEmbed] }); + }, +}); diff --git a/src/commands/handlers/giveawayEnd.ts b/src/commands/handlers/giveawayEnd.ts new file mode 100644 index 0000000..8956d54 --- /dev/null +++ b/src/commands/handlers/giveawayEnd.ts @@ -0,0 +1,83 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { db } from "#db"; +import { ownerIDs } from "#constants"; +import { discardRows } from "#commands/giveaway.js"; + +export default commandModule({ + type: CommandType.Button, + name: "end", + async execute(ctx) { + if (!ownerIDs.includes(ctx.user.id)) + return ctx.reply({ + ephemeral: true, + content: `You cannot end the giveaway because you are not one of the owners`, + }); + + const message = db + .prepare(`SELECT * FROM giveaway_message WHERE message_id = ?`) + .get(ctx.message.id); + + if (Date.now() > message.end_time) { + await ctx.reply({ + ephemeral: true, + content: `This giveaway has already ended!`, + }); + return; + } + await ctx.reply({ + ephemeral: true, + content: `Giveaway ended by <@${ctx.user.id}>`, + }); + + let giveawayEnded = false; + let item = message.item; + + const stmt = db.prepare(`SELECT * FROM entries WHERE message_id = ?`).all(ctx.message.id); + + const eligible = stmt.filter( + (entry: { user_id: string }) => + entry.user_id !== ctx.message.author.id && entry.user_id !== ctx.user.id + ); + + let winnerIndex = Math.floor(Math.random() * eligible.length); + + if (eligible.length > 0 && eligible[winnerIndex].user_id !== ctx.user.id) { + const winnerId = stmt[winnerIndex].user_id; + + ctx.message.edit({ + content: `Congratulations <@${winnerId}> on winning the ${item} giveaway! ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } else if (eligible.length > 1 && eligible[winnerIndex].user_id === ctx.user.id) { + while (eligible[winnerIndex].user_id === ctx.user.id) { + winnerIndex = Math.floor(Math.random() * eligible.length); + } + const winnerId = eligible[winnerIndex].user_id; + + ctx.message.edit({ + content: `Congratulations <@${winnerId}> on winning the ${item} giveaway! ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } else if ( + (eligible.length === 1 && eligible[winnerIndex].user_id === ctx.user.id) || + eligible.length === 0 + ) { + ctx.message.edit({ + content: `Couldn't determine a winner: Not enough eligible users. ${eligible.length} users entered`, + embeds: [], + components: [discardRows()], + }); + giveawayEnded = true; + } + + if (giveawayEnded) { + db.prepare(`UPDATE giveaway_message SET ended = 1 WHERE message_id = ?`).run( + ctx.message.id + ); + } + }, +}); diff --git a/src/commands/handlers/giveawayEnter.ts b/src/commands/handlers/giveawayEnter.ts new file mode 100644 index 0000000..d88e995 --- /dev/null +++ b/src/commands/handlers/giveawayEnter.ts @@ -0,0 +1,61 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { db } from "#db"; +import { EmbedBuilder } from "discord.js"; +import { Timestamp } from "#utils"; + +export default commandModule({ + type: CommandType.Button, + name: "enter", + async execute(ctx) { + const messages = db.prepare(`SELECT * FROM giveaway_message`).all(); + + messages.map(async (message: { message_id: string; end_time: number; host_id: string }) => { + if (ctx.message.id === message.message_id && !ctx.user.bot) { + const host = db + .prepare(`SELECT host_id FROM giveaway_message WHERE message_id = ?`) + .get(message.message_id); + if (host && host.host_id === ctx.user.id) { + await ctx.reply({ + ephemeral: true, + content: `You cannot enter the giveaway as the host!`, + }); + return; + } + + const checkUser = db + .prepare( + `SELECT COUNT(*) as count FROM entries WHERE message_id = ? AND user_id = ?` + ) + .get(message.message_id, ctx.user.id); + + if (checkUser.count === 0) { + db.prepare(`INSERT INTO entries(message_id, user_id) VALUES (?, ?)`).run([ + message.message_id, + ctx.user.id, + ]); + await ctx.reply({ ephemeral: true, content: `Giveaway entered!` }); + } else await ctx.reply({ ephemeral: true, content: `You are already entered!` }); + + const entryCount = db + .prepare(`SELECT COUNT(*) as count FROM entries WHERE message_id = ?`) + .get(message.message_id).count; + + const endTime = message.end_time; + + const endTimeStamp: string = ``; + const endTimeStamp2 = new Timestamp(endTime).timestamp; + + const embed = EmbedBuilder.from(ctx.message.embeds[0]).spliceFields(0, 1, { + name: "\u200b", + value: `Hosted by: <@${host?.host_id ?? "unknown"}> + Entries: ${entryCount} + Ends: ${new Timestamp( + Number(endTimeStamp2) + ).getRelativeTime()} (${endTimeStamp})`, + }); + + await ctx.message.edit({ embeds: [embed] }); + } + }); + }, +}); diff --git a/src/commands/handlers/giveawayLeave.ts b/src/commands/handlers/giveawayLeave.ts new file mode 100644 index 0000000..fc032e6 --- /dev/null +++ b/src/commands/handlers/giveawayLeave.ts @@ -0,0 +1,56 @@ +import { commandModule, CommandType } from "@sern/handler"; +import { db } from "#db"; +import { Timestamp } from "#utils"; +import { EmbedBuilder } from "discord.js"; + +export default commandModule({ + type: CommandType.Button, + name: "leave", + async execute(ctx) { + const deletedId = ctx.user.id; + + const message = db + .prepare(`SELECT * FROM giveaway_message WHERE message_id = ?`) + .get(ctx.message.id); + + const host = db + .prepare(`SELECT host_id FROM giveaway_message WHERE message_id = ?`) + .get(message.message_id); + + const checkUser = db + .prepare(`SELECT COUNT(*) as count FROM entries WHERE message_id = ? AND user_id = ?`) + .get(message.message_id, ctx.user.id); + + if (ctx.message.id === message.message_id && checkUser.count == 1) { + db.prepare(`DELETE FROM entries WHERE message_id = ? AND user_id = ?`).run( + message.message_id, + deletedId + ); + await ctx.reply({ ephemeral: true, content: `Giveaway left` }); + } else + await ctx.reply({ + ephemeral: true, + content: `You cannot leave a giveaway you were not entered in`, + }); + + const entryCount = db + .prepare(`SELECT COUNT(*) as count FROM entries WHERE message_id = ?`) + .get(message.message_id).count; + + const endTime = message.end_time; + + const endTimeStamp: string = ``; + const endTimeStamp2 = new Timestamp(endTime).timestamp; + + const embed = EmbedBuilder.from(ctx.message.embeds[0]).spliceFields(0, 1, { + name: "\u200b", + value: `Hosted by: <@${host?.host_id ?? "unknown"}> + Entries: ${entryCount} + Ends: ${new Timestamp( + Number(endTimeStamp2) + ).getRelativeTime()} (${endTimeStamp})`, + }); + + await ctx.message.edit({ embeds: [embed] }); + }, +}); diff --git a/src/events/embedReact.ts b/src/events/embedReact.ts deleted file mode 100644 index 10bd8b0..0000000 --- a/src/events/embedReact.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { discordEvent } from "@sern/handler"; -import { db } from "../utils/db.js" - -export default discordEvent({ - name: 'messageReactionAdd', - execute: async (reaction, potentialWinners) => { - const startTime = reaction.message.createdTimestamp - - const messages = db.prepare(`SELECT * FROM giveaway_message`).all() - - messages.map((message: { message_id: string, host_id: string }) => { - if (reaction.emoji.name === '🎉' && reaction.message.id === message.message_id && !potentialWinners.bot && message.host_id !== potentialWinners.id) { - const checkUser = db.prepare(`SELECT COUNT(*) as count FROM entries WHERE message_id = ? AND user_id = ?`).get(message.message_id, potentialWinners.id); - - if (checkUser.count === 0) { - db.prepare(`INSERT INTO entries(message_id, timestamp, user_id) VALUES (?, ?, ?)`).run([message.message_id, startTime, potentialWinners.id]) - } - } - }) - } -}) \ No newline at end of file diff --git a/src/events/embedRemoveReact.ts b/src/events/embedRemoveReact.ts deleted file mode 100644 index 7615866..0000000 --- a/src/events/embedRemoveReact.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { discordEvent } from "@sern/handler"; -import { db } from "../utils/db.js" - -export default discordEvent({ - name: 'messageReactionRemove', - execute: async (reaction, deletedEntry) => { - const deletedId = deletedEntry.id - - const message = db.prepare(`SELECT message_id FROM giveaway_message WHERE message_id = ?`).get(reaction.message.id) - - if (reaction.emoji.name === '🎉' && reaction.message.id === message.message_id) { - db.prepare(`DELETE FROM entries WHERE message_id = ? AND user_id = ?`).run(message.message_id, deletedId) - } - } -}) \ No newline at end of file diff --git a/src/utils/db.ts b/src/utils/db.ts index dd2cc9f..419b880 100644 --- a/src/utils/db.ts +++ b/src/utils/db.ts @@ -3,5 +3,5 @@ export const db = new Database('giveaway.db'); db.pragma('journal_mode = WAL'); -db.exec(`CREATE TABLE IF NOT EXISTS entries(message_id, timestamp, user_id)`); -db.exec(`CREATE TABLE IF NOT EXISTS giveaway_message(message_id, host_id)`) \ No newline at end of file +db.exec(`CREATE TABLE IF NOT EXISTS entries(message_id, user_id)`); +db.exec(`CREATE TABLE IF NOT EXISTS giveaway_message(message_id, start_timestamp, end_time, host_id, item, ended DEFAULT 0)`) \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d5b76a9..dfd8258 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,9 @@ "paths": { "#plugins": ["src/plugins/index.js"], "#utils": ["src/utils/index.js"], - "#constants": ["src/constants.js"] + "#constants": ["src/constants.js"], + "#db": ["src/utils/db.js"], + "#commands/*": ["src/commands/*"], } } }