mirror of
https://github.com/sern-handler/sern-community
synced 2026-06-06 01:16:57 +00:00
feat: giveaway improvements (#57)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 = `<t:${Math.floor(endTime!.getTime() / 1000)}:f>`;
|
||||
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 = `<t:${Math.floor(endTime!.getTime() / 1000)}:f>`
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
}, 1000);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export function discardRows() {
|
||||
const discardGiveaway = new ButtonBuilder({
|
||||
customId: "discard",
|
||||
label: "Discard",
|
||||
style: ButtonStyle.Primary,
|
||||
});
|
||||
|
||||
return new ActionRowBuilder<ButtonBuilder>().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<ButtonBuilder>().addComponents(
|
||||
enterGiveaway,
|
||||
leaveGiveaway,
|
||||
editGiveaway,
|
||||
endGiveaway
|
||||
);
|
||||
}
|
||||
|
||||
21
src/commands/handlers/giveawayDiscard.ts
Normal file
21
src/commands/handlers/giveawayDiscard.ts
Normal file
@@ -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!` });
|
||||
},
|
||||
});
|
||||
36
src/commands/handlers/giveawayEdit.ts
Normal file
36
src/commands/handlers/giveawayEdit.ts
Normal file
@@ -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<TextInputBuilder>().addComponents(itemInput),
|
||||
new ActionRowBuilder<TextInputBuilder>().addComponents(timeInput)
|
||||
);
|
||||
|
||||
await ctx.showModal(modal);
|
||||
},
|
||||
});
|
||||
109
src/commands/handlers/giveawayEditModal.ts
Normal file
109
src/commands/handlers/giveawayEditModal.ts
Normal file
@@ -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 = `<t:${Math.floor(endTime!.getTime() / 1000)}:f>`;
|
||||
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] });
|
||||
},
|
||||
});
|
||||
83
src/commands/handlers/giveawayEnd.ts
Normal file
83
src/commands/handlers/giveawayEnd.ts
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
61
src/commands/handlers/giveawayEnter.ts
Normal file
61
src/commands/handlers/giveawayEnter.ts
Normal file
@@ -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 = `<t:${Math.floor(endTime! / 1000)}:f>`;
|
||||
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] });
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
56
src/commands/handlers/giveawayLeave.ts
Normal file
56
src/commands/handlers/giveawayLeave.ts
Normal file
@@ -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 = `<t:${Math.floor(endTime! / 1000)}:f>`;
|
||||
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] });
|
||||
},
|
||||
});
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -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)`)
|
||||
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)`)
|
||||
@@ -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/*"],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user