UNPKG

givify

Version:

A Advance Discord Giveaway System

846 lines (771 loc) 38.3 kB
const { EventEmitter } = require('node:events'); const { setTimeout, setInterval } = require('node:timers'); const { writeFile, readFile, access } = require('node:fs/promises'); const Discord = require('discord.js'); const serialize = require('serialize-javascript'); const { deepmerge } = require('deepmerge-ts'); const { GiveawayMessages, GiveawayEditOptions, GiveawayData, GiveawayRerollOptions, GiveawaysManagerOptions, GiveawayStartOptions, PauseOptions, MessageObject, 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'); /** * Giveaways Manager */ class GiveawaysManager extends EventEmitter { /** * @param {Discord.Client} client The Discord Client * @param {GiveawaysManagerOptions} [options] The manager options * @param {boolean} [init=true] If the manager should start automatically. If set to "false", for example to create a delay, the manager can be started manually with "manager._init()". */ constructor(client, options, init = true) { super(); if (!client?.options) throw new Error(`Client is a required option. (val=${client})`); if (!(client instanceof Discord.Client)) { throw new Error('Client is missing the "GuildIntegrations" intent.'); } /** * The Discord Client * @type {Discord.Client} */ this.client = client; /** * Whether the manager is ready * @type {boolean} */ this.ready = false; /** * The giveaways managed by this manager * @type {Giveaway[]} */ this.giveaways = []; /** * The manager options * @type {GiveawaysManagerOptions} */ 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(); } /** * Generate an embed displayed when a giveaway is running (with the remaining time) * @param {Giveaway} giveaway The giveaway the embed needs to be generated for * @param {boolean} [lastChanceEnabled=false] Whether or not to include the last chance text * @returns {Discord.EmbedBuilder} The generated embed */ generateMainEmbed(giveaway, lastChanceEnabled = false) { const embed = new Discord.EmbedBuilder() .setTitle(typeof giveaway.messages.title === 'string' ? giveaway.messages.title : giveaway.prize) .setColor( giveaway.isDrop ? giveaway.embedColor : giveaway.pauseOptions.isPaused && giveaway.pauseOptions.embedColor ? giveaway.pauseOptions.embedColor : lastChanceEnabled ? giveaway.lastChance.embedColor : giveaway.embedColor ) .setFooter({ text: giveaway.messages.embedFooter.text ?? (typeof giveaway.messages.embedFooter === 'string' ? giveaway.messages.embedFooter : ''), iconURL: giveaway.messages.embedFooter.iconURL }) .setDescription( ` ${giveaway.isDrop ? giveaway.messages.dropMessage : ''} ${giveaway.pauseOptions.isPaused ? `${giveaway.pauseOptions.content}\n\n` : ''} ${lastChanceEnabled ? `${giveaway.lastChance.content}\n\n` : ''} ${giveaway.messages.inviteToParticipate} ${giveaway.messages.drawing.replace( '{timestamp}', giveaway.endAt === Infinity ? giveaway.pauseOptions.infiniteDurationText : `<t:${Math.round(giveaway.endAt / 1000)}:R>` )} ${giveaway.hostedBy ? '\n' + giveaway.messages.hostedBy : ''} ` ) .setThumbnail(giveaway.thumbnail) .setImage(giveaway.image); if (giveaway.endAt !== Infinity) embed.setTimestamp(giveaway.endAt); return giveaway.fillInEmbed(embed); } /** * Generate an embed displayed when a giveaway is ended (with the winners list) * @param {Giveaway} giveaway The giveaway the embed needs to be generated for * @param {Discord.GuildMember[]} winners The giveaway winners * @returns {Discord.EmbedBuilder} The generated embed */ generateEndEmbed(giveaway, winners) { let formattedWinners = winners.map((w) => `${w}`).join(', '); const strings = { winners: giveaway.fillInString(giveaway.messages.winners), hostedBy: giveaway.fillInString(giveaway.messages.hostedBy), endedAt: giveaway.fillInString(giveaway.messages.endedAt), title: giveaway.fillInString(giveaway.messages.title) ?? giveaway.fillInString(giveaway.prize) }; const descriptionString = (formattedWinners) => `${strings.winners} ${formattedWinners}${giveaway.hostedBy ? '\n' + strings.hostedBy : ''}`; let i = 1; while ( descriptionString(formattedWinners).length > 4096 || strings.title.length + strings.endedAt.length + descriptionString(formattedWinners).length > 6000 ) { formattedWinners = formattedWinners.slice(0, formattedWinners.lastIndexOf(', <@')) + `, ${i} more`; i++; } return new Discord.EmbedBuilder() .setTitle(strings.title) .setColor(giveaway.embedColorEnd) .setFooter({ text: strings.endedAt, iconURL: giveaway.messages.embedFooter.iconURL }) .setDescription(descriptionString(formattedWinners)) .setTimestamp(giveaway.endAt) .setThumbnail(giveaway.thumbnail) .setImage(giveaway.image); } /** * Generate an embed displayed when a giveaway is ended and when there is no valid participant * @param {Giveaway} giveaway The giveaway the embed needs to be generated for * @returns {Discord.EmbedBuilder} The generated embed */ generateNoValidParticipantsEndEmbed(giveaway) { const embed = new Discord.EmbedBuilder() .setTitle(typeof giveaway.messages.title === 'string' ? giveaway.messages.title : giveaway.prize) .setColor(giveaway.embedColorEnd) .setFooter({ text: giveaway.messages.endedAt, iconURL: giveaway.messages.embedFooter.iconURL }) .setDescription(giveaway.messages.noWinner + (giveaway.hostedBy ? '\n' + giveaway.messages.hostedBy : '')) .setTimestamp(giveaway.endAt) .setThumbnail(giveaway.thumbnail) .setImage(giveaway.image); return giveaway.fillInEmbed(embed); } /** * Ends a giveaway. This method is automatically called when a giveaway ends. * @param {Discord.Snowflake} messageId The message id of the giveaway * @param {?string|MessageObject} [noWinnerMessage=null] Sent in the channel if there is no valid winner for the giveaway. * @returns {Promise<Discord.GuildMember[]>} The winners * * @example * manager.end('664900661003157510'); */ end(messageId, noWinnerMessage = null) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); giveaway .end(noWinnerMessage) .then((winners) => { this.emit(Events.GiveawayEnded, giveaway, winners); resolve(winners); }) .catch(reject); }); } /** * Starts a new giveaway * @param {Discord.GuildTextBasedChannel} channel The channel in which the giveaway will be created * @param {GiveawayStartOptions} options The options for the giveaway * @returns {Promise<Giveaway>} The created giveaway. * * @example * manager.start(interaction.channel, { * prize: 'Free Steam Key', * // Giveaway will last 10 seconds * duration: 10000, * // One winner * winnerCount: 1, * // Limit the giveaway to members who have the "Nitro Boost" role * exemptMembers: (member) => !member.roles.cache.some((r) => r.name === 'Nitro Boost') * }); */ start(channel, options) { 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})`); } if (channel.isThread() && !channel.sendable) { return reject( `The manager is unable to send messages in the provided ThreadChannel. (id=${channel.id})` ); } 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})`); } if (!Number.isInteger(options.winnerCount) || options.winnerCount < 1) { 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})`); } if (!options.isDrop && (!Number.isFinite(options.duration) || options.duration < 1)) { return reject(`"options.duration is not a positive number. (val=${options.duration})`); } 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 ( 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.`); } if (options.isDrop && options.buttons?.leave) { return reject(`"options.buttons.leave" is not supported in drop`); } const giveaway = new Giveaway(this, { startAt: Date.now(), endAt: options.isDrop ? Infinity : Date.now() + options.duration, winnerCount: options.winnerCount, channelId: channel.id, guildId: channel.guildId, prize: options.prize, hostedBy: options.hostedBy ? options.hostedBy.toString() : undefined, messages: options.messages && typeof options.messages === 'object' ? deepmerge(GiveawayMessages, options.messages) : GiveawayMessages, thumbnail: typeof options.thumbnail === 'string' ? options.thumbnail : undefined, image: typeof options.image === 'string' ? options.image : undefined, reaction, buttons: !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, exemptPermissions: Array.isArray(options.exemptPermissions) ? options.exemptPermissions : undefined, exemptMembers: typeof options.exemptMembers === 'function' ? options.exemptMembers : undefined, bonusEntries: Array.isArray(options.bonusEntries) && !options.isDrop ? options.bonusEntries.filter((elem) => typeof elem === 'object') : undefined, embedColor: validateEmbedColor(options.embedColor) ? options.embedColor : undefined, embedColorEnd: validateEmbedColor(options.embedColorEnd) ? options.embedColorEnd : undefined, extraData: options.extraData, lastChance: options.lastChance && typeof options.lastChance === 'object' && !options.isDrop ? options.lastChance : undefined, pauseOptions: options.pauseOptions && typeof options.pauseOptions === 'object' && !options.isDrop ? options.pauseOptions : undefined, allowedMentions: options.allowedMentions && typeof options.allowedMentions === 'object' ? options.allowedMentions : undefined, isDrop: options.isDrop }); const embed = this.generateMainEmbed(giveaway); const message = await channel.send({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [embed], allowedMentions: giveaway.allowedMentions, components: giveaway.buttons ? giveaway.fillInComponents([ { components: [giveaway.buttons.join, giveaway.buttons.leave].filter(Boolean) } ]) : undefined }); giveaway.messageId = message.id; 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) 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.data.custom_id || interaction.customId === giveaway.buttons.join.custom_id) && (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(); } }); // } }); } /** * Choose new winner(s) for the giveaway * @param {Discord.Snowflake} messageId The message Id of the giveaway to reroll * @param {GiveawayRerollOptions} [options] The reroll options * @returns {Promise<Discord.GuildMember[]>} The new winners * * @example * manager.reroll('664900661003157510'); */ reroll(messageId, options = {}) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); giveaway .reroll(options) .then((winners) => { this.emit(Events.GiveawayRerolled, giveaway, winners); resolve(winners); }) .catch(reject); }); } /** * Pauses a giveaway. * @param {Discord.Snowflake} messageId The message Id of the giveaway to pause. * @param {PauseOptions} [options=giveaway.pauseOptions] The pause options. * @returns {Promise<Giveaway>} The paused giveaway. * * @example * manager.pause('664900661003157510'); */ pause(messageId, options = {}) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); giveaway.pause(options).then(resolve).catch(reject); }); } /** * Unpauses a giveaway. * @param {Discord.Snowflake} messageId The message Id of the giveaway to unpause. * @returns {Promise<Giveaway>} The unpaused giveaway. * * @example * manager.unpause('664900661003157510'); */ unpause(messageId) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); giveaway.unpause().then(resolve).catch(reject); }); } /** * Edits a giveaway. The modifications will be applicated when the giveaway will be updated. * @param {Discord.Snowflake} messageId The message Id of the giveaway to edit * @param {GiveawayEditOptions} [options={}] The edit options * @returns {Promise<Giveaway>} The edited giveaway * * @example * manager.edit('664900661003157510', { * newWinnerCount: 2, * newPrize: 'Something new!', * addTime: -10000 // The giveaway will end 10 seconds earlier * }); */ edit(messageId, options = {}) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); giveaway.edit(options).then(resolve).catch(reject); }); } /** * Deletes a giveaway. It will delete the message and all the giveaway data. * @param {Discord.Snowflake} messageId The message Id of the giveaway * @param {boolean} [doNotDeleteMessage=false] Whether the giveaway message shouldn't be deleted * @returns {Promise<Giveaway>} */ delete(messageId, doNotDeleteMessage = false) { return new Promise(async (resolve, reject) => { const giveaway = this.giveaways.find((g) => g.messageId === messageId); if (!giveaway) return reject('No giveaway found with message Id ' + messageId + '.'); if (!doNotDeleteMessage) { giveaway.message ??= await giveaway.fetchMessage().catch(() => {}); giveaway.message?.delete(); } this.giveaways = this.giveaways.filter((g) => g.messageId !== messageId); await this.deleteGiveaway(messageId); this.emit(Events.GiveawayDeleted, giveaway); resolve(giveaway); }); } /** * Delete a giveaway from the database * @param {Discord.Snowflake} messageId The message Id of the giveaway to delete * @returns {Promise<boolean>} */ async deleteGiveaway(messageId) { await writeFile( this.options.storage, JSON.stringify( this.giveaways.map((giveaway) => giveaway.data), (_, v) => (typeof v === 'bigint' ? serialize(v) : v) ), 'utf-8' ); return true; } /** * Gets the giveaways from the storage file, or create it * @ignore * @returns {Promise<GiveawayData[]>} */ async getAllGiveaways() { // Whether the storage file exists, or not const storageExists = await access(this.options.storage) .then(() => true) .catch(() => false); // If it doesn't exists if (!storageExists) { // Create the file with an empty array await writeFile(this.options.storage, '[]', 'utf-8'); return []; } else { // If the file exists, read it const storageContent = await readFile(this.options.storage, { encoding: 'utf-8' }); if (!storageContent.trim().startsWith('[') || !storageContent.trim().endsWith(']')) { console.log(storageContent); throw new SyntaxError('The storage file is not properly formatted (does not contain an array).'); } try { return await JSON.parse(storageContent, (_, v) => typeof v === 'string' && /BigInt\("(-?\d+)"\)/.test(v) ? eval(v) : v ); } catch (err) { if (err.message.startsWith('Unexpected token')) { throw new SyntaxError( `${err.message} | LINK: (${require('path').resolve(this.options.storage)}:1:${err.message .split(' ') .at(-1)})` ); } throw err; } } } /** * Edit the giveaway in the database * @ignore * @param {Discord.Snowflake} messageId The message Id identifying the giveaway * @param {GiveawayData} giveawayData The giveaway data to save */ async editGiveaway(messageId, giveawayData) { await writeFile( this.options.storage, JSON.stringify( this.giveaways.map((giveaway) => giveaway.data), (_, v) => (typeof v === 'bigint' ? serialize(v) : v) ), 'utf-8' ); return; } /** * Save the giveaway in the database * @ignore * @param {Discord.Snowflake} messageId The message Id identifying the giveaway * @param {GiveawayData} giveawayData The giveaway data to save */ async saveGiveaway(messageId, giveawayData) { await writeFile( this.options.storage, JSON.stringify( this.giveaways.map((giveaway) => giveaway.data), (_, v) => (typeof v === 'bigint' ? serialize(v) : v) ), 'utf-8' ); return; } /** * Checks each giveaway and update it if needed * @ignore */ _checkGiveaway() { if (this.giveaways.length <= 0) return; this.giveaways.forEach(async (giveaway) => { // First case: giveaway is ended and we need to check if it should be deleted if (giveaway.ended) { if ( Number.isFinite(this.options.endedGiveawaysLifetime) && giveaway.endAt + this.options.endedGiveawaysLifetime <= Date.now() ) { this.giveaways = this.giveaways.filter((g) => g.messageId !== giveaway.messageId); await this.deleteGiveaway(giveaway.messageId); } return; } // Second case: the giveaway is a drop if (giveaway.isDrop) { giveaway.message = await giveaway.fetchMessage().catch(() => {}); if (giveaway.messageReaction?.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; } } } // Delete the data of a drop which did not end within 1 week if (giveaway.startAt + DELETE_DROP_DATA_AFTER <= Date.now()) { this.giveaways = this.giveaways.filter((g) => g.messageId !== giveaway.messageId); return await this.deleteGiveaway(giveaway.messageId); } } // Third case: the giveaway is paused and we should check whether it should be unpaused if (giveaway.pauseOptions.isPaused) { if ( !Number.isFinite(giveaway.pauseOptions.unpauseAfter) && !Number.isFinite(giveaway.pauseOptions.durationAfterPause) ) { giveaway.options.pauseOptions.durationAfterPause = giveaway.remainingTime; giveaway.endAt = Infinity; await this.editGiveaway(giveaway.messageId, giveaway.data); } if ( Number.isFinite(giveaway.pauseOptions.unpauseAfter) && Date.now() > giveaway.pauseOptions.unpauseAfter ) { return this.unpause(giveaway.messageId).catch(() => {}); } } // Fourth case: giveaway should be ended right now. this case should only happen after a restart // Because otherwise, the giveaway would have been ended already (using the next case) if (giveaway.remainingTime <= 0) return this.end(giveaway.messageId).catch(() => {}); // Fifth case: the giveaway will be ended soon, we add a timeout so it ends at the right time // And it does not need to wait for _checkGiveaway to be called again giveaway.ensureEndTimeout(); // Sixth case: the giveaway will be in the last chance state soon, we add a timeout so it's updated at the right time // And it does not need to wait for _checkGiveaway to be called again if ( giveaway.lastChance.enabled && giveaway.remainingTime - giveaway.lastChance.threshold < (this.options.forceUpdateEvery || DEFAULT_CHECK_INTERVAL) ) { setTimeout(async () => { giveaway.message ??= await giveaway.fetchMessage().catch(() => {}); const embed = this.generateMainEmbed(giveaway, true); await giveaway.message ?.edit({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [embed], allowedMentions: giveaway.allowedMentions }) .catch(() => {}); }, giveaway.remainingTime - giveaway.lastChance.threshold); } // Fetch the message if necessary and make sure the embed is alright giveaway.message ??= await giveaway.fetchMessage().catch(() => {}); if (!giveaway.message) return; if (!giveaway.message.embeds[0]) await giveaway.message.suppressEmbeds(false).catch(() => {}); // Regular case: the giveaway is not ended and we need to update it 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.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, components: updatedButtons ?? undefined }) .catch(() => {}); } }); } /** * @ignore */ 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; } } }; // 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 || !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(Events.EndedGiveawayReactionAdded, giveaway, member, messageReaction); // this.emit(Events.GiveawayMemberJoined, giveaway, member, messageReaction); // if (giveaway.isDrop && messageReaction.count - 1 >= giveaway.winnerCount) await checkForDropEnd(giveaway); // }); // 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 || giveaway.ended) 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(Events.GiveawayMemberLeft, giveaway, member, messageReaction); // }); this.client.on(Discord.Events.InteractionCreate, async (interaction) => { 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) { this.emit(Events.GiveawayAlreadyEnded, giveaway, interaction, this, Events); 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?.custom_id === interaction.customId || giveaway.buttons.join.data?.custom_id === 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)) { // if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); this.emit(Events.GiveawayMemberTryLeft, giveaway, interaction.member, interaction, this, Events); return; } if (giveaway.entrantIds.includes(interaction.member.id)) { this.emit( Events.GiveawayMemberAlreadyJoined, giveaway, interaction.member, interaction, this, Events ); return; } if (giveaway.isDrop && giveaway.entrantIds.length == 0) { giveaway.entrantIds.push(interaction.member.id); } if (giveaway.isDrop && giveaway.entrantIds.length >= giveaway.winnerCount) { interaction.deferUpdate(); await checkForDropEnd(giveaway); return; } // if(giveaway.isDrop){ // 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; // } // } // } // giveaway.entrantIds.push(interaction.member.id); // if (giveaway.buttons.joinReply) await replyToInteraction(giveaway.buttons.joinReply); this.emit(Events.GiveawayMemberJoined, giveaway, interaction.member, interaction, this, Events); } else if ( (giveaway.buttons.leave?.data.custom_id === interaction.customId && giveaway.entrantIds.includes(interaction.member.id)) || (giveaway.buttons.leave?.custom_id === interaction.customId && giveaway.entrantIds.includes(interaction.member.id)) ) { // const index = giveaway.entrantIds.indexOf(interaction.member.id); // giveaway.entrantIds.splice(index, 1); //if (giveaway.buttons.leaveReply) await replyToInteraction(giveaway.buttons.leaveReply); this.emit(Events.GiveawayMemberLeft, giveaway, interaction.member, interaction, this, Events); } await this.editGiveaway(giveaway.messageId, giveaway.data); }); } /** * Inits the manager * @ignore */ async _init() { let rawGiveaways = await this.getAllGiveaways(); 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) { const shardId = Discord.ShardClientUtil.shardIdForGuildId( this.client.guilds.cache.first().id, this.client.shard.count ); rawGiveaways = rawGiveaways.filter( (g) => shardId === Discord.ShardClientUtil.shardIdForGuildId(g.guildId, this.client.shard.count) ); } rawGiveaways.forEach((giveaway) => this.giveaways.push(new Giveaway(this, giveaway))); setInterval(() => { if (this.client.readyAt) this._checkGiveaway.call(this); }, this.options.forceUpdateEvery || DEFAULT_CHECK_INTERVAL); this.ready = true; // Delete data of ended giveaways if (Number.isFinite(this.options.endedGiveawaysLifetime)) { const endedGiveaways = this.giveaways.filter( (g) => g.ended && g.endAt + this.options.endedGiveawaysLifetime <= Date.now() ); this.giveaways = this.giveaways.filter( (g) => !endedGiveaways.map((giveaway) => giveaway.messageId).includes(g.messageId) ); for (const giveaway of endedGiveaways) await this.deleteGiveaway(giveaway.messageId); } this._handleEvents(); } } module.exports = GiveawaysManager;