From d1d5b08fe91808b057ff807639f6cc0acac7cb48 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 12 Sep 2022 23:34:05 +0200 Subject: [PATCH 01/27] intial --- src/Constants.js | 1 + src/Giveaway.js | 28 ++++++++-- src/Manager.js | 136 ++++++++++++++++++++++++++++------------------- 3 files changed, 106 insertions(+), 59 deletions(-) diff --git a/src/Constants.js b/src/Constants.js index 31fc578e..8281076e 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -167,6 +167,7 @@ exports.GiveawaysManagerOptions = { embedColor: '#FF0000', embedColorEnd: '#000000', reaction: 'πŸŽ‰', + buttons: { join: null, leave: null }, lastChance: { enabled: false, content: '⚠️ **LAST CHANCE TO ENTER !** ⚠️', diff --git a/src/Giveaway.js b/src/Giveaway.js index 444e47ba..8f07fe89 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -122,6 +122,8 @@ class Giveaway extends EventEmitter { * @type {Discord.MessageMentionOptions} */ this.allowedMentions = options.allowedMentions; + this.buttons = options.buttons ?? null; + this.entrantIds = options.entrantIds ?? this.buttons ? [] : null; /** * The giveaway data. * @type {GiveawayData} @@ -179,9 +181,11 @@ class Giveaway extends EventEmitter { /** * The emoji used for the reaction on the giveaway message. - * @type {Discord.EmojiIdentifierResolvable} + * @type {?Discord.EmojiIdentifierResolvable} */ get reaction() { + if (this.buttons) return null; + if (!this.options.reaction && this.message) { const emoji = Discord.resolvePartialEmoji(this.manager.options.default.reaction); if (!this.message.reactions.cache.has(emoji.id ?? emoji.name)) { @@ -239,7 +243,7 @@ class Giveaway extends EventEmitter { /** * If the giveaway is a drop, or not. - * Drop means that if the amount of valid entrants to the giveaway is the same as "winnerCount" then it immediately ends. + * Drop means that if the amount of valid entrantIds to the giveaway is the same as "winnerCount" then it immediately ends. * @type {boolean} */ get isDrop() { @@ -264,6 +268,8 @@ class Giveaway extends EventEmitter { * @type {?Discord.MessageReaction} */ get messageReaction() { + if (this.buttons) return null; + const emoji = Discord.resolvePartialEmoji(this.reaction); return ( this.message?.reactions.cache.find((r) => @@ -296,7 +302,7 @@ class Giveaway extends EventEmitter { } /** - * The raw giveaway object for this giveaway. + * The raw giveaway object for this giveaway. This is what is stored in the database. * @type {GiveawayData} */ get data() { @@ -326,12 +332,14 @@ class Giveaway extends EventEmitter { ? this.options.bonusEntries || undefined : serialize(this.options.bonusEntries), reaction: this.options.reaction, + buttons: this.options.buttons.join ? this.options.buttons : undefined, winnerIds: this.winnerIds.length ? this.winnerIds : undefined, extraData: this.extraData, lastChance: this.options.lastChance, pauseOptions: this.options.pauseOptions, isDrop: this.options.isDrop || undefined, - allowedMentions: this.allowedMentions + allowedMentions: this.allowedMentions, + entrantIds: this.entrantIds ?? undefined }; } @@ -448,11 +456,21 @@ class Giveaway extends EventEmitter { } /** - * Fetches all users of the giveaway reaction, except bots, if not otherwise specified. + * Fetches all valid users which entered the giveaway * @returns {Promise>} The collection of reaction users. */ async fetchAllEntrants() { return new Promise(async (resolve, reject) => { + if (this.entrantIds) { + const users = await Promise.all( + this.entrantIds.map(async (id) => { + const user = await this.client.users.fetch(id).catch(() => {}); + return [id, user]; + }) + ); + return resolve(new Discord.Collection(users)); + } + const message = await this.fetchMessage().catch((err) => reject(err)); if (!message) return; this.message = message; diff --git a/src/Manager.js b/src/Manager.js index 046dfe21..3e398ca2 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -213,7 +213,7 @@ class GiveawaysManager extends EventEmitter { return new Promise(async (resolve, reject) => { if (!this.ready) return reject('The manager is not ready yet.'); if (!channel?.id || !channel.isTextBased()) { - return reject(`channel is not a valid text based channel. (val=${channel})`); + return reject(`"channel" is not a valid text based channel. (val=${channel})`); } if (channel.isThread() && !channel.sendable) { return reject( @@ -221,16 +221,19 @@ class GiveawaysManager extends EventEmitter { ); } if (typeof options.prize !== 'string' || (options.prize = options.prize.trim()).length > 256) { - return reject(`options.prize is not a string or longer than 256 characters. (val=${options.prize})`); + return reject(`"options.prize" is not a string or longer than 256 characters. (val=${options.prize})`); } if (!Number.isInteger(options.winnerCount) || options.winnerCount < 1) { - return reject(`options.winnerCount is not a positive integer. (val=${options.winnerCount})`); + return reject(`"options.winnerCount" is not a positive integer. (val=${options.winnerCount})`); } if (options.isDrop && typeof options.isDrop !== 'boolean') { - return reject(`options.isDrop is not a boolean. (val=${options.isDrop})`); + return reject(`"options.isDrop" is not a boolean. (val=${options.isDrop})`); } if (!options.isDrop && (!Number.isFinite(options.duration) || options.duration < 1)) { - return reject(`options.duration is not a positive number. (val=${options.duration})`); + return reject(`"options.duration is not a positive number. (val=${options.duration})`); + } + if (Discord.resolvePartialEmoji(options.reaction) && this.options.buttons?.join) { + return reject(`Both "options.reaction" and "options.buttons.join" are set.`); } const giveaway = new Giveaway(this, { @@ -248,6 +251,12 @@ class GiveawaysManager extends EventEmitter { thumbnail: typeof options.thumbnail === 'string' ? options.thumbnail : undefined, image: typeof options.image === 'string' ? options.image : undefined, reaction: Discord.resolvePartialEmoji(options.reaction) ? options.reaction : undefined, + buttons: + !Discord.resolvePartialEmoji(this.options.defaults.reaction) && + !Discord.resolvePartialEmoji(options.reaction) && + options.buttons?.join + ? deepmerge(this.options.buttons ?? {}, options.buttons) + : undefined, botsCanWin: typeof options.botsCanWin === 'boolean' ? options.botsCanWin : undefined, exemptPermissions: Array.isArray(options.exemptPermissions) ? options.exemptPermissions : undefined, exemptMembers: typeof options.exemptMembers === 'function' ? options.exemptMembers : undefined, @@ -277,15 +286,25 @@ class GiveawaysManager extends EventEmitter { const message = await channel.send({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [embed], - allowedMentions: giveaway.allowedMentions + allowedMentions: giveaway.allowedMentions, + components: giveaway.buttons + ? [ + new Discord.ActionRowBuilder().addComponents( + giveaway.fillInComponents([giveaway.buttons.join, giveaway.buttons.leave]) + ) + ] + : undefined }); giveaway.messageId = message.id; - const reaction = await message.react(giveaway.reaction); - giveaway.message = reaction.message; + + const reaction = giveaway.reaction ? await message.react(giveaway.reaction) : null; + + giveaway.message = reaction?.message ?? message; this.giveaways.push(giveaway); await this.saveGiveaway(giveaway.messageId, giveaway.data); resolve(giveaway); - if (giveaway.isDrop) { + + if (giveaway.isDrop && !giveaway.buttons) { reaction.message .awaitReactions({ filter: async (r, u) => @@ -612,54 +631,63 @@ class GiveawaysManager extends EventEmitter { /** * @ignore - * @param {any} packet */ - async _handleRawPacket(packet) { - if (!['MESSAGE_REACTION_ADD', 'MESSAGE_REACTION_REMOVE'].includes(packet.t)) return; - if (packet.d.user_id === this.client.user.id) return; - - const giveaway = this.giveaways.find((g) => g.messageId === packet.d.message_id); - if (!giveaway || (giveaway.ended && packet.t === 'MESSAGE_REACTION_REMOVE')) return; - - const guild = - this.client.guilds.cache.get(packet.d.guild_id) || - (await this.client.guilds.fetch(packet.d.guild_id).catch(() => {})); - if (!guild || !guild.available) return; - - const member = await guild.members.fetch(packet.d.user_id).catch(() => {}); - if (!member) return; + async _handleEvents() { + const checkForDropEnd = async (giveaway) => { + const users = await giveaway.fetchAllEntrants().catch(() => {}); + + let validUsers = 0; + for (const user of [...(users?.values() || [])]) { + if (await giveaway.checkWinnerEntry(user)) validUsers++; + if (validUsers === giveaway.winnerCount) { + await this.end(giveaway.messageId).catch(() => {}); + break; + } + } + }; - const channel = await this.client.channels.fetch(packet.d.channel_id).catch(() => {}); - if (!channel) return; + this.client.on('messageReactionAdd', async (messageReaction, user) => { + const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); + if (!giveaway) return; + if (!messageReaction.message.guild?.available) return; + if (!messageReaction.message.channel.viewable) return; + const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); + if (!member) return; + const emoji = Discord.resolvePartialEmoji(giveaway.reaction); + if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + if (giveaway.ended) return this.emit('endedGiveawayReactionAdded', giveaway, member, messageReaction); + this.emit('giveawayReactionAdded', giveaway, member, messageReaction); + + if (giveaway.isDrop && messageReaction.count - 1 >= giveaway.winnerCount) await checkForDropEnd(giveaway); + }); - const message = await channel.messages.fetch(packet.d.message_id).catch(() => {}); - if (!message) return; + this.client.on('messageReactionRemove', async (messageReaction, user) => { + const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); + if (!giveaway) return; + if (!messageReaction.message.guild?.available) return; + if (!messageReaction.message.channel.viewable) return; + const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); + if (!member) return; + const emoji = Discord.resolvePartialEmoji(giveaway.reaction); + if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + this.emit('giveawayReactionAdded', giveaway, member, messageReaction); + }); - const emoji = Discord.resolvePartialEmoji(giveaway.reaction); - const reaction = message.reactions.cache.find((r) => - [r.emoji.name, r.emoji.id].filter(Boolean).includes(emoji?.id ?? emoji?.name) - ); - if (!reaction || reaction.emoji.name !== packet.d.emoji.name) return; - if (reaction.emoji.id && reaction.emoji.id !== packet.d.emoji.id) return; - - if (packet.t === 'MESSAGE_REACTION_ADD') { - if (giveaway.ended) return this.emit('endedGiveawayReactionAdded', giveaway, member, reaction); - this.emit('giveawayReactionAdded', giveaway, member, reaction); - - // Only end drops if the amount of available, valid winners is equal to the winnerCount - if (giveaway.isDrop && reaction.count - 1 >= giveaway.winnerCount) { - const users = await giveaway.fetchAllEntrants().catch(() => {}); - - let validUsers = 0; - for (const user of [...(users?.values() || [])]) { - if (await giveaway.checkWinnerEntry(user)) validUsers++; - if (validUsers === giveaway.winnerCount) { - await this.end(giveaway.messageId).catch(() => {}); - break; - } - } - } - } else this.emit('giveawayReactionRemoved', giveaway, member, reaction); + this.client.on('interactionCreated', async (interaction) => { + if (!interaction.isButton()) return; + const giveaway = this.giveaways.find((g) => g.messageId === interaction.message.id); + if (!giveaway) return; + if (!interaction.guild?.available) return; + if (!interaction.channel.viewable) return; + if (giveaway.buttons.join?.customId === interaction.customId) + this.emit('giveawayJoined', giveaway, interaction.member, interaction); + else if (giveaway.buttons.leave?.customId === interaction.customId) + this.emit('giveawayLeft', giveaway, interaction.member, interaction); + else return; + giveaway.entrantIds.push(interaction.member.id); + + if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) await checkForDropEnd(giveaway); + }); } /** @@ -700,7 +728,7 @@ class GiveawaysManager extends EventEmitter { for (const giveaway of endedGiveaways) await this.deleteGiveaway(giveaway.messageId); } - this.client.on('raw', (packet) => this._handleRawPacket(packet)); + this._handleEvents(); } } From 66b36b707f5d68fd6d56c9cfe7369e82c74d5e3e Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 16:52:29 +0200 Subject: [PATCH 02/27] fix --- src/Manager.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 3e398ca2..223fa24e 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -252,9 +252,7 @@ class GiveawaysManager extends EventEmitter { image: typeof options.image === 'string' ? options.image : undefined, reaction: Discord.resolvePartialEmoji(options.reaction) ? options.reaction : undefined, buttons: - !Discord.resolvePartialEmoji(this.options.defaults.reaction) && - !Discord.resolvePartialEmoji(options.reaction) && - options.buttons?.join + !Discord.resolvePartialEmoji(options.reaction) && options.buttons?.join ? deepmerge(this.options.buttons ?? {}, options.buttons) : undefined, botsCanWin: typeof options.botsCanWin === 'boolean' ? options.botsCanWin : undefined, @@ -288,11 +286,9 @@ class GiveawaysManager extends EventEmitter { embeds: [embed], allowedMentions: giveaway.allowedMentions, components: giveaway.buttons - ? [ - new Discord.ActionRowBuilder().addComponents( - giveaway.fillInComponents([giveaway.buttons.join, giveaway.buttons.leave]) - ) - ] + ? giveaway.fillInComponents([ + { components: [giveaway.buttons.join, giveaway.buttons.leave].filter(Boolean) } + ]) : undefined }); giveaway.messageId = message.id; From 82eb1525625a9a15800e27432ff849f4547456a5 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:25:45 +0200 Subject: [PATCH 03/27] use discord enums for events --- src/Manager.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 223fa24e..054ac7a3 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -642,7 +642,7 @@ class GiveawaysManager extends EventEmitter { } }; - this.client.on('messageReactionAdd', async (messageReaction, user) => { + this.client.on(Discord.Events.MessageReactionAdd, async (messageReaction, user) => { const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); if (!giveaway) return; if (!messageReaction.message.guild?.available) return; @@ -657,7 +657,7 @@ class GiveawaysManager extends EventEmitter { if (giveaway.isDrop && messageReaction.count - 1 >= giveaway.winnerCount) await checkForDropEnd(giveaway); }); - this.client.on('messageReactionRemove', async (messageReaction, user) => { + this.client.on(Discord.Events.MessageReactionRemove, async (messageReaction, user) => { const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); if (!giveaway) return; if (!messageReaction.message.guild?.available) return; @@ -669,7 +669,7 @@ class GiveawaysManager extends EventEmitter { this.emit('giveawayReactionAdded', giveaway, member, messageReaction); }); - this.client.on('interactionCreated', async (interaction) => { + this.client.on(Discord.Events.InteractionCreate, async (interaction) => { if (!interaction.isButton()) return; const giveaway = this.giveaways.find((g) => g.messageId === interaction.message.id); if (!giveaway) return; @@ -693,7 +693,9 @@ class GiveawaysManager extends EventEmitter { async _init() { let rawGiveaways = await this.getAllGiveaways(); - await (this.client.readyAt ? Promise.resolve() : new Promise((resolve) => this.client.once('ready', resolve))); + await (this.client.readyAt + ? Promise.resolve() + : new Promise((resolve) => this.client.once(Discord.Events.ClientReady, resolve))); // Filter giveaways for each shard if (this.client.shard && this.client.guilds.cache.size) { From 5d0dbbc9d8603b4e8a4aeadbfae70e1bff4c7208 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:27:03 +0200 Subject: [PATCH 04/27] add spaces for visibility --- src/Manager.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 054ac7a3..b73480f2 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -651,6 +651,7 @@ class GiveawaysManager extends EventEmitter { if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + if (giveaway.ended) return this.emit('endedGiveawayReactionAdded', giveaway, member, messageReaction); this.emit('giveawayReactionAdded', giveaway, member, messageReaction); @@ -666,6 +667,7 @@ class GiveawaysManager extends EventEmitter { if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + this.emit('giveawayReactionAdded', giveaway, member, messageReaction); }); @@ -675,11 +677,13 @@ class GiveawaysManager extends EventEmitter { if (!giveaway) return; if (!interaction.guild?.available) return; if (!interaction.channel.viewable) return; + if (giveaway.buttons.join?.customId === interaction.customId) this.emit('giveawayJoined', giveaway, interaction.member, interaction); else if (giveaway.buttons.leave?.customId === interaction.customId) this.emit('giveawayLeft', giveaway, interaction.member, interaction); else return; + giveaway.entrantIds.push(interaction.member.id); if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) await checkForDropEnd(giveaway); @@ -693,9 +697,7 @@ class GiveawaysManager extends EventEmitter { async _init() { let rawGiveaways = await this.getAllGiveaways(); - await (this.client.readyAt - ? Promise.resolve() - : new Promise((resolve) => this.client.once(Discord.Events.ClientReady, resolve))); + await (this.client.readyAt ? Promise.resolve() : new Promise((resolve) => this.client.once(Discord.Events.InteractionCreate, resolve))); // Filter giveaways for each shard if (this.client.shard && this.client.guilds.cache.size) { From cf130b52d6492eb848a2585755f56e61233ea0a6 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:39:33 +0200 Subject: [PATCH 05/27] properly handle leave and join --- src/Manager.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index b73480f2..708b17aa 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -678,13 +678,20 @@ class GiveawaysManager extends EventEmitter { if (!interaction.guild?.available) return; if (!interaction.channel.viewable) return; - if (giveaway.buttons.join?.customId === interaction.customId) + if ( + giveaway.buttons.join?.customId === interaction.customId && + !giveaway.entrantIds.includes(interaction.member.id) + ) { + giveaway.entrantIds.push(interaction.member.id); this.emit('giveawayJoined', giveaway, interaction.member, interaction); - else if (giveaway.buttons.leave?.customId === interaction.customId) + } else if ( + giveaway.buttons.leave?.customId === interaction.customId && + giveaway.entrantIds.includes(interaction.member.id) + ) { + const index = giveaway.entrantIds.indexOf(interaction.member.id); + giveaway.entrantIds.splice(index, 1); this.emit('giveawayLeft', giveaway, interaction.member, interaction); - else return; - - giveaway.entrantIds.push(interaction.member.id); + } else return; if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) await checkForDropEnd(giveaway); }); @@ -697,7 +704,9 @@ class GiveawaysManager extends EventEmitter { async _init() { let rawGiveaways = await this.getAllGiveaways(); - await (this.client.readyAt ? Promise.resolve() : new Promise((resolve) => this.client.once(Discord.Events.InteractionCreate, resolve))); + await (this.client.readyAt + ? Promise.resolve() + : new Promise((resolve) => this.client.once(Discord.Events.InteractionCreate, resolve))); // Filter giveaways for each shard if (this.client.shard && this.client.guilds.cache.size) { From 05f82ae13ad5463bbe89de3ace6c9b7fe08f7eaa Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:42:20 +0200 Subject: [PATCH 06/27] typo --- src/Manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Manager.js b/src/Manager.js index 708b17aa..9282442d 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -706,7 +706,7 @@ class GiveawaysManager extends EventEmitter { await (this.client.readyAt ? Promise.resolve() - : new Promise((resolve) => this.client.once(Discord.Events.InteractionCreate, resolve))); + : new Promise((resolve) => this.client.once(Discord.Events.ClientReady, resolve))); // Filter giveaways for each shard if (this.client.shard && this.client.guilds.cache.size) { From 40bd614351dfb50c378a62ee2aa362512509b84b Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 17:46:28 +0200 Subject: [PATCH 07/27] fix --- src/Giveaway.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Giveaway.js b/src/Giveaway.js index 8f07fe89..c99d4ccb 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -332,7 +332,7 @@ class Giveaway extends EventEmitter { ? this.options.bonusEntries || undefined : serialize(this.options.bonusEntries), reaction: this.options.reaction, - buttons: this.options.buttons.join ? this.options.buttons : undefined, + buttons: this.options.buttons ? this.options.buttons : undefined, winnerIds: this.winnerIds.length ? this.winnerIds : undefined, extraData: this.extraData, lastChance: this.options.lastChance, From d75b28339c615b48251051f280f41aa448c10709 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 20:00:18 +0200 Subject: [PATCH 08/27] jsdoc --- src/Constants.js | 12 ++++++++++++ src/Giveaway.js | 32 +++++++++++++++++++++++++++----- src/Manager.js | 5 +---- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Constants.js b/src/Constants.js index 8281076e..e177011b 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -61,6 +61,13 @@ exports.GiveawayMessages = { * @returns {Promise|boolean} */ +/** + * @typedef {Object} ButtonsObject + * + * @property {Discord.JSONEncodable|Discord.APIButtonComponent} join The button to join the giveaway. + * @property {Discord.JSONEncodable|Discord.APIButtonComponent} [leave] The button to leave the giveaway. + */ + /** * The start options for new giveaways. * @typedef GiveawayStartOptions @@ -76,6 +83,7 @@ exports.GiveawayMessages = { * @property {Discord.ColorResolvable} [embedColor] The color of the giveaway embed when it is running. * @property {Discord.ColorResolvable} [embedColorEnd] The color of the giveaway embed when it has ended. * @property {Discord.EmojiIdentifierResolvable} [reaction] The reaction to participate in the giveaway. + * @property {ButtonsObject} [buttons] The buttons for the giveaway. * @property {GiveawayMessages} [messages] The giveaway messages. * @property {string} [thumbnail] The URL appearing as the thumbnail on the giveaway embed. * @property {string} [image] The URL appearing as the image on the giveaway embed. @@ -155,6 +163,7 @@ exports.PauseOptions = { * @property {Discord.ColorResolvable} [default.embedColorEnd='#000000'] The color of the giveaway embeds when they have ended. * @property {Discord.EmojiIdentifierResolvable} [default.reaction='πŸŽ‰'] The reaction to participate in giveaways. * @property {LastChanceOptions} [default.lastChance] The options for the last chance system. + * @property {ButtonsObject} [default.buttons] The buttons for the giveaways. */ exports.GiveawaysManagerOptions = { storage: './giveaways.json', @@ -211,6 +220,7 @@ exports.GiveawayRerollOptions = { * @property {BonusEntry[]} [newBonusEntries] The new BonusEntry objects. * @property {ExemptMembersFunction} [newExemptMembers] The new filter function to exempt members from winning the giveaway. * @property {LastChanceOptions} [newLastChance] The new options for the last chance system.
Will get merged with the existing object, if there. + * @property {ButtonsObject} [newButtons] The new buttons for the giveaway. */ exports.GiveawayEditOptions = {}; @@ -231,6 +241,7 @@ exports.GiveawayEditOptions = {}; * @property {Discord.Snowflake[]} [winnerIds] The winner Ids of the giveaway after it ended. * @property {Discord.Snowflake} messageId The Id of the message. * @property {Discord.EmojiIdentifierResolvable} [reaction] The reaction to participate in the giveaway. + * @property {ButtonsObject} [buttons] The buttons for the giveaway. * @property {boolean} [botsCanWin] If bots can win the giveaway. * @property {Discord.PermissionResolvable[]} [exemptPermissions] Members with any of these permissions will not be able to win the giveaway. * @property {string} [exemptMembers] Filter function to exempt members from winning the giveaway. @@ -243,5 +254,6 @@ exports.GiveawayEditOptions = {}; * @property {PauseOptions} [pauseOptions] The options for the pause system. * @property {boolean} [isDrop] If the giveaway is a drop, or not.
Drop means that if the amount of valid entrants to the giveaway is the same as "winnerCount" then it immediately ends. * @property {Discord.MessageMentionOptions} [allowedMentions] Which mentions should be parsed from the giveaway messages content. + * @property {Snowflake[]} [entrantIds] The entrant ids for this giveaway, if buttons are used. */ exports.GiveawayData = {}; diff --git a/src/Giveaway.js b/src/Giveaway.js index c99d4ccb..91e39dae 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -14,6 +14,7 @@ const { BonusEntry, PauseOptions, MessageObject, + ButtonsObject, DEFAULT_CHECK_INTERVAL } = require('./Constants.js'); const GiveawaysManager = require('./Manager.js'); @@ -122,13 +123,34 @@ class Giveaway extends EventEmitter { * @type {Discord.MessageMentionOptions} */ this.allowedMentions = options.allowedMentions; - this.buttons = options.buttons ?? null; + /** + * The buttons of the giveaway, if any. + * @type {?ButtonsObject} + */ + this.buttons = options.buttons?.join ? options.buttons : null; + /** + * The entrant ids for this giveaway, if buttons are used. + * @type {?Discord.Snowflake[]} + */ this.entrantIds = options.entrantIds ?? this.buttons ? [] : null; /** - * The giveaway data. - * @type {GiveawayData} + * Giveaway options which need to be processed in a getter or function. + * @type {Object} + * @property {Discord.EmojiIdentifierResolvable} [reaction] The reaction to participate in the giveaway. + * @property {boolean} [botsCanWin] If bots can win the giveaway. + * @property {Discord.PermissionResolvable[]} [exemptPermissions] Members with any of these permissions will not be able to win the giveaway. + * @property {string} [exemptMembers] Filter function to exempt members from winning the giveaway. + * @property {string} [bonusEntries] The array of BonusEntry objects for the giveaway. + * @property {Discord.ColorResolvable} [embedColor] The color of the giveaway embed when it is running. + * @property {Discord.ColorResolvable} [embedColorEnd] The color of the giveaway embed when it has ended. + * @property {LastChanceOptions} [lastChance] The options for the last chance system. + * @property {PauseOptions} [pauseOptions] The options for the pause system. + * @property {boolean} [isDrop] If the giveaway is a drop, or not.
Drop means that if the amount of valid entrants to the giveaway is the same as "winnerCount" then it immediately ends. */ - this.options = options; + this.options = Object.keys(options).reduce((obj, key) => { + if (!Object.keys(this).includes(key)) obj[key] = options[key]; + return obj; + }, {}); /** * The message instance of the embed of this giveaway. * @type {?Discord.Message} @@ -332,7 +354,7 @@ class Giveaway extends EventEmitter { ? this.options.bonusEntries || undefined : serialize(this.options.bonusEntries), reaction: this.options.reaction, - buttons: this.options.buttons ? this.options.buttons : undefined, + buttons: this.buttons ?? undefined, winnerIds: this.winnerIds.length ? this.winnerIds : undefined, extraData: this.extraData, lastChance: this.options.lastChance, diff --git a/src/Manager.js b/src/Manager.js index 9282442d..087cbd80 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -251,10 +251,7 @@ class GiveawaysManager extends EventEmitter { thumbnail: typeof options.thumbnail === 'string' ? options.thumbnail : undefined, image: typeof options.image === 'string' ? options.image : undefined, reaction: Discord.resolvePartialEmoji(options.reaction) ? options.reaction : undefined, - buttons: - !Discord.resolvePartialEmoji(options.reaction) && options.buttons?.join - ? deepmerge(this.options.buttons ?? {}, options.buttons) - : undefined, + buttons: deepmerge(this.options.buttons ?? {}, options.buttons ?? {}), botsCanWin: typeof options.botsCanWin === 'boolean' ? options.botsCanWin : undefined, exemptPermissions: Array.isArray(options.exemptPermissions) ? options.exemptPermissions : undefined, exemptMembers: typeof options.exemptMembers === 'function' ? options.exemptMembers : undefined, From 9035916fc52c9bc3e248320c3a651976af9bda48 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 18 Sep 2022 20:02:40 +0200 Subject: [PATCH 09/27] jsdoc --- src/Constants.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Constants.js b/src/Constants.js index e177011b..381f27e4 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -63,7 +63,7 @@ exports.GiveawayMessages = { /** * @typedef {Object} ButtonsObject - * + * * @property {Discord.JSONEncodable|Discord.APIButtonComponent} join The button to join the giveaway. * @property {Discord.JSONEncodable|Discord.APIButtonComponent} [leave] The button to leave the giveaway. */ @@ -153,8 +153,8 @@ exports.PauseOptions = { * @typedef GiveawaysManagerOptions * * @property {string} [storage='./giveaways.json'] The storage path for the giveaways. - * @property {number} [forceUpdateEvery=null] Force the giveaway messages to be updated at a specific interval. - * @property {number} [endedGiveawaysLifetime=null] The number of milliseconds after which ended giveaways should get deleted from the DB.
⚠ Giveaways deleted from the DB cannot get rerolled anymore! + * @property {?number} [forceUpdateEvery=null] Force the giveaway messages to be updated at a specific interval. + * @property {?number} [endedGiveawaysLifetime=null] The number of milliseconds after which ended giveaways should get deleted from the DB.
⚠ Giveaways deleted from the DB cannot get rerolled anymore! * @property {Object} [default] The default options for new giveaways. * @property {boolean} [default.botsCanWin=false] If bots can win giveaways. * @property {Discord.PermissionResolvable[]} [default.exemptPermissions=[]] Members with any of these permissions won't be able to win a giveaway. @@ -163,7 +163,7 @@ exports.PauseOptions = { * @property {Discord.ColorResolvable} [default.embedColorEnd='#000000'] The color of the giveaway embeds when they have ended. * @property {Discord.EmojiIdentifierResolvable} [default.reaction='πŸŽ‰'] The reaction to participate in giveaways. * @property {LastChanceOptions} [default.lastChance] The options for the last chance system. - * @property {ButtonsObject} [default.buttons] The buttons for the giveaways. + * @property {?ButtonsObject} [default.buttons=null] The buttons for the giveaways. */ exports.GiveawaysManagerOptions = { storage: './giveaways.json', @@ -176,7 +176,7 @@ exports.GiveawaysManagerOptions = { embedColor: '#FF0000', embedColorEnd: '#000000', reaction: 'πŸŽ‰', - buttons: { join: null, leave: null }, + buttons: null, lastChance: { enabled: false, content: '⚠️ **LAST CHANCE TO ENTER !** ⚠️', From 9b2b39a314bb706e227e76cba99759a6677700ae Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sun, 25 Sep 2022 17:12:33 +0200 Subject: [PATCH 10/27] check for updated buttons --- src/Manager.js | 23 ++++++++++++++++++++--- src/utils.js | 11 +++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 087cbd80..747ba2f2 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -19,7 +19,7 @@ const { DELETE_DROP_DATA_AFTER } = require('./Constants.js'); const Giveaway = require('./Giveaway.js'); -const { validateEmbedColor, embedEqual } = require('./utils.js'); +const { validateEmbedColor, embedEqual, buttonEqual } = require('./utils.js'); /** * Giveaways Manager @@ -606,16 +606,33 @@ class GiveawaysManager extends EventEmitter { const lastChanceEnabled = giveaway.lastChance.enabled && giveaway.remainingTime < giveaway.lastChance.threshold; const updatedEmbed = this.generateMainEmbed(giveaway, lastChanceEnabled); + const updatedButtons = giveaway.buttons + ? giveaway.fillInComponents([ + { components: [giveaway.buttons.join, giveaway.buttons.leave].filter(Boolean) } + ]) + : null; + const needUpdate = !embedEqual(giveaway.message.embeds[0].data, updatedEmbed.data) || - giveaway.message.content !== giveaway.fillInString(giveaway.messages.giveaway); + giveaway.message.content !== giveaway.fillInString(giveaway.messages.giveaway) || + (giveaway.buttons && + (!buttonEqual( + updatedButtons[0].components[0].data, + giveaway.message.components[0].components[0].data + ) || + (giveaway.buttons.leave && + !buttonEqual( + updatedButtons[0].components[1].data, + giveaway.message.components[0].components[1].data + )))); if (needUpdate || this.options.forceUpdateEvery) { await giveaway.message .edit({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [updatedEmbed], - allowedMentions: giveaway.allowedMentions + allowedMentions: giveaway.allowedMentions, + components: updatedButtons ?? undefined }) .catch(() => {}); } diff --git a/src/utils.js b/src/utils.js index dcafddf2..dae8c50c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -28,3 +28,14 @@ exports.embedEqual = (embed1, embed2) => { } return true; }; + +exports.buttonEqual = (button1, button2) => { + if (button1.custom_Id !== button2.custom_Id) return false; + if (button1.label !== button2.label) return false; + if (button1.style !== button2.style) return false; + if (button1.emoji?.name !== button2.emoji?.name) return false; + if (button1.emoji?.id !== button2.emoji?.id) return false; + if (button1.url !== button2.url) return false; + if (button1.disabled !== button2.disabled) return false; + return true; +}; From 044318dc233c498c05800634a875943c00373857 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:35:03 +0200 Subject: [PATCH 11/27] add readme example --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index 16485432..1a9830fe 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,25 @@ client.giveawaysManager.reroll(messageId, { You can [access giveaway properties](https://github.com/Androz2091/discord-giveaways#access-giveaway-properties-in-messages) in these messages. [Message options](https://github.com/Androz2091/discord-giveaways#message-options) are available in these messages. +## Buttons instead of reaction + +```js +// Requires Manager from discord-giveaways +const { GiveawaysManager } = require('discord-giveaways'); +const manager = new GiveawaysManager(client, { + storage: './giveaways.json', + default: { + botsCanWin: false, + embedColor: '#FF0000', + embedColorEnd: '#000000', + buttons: { + join: new Discord.ButtonBuilder().setLabel('Join').setStyle('PRIMARY').setCustomId('123'), + leave: new Discord.ButtonBuilder().setLabel('Leave').setStyle('SECONDARY').setCustomId('1234') + } + } +}); +``` + ## Custom Database You can use your custom database to save giveaways, instead of the json files (the "database" by default for `discord-giveaways`). From dad94cdf95001f18ed62a282049b605cca5b2654 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:36:46 +0200 Subject: [PATCH 12/27] improve example --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1a9830fe..b3c378f2 100644 --- a/README.md +++ b/README.md @@ -521,8 +521,14 @@ const manager = new GiveawaysManager(client, { embedColor: '#FF0000', embedColorEnd: '#000000', buttons: { - join: new Discord.ButtonBuilder().setLabel('Join').setStyle('PRIMARY').setCustomId('123'), - leave: new Discord.ButtonBuilder().setLabel('Leave').setStyle('SECONDARY').setCustomId('1234') + join: new Discord.ButtonBuilder() + .setLabel('Join') + .setStyle(Discord.ButtonStyle.Primary) + .setCustomId('123'), + leave: new Discord.ButtonBuilder() + .setLabel('Leave') + .setStyle(Discord.ButtonStyle.Secondary) + .setCustomId('1234') } } }); From 3e4fe6981199a851d813a68c55f1fd504d0512e2 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:48:52 +0200 Subject: [PATCH 13/27] reject link buttons --- src/Manager.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Manager.js b/src/Manager.js index 747ba2f2..d19212ae 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -235,6 +235,12 @@ class GiveawaysManager extends EventEmitter { if (Discord.resolvePartialEmoji(options.reaction) && this.options.buttons?.join) { return reject(`Both "options.reaction" and "options.buttons.join" are set.`); } + if ( + this.options.buttons?.join?.style === Discord.ButtonStyle.Link || + this.options.buttons?.leave?.style === Discord.ButtonStyle.Link + ) { + return reject(`"options.buttons.join" or "options.buttons.leave" is a "Link" button.`); + } const giveaway = new Giveaway(this, { startAt: Date.now(), From 5edd9e2e3e9d66ea57550072cc8526680f511738 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 3 Oct 2022 22:56:11 +0200 Subject: [PATCH 14/27] handle 1 button support --- src/Manager.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index d19212ae..ad99b09d 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -698,10 +698,14 @@ class GiveawaysManager extends EventEmitter { if (!interaction.guild?.available) return; if (!interaction.channel.viewable) return; - if ( - giveaway.buttons.join?.customId === interaction.customId && - !giveaway.entrantIds.includes(interaction.member.id) - ) { + if (giveaway.buttons.join?.customId === interaction.customId) { + // If only one button is used, remove the user if he has already joined + if (!giveaway.buttons.leave && giveaway.entrantIds.includes(interaction.member.id)) { + const index = giveaway.entrantIds.indexOf(interaction.member.id); + giveaway.entrantIds.splice(index, 1); + return this.emit('giveawayLeft', giveaway, interaction.member, interaction); + } + giveaway.entrantIds.push(interaction.member.id); this.emit('giveawayJoined', giveaway, interaction.member, interaction); } else if ( From f6abd6c9900c802af290879036c6847c0a52d26c Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Mon, 3 Oct 2022 23:00:10 +0200 Subject: [PATCH 15/27] fix --- src/Manager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index ad99b09d..d58d09e1 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -236,8 +236,8 @@ class GiveawaysManager extends EventEmitter { return reject(`Both "options.reaction" and "options.buttons.join" are set.`); } if ( - this.options.buttons?.join?.style === Discord.ButtonStyle.Link || - this.options.buttons?.leave?.style === Discord.ButtonStyle.Link + options.buttons?.join?.style === Discord.ButtonStyle.Link || + options.buttons?.leave?.style === Discord.ButtonStyle.Link ) { return reject(`"options.buttons.join" or "options.buttons.leave" is a "Link" button.`); } From a9c79bca639be5aab6d857a63d572df7e3d08f73 Mon Sep 17 00:00:00 2001 From: Nico <63612668+Nico105@users.noreply.github.com> Date: Tue, 4 Oct 2022 14:40:11 +0200 Subject: [PATCH 16/27] Update src/Giveaway.js --- src/Giveaway.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Giveaway.js b/src/Giveaway.js index 91e39dae..67316444 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -265,7 +265,7 @@ class Giveaway extends EventEmitter { /** * If the giveaway is a drop, or not. - * Drop means that if the amount of valid entrantIds to the giveaway is the same as "winnerCount" then it immediately ends. + * Drop means that if the amount of valid entrants to the giveaway is the same as "winnerCount" then it immediately ends. * @type {boolean} */ get isDrop() { From 2288c94d31ff52745473d9b3c199a9effcd6f382 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Tue, 11 Oct 2022 19:52:34 +0200 Subject: [PATCH 17/27] fix reaction and button priority, hopefully? --- src/Constants.js | 2 +- src/Manager.js | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Constants.js b/src/Constants.js index 381f27e4..868310b8 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -163,7 +163,7 @@ exports.PauseOptions = { * @property {Discord.ColorResolvable} [default.embedColorEnd='#000000'] The color of the giveaway embeds when they have ended. * @property {Discord.EmojiIdentifierResolvable} [default.reaction='πŸŽ‰'] The reaction to participate in giveaways. * @property {LastChanceOptions} [default.lastChance] The options for the last chance system. - * @property {?ButtonsObject} [default.buttons=null] The buttons for the giveaways. + * @property {ButtonsObject} [default.buttons=null] The buttons for the giveaways. */ exports.GiveawaysManagerOptions = { storage: './giveaways.json', diff --git a/src/Manager.js b/src/Manager.js index d58d09e1..d6c85d55 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -62,6 +62,9 @@ class GiveawaysManager extends EventEmitter { */ this.options = deepmerge(GiveawaysManagerOptions, options || {}); + // Ensure correct merge order + if (!options?.default?.reaction && options?.default?.buttons?.join) this.options.default.reaction = null; + if (init) this._init(); } @@ -232,7 +235,8 @@ class GiveawaysManager extends EventEmitter { if (!options.isDrop && (!Number.isFinite(options.duration) || options.duration < 1)) { return reject(`"options.duration is not a positive number. (val=${options.duration})`); } - if (Discord.resolvePartialEmoji(options.reaction) && this.options.buttons?.join) { + let reaction = Discord.resolvePartialEmoji(options.reaction) ? options.reaction : undefined; + if (reaction && options.buttons?.join) { return reject(`Both "options.reaction" and "options.buttons.join" are set.`); } if ( @@ -256,8 +260,11 @@ class GiveawaysManager extends EventEmitter { : GiveawayMessages, thumbnail: typeof options.thumbnail === 'string' ? options.thumbnail : undefined, image: typeof options.image === 'string' ? options.image : undefined, - reaction: Discord.resolvePartialEmoji(options.reaction) ? options.reaction : undefined, - buttons: deepmerge(this.options.buttons ?? {}, options.buttons ?? {}), + reaction, + buttons: + !reaction && !(this.options.default.reaction && this.options.default.buttons?.join) + ? deepmerge(this.options.default.buttons ?? {}, options.buttons ?? {}) + : undefined, botsCanWin: typeof options.botsCanWin === 'boolean' ? options.botsCanWin : undefined, exemptPermissions: Array.isArray(options.exemptPermissions) ? options.exemptPermissions : undefined, exemptMembers: typeof options.exemptMembers === 'function' ? options.exemptMembers : undefined, @@ -296,7 +303,7 @@ class GiveawaysManager extends EventEmitter { }); giveaway.messageId = message.id; - const reaction = giveaway.reaction ? await message.react(giveaway.reaction) : null; + reaction = giveaway.reaction ? await message.react(giveaway.reaction) : null; giveaway.message = reaction?.message ?? message; this.giveaways.push(giveaway); From e19c7b44672e7711038aadf0a1ca4b5ac17442ee Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Wed, 12 Oct 2022 20:24:15 +0200 Subject: [PATCH 18/27] improve drop system --- src/Manager.js | 64 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 22 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index d6c85d55..da26092e 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -262,7 +262,8 @@ class GiveawaysManager extends EventEmitter { image: typeof options.image === 'string' ? options.image : undefined, reaction, buttons: - !reaction && !(this.options.default.reaction && this.options.default.buttons?.join) + !reaction && + !(this.options.default.reaction && this.options.default.buttons?.join && !options.buttons?.join) ? deepmerge(this.options.default.buttons ?? {}, options.buttons ?? {}) : undefined, botsCanWin: typeof options.botsCanWin === 'boolean' ? options.botsCanWin : undefined, @@ -310,19 +311,34 @@ class GiveawaysManager extends EventEmitter { await this.saveGiveaway(giveaway.messageId, giveaway.data); resolve(giveaway); - if (giveaway.isDrop && !giveaway.buttons) { - reaction.message - .awaitReactions({ - filter: async (r, u) => - [r.emoji.name, r.emoji.id] - .filter(Boolean) - .includes(reaction.emoji.id ?? reaction.emoji.name) && - u.id !== this.client.user.id && - (await giveaway.checkWinnerEntry(u)), - maxUsers: giveaway.winnerCount - }) - .then(() => this.end(giveaway.messageId)) - .catch(() => {}); + if (!giveaway.isDrop) return; + + if (!giveaway.buttons) { + const collector = reaction.message.createReactionCollector({ + filter: async (r, u) => + [r.emoji.name, r.emoji.id].filter(Boolean).includes(reaction.emoji.id ?? reaction.emoji.name) && + u.id !== this.client.user.id && + (await giveaway.checkWinnerEntry(u)) + }); + collector.on('collect', (r) => { + if (r.count - 1 >= giveaway.winnerCount) { + this.end(giveaway.messageId).catch(() => {}); + collector.stop(); + } + }); + } else { + const collector = giveaway.message.createMessageComponentCollector({ + filter: async (interaction) => + interaction.customId === giveaway.buttons.join.customId && + (await giveaway.checkWinnerEntry(interaction.user)), + componentType: Discord.ComponentType.Button + }); + collector.on('collect', () => { + if (giveaway.entrantIds.length >= giveaway.winnerCount) { + this.end(giveaway.messageId).catch(() => {}); + collector.stop(); + } + }); } }); } @@ -670,6 +686,7 @@ class GiveawaysManager extends EventEmitter { }; this.client.on(Discord.Events.MessageReactionAdd, async (messageReaction, user) => { + if (user.id === this.client.user.id) return; const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); if (!giveaway) return; if (!messageReaction.message.guild?.available) return; @@ -677,7 +694,7 @@ class GiveawaysManager extends EventEmitter { const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); - if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + if (messageReaction.emoji.name != emoji.name || messageReaction.emoji.id != emoji.id) return; if (giveaway.ended) return this.emit('endedGiveawayReactionAdded', giveaway, member, messageReaction); this.emit('giveawayReactionAdded', giveaway, member, messageReaction); @@ -686,32 +703,35 @@ class GiveawaysManager extends EventEmitter { }); this.client.on(Discord.Events.MessageReactionRemove, async (messageReaction, user) => { + if (user.id === this.client.user.id) return; const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); - if (!giveaway) return; + if (!giveaway || giveaway.ended) return; if (!messageReaction.message.guild?.available) return; if (!messageReaction.message.channel.viewable) return; const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); - if (messageReaction.emoji.name !== emoji.name || messageReaction.emoji.id !== emoji.id) return; + if (messageReaction.emoji.name != emoji.name || messageReaction.emoji.id != emoji.id) return; - this.emit('giveawayReactionAdded', giveaway, member, messageReaction); + this.emit('giveawayReactionRemoved', giveaway, member, messageReaction); }); this.client.on(Discord.Events.InteractionCreate, async (interaction) => { if (!interaction.isButton()) return; const giveaway = this.giveaways.find((g) => g.messageId === interaction.message.id); - if (!giveaway) return; + if (!giveaway || !giveaway.buttons || giveaway.ended) return; if (!interaction.guild?.available) return; if (!interaction.channel.viewable) return; - if (giveaway.buttons.join?.customId === interaction.customId) { + if (giveaway.buttons.join.customId === interaction.customId) { // If only one button is used, remove the user if he has already joined if (!giveaway.buttons.leave && giveaway.entrantIds.includes(interaction.member.id)) { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - return this.emit('giveawayLeft', giveaway, interaction.member, interaction); + if (!giveaway.ended) this.emit('giveawayLeft', giveaway, interaction.member, interaction); + return; } + if (giveaway.entrantIds.includes(interaction.member.id)) return; giveaway.entrantIds.push(interaction.member.id); this.emit('giveawayJoined', giveaway, interaction.member, interaction); @@ -721,7 +741,7 @@ class GiveawaysManager extends EventEmitter { ) { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - this.emit('giveawayLeft', giveaway, interaction.member, interaction); + if (!giveaway.ended) this.emit('giveawayLeft', giveaway, interaction.member, interaction); } else return; if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) await checkForDropEnd(giveaway); From d9eb6f5bf8efee839b8832370372fc338f79de2a Mon Sep 17 00:00:00 2001 From: Nico <63612668+Nico105@users.noreply.github.com> Date: Sat, 5 Nov 2022 14:27:50 +0100 Subject: [PATCH 19/27] Update src/Manager.js Co-authored-by: Andromeda <46562212+archaeopteryx1@users.noreply.github.com> --- src/Manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Manager.js b/src/Manager.js index da26092e..e23dfabd 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -676,7 +676,7 @@ class GiveawaysManager extends EventEmitter { const users = await giveaway.fetchAllEntrants().catch(() => {}); let validUsers = 0; - for (const user of [...(users?.values() || [])]) { + for (const user of (users?.values() || [])) { if (await giveaway.checkWinnerEntry(user)) validUsers++; if (validUsers === giveaway.winnerCount) { await this.end(giveaway.messageId).catch(() => {}); From 9bffbff2796cfec6f6fcd8a05bd7d16364a82999 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Fri, 6 Jan 2023 05:06:58 +0100 Subject: [PATCH 20/27] default interaction replies --- README.md | 12 ++++++---- src/Constants.js | 31 ++++++++++++++---------- src/Giveaway.js | 4 +++- src/Manager.js | 59 ++++++++++++++++++++++++++++++++++++++++------ typings/index.d.ts | 23 ++++++++++++++---- 5 files changed, 100 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b3c378f2..ba2b9f34 100644 --- a/README.md +++ b/README.md @@ -512,14 +512,11 @@ You can [access giveaway properties](https://github.com/Androz2091/discord-givea ## Buttons instead of reaction ```js -// Requires Manager from discord-giveaways +const Discord = require('discord.js'); const { GiveawaysManager } = require('discord-giveaways'); const manager = new GiveawaysManager(client, { storage: './giveaways.json', default: { - botsCanWin: false, - embedColor: '#FF0000', - embedColorEnd: '#000000', buttons: { join: new Discord.ButtonBuilder() .setLabel('Join') @@ -534,6 +531,13 @@ const manager = new GiveawaysManager(client, { }); ``` +- **options.default.buttons.join**: the button to join the giveaway. +- **options.default.buttons.leave**: the button to leave the giveaway. If not set, the join-button doubles as the leave-button +- **options.default.buttons.joinReply**: sent in the channel as the ephemeral interaction reply to the join-button. If set to "null", custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayJoined). +- **options.default.buttons.leaveReply**: sent in the channel as the ephemeral interaction reply to the leave-button. If set to "null", custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayLeft). + +You can [access other giveaway properties](https://github.com/Androz2091/discord-giveaways#access-giveaway-properties-in-messages) in these properties. + ## Custom Database You can use your custom database to save giveaways, instead of the json files (the "database" by default for `discord-giveaways`). diff --git a/src/Constants.js b/src/Constants.js index 868310b8..b8671df1 100644 --- a/src/Constants.js +++ b/src/Constants.js @@ -54,18 +54,20 @@ exports.GiveawayMessages = { */ /** - * @typedef {Function} ExemptMembersFunction + * @typedef {Object} ButtonsObject * - * @param {Discord.GuildMember} member - * @param {Giveaway} giveaway - * @returns {Promise|boolean} + * @property {Discord.JSONEncodable|Discord.APIButtonComponent} join The button to join the giveaway. + * @property {Discord.JSONEncodable|Discord.APIButtonComponent} [leave] The button to leave the giveaway. + * @property {?(string|MessageObject)} [joinReply] Sent in the channel as the ephemeral interaction reply to the join-button. + * @property {?(string|MessageObject)} [leaveReply] Sent in the channel as the ephemeral interaction reply to the leave-button. */ /** - * @typedef {Object} ButtonsObject + * @typedef {Function} ExemptMembersFunction * - * @property {Discord.JSONEncodable|Discord.APIButtonComponent} join The button to join the giveaway. - * @property {Discord.JSONEncodable|Discord.APIButtonComponent} [leave] The button to leave the giveaway. + * @param {Discord.GuildMember} member + * @param {Giveaway} giveaway + * @returns {Promise|boolean} */ /** @@ -134,9 +136,9 @@ exports.LastChanceOptions = { * * @property {boolean} [isPaused=false] If the giveaway is paused. * @property {string} [content='⚠️ **THIS GIVEAWAY IS PAUSED !** ⚠️'] The text of the embed when the giveaway is paused. - * @property {number} [unpauseAfter=null] The number of milliseconds, or a timestamp in milliseconds, after which the giveaway will automatically unpause. + * @property {?number} [unpauseAfter=null] The number of milliseconds, or a timestamp in milliseconds, after which the giveaway will automatically unpause. * @property {Discord.ColorResolvable} [embedColor='#FFFF00'] The color of the embed when the giveaway is paused. - * @private @property {number} [durationAfterPause=null|giveaway.remainingTime] The remaining duration after the giveaway is unpaused.
⚠ This property gets set by the manager so that the pause system works properly. It is not recommended to set it manually! + * @private @property {?number} [durationAfterPause=null|giveaway.remainingTime] The remaining duration after the giveaway is unpaused.
⚠ This property gets set by the manager so that the pause system works properly. It is not recommended to set it manually! * @property {string} [infiniteDurationText='`NEVER`'] The text that gets displayed next to "GiveawayMessages#drawing" in the paused embed, when there is no "unpauseAfter". */ exports.PauseOptions = { @@ -161,9 +163,11 @@ exports.PauseOptions = { * @property {ExemptMembersFunction} [default.exemptMembers] Function to filter members.
If true is returned, the member won't be able to win a giveaway. * @property {Discord.ColorResolvable} [default.embedColor='#FF0000'] The color of the giveaway embeds when they are running. * @property {Discord.ColorResolvable} [default.embedColorEnd='#000000'] The color of the giveaway embeds when they have ended. - * @property {Discord.EmojiIdentifierResolvable} [default.reaction='πŸŽ‰'] The reaction to participate in giveaways. + * @property {?Discord.EmojiIdentifierResolvable} [default.reaction='πŸŽ‰'] The reaction to participate in giveaways. * @property {LastChanceOptions} [default.lastChance] The options for the last chance system. - * @property {ButtonsObject} [default.buttons=null] The buttons for the giveaways. + * @property {?ButtonsObject} [default.buttons] The buttons for the giveaways. + * @property {?(string|MessageObject)} [default.buttons.joinReply='βœ… joined'] Sent in the channel as the ephemeral interaction reply to the join-button. + * @property {?(string|MessageObject)} [default.buttons.leaveReply='βœ… left'] Sent in the channel as the ephemeral interaction reply to the leave-button. */ exports.GiveawaysManagerOptions = { storage: './giveaways.json', @@ -176,7 +180,10 @@ exports.GiveawaysManagerOptions = { embedColor: '#FF0000', embedColorEnd: '#000000', reaction: 'πŸŽ‰', - buttons: null, + buttons: { + joinReply: 'βœ… joined', + leaveReply: 'βœ… left' + }, lastChance: { enabled: false, content: '⚠️ **LAST CHANCE TO ENTER !** ⚠️', diff --git a/src/Giveaway.js b/src/Giveaway.js index 67316444..3689899d 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -354,7 +354,9 @@ class Giveaway extends EventEmitter { ? this.options.bonusEntries || undefined : serialize(this.options.bonusEntries), reaction: this.options.reaction, - buttons: this.buttons ?? undefined, + buttons: this.buttons + ? Object.fromEntries(Object.entries(this.buttons).filter(([_, v]) => v !== null)) + : undefined, winnerIds: this.winnerIds.length ? this.winnerIds : undefined, extraData: this.extraData, lastChance: this.options.lastChance, diff --git a/src/Manager.js b/src/Manager.js index be46c89e..9b7ae4f7 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -717,34 +717,79 @@ class GiveawaysManager extends EventEmitter { }); this.client.on(Discord.Events.InteractionCreate, async (interaction) => { - if (!interaction.isButton()) return; + if (!interaction.isButton() || !interaction.guild?.available || !interaction.channel?.viewable) return; const giveaway = this.giveaways.find((g) => g.messageId === interaction.message.id); if (!giveaway || !giveaway.buttons || giveaway.ended) return; - if (!interaction.guild?.available) return; - if (!interaction.channel.viewable) return; if (giveaway.buttons.join.customId === interaction.customId) { // If only one button is used, remove the user if he has already joined if (!giveaway.buttons.leave && giveaway.entrantIds.includes(interaction.member.id)) { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - if (!giveaway.ended) this.emit('giveawayLeft', giveaway, interaction.member, interaction); + + if (giveaway.buttons.leaveReply) { + const embed = this.fillInEmbed(giveaway.buttons.leaveReply.embed); + await interaction + .reply({ + content: this.fillInString( + giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply + ), + embeds: embed ? [embed] : null, + components: this.fillInComponents(giveaway.buttons.leaveReply.components), + ephemeral: true + }) + .catch(() => {}); + } + + this.emit('giveawayLeft', giveaway, interaction.member, interaction); return; } if (giveaway.entrantIds.includes(interaction.member.id)) return; giveaway.entrantIds.push(interaction.member.id); + + if (giveaway.buttons.joinReply) { + const embed = this.fillInEmbed(giveaway.buttons.joinReply.embed); + await interaction + .reply({ + content: this.fillInString( + giveaway.buttons.joinReply.content || giveaway.buttons.joinReply + ), + embeds: embed ? [embed] : null, + components: this.fillInComponents(giveaway.buttons.joinReply.components), + ephemeral: true + }) + .catch(() => {}); + } + this.emit('giveawayJoined', giveaway, interaction.member, interaction); + + if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) { + await checkForDropEnd(giveaway); + } } else if ( giveaway.buttons.leave?.customId === interaction.customId && giveaway.entrantIds.includes(interaction.member.id) ) { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - if (!giveaway.ended) this.emit('giveawayLeft', giveaway, interaction.member, interaction); - } else return; - if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) await checkForDropEnd(giveaway); + if (giveaway.buttons.leaveReply) { + const embed = this.fillInEmbed(giveaway.buttons.leaveReply.embed); + await interaction + .reply({ + content: this.fillInString( + giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply + ), + embeds: embed ? [embed] : null, + components: this.fillInComponents(giveaway.buttons.leaveReply.components), + ephemeral: true + }) + .catch(() => {}); + } + + this.emit('giveawayLeft', giveaway, interaction.member, interaction); + } }); } diff --git a/typings/index.d.ts b/typings/index.d.ts index 8df936fa..0e8fb937 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -20,7 +20,8 @@ import type { JSONEncodable, APIActionRowComponent, APIMessageActionRowComponent, - APIModalActionRowComponent + APIModalActionRowComponent, + APIButtonComponent } from 'discord.js'; export const version: string; @@ -88,15 +89,16 @@ export interface PauseOptions { } export interface GiveawaysManagerOptions { storage?: string; - forceUpdateEvery?: number; - endedGiveawaysLifetime?: number; + forceUpdateEvery?: number | null; + endedGiveawaysLifetime?: number | null; default?: { botsCanWin?: boolean; exemptPermissions?: PermissionResolvable[]; exemptMembers?: (member: GuildMember, giveaway: Giveaway) => Awaitable; embedColor?: ColorResolvable; embedColorEnd?: ColorResolvable; - reaction?: EmojiIdentifierResolvable; + reaction?: EmojiIdentifierResolvable | null; + buttons?: ButtonsObject | null; lastChance?: LastChanceOptions; }; } @@ -112,6 +114,7 @@ export interface GiveawayStartOptions { embedColor?: ColorResolvable; embedColorEnd?: ColorResolvable; reaction?: EmojiIdentifierResolvable; + buttons?: ButtonsObject; messages?: GiveawayMessages; thumbnail?: string; image?: string; @@ -145,6 +148,12 @@ export interface MessageObject { )[]; replyToGiveaway?: boolean; } +export interface ButtonsObject { + join: JSONEncodable | APIButtonComponent; + leave?: JSONEncodable | APIButtonComponent; + joinReply?: string | Omit; + leaveReply?: string | Omit; +} export interface GiveawaysManagerEvents { giveawayDeleted: [Giveaway]; giveawayEnded: [Giveaway, GuildMember[]]; @@ -183,7 +192,7 @@ export class Giveaway extends EventEmitter { readonly embedColor: ColorResolvable; readonly embedColorEnd: ColorResolvable; readonly botsCanWin: boolean; - readonly reaction: EmojiIdentifierResolvable; + readonly reaction: EmojiIdentifierResolvable | null; readonly lastChance: Required; // getters calculated using other values @@ -196,8 +205,10 @@ export class Giveaway extends EventEmitter { readonly pauseOptions: Required; readonly isDrop: boolean; readonly messageReaction: MessageReaction | null; + private ensureEndTimeout(): void; private checkWinnerEntry(user: User): Promise; + public checkBonusEntries(user: User): Promise; public fetchAllEntrants(): Promise>; public fillInString(string: string): string; @@ -225,6 +236,7 @@ export interface GiveawayEditOptions { newPrize?: string; addTime?: number; setEndTimestamp?: number; + newButtons?: ButtonsObject; newMessages?: GiveawayMessages; newThumbnail?: string; newImage?: string; @@ -253,6 +265,7 @@ export interface GiveawayData { winnerIds?: Snowflake[]; messageId: Snowflake; reaction?: EmojiIdentifierResolvable; + buttons?: ButtonsObject; exemptPermissions?: PermissionResolvable[]; exemptMembers?: string; bonusEntries?: string; From 24c7d753aa1ae6d8ac2d1eca3503aa19322a20d9 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Fri, 6 Jan 2023 05:11:40 +0100 Subject: [PATCH 21/27] formatting --- README.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ba2b9f34..a51af63f 100644 --- a/README.md +++ b/README.md @@ -531,10 +531,13 @@ const manager = new GiveawaysManager(client, { }); ``` -- **options.default.buttons.join**: the button to join the giveaway. -- **options.default.buttons.leave**: the button to leave the giveaway. If not set, the join-button doubles as the leave-button -- **options.default.buttons.joinReply**: sent in the channel as the ephemeral interaction reply to the join-button. If set to "null", custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayJoined). -- **options.default.buttons.leaveReply**: sent in the channel as the ephemeral interaction reply to the leave-button. If set to "null", custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayLeft). +- **options.default.buttons.join**: the button to join giveaways. +- **options.default.buttons.leave**: the button to leave giveaways. + ^^^ If not set, the join-button doubles as the leave-button. +- **options.default.buttons.joinReply**: sent in the channel as the ephemeral interaction reply to the join-button. + ^^^ If set to `null`, custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayJoined). +- **options.default.buttons.leaveReply**: sent in the channel as the ephemeral interaction reply to the leave-button. + ^^^ If set to `null`, custom behaviour can be added via the [event](https://discord-giveaways.js.org/GiveawaysManager.html#event:giveawayLeft). You can [access other giveaway properties](https://github.com/Androz2091/discord-giveaways#access-giveaway-properties-in-messages) in these properties. From e9454976ba43d451d9f1ec30c22736ccc50a154b Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sat, 7 Jan 2023 17:06:55 +0100 Subject: [PATCH 22/27] fix --- src/Manager.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 9b7ae4f7..04e3e452 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -728,14 +728,14 @@ class GiveawaysManager extends EventEmitter { giveaway.entrantIds.splice(index, 1); if (giveaway.buttons.leaveReply) { - const embed = this.fillInEmbed(giveaway.buttons.leaveReply.embed); + const embed = giveaway.fillInEmbed(giveaway.buttons.leaveReply.embed); await interaction .reply({ - content: this.fillInString( + content: giveaway.fillInString( giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply ), embeds: embed ? [embed] : null, - components: this.fillInComponents(giveaway.buttons.leaveReply.components), + components: giveaway.fillInComponents(giveaway.buttons.leaveReply.components), ephemeral: true }) .catch(() => {}); @@ -749,14 +749,14 @@ class GiveawaysManager extends EventEmitter { giveaway.entrantIds.push(interaction.member.id); if (giveaway.buttons.joinReply) { - const embed = this.fillInEmbed(giveaway.buttons.joinReply.embed); + const embed = giveaway.fillInEmbed(giveaway.buttons.joinReply.embed); await interaction .reply({ - content: this.fillInString( + content: giveaway.fillInString( giveaway.buttons.joinReply.content || giveaway.buttons.joinReply ), embeds: embed ? [embed] : null, - components: this.fillInComponents(giveaway.buttons.joinReply.components), + components: giveaway.fillInComponents(giveaway.buttons.joinReply.components), ephemeral: true }) .catch(() => {}); @@ -775,14 +775,14 @@ class GiveawaysManager extends EventEmitter { giveaway.entrantIds.splice(index, 1); if (giveaway.buttons.leaveReply) { - const embed = this.fillInEmbed(giveaway.buttons.leaveReply.embed); + const embed = giveaway.fillInEmbed(giveaway.buttons.leaveReply.embed); await interaction .reply({ - content: this.fillInString( + content: giveaway.fillInString( giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply ), embeds: embed ? [embed] : null, - components: this.fillInComponents(giveaway.buttons.leaveReply.components), + components: giveaway.fillInComponents(giveaway.buttons.leaveReply.components), ephemeral: true }) .catch(() => {}); From 9939f9566c4dc7eab02f93fd2b27bcf6ff6b1ea6 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Sat, 7 Jan 2023 17:25:01 +0100 Subject: [PATCH 23/27] refactor --- src/Manager.js | 54 ++++++++++++++------------------------------------ 1 file changed, 15 insertions(+), 39 deletions(-) diff --git a/src/Manager.js b/src/Manager.js index 04e3e452..b6799327 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -721,25 +721,25 @@ class GiveawaysManager extends EventEmitter { const giveaway = this.giveaways.find((g) => g.messageId === interaction.message.id); if (!giveaway || !giveaway.buttons || giveaway.ended) return; + const replyToInteraction = async (message) => { + const embed = giveaway.fillInEmbed(message.embed); + await interaction + .reply({ + content: giveaway.fillInString(message.content || message), + embeds: embed ? [embed] : null, + components: giveaway.fillInComponents(message.components), + ephemeral: true + }) + .catch(() => {}); + }; + if (giveaway.buttons.join.customId === interaction.customId) { // If only one button is used, remove the user if he has already joined if (!giveaway.buttons.leave && giveaway.entrantIds.includes(interaction.member.id)) { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - if (giveaway.buttons.leaveReply) { - const embed = giveaway.fillInEmbed(giveaway.buttons.leaveReply.embed); - await interaction - .reply({ - content: giveaway.fillInString( - giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply - ), - embeds: embed ? [embed] : null, - components: giveaway.fillInComponents(giveaway.buttons.leaveReply.components), - ephemeral: true - }) - .catch(() => {}); - } + if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); this.emit('giveawayLeft', giveaway, interaction.member, interaction); return; @@ -748,19 +748,7 @@ class GiveawaysManager extends EventEmitter { giveaway.entrantIds.push(interaction.member.id); - if (giveaway.buttons.joinReply) { - const embed = giveaway.fillInEmbed(giveaway.buttons.joinReply.embed); - await interaction - .reply({ - content: giveaway.fillInString( - giveaway.buttons.joinReply.content || giveaway.buttons.joinReply - ), - embeds: embed ? [embed] : null, - components: giveaway.fillInComponents(giveaway.buttons.joinReply.components), - ephemeral: true - }) - .catch(() => {}); - } + if (giveaway.buttons.joinReply) await replyToInteraction(giveaway.buttons.joinReply); this.emit('giveawayJoined', giveaway, interaction.member, interaction); @@ -774,19 +762,7 @@ class GiveawaysManager extends EventEmitter { const index = giveaway.entrantIds.indexOf(interaction.member.id); giveaway.entrantIds.splice(index, 1); - if (giveaway.buttons.leaveReply) { - const embed = giveaway.fillInEmbed(giveaway.buttons.leaveReply.embed); - await interaction - .reply({ - content: giveaway.fillInString( - giveaway.buttons.leaveReply.content || giveaway.buttons.leaveReply - ), - embeds: embed ? [embed] : null, - components: giveaway.fillInComponents(giveaway.buttons.leaveReply.components), - ephemeral: true - }) - .catch(() => {}); - } + if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); this.emit('giveawayLeft', giveaway, interaction.member, interaction); } From 8e5c887fe523303b169a728d7c66cd7353a424e5 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Thu, 23 Feb 2023 22:50:48 +0100 Subject: [PATCH 24/27] refactor and export event constants --- README.md | 8 +-- index.js | 1 + src/Events.js | 121 +++++++++++++++++++++++++++++++++++++++++++++ src/Manager.js | 113 +++++------------------------------------- typings/index.d.ts | 72 ++++++++++++++++++++------- 5 files changed, 192 insertions(+), 123 deletions(-) create mode 100644 src/Events.js diff --git a/README.md b/README.md index a51af63f..49a7e79d 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ Discord Giveaways is a powerful [Node.js](https://nodejs.org) module that allows - πŸ“ Support for all databases! (default is json) - βš™οΈ Very customizable! (prize, duration, winners, ignored permissions, bonus entries, etc...) - πŸš€ Super powerful: start, edit, reroll, end, delete and pause giveaways! -- πŸ’₯ Events: giveawayEnded, giveawayRerolled, giveawayDeleted, giveawayReactionAdded, giveawayReactionRemoved, endedGiveawayReactionAdded -- πŸ•ΈοΈ Support for shards! +- πŸ’₯ Events: giveawayEnded, giveawayRerolled, giveawayDeleted, giveawayMemberJoined, giveawayMemberLeft +- πŸ•ΈοΈ Support for buttons and shards! - and much more! ## Installation @@ -521,11 +521,11 @@ const manager = new GiveawaysManager(client, { join: new Discord.ButtonBuilder() .setLabel('Join') .setStyle(Discord.ButtonStyle.Primary) - .setCustomId('123'), + .setCustomId('join'), leave: new Discord.ButtonBuilder() .setLabel('Leave') .setStyle(Discord.ButtonStyle.Secondary) - .setCustomId('1234') + .setCustomId('leave') } } }); diff --git a/index.js b/index.js index 2399f583..df20df4d 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ exports.version = require('./package.json').version; exports.GiveawaysManager = require('./src/Manager'); exports.Giveaway = require('./src/Giveaway'); +exports.Events = require('./src/Events'); diff --git a/src/Events.js b/src/Events.js new file mode 100644 index 00000000..ba4e5ad7 --- /dev/null +++ b/src/Events.js @@ -0,0 +1,121 @@ +/** + * @typedef {Object} Events + * @property {string} EndedGiveawayReactionAdded endedGiveawayReactionAdded + * @property {string} GiveawayDeleted giveawayDeleted' + * @property {string} GiveawayEnded giveawayEnded + * @property {string} GiveawayMemberJoined giveawayMemberJoined + * @property {string} GiveawayMemberLeft giveawayMemberLeft + * @property {string} GiveawayRerolled giveawayRerolled + */ + +// JSDoc for IntelliSense purposes +/** + * @type {Events} + * @ignore + */ +module.exports = { + EndedGiveawayReactionAdded: 'endedGiveawayReactionAdded', + GiveawayDeleted: 'giveawayDeleted', + GiveawayEnded: 'giveawayEnded', + GiveawayMemberJoined: 'giveawayMemberJoined', + GiveawayMemberLeft: 'giveawayMemberLeft', + GiveawayRerolled: 'giveawayRerolled' +}; + +/** + * Emitted when someone reacted to an ended giveaway. + * @event GiveawaysManager#endedGiveawayReactionAdded + * @param {Giveaway} giveaway The giveaway instance + * @param {Discord.GuildMember} member The member who reacted to the ended giveaway + * @param {Discord.MessageReaction} reaction The reaction object + * + * @example + * // This can be used to prevent new participants when giveaways with reactions get rerolled + * manager.on('endedGiveawayReactionAdded', (giveaway, member, reaction) => { + * return reaction.users.remove(member.user); + * }); + */ + +/** + * Emitted when a giveaway was deleted. + * @event GiveawaysManager#giveawayDeleted + * @param {Giveaway} giveaway The giveaway instance + * + * @example + * // This can be used to add logs + * manager.on('giveawayDeleted', (giveaway) => { + * console.log('Giveaway with message Id ' + giveaway.messageId + ' was deleted.') + * }); + */ + +/** + * Emitted when a giveaway ended. + * @event GiveawaysManager#giveawayEnded + * @param {Giveaway} giveaway The giveaway instance + * @param {Discord.GuildMember[]} winners The giveaway winners + * + * @example + * // This can be used to add features such as a congratulatory message in DM + * manager.on('giveawayEnded', (giveaway, winners) => { + * winners.forEach((member) => { + * member.send('Congratulations, ' + member.user.username + ', you won: ' + giveaway.prize); + * }); + * }); + */ + +/** + * Emitted when someone joined a giveaway. + * @event GiveawaysManager#giveawayMemberJoined + * @param {Giveaway} giveaway The giveaway instance + * @param {Discord.GuildMember} member The member who joined the giveaway + * @param {Discord.MessageReaction|Discord.ButtonInteraction} interaction The reaction to enter the giveaway + * + * @example + * // This can be used to add features such as giveaway requirements + * // Best used with the "exemptMembers" property of the giveaways + * manager.on('giveawayMemberJoined', (giveaway, member, reaction) => { + * if (!member.roles.cache.get('123456789')) { + * const index = giveaway.entrantIds.indexOf(member.id); + giveaway.entrantIds.splice(index, 1); + * member.send('You must have this role to participate in the giveaway: Staff'); + * } + * }); + * @example + * // This can be used to add features such as giveaway requirements + * // Best used with the "exemptMembers" property of the giveaways + * manager.on('giveawayMemberJoined', (giveaway, member, reaction) => { + * if (!member.roles.cache.get('123456789')) { + * reaction.users.remove(member.user); + * member.send('You must have this role to participate in the giveaway: Staff'); + * } + * }); + */ + +/** + * Emitted when someone left a giveaway. + * @event GiveawaysManager#giveawayMemberLeft + * @param {Giveaway} giveaway The giveaway instance + * @param {Discord.GuildMember} member The member who remove their reaction giveaway + * @param {Discord.MessageReaction} reaction The reaction to enter the giveaway + * + * @example + * // This can be used to add features such as a leave message in DM + * manager.on('giveawayMemberLeft', (giveaway, member, interaction) => { + * return member.send('That\'s sad, you won\'t be able to win the super cookie!'); + * }); + */ + +/** + * Emitted when a giveaway was rerolled. + * @event GiveawaysManager#giveawayRerolled + * @param {Giveaway} giveaway The giveaway instance + * @param {Discord.GuildMember[]} winners The winners of the giveaway + * + * @example + * // This can be used to add features such as a congratulatory message per DM + * manager.on('giveawayRerolled', (giveaway, winners) => { + * winners.forEach((member) => { + * member.send('Congratulations, ' + member.user.username + ', you won: ' + giveaway.prize); + * }); + * }); + */ diff --git a/src/Manager.js b/src/Manager.js index b6799327..76d0009d 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -18,6 +18,7 @@ const { DEFAULT_CHECK_INTERVAL, DELETE_DROP_DATA_AFTER } = require('./Constants.js'); +const Events = require('./Events'); const Giveaway = require('./Giveaway.js'); const { validateEmbedColor, embedEqual, buttonEqual } = require('./utils.js'); @@ -188,7 +189,7 @@ class GiveawaysManager extends EventEmitter { giveaway .end(noWinnerMessage) .then((winners) => { - this.emit('giveawayEnded', giveaway, winners); + this.emit(Events.GiveawayEnded, giveaway, winners); resolve(winners); }) .catch(reject); @@ -360,7 +361,7 @@ class GiveawaysManager extends EventEmitter { giveaway .reroll(options) .then((winners) => { - this.emit('giveawayRerolled', giveaway, winners); + this.emit(Events.GiveawayRerolled, giveaway, winners); resolve(winners); }) .catch(reject); @@ -438,7 +439,7 @@ class GiveawaysManager extends EventEmitter { } this.giveaways = this.giveaways.filter((g) => g.messageId !== messageId); await this.deleteGiveaway(messageId); - this.emit('giveawayDeleted', giveaway); + this.emit(Events.GiveawayDeleted, giveaway); resolve(giveaway); }); } @@ -689,15 +690,14 @@ class GiveawaysManager extends EventEmitter { if (user.id === this.client.user.id) return; const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); if (!giveaway) return; - if (!messageReaction.message.guild?.available) return; - if (!messageReaction.message.channel.viewable) return; + if (!messageReaction.message.guild?.available || !messageReaction.message.channel.viewable) return; const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); if (messageReaction.emoji.name != emoji.name || messageReaction.emoji.id != emoji.id) return; - if (giveaway.ended) return this.emit('endedGiveawayReactionAdded', giveaway, member, messageReaction); - this.emit('giveawayReactionAdded', giveaway, member, messageReaction); + if (giveaway.ended) return this.emit(Events.EndedGiveawayReactionAdded, giveaway, member, messageReaction); + this.emit(Events.GiveawayMemberJoined, giveaway, member, messageReaction); if (giveaway.isDrop && messageReaction.count - 1 >= giveaway.winnerCount) await checkForDropEnd(giveaway); }); @@ -706,14 +706,13 @@ class GiveawaysManager extends EventEmitter { if (user.id === this.client.user.id) return; const giveaway = this.giveaways.find((g) => g.messageId === messageReaction.message.id); if (!giveaway || giveaway.ended) return; - if (!messageReaction.message.guild?.available) return; - if (!messageReaction.message.channel.viewable) return; + if (!messageReaction.message.guild?.available || !messageReaction.message.channel.viewable) return; const member = await messageReaction.message.guild.members.fetch(user).catch(() => {}); if (!member) return; const emoji = Discord.resolvePartialEmoji(giveaway.reaction); if (messageReaction.emoji.name != emoji.name || messageReaction.emoji.id != emoji.id) return; - this.emit('giveawayReactionRemoved', giveaway, member, messageReaction); + this.emit(Events.GiveawayMemberLeft, giveaway, member, messageReaction); }); this.client.on(Discord.Events.InteractionCreate, async (interaction) => { @@ -741,7 +740,7 @@ class GiveawaysManager extends EventEmitter { if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); - this.emit('giveawayLeft', giveaway, interaction.member, interaction); + this.emit(Events.GiveawayMemberLeft, giveaway, interaction.member, interaction); return; } if (giveaway.entrantIds.includes(interaction.member.id)) return; @@ -750,7 +749,7 @@ class GiveawaysManager extends EventEmitter { if (giveaway.buttons.joinReply) await replyToInteraction(giveaway.buttons.joinReply); - this.emit('giveawayJoined', giveaway, interaction.member, interaction); + this.emit(Events.GiveawayMemberJoined, giveaway, interaction.member, interaction); if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) { await checkForDropEnd(giveaway); @@ -764,7 +763,7 @@ class GiveawaysManager extends EventEmitter { if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); - this.emit('giveawayLeft', giveaway, interaction.member, interaction); + this.emit(Events.GiveawayMemberLeft, giveaway, interaction.member, interaction); } }); } @@ -813,92 +812,4 @@ class GiveawaysManager extends EventEmitter { } } -/** - * Emitted when a giveaway ended. - * @event GiveawaysManager#giveawayEnded - * @param {Giveaway} giveaway The giveaway instance - * @param {Discord.GuildMember[]} winners The giveaway winners - * - * @example - * // This can be used to add features such as a congratulatory message in DM - * manager.on('giveawayEnded', (giveaway, winners) => { - * winners.forEach((member) => { - * member.send('Congratulations, ' + member.user.username + ', you won: ' + giveaway.prize); - * }); - * }); - */ - -/** - * Emitted when someone entered a giveaway. - * @event GiveawaysManager#giveawayReactionAdded - * @param {Giveaway} giveaway The giveaway instance - * @param {Discord.GuildMember} member The member who entered the giveaway - * @param {Discord.MessageReaction} reaction The reaction to enter the giveaway - * - * @example - * // This can be used to add features such as removing reactions of members when they do not have a specific role (= giveaway requirements) - * // Best used with the "exemptMembers" property of the giveaways - * manager.on('giveawayReactionAdded', (giveaway, member, reaction) => { - * if (!member.roles.cache.get('123456789')) { - * reaction.users.remove(member.user); - * member.send('You must have this role to participate in the giveaway: Staff'); - * } - * }); - */ - -/** - * Emitted when someone removed their reaction to a giveaway. - * @event GiveawaysManager#giveawayReactionRemoved - * @param {Giveaway} giveaway The giveaway instance - * @param {Discord.GuildMember} member The member who remove their reaction giveaway - * @param {Discord.MessageReaction} reaction The reaction to enter the giveaway - * - * @example - * // This can be used to add features such as a member-left-giveaway message per DM - * manager.on('giveawayReactionRemoved', (giveaway, member, reaction) => { - * return member.send('That\'s sad, you won\'t be able to win the super cookie!'); - * }); - */ - -/** - * Emitted when someone reacted to a ended giveaway. - * @event GiveawaysManager#endedGiveawayReactionAdded - * @param {Giveaway} giveaway The giveaway instance - * @param {Discord.GuildMember} member The member who reacted to the ended giveaway - * @param {Discord.MessageReaction} reaction The reaction to enter the giveaway - * - * @example - * // This can be used to prevent new participants when giveaways get rerolled - * manager.on('endedGiveawayReactionAdded', (giveaway, member, reaction) => { - * return reaction.users.remove(member.user); - * }); - */ - -/** - * Emitted when a giveaway was rerolled. - * @event GiveawaysManager#giveawayRerolled - * @param {Giveaway} giveaway The giveaway instance - * @param {Discord.GuildMember[]} winners The winners of the giveaway - * - * @example - * // This can be used to add features such as a congratulatory message per DM - * manager.on('giveawayRerolled', (giveaway, winners) => { - * winners.forEach((member) => { - * member.send('Congratulations, ' + member.user.username + ', you won: ' + giveaway.prize); - * }); - * }); - */ - -/** - * Emitted when a giveaway was deleted. - * @event GiveawaysManager#giveawayDeleted - * @param {Giveaway} giveaway The giveaway instance - * - * @example - * // This can be used to add logs - * manager.on('giveawayDeleted', (giveaway) => { - * console.log('Giveaway with message Id ' + giveaway.messageId + ' was deleted.') - * }); - */ - module.exports = GiveawaysManager; diff --git a/typings/index.d.ts b/typings/index.d.ts index 0e8fb937..116ca2ef 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,30 +1,32 @@ import type { EventEmitter } from 'node:events'; import type { + ActionRowBuilder, + APIActionRowComponent, + APIButtonComponent, + APIEmbed, + APIMessageActionRowComponent, + APIModalActionRowComponent, + Awaitable, + ButtonComponent, Client, Collection, ColorResolvable, + EmbedBuilder, EmojiIdentifierResolvable, GuildMember, + GuildTextBasedChannel, + JSONEncodable, Message, - ActionRowBuilder, - EmbedBuilder, + MessageActionRowComponentBuilder, MessageMentionOptions, MessageReaction, PermissionResolvable, Snowflake, - User, - Awaitable, - APIEmbed, - MessageActionRowComponentBuilder, - GuildTextBasedChannel, - JSONEncodable, - APIActionRowComponent, - APIMessageActionRowComponent, - APIModalActionRowComponent, - APIButtonComponent + User } from 'discord.js'; export const version: string; + export class GiveawaysManager extends EventEmitter { constructor(client: Client, options?: GiveawaysManagerOptions, init?: boolean); @@ -69,16 +71,19 @@ export class GiveawaysManager extends EventEmitter { ...args: GiveawaysManagerEvents[K] ): boolean; } + export interface BonusEntry { bonus(member: GuildMember, giveaway: Giveaway): Awaitable; cumulative?: boolean; } + export interface LastChanceOptions { enabled?: boolean; embedColor?: ColorResolvable; content?: string; threshold?: number; } + export interface PauseOptions { isPaused?: boolean; content?: string; @@ -87,6 +92,7 @@ export interface PauseOptions { durationAfterPause?: number | null; infiniteDurationText?: string; } + export interface GiveawaysManagerOptions { storage?: string; forceUpdateEvery?: number | null; @@ -102,6 +108,7 @@ export interface GiveawaysManagerOptions { lastChance?: LastChanceOptions; }; } + export interface GiveawayStartOptions { prize: string; winnerCount: number; @@ -124,6 +131,7 @@ export interface GiveawayStartOptions { isDrop?: boolean; allowedMentions?: Omit; } + export interface GiveawayMessages { giveaway?: string; giveawayEnded?: string; @@ -139,6 +147,7 @@ export interface GiveawayMessages { endedAt?: string; hostedBy?: string; } + export interface MessageObject { content?: string; embed?: JSONEncodable | APIEmbed; @@ -148,20 +157,35 @@ export interface MessageObject { )[]; replyToGiveaway?: boolean; } + export interface ButtonsObject { join: JSONEncodable | APIButtonComponent; leave?: JSONEncodable | APIButtonComponent; joinReply?: string | Omit; leaveReply?: string | Omit; } + export interface GiveawaysManagerEvents { - giveawayDeleted: [Giveaway]; - giveawayEnded: [Giveaway, GuildMember[]]; - giveawayRerolled: [Giveaway, GuildMember[]]; - giveawayReactionAdded: [Giveaway, GuildMember, MessageReaction]; - giveawayReactionRemoved: [Giveaway, GuildMember, MessageReaction]; - endedGiveawayReactionAdded: [Giveaway, GuildMember, MessageReaction]; + giveawayDeleted: [giveaway: Giveaway]; + giveawayEnded: [giveaway: Giveaway, member: GuildMember[]]; + giveawayRerolled: [giveaway: Giveaway, member: GuildMember[]]; + giveawayReactionAdded: [ + giveaway: Giveaway, + member: GuildMember, + interaction: MessageReaction | ButtonComponent + ]; + giveawayReactionRemoved: [ + giveaway: Giveaway, + member: GuildMember, + interaction: MessageReaction | ButtonComponent + ]; + endedGiveawayReactionAdded: [ + giveaway: Giveaway, + member: GuildMember, + interaction: MessageReaction | ButtonComponent + ]; } + export class Giveaway extends EventEmitter { constructor(manager: GiveawaysManager, options: GiveawayData); @@ -231,6 +255,7 @@ export class Giveaway extends EventEmitter { public pause(options?: Omit): Promise>; public unpause(): Promise>; } + export interface GiveawayEditOptions { newWinnerCount?: number; newPrize?: string; @@ -245,6 +270,7 @@ export interface GiveawayEditOptions { newExtraData?: ExtraData; newLastChance?: LastChanceOptions; } + export interface GiveawayRerollOptions { winnerCount?: number; messages?: { @@ -253,6 +279,7 @@ export interface GiveawayRerollOptions { replyWhenNoWinner?: boolean; }; } + export interface GiveawayData { startAt: number; endAt: number; @@ -280,3 +307,12 @@ export interface GiveawayData { isDrop?: boolean; allowedMentions?: Omit; } + +export enum Events { + EndedGiveawayReactionAdded = 'endedGiveawayReactionAdded', + GiveawayDeleted = 'giveawayDeleted', + GiveawayEnded = 'giveawayEnded', + GiveawayMemberJoined = 'giveawayMemberJoined', + GiveawayMemberLeft = 'giveawayMemberLeft', + GiveawayRerolled = 'giveawayRerolled' +} From dcaf010f472d41f7abe47f9b88e02c41581d7f80 Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Thu, 23 Feb 2023 23:38:15 +0100 Subject: [PATCH 25/27] types and edit functionality --- examples/custom-databases/mongoose.js | 7 +++++++ src/Giveaway.js | 1 + typings/index.d.ts | 17 +++++++++-------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/examples/custom-databases/mongoose.js b/examples/custom-databases/mongoose.js index 2a74ce22..48ee8da9 100644 --- a/examples/custom-databases/mongoose.js +++ b/examples/custom-databases/mongoose.js @@ -44,6 +44,13 @@ const giveawaySchema = new mongoose.Schema( hostedBy: String, winnerIds: { type: [String], default: undefined }, reaction: mongoose.Mixed, + buttons: { + join: mongoose.Mixed, + leave: mongoose.Mixed, + joinReply: mongoose.Mixed, + leaveReply: mongoose.Mixed + }, + entrantIds: { type: [String], default: undefined }, botsCanWin: Boolean, embedColor: mongoose.Mixed, embedColorEnd: mongoose.Mixed, diff --git a/src/Giveaway.js b/src/Giveaway.js index 3689899d..2ad9f249 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -679,6 +679,7 @@ class Giveaway extends EventEmitter { if (options.newLastChance && typeof options.newLastChance === 'object' && !this.isDrop) { this.options.lastChance = deepmerge(this.options.lastChance || {}, options.newLastChance); } + if (this.newButtons?.join && this.buttons) this.buttons = this.newButtons; await this.manager.editGiveaway(this.messageId, this.data); if (this.remainingTime <= 0) this.manager.end(this.messageId).catch(() => {}); diff --git a/typings/index.d.ts b/typings/index.d.ts index 116ca2ef..82002d98 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -191,25 +191,25 @@ export class Giveaway extends EventEmitter { public channelId: Snowflake; public client: Client; + public manager: GiveawaysManager; + public messageId: Snowflake; + public guildId: Snowflake; + public prize: string; + public winnerCount: number; + public startAt: number; public endAt: number; public ended: boolean; - public guildId: Snowflake; - public hostedBy?: User; - public manager: GiveawaysManager; public message: Message | null; - public messageId: Snowflake; + public hostedBy?: User; public messages: Required; public thumbnail?: string; public image?: string; public extraData?: ExtraData; public options: GiveawayData; - public prize: string; - public startAt: number; - public winnerCount: number; + public entrantIds?: Snowflake[]; public winnerIds: Snowflake[]; public allowedMentions?: Omit; private endTimeout?: NodeJS.Timeout; - private isEnding?: boolean; // getters calculated using default manager options readonly exemptPermissions: PermissionResolvable[]; @@ -306,6 +306,7 @@ export interface GiveawayData { pauseOptions?: PauseOptions; isDrop?: boolean; allowedMentions?: Omit; + entrantIds?: Snowflake[]; } export enum Events { From 62b5ec63f4d28b3514ae761fe24fef34881e4f8c Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Fri, 24 Feb 2023 21:29:51 +0100 Subject: [PATCH 26/27] safe every entrant to the db --- src/Manager.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Manager.js b/src/Manager.js index 76d0009d..f599712a 100644 --- a/src/Manager.js +++ b/src/Manager.js @@ -765,6 +765,8 @@ class GiveawaysManager extends EventEmitter { this.emit(Events.GiveawayMemberLeft, giveaway, interaction.member, interaction); } + + await this.editGiveaway(giveaway.messageId, giveaway.data); }); } From e34ebb2251faef21498f6ff883a2d58d16a68c4c Mon Sep 17 00:00:00 2001 From: Nico105 <63612668+Nico105@users.noreply.github.com> Date: Fri, 24 Feb 2023 21:38:13 +0100 Subject: [PATCH 27/27] fix restart --- src/Giveaway.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Giveaway.js b/src/Giveaway.js index 2ad9f249..e852a8cd 100644 --- a/src/Giveaway.js +++ b/src/Giveaway.js @@ -132,7 +132,7 @@ class Giveaway extends EventEmitter { * The entrant ids for this giveaway, if buttons are used. * @type {?Discord.Snowflake[]} */ - this.entrantIds = options.entrantIds ?? this.buttons ? [] : null; + this.entrantIds = options.entrantIds ?? (this.buttons ? [] : null); /** * Giveaway options which need to be processed in a getter or function. * @type {Object}