givify
Version:
A Advance Discord Giveaway System
846 lines (771 loc) • 38.3 kB
JavaScript
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;