Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ requiredParticipationCount, requiredWinnercount, noValidEndingInterval #213

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ You can pass an options object to customize the giveaways. Here is a list of the
- **options.hasGuildMembersIntent**: whether the bot has access to the GUILD_MEMBERS intent. It works without, but it will be faster with.
- **options.default.botsCanWin**: whether bots can win a giveaway.
- **options.default.exemptPermissions**: an array of discord permissions. Members who have at least one of these permissions will not be able to win a giveaway even if they react to it.
- **options.default.requiredParticipationCount**: the required number of participations a giveaway needs to have before it is able to end
- **options.default.noValidEndingInterval**: the amount of time a giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
- **options.default.embedColor**: a hexadecimal color for the embeds of giveaways.
- **options.default.embedColorEnd**: a hexadecimal color the embeds of giveaways when they are ended.
- **options.default.reaction**: the reaction that users will have to react to in order to participate.
Expand Down Expand Up @@ -105,9 +107,12 @@ client.on('message', (message) => {
- **options.prize**: the giveaway prize.
- **options.hostedBy**: the user who hosts the giveaway.
- **options.winnerCount**: the number of giveaway winners.
- **options.requiredWinnerCount**: the required number of giveaway winners.
- **options.winnerIDs**: the IDs of the giveaway winners. ⚠ You do not have to and would not even be able to set this as a start option! The array only gets filled when a giveaway ends or is rerolled!
- **options.botsCanWin**: whether bots can win the giveaway.
- **options.exemptPermissions**: an array of discord permissions. Server members who have at least one of these permissions will not be able to win a giveaway even if they react to it.
- **options.default.requiredParticipationCount**: the required number of participations the giveaway needs to have before it is able to end
- **options.default.noValidEndingInterval**: the amount of time the giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
- **options.embedColor**: a hexadecimal color for the embeds of giveaways.
- **options.embedColorEnd**: a hexadecimal color the embeds of giveaways when they are ended.
- **options.reaction**: the reaction that users will have to react to in order to participate.
Expand Down Expand Up @@ -182,9 +187,12 @@ client.on('message', (message) => {
```

**options.newWinnerCount**: the new number of winners.
**options.newRequiredWinnerCount**: the new required number of winners.
**options.newPrize**: the new prize.
**options.addTime**: the number of milliseconds to add to the giveaway duration.
**options.setEndTimestamp**: the timestamp of the new end date. (for example, for the giveaway to be ended in 1 hour, set it to `Date.now() + 60000`).
**options.requiredParticipationCount**: the new required number of participations the giveaway needs to have before it is able to end
**options.noValidEndingInterval**: the new amount of time the giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached

⚠️ **Note**: to reduce giveaway time, define `addTime` with a negative number! For example `addTime: -5000` will reduce giveaway time by 5 seconds!

Expand Down
5 changes: 5 additions & 0 deletions examples/custom-databases/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const giveawaySchema = new mongoose.Schema({
endAt: Number,
ended: Boolean,
winnerCount: Number,
requiredWinnerCount: Number,
prize: String,
messages: {
giveaway: String,
Expand All @@ -37,6 +38,8 @@ const giveawaySchema = new mongoose.Schema({
winners: String,
endedAt: String,
hostedBy: String,
requiredParticipationCount: String,
requiredWinnerCount: String,
units: {
seconds: String,
minutes: String,
Expand All @@ -49,6 +52,8 @@ const giveawaySchema = new mongoose.Schema({
winnerIDs: [],
reaction: String,
botsCanWin: Boolean,
requiredParticipationCount: Number,
noValidEndingInterval: Number,
embedColor: String,
embedColorEnd: String,
exemptPermissions: [],
Expand Down
17 changes: 17 additions & 0 deletions src/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ const Discord = require('discord.js');
* @property {string} [winners='winner(s)'] Displayed next to the embed footer, used to display the number of winners of the giveaways.
* @property {string} [endedAt='End at'] Displayed next to the embed footer, used to display the giveaway end date.
* @property {string} [hostedBy='Hosted by: {user}'] Below the inviteToParticipate message, in the description of the embed.
* @property {string} [requiredParticipationCount] Sent in the channel if the required paricipation count did not get reached
* @property {string} [requiredWinnerCount] Sent in the channel if the required winner count did not get reached
* @property {Object} [units]
* @property {string} [units.seconds='seconds'] The name of the 'seconds' units
* @property {string} [units.minutes='minutes'] The name of the 'minutes' units
Expand All @@ -29,11 +31,14 @@ exports.GiveawayMessages = {};
*
* @property {number} time The giveaway duration
* @property {number} winnerCount The number of winners for the giveaway
* @property {number} [requiredWinnerCount] The required number of winners for the giveaway
* @property {string} prize The giveaway prize
* @property {Discord.User} [hostedBy] The user who hosts the giveaway
* @property {Boolean} [botsCanWin] Whether the bots are able to win a giveaway.
* @property {Array<Discord.PermissionResolvable>} [exemptPermissions] Members with any of these permissions won't be able to win a giveaway.
* @property {Function} [exemptMembers] Function to filter members. If true is returned, the member won't be able to win the giveaway.
* @property {number} [requiredParticipationCount] The required number of participations the giveaway needs to have before it's able to end
* @property {number} [noValidEndingInterval] The amount of time the giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
* @property {Discord.ColorResolvable} [embedColor] The giveaway embeds color when they are running
* @property {Discord.ColorResolvable} [embedColorEnd] The giveaway embeds color when they are ended
* @property {string} [reaction] The reaction to participate to the giveaways
Expand All @@ -57,6 +62,8 @@ exports.defaultGiveawayMessages = {
winners: 'winner(s)',
endedAt: 'End at',
hostedBy: 'Hosted by: {user}',
requiredParticipationCount: 'The **{prize}** giveaway needs **{requiredParticipationCount}** participation(s) before it can end!\n{messageURL}',
requiredWinnerCount: 'The **{prize}** giveaway needs at least **{requiredWinnerCount}** valid winner(s) before it can end!\n{messageURL}',
units: {
seconds: 'seconds',
minutes: 'minutes',
Expand All @@ -79,6 +86,8 @@ exports.defaultGiveawayMessages = {
* @property {Boolean} [default.botsCanWin=false] Whether the bots are able to win a giveaway.
* @property {Discord.PermissionResolvable[]} [default.exemptPermissions=[]] Members with any of these permissions won't be able to win a giveaway.
* @property {Function} [default.exemptMembers] Function to filter members. If true is returned, the member won't be able to win the giveaway.
* @property {number} [default.requiredParticipationCount=null] The required number of participations a giveaway needs to have before it is able to end
* @property {number} [default.noValidEndingInterval=null] The amount of time a giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
* @property {Discord.ColorResolvable} [default.embedColor='#FF0000'] The giveaway embeds color when they are running
* @property {Discord.ColorResolvable} [default.embedColorEnd='#000000'] The giveaway embeds color when they are ended
* @property {string} [default.reaction='🎉'] The reaction to participate to the giveaways
Expand All @@ -98,6 +107,8 @@ exports.defaultManagerOptions = {
botsCanWin: false,
exemptPermissions: [],
exemptMembers: () => false,
requiredParticipationCount: null,
noValidEndingInterval: null,
embedColor: '#FF0000',
embedColorEnd: '#000000',
reaction: '🎉'
Expand Down Expand Up @@ -132,9 +143,12 @@ exports.defaultRerollOptions = {
* @typedef GiveawayEditOptions
*
* @property {number} [newWinnerCount] The new number of winners
* @property {number} [newRequiredWinnerCount] The new number of required winners
* @property {string} [newPrize] The new giveaway prize
* @property {number} [addTime] Number of milliseconds to add to the giveaway duration
* @property {number} [setEndTimestamp] The timestamp of the new end date
* @property {number} [requiredParticipationCount] The new required number of participations thr giveaway needs to have before it is able to end
* @property {number} [noValidEndingInterval] The new amount of time the giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
* @property {GiveawayMessages} [newMessages] The new giveaway messages
* @property {any} [newExtraData] The new extra data value for this giveaway
*/
Expand All @@ -147,6 +161,7 @@ exports.GiveawayEditOptions = {};
* @property {number} startAt The start date of the giveaway
* @property {number} endAt The end date of the giveaway
* @property {number} winnerCount The number of winners of the giveaway
* @property {number} [requiredWinnerCount] The required number of winners of the giveaway
* @property {Array<string>} winnerIDs The winner IDs of the giveaway after it ended
* @property {GiveawayMessages} messages The giveaway messages
* @property {boolean} ended Whether the giveaway is ended
Expand All @@ -158,6 +173,8 @@ exports.GiveawayEditOptions = {};
* @property {boolean} [botsCanWin] Whether the bots can win the giveaway
* @property {Discord.PermissionResolvable[]} [exemptPermissions] Members with any of these permissions won't be able to win the giveaway
* @property {Function} [exemptMembers] Filter function to exempt members from winning the giveaway
* @property {number} [requiredParticipationCount] The required number of participations the giveaway needs to have before it is able to end
* @property {number} [noValidEndingInterval] The amount of time the giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
* @property {Discord.ColorResolvable} [embedColor] The color of the giveaway embed
* @property {Discord.ColorResolvable} [embedColorEnd] The color of the giveaway ended when it's ended
* @property {string?} [hostedBy] Mention of user who hosts the giveaway
Expand Down
86 changes: 74 additions & 12 deletions src/Giveaway.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ class Giveaway extends EventEmitter {
* @type {number}
*/
this.winnerCount = options.winnerCount;
/** The required number of winners for this giveaway
* @type {number}
*/
this.requiredWinnerCount = options.requiredWinnerCount;
/**
* The winner IDs for this giveaway after it ended
* @type {Array<string>}
Expand Down Expand Up @@ -163,6 +167,22 @@ class Giveaway extends EventEmitter {
return this.options.exemptPermissions || this.manager.options.default.exemptPermissions;
}

/**
* The number of required participations
* @type {Number}
*/
get requiredParticipationCount() {
return this.options.requiredParticipationCount || this.manager.options.default.requiredParticipationCount;
}

/**
* The amount of time a giveaway should get extended for if "requiredParticipationCount" or "requiredWinnerCount" get not reached
* @type {Number}
*/
get noValidEndingInterval() {
return this.options.noValidEndingInterval || this.manager.options.default.noValidEndingInterval;
}

/**
* Function to filter members. If true is returned, the member won't be able to win the giveaway.
* @type {Function}
Expand Down Expand Up @@ -258,6 +278,7 @@ class Giveaway extends EventEmitter {
endAt: this.endAt,
ended: this.ended,
winnerCount: this.winnerCount,
requiredWinnerCount: this.requiredWinnerCount,
prize: this.prize,
messages: this.messages,
hostedBy: this.options.hostedBy,
Expand All @@ -266,6 +287,8 @@ class Giveaway extends EventEmitter {
botsCanWin: this.options.botsCanWin,
exemptPermissions: this.options.exemptPermissions,
exemptMembers: this.options.exemptMembers,
requiredParticipationCount: this.options.requiredParticipationCount,
noValidEndingInterval: this.options.noValidEndingInterval,
reaction: this.options.reaction,
requirements: this.requirements,
winnerIDs: this.winnerIDs,
Expand All @@ -274,6 +297,31 @@ class Giveaway extends EventEmitter {
return baseData;
}

/**
* Fetches the giveaway reaction users
* @returns {Promise<Collection<Snowflake, User>>|Promise<Map<undefined, undefined>>} The reaction user collection or an empty map
*/
get reactionUsers() {
return (async () => {
try {
if (!this.message) return new Map();
const reactions = this.message.reactions.cache;
const reaction = reactions.get(this.reaction) || reactions.find((r) => r.emoji.name === this.reaction);
if (!reaction) return new Map()
const guild = this.channel.guild;
// Fetch guild members
if (this.manager.options.hasGuildMembersIntent) await guild.members.fetch();
const users = (await reaction.users.fetch())
.filter((u) => !u.bot || u.bot === this.botsCanWin)
.filter((u) => u.id !== this.message.client.user.id);
if (!users.size) return new Map()
return users;
} catch (err) {
return console.error(err);
}
})();
}

/**
* Fetches the giveaway message in its channel
* @returns {Promise<Discord.Message>} The Discord message
Expand Down Expand Up @@ -314,17 +362,7 @@ class Giveaway extends EventEmitter {
* @returns {Promise<Discord.GuildMember[]>} The winner(s)
*/
async roll(winnerCount) {
if (!this.message) return [];
// Pick the winner
const reactions = this.message.reactions.cache;
const reaction = reactions.get(this.reaction) || reactions.find((r) => r.emoji.name === this.reaction);
if (!reaction) return [];
const guild = this.channel.guild;
// Fetch guild members
if (this.manager.options.hasGuildMembersIntent) await guild.members.fetch();
const users = (await reaction.users.fetch())
.filter((u) => !u.bot || u.bot === this.botsCanWin)
.filter((u) => u.id !== this.message.client.user.id);
const users = await this.reactionUsers;
if (!users.size) return [];

const rolledWinners = users.random(Math.min(winnerCount || this.winnerCount, users.size));
Expand All @@ -345,7 +383,7 @@ class Giveaway extends EventEmitter {
}
}

return winners.map((user) => guild.member(user) || user);
return winners.map((user) => this.channel.guild.member(user) || user);
}

/**
Expand All @@ -367,9 +405,14 @@ class Giveaway extends EventEmitter {
}
// Update data
if (options.newWinnerCount) this.winnerCount = options.newWinnerCount;
if (!isNaN(options.newRequiredWinnerCount)) this.requiredWinnerCount = options.newRequiredWinnerCount;
if (options.newPrize) this.prize = options.newPrize;
if (options.addTime) this.endAt = this.endAt + options.addTime;
if (options.setEndTimestamp) this.endAt = options.setEndTimestamp;
if (!isNaN(options.newRequiredParticipationCount))
this.options.requiredParticipationCount = options.newRequiredParticipationCount;
if (!isNaN(options.newNoValidEndingInterval))
this.options.noValidEndingInterval = options.newNoValidEndingInterval;
if (options.newMessages) this.messages = merge(this.messages, options.newMessages);
if (options.newExtraData) this.extraData = options.newExtraData;
// Call the db method
Expand All @@ -396,6 +439,25 @@ class Giveaway extends EventEmitter {
return reject('Unable to fetch message with ID ' + this.messageID + '.');
}
const winners = await this.roll();
if (this.requiredWinnerCount && this.requiredWinnerCount > winners.length) {
if (this.noValidEndingInterval) {
if (this.remainingTime <= 60000) this.endAt = this.endAt + this.noValidEndingInterval;
this.ended = false;
this.channel.send(
this.messages.requiredWinnerCount
.replace('{prize}', this.prize)
.replace('{messageURL}', this.messageURL)
.replace('{requiredWinnerCount}', this.requiredWinnerCount)
).then((msg) => msg.delete({ timeout: this.noValidEndingInterval })).catch(() => {});
reject('Giveaway with message ID ' + this.messageID + ' is has not reached its "requiredWinnerCount" yet');
} else {
const embed = this.manager.generateNoValidParticipantsEndEmbed(this);
this.message.edit(this.messages.giveawayEnded, { embed }).catch(() => {});
resolve([])
}
await this.manager.editGiveaway(this.messageID, this.data);
return;
}
this.manager.editGiveaway(this.messageID, this.data);
if (winners.length > 0) {
this.winnerIDs = winners.map((w) => w.id);
Expand Down
Loading