UNPKG

vante-giveaways

Version:

A complete framework to facilitate the creation of giveaways using discord.js

789 lines (715 loc) 36 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 Giveaway = require('./Giveaway.js'); const { validateEmbedColor, embedEqual } = 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})`); /** * 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 || {}); 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, button = 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 + '\n' + giveaway.messages.drawing.replace( '{timestamp-relative}', giveaway.endAt === Infinity ? giveaway.pauseOptions.infiniteDurationText : `<t:${Math.round(giveaway.endAt / 1000)}:R>` ).replace( '{timestamp-default}', giveaway.endAt === Infinity ? giveaway.pauseOptions.infiniteDurationText : `<t:${Math.round(giveaway.endAt / 1000)}>` ) + (giveaway.hostedBy ? '\n' + giveaway.messages.hostedBy : '') + (button ? '\n\n' + giveaway.messages.participants.replace('{participants}', giveaway.participants.length).replace('{member}', `${giveaway.participants.length > 0 ? `<@${giveaway.participants[giveaway.participants.length - 1]}>` : '@Unknown'}`) : '') ) .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 : ''); for ( let i = 1; descriptionString(formattedWinners).length > 4096 || strings.title.length + strings.endedAt.length + descriptionString(formattedWinners).length > 6000; i++ ) { formattedWinners = formattedWinners.slice(0, formattedWinners.lastIndexOf(', <@')) + `, ${i} more`; } 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('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})`); } 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, emoji: this.options.default.buttonEmoji, 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 row = new Discord.ActionRowBuilder() .addComponents(new Discord.ButtonBuilder() .setEmoji(`${this.options.default.buttonEmoji}`) .setStyle(typeof this.options.default.buttonStyle === 'number' ? this.options.default.buttonStyle : Discord.ButtonStyle.Secondary) .setCustomId(`${giveaway.isDrop ? 'vante-drop' : 'vante-enter'}`) ) const embed = this.generateMainEmbed(giveaway); var message = await channel.send({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [embed], components: [row], }) giveaway.messageId = message.id; giveaway.message = message; this.giveaways.push(giveaway); await this.saveGiveaway(giveaway.messageId, giveaway.data); resolve(giveaway); if (giveaway.isDrop) { const collector = message.createMessageComponentCollector({ filter: (interaction) => interaction.customId === 'vante-drop' && interaction.user.id !== this.client.user.id && !giveaway.participants.includes(interaction.user.id), max: giveaway.winnerCount, }) collector.on('collect', async (interaction) => { if (!giveaway.participants.includes(interaction.user.id)) { const member = await interaction.guild.members.fetch(interaction.user.id).catch(() => {}); this.emit('giveawayJoined', giveaway, member, interaction); giveaway.participants.push(interaction.user.id); await this.editGiveaway(giveaway.messageId, giveaway.data, true); const row = new Discord.ActionRowBuilder() .addComponents(new Discord.ButtonBuilder() .setEmoji(`${this.options.default.buttonEmoji}`) .setLabel(`${giveaway.participants.length}`) .setStyle(typeof this.options.default.buttonStyle === 'number' ? this.options.default.buttonStyle : Discord.ButtonStyle.Secondary) .setCustomId('vante-drop') ) const vante = (await giveaway.fetchMessage().catch(() => { })) ?? giveaway.message; vante.edit({ components: [row] }) } }); collector.on('end', () => { this.end(giveaway.messageId); }); }; }); } /** * 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('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('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, message = false) { await writeFile( this.options.storage, JSON.stringify( this.giveaways.map((giveaway) => giveaway.data), (_, v) => (typeof v === 'bigint' ? serialize(v) : v) ), 'utf-8' ); if (message) { this.gi } 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) => { 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; } // 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, giveaway.participants.length > 0); 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, giveaway.participants.length > 0); const needUpdate = !embedEqual(giveaway.message.embeds[0].data, updatedEmbed.data) || giveaway.message.content !== giveaway.fillInString(giveaway.messages.giveaway); if (needUpdate || this.options.forceUpdateEvery) { await giveaway.message .edit({ content: giveaway.fillInString(giveaway.messages.giveaway), embeds: [updatedEmbed], allowedMentions: giveaway.allowedMentions }) .catch(() => {}); } }); } /** * @ignore * @param {any} packet */ async _handleRawPacket(packet) { if (!packet.isButton()) return; if (packet.user.id === this.client.user.id) return; if (packet.customId !== 'vante-enter') return let giveaway = this.giveaways.find((g) => g.messageId === packet.message?.id); if (!giveaway || (giveaway.ended)) return; const guild = this.client.guilds.cache.get(packet.guild?.id) || (await this.client.guilds.fetch(packet.guild?.id).catch(() => {})); if (!guild || !guild.available) return; const member = await guild.members.fetch(packet.user.id).catch(() => {}); if (!member) return; const channel = await this.client.channels.fetch(packet.channel?.id).catch(() => {}); if (!channel) return; const message = await channel.messages.fetch(packet.message?.id).catch(() => {}); if (!message) return; if (!giveaway.participants.includes(packet.user.id)) { this.emit('giveawayJoined', giveaway, member, packet); giveaway.participants.push(packet.user.id); await this.editGiveaway(giveaway.messageId, giveaway.data, true); const row = new Discord.ActionRowBuilder() .addComponents(new Discord.ButtonBuilder() .setEmoji(`${this.options.default.buttonEmoji}`) .setLabel(`${giveaway.participants.length}`) .setStyle(typeof this.options.default.buttonStyle === 'number' ? this.options.default.buttonStyle : Discord.ButtonStyle.Secondary) .setCustomId('vante-enter') ) const lastChanceEnabled = giveaway.lastChance.enabled && giveaway.remainingTime < giveaway.lastChance.threshold; const buttonClickedEnabled = giveaway.participants.length > 0 const embed = this.generateMainEmbed(giveaway, lastChanceEnabled, buttonClickedEnabled); const vante = (await giveaway.fetchMessage().catch(() => {})) ?? giveaway.message; vante.edit({ embeds: [embed], components: [row] }) } else { this.emit('giveawayLeaved', giveaway, member, packet); giveaway.participants.splice(giveaway.participants.indexOf(packet.user.id), 1); await this.editGiveaway(giveaway.messageId, giveaway.data, true); const row = new Discord.ActionRowBuilder() .addComponents(new Discord.ButtonBuilder() .setEmoji(`${this.options.default.buttonEmoji}`) .setLabel(`${giveaway.participants.length == 0 ? ' ' : giveaway.participants.length}`) .setStyle(typeof this.options.default.buttonStyle === 'number' ? this.options.default.buttonStyle : Discord.ButtonStyle.Secondary) .setCustomId('vante-enter') ) const lastChanceEnabled = giveaway.lastChance.enabled && giveaway.remainingTime < giveaway.lastChance.threshold; const buttonClickedEnabled = giveaway.participants.length > 0 const embed = this.generateMainEmbed(giveaway, lastChanceEnabled ?? false, buttonClickedEnabled); const vante = (await giveaway.fetchMessage().catch(() => {})) ?? giveaway.message; vante.edit({ embeds: [embed], components: [row] }) } } /** * Inits the manager * @ignore */ async _init() { let rawGiveaways = await this.getAllGiveaways(); await (this.client.readyAt ? Promise.resolve() : new Promise((resolve) => this.client.once('ready', 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.client.on('interactionCreate', (packet) => this._handleRawPacket(packet)); } } /** * 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#giveawayJoined * @param {Giveaway} giveaway The giveaway instance * @param {Discord.GuildMember} member The member who entered the giveaway * @param {Discord.Interaction} interaction The interaction that gets triggered when clicked the button * * @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('giveawayJoined', (giveaway, member, interaction) => { * if (!giveaway.isDrop) return interaction.reply({ content: `:tada: Congratulations **${member.user.username}**, you have joined the giveaway`, ephemeral: true }) * * interaction.reply({ content: `:tada: Congratulations **${member.user.username}**, you have joined the drop giveaway`, ephemeral: true }) * }); */ /** * Emitted when someone leaved a giveaway. * @event GiveawaysManager#giveawayReactionRemoved * @param {Giveaway} giveaway The giveaway instance * @param {Discord.GuildMember} member The member who remove their reaction giveaway * @param {Discord.Interaction} interaction The interaction that gets triggered when clicked the button * * @example * // This can be used to add features such as a member-left-giveaway message per DM * manager.on('giveawayLeaved', (giveaway, member, interaction) => { * interaction.reply({ content: `**${member.user.username}**, you have left the drop giveaway`, ephemeral: true }) * }); */ /** * 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;