discordjs-giveaways
Version:
A customisable giveaways manager for Discord.js v14 and mysql
702 lines (701 loc) • 28.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GiveawayManager = void 0;
const tslib_1 = require("tslib");
const discord_js_1 = require("discord.js");
const embeds = tslib_1.__importStar(require("../assets/embeds"));
const buttons = tslib_1.__importStar(require("../assets/buttons"));
const easy_json_database_1 = tslib_1.__importDefault(require("easy-json-database"));
const sequelize_1 = require("sequelize");
const SequelizeModel_1 = require("./SequelizeModel");
const mongoose_1 = tslib_1.__importDefault(require("mongoose"));
class GiveawayManager {
constructor(client, database, options) {
this.embeds = embeds;
this.buttons = buttons;
this.listeners = [];
this.sendMessages = true;
this.query = (search) => {
if (this.mode === 'mysql') {
return new Promise((resolve, reject) => {
this.database.connection.query(search, (error, request) => {
if (error)
reject(error);
else
resolve(request);
});
});
}
};
this.client = client;
this.cache = new discord_js_1.Collection();
this.ended = new discord_js_1.Collection();
this.mode = database.mode;
if (this.isJSON()) {
const opts = database;
this.database = {
file: new easy_json_database_1.default(opts.path),
mode: 'json',
path: opts.path
};
}
else if (this.isMySQL()) {
const opts = database;
this.database = {
mode: 'mysql',
connection: opts.connection
};
}
else if (this.isSequelize()) {
const opts = database;
this.database = {
mode: 'sequelize',
sequelize: opts.sequelize,
tableName: opts.tableName
};
}
else if (this.isMongoDB()) {
const opts = database;
this.database = {
mode: 'mongodb',
modelName: opts.modelName,
connection: opts.connection
};
}
this.embeds = options?.embeds ? Object.assign(this.embeds, options.embeds) : this.embeds;
this.buttons = options?.buttons ? Object.assign(this.buttons, options.buttons) : this.buttons;
this.sendMessages = ![null, undefined].includes(options?.sendMessages)
? options.sendMessages
: this.sendMessages;
if (this.isJSON()) {
if (!this.database.file.get('giveaways'))
this.database.file.set('giveaways', []);
}
}
// Is
isMySQL() {
return this.mode === 'mysql';
}
isJSON() {
return this.mode === 'json';
}
isSequelize() {
return this.mode === 'sequelize';
}
isMongoDB() {
return this.mode === 'mongodb';
}
/**
* @description Get the list of all the giveaways in JSON format.
* Use `map` to get it as a map, and `collection` to get it as a Discord collection
*/
get list() {
return {
ended: this.ended.toJSON(),
giveaways: this.cache.toJSON()
};
}
/**
* @description Get the list of all the giveaways in a map
* Use `list` to get it as a JSON array, or `collection` to get it as a Discord collection
*/
get map() {
const ended = new Map();
const giveaways = new Map();
this.ended.forEach((x) => ended.set(x.message_id, x));
this.cache.forEach((x) => giveaways.set(x.message_id, x));
return { ended, giveaways };
}
/**
* @description Get the list of all the giveaways in a Discord collection
* Use `list` to get it as a JSON array, or `map` to get it as a map
*/
get collection() {
return {
ended: this.ended,
giveaways: this.cache
};
}
on(event, run) {
this.listeners.push({
event,
run
});
}
async start() {
if (this.isMySQL()) {
await this.query(`CREATE TABLE IF NOT EXISTS giveaways ( guild_id TEXT(255) NOT NULL, channel_id TEXT(255) NOT NULL, message_id TEXT(255) NOT NULL, hoster_id TEXT(255) NOT NULL, reward TEXT(255) NOT NULL, winnerCount INTEGER(255) NOT NULL DEFAULT "1", endsAt VARCHAR(1024) NOT NULL, participants LONGTEXT, required_roles LONGTEXT, denied_roles LONGTEXT, bonus_roles LONGTEXT, winners LONGTEXT, ended TINYINT(1) NOT NULL DEFAULT "0", required_servers LONGTEXT NOT NULL DEFAULT '[]' );`);
}
if (this.isJSON()) {
if (!this.database.file.get('giveaways'))
this.database.file.set('giveaways', []);
}
if (this.isSequelize()) {
SequelizeModel_1.GiveawaysSequelizeModel.init(SequelizeModel_1.giveawaySequelizeAttributes, {
sequelize: this.database.sequelize,
modelName: this.database.tableName
});
await SequelizeModel_1.GiveawaysSequelizeModel.sync({ alter: true }).catch((err) => {
console.log('Error while syncing the database');
throw err;
});
}
if (this.isMongoDB()) {
this.database.connection.model(this.database.modelName, new mongoose_1.default.Schema({
guild_id: { type: String, required: true },
channel_id: { type: String, required: true },
message_id: { type: String, required: true },
hoster_id: { type: String, required: true },
reward: { type: String, required: true },
winnerCount: { type: Number, required: true, default: 1 },
endsAt: { type: Date, required: true },
participants: { type: Array, default: [] },
required_roles: { type: Array, default: [] },
denied_roles: { type: Array, default: [] },
bonus_roles: { type: Array, default: [] },
winners: { type: Array, default: [] },
ended: { type: Boolean, default: false },
required_servers: { type: Array, default: [] }
}));
}
this.fillCache();
setInterval(() => {
this.cache
.filter((x) => x.endsAt <= Date.now() && !x.ended)
.forEach((x) => {
this.endGiveaway(x.message_id);
});
}, 15000);
this.setOnInteraction();
}
/**
* @description Create a giveaway in a server with the data that you specified
* @param input Giveaway datas
*/
createGiveaway(input) {
return new Promise(async (resolve, reject) => {
const embed = this.embeds.giveaway(input);
const msg = await input.channel
.send({
embeds: [embed],
components: [
buttons.getAsRow([
this.buttons.participate('gw-participate'),
this.buttons.cancelParticipation('gw-unparticipate')
])
]
})
.catch(() => { });
if (!msg)
return reject('No message');
if (input.required_servers.length > 0) {
if (input.required_servers.some(async (x) => !(this.client.guilds.cache.has(x.id) || await this.client.guilds.fetch(x.id).catch(() => { }))))
return reject('Client is not on required servers');
}
const data = {
guild_id: input.channel.guild.id,
channel_id: input.channel.id,
message_id: msg.id,
hoster_id: input.hoster_id,
endsAt: input.time + Date.now(),
ended: false,
required_roles: input?.required_roles ?? [],
denied_roles: input?.denied_roles ?? [],
required_servers: input?.required_servers ?? [],
participants: [],
bonus_roles: input?.bonus_roles ?? [],
winners: [],
winnerCount: input.winnerCount,
reward: input.reward
};
this.cache.set(data.message_id, data);
this.insertGiveaway(data);
this.client.emit('giveawayStarted', data, input.channel, input.hoster_id);
resolve(data);
});
}
async purgeGiveaways(endedSinceMs) {
const giveaways = this.cache.filter(x => x.ended && x.endsAt <= Date.now() - endedSinceMs);
if (giveaways.size === 0)
return [];
if (this.isMySQL()) {
this.query(`DELETE FROM giveaways WHERE ended = 1 AND endsAt <= ${Date.now() - endedSinceMs}`).catch(() => { });
}
if (this.isJSON()) {
const array = this.database.file.get('giveaways');
const filtered = array.filter((x) => x.ended && x.endsAt <= Date.now() - endedSinceMs);
this.database.file.set('giveaways', array.filter((x) => !filtered.includes(x)));
}
if (this.isMongoDB()) {
const model = this.database.connection.model(this.database.modelName);
await model.deleteMany({ ended: true, endsAt: { $lte: new Date(Date.now() - endedSinceMs) } }).catch(() => { });
}
if (this.isSequelize()) {
await SequelizeModel_1.GiveawaysSequelizeModel.destroy({
where: {
ended: true,
endsAt: {
[sequelize_1.Op.lte]: new Date(Date.now() - endedSinceMs)
}
}
}).catch(() => { });
}
return giveaways.toJSON();
}
/**
* @description Use it to fetch a giveaway. You can use the message ID, the channel ID or the guild ID.
* In the case of the channel ID, it will return the last launched giveaway in the channel.
* In the case of the guild ID, it will return the last launched giveaway of the server
* @param input The search you want to do. You can use message, channel or guild ID
* @param force This parameter makes the function searching giveaways even the ended ones.
* Default value is false
*/
fetchGiveaway(input, force) {
if (this.cache.has(input))
return this.cache.get(input);
const channel = this.cache.filter((x) => x.channel_id === input);
if (channel.size > 0)
return channel.last();
const guild = this.cache.filter((x) => x.guild_id === input);
if (guild.size > 0)
return guild.last();
if (force === true) {
if (this.ended.has(input))
return this.ended.get(input);
const channel = this.ended.filter((x) => x.channel_id === input);
if (channel.size > 0)
return channel.last();
const guild = this.ended.filter((x) => x.guild_id === input);
if (guild.size > 0)
return guild.last();
}
return undefined;
}
/**
* @description End a giveaway and return an array with the winners
* @param input ID of the message of the giveaway you want to end
* You can use only the message ID
* @returns The promise is always resolved.
* Be aware, it can return an ampty array or string values
*/
endGiveaway(input) {
return new Promise(async (resolve) => {
const gw = this.cache.get(input);
if (!gw)
return resolve('no giveaway');
const guild = this.client.guilds.cache.get(gw.guild_id);
if (!guild)
return resolve('no guild');
const channel = (await guild.channels.fetch(gw.channel_id));
if (!channel)
return resolve('no channel');
await channel.messages.fetch();
const message = channel.messages.cache.get(gw.message_id);
if (!message)
return resolve('no message');
const winners = await this.roll(gw, guild);
const embed = this.embeds.ended(gw, winners);
gw.winners = winners;
await message.edit({ embeds: [embed], components: [] }).catch((e) => {
console.log(e);
});
const em = gw.winners.length === 0
? this.embeds.noEntries(this.getUrl(gw))
: this.embeds.winners(winners, gw, this.getUrl(gw));
if (this.sendMessages)
await channel.send({ reply: { messageReference: message }, embeds: [em] }).catch((e) => {
console.log(e);
});
gw.ended = true;
this.cache.delete(gw.message_id);
this.ended.set(gw.message_id, gw);
this.updateGiveaway(gw.message_id, gw);
this.client.emit('giveawayEnded', gw, channel, winners);
return resolve(winners);
});
}
/**
* @description Reroll a giveaway by it's ID and return an array with the new winners
* @param input ID of the message of the giveaway you want to reroll
* You can use only the message ID
* @returns The promise is always resolved.
* Be aware, it can return an empty array or string values
*/
reroll(input) {
return new Promise(async (resolve) => {
let gw = this.ended.get(input);
if (!gw && this.cache.has(input))
return resolve('not ended');
if (!gw)
return resolve('no giveaway');
const guild = this.client.guilds.cache.get(gw.guild_id);
if (!guild)
return resolve('no guild');
const channel = guild.channels.cache.get(gw.channel_id);
if (!channel)
return resolve('no channel');
await channel.messages.fetch();
const message = channel.messages.cache.get(input);
if (!message)
return resolve('no message');
let old = gw.winners ?? [];
let winners = await this.roll(gw, guild);
gw.winners = winners;
const embed = this.embeds.ended(gw, winners);
await message.edit({ embeds: [embed] }).catch(() => { });
const em = gw.winners.length === 0
? this.embeds.noEntries(this.getUrl(gw))
: this.embeds.winners(winners, gw, this.getUrl(gw));
if (this.sendMessages)
await channel.send({ reply: { messageReference: message }, embeds: [em] }).catch(() => { });
this.updateGiveaway(gw.message_id, gw);
this.ended.set(input, gw);
this.client.emit('giveawayRerolled', gw, channel, old, winners);
return resolve(winners);
});
}
/**
* @description Delete a giveaway. You can delete a finished or a current giveaway.
* Suppress the message, erase it from the database and returns the values of the giveaway
* @param input Message ID of the giveaway you want to delete.
* You can use only the message ID
* @returns Returns the values stored for the giveaway.
*/
deleteGiveaway(input) {
return new Promise(async (resolve) => {
const gw = this.fetchGiveaway(input, true);
if (!gw)
return resolve('no giveaway');
const guild = this.client.guilds.cache.get(gw.guild_id);
if (!guild)
return resolve('no guild');
const channel = (await guild.channels.fetch(gw.channel_id));
if (!channel)
return resolve('no channel');
await channel.messages.fetch();
const message = channel.messages.cache.get(gw.message_id);
if (!message)
return resolve('no message');
await message.delete().catch(() => { });
this[this.cache.has(gw.message_id) ? 'cache' : 'ended'].delete(gw.message_id);
this.deleteGwDb(gw.message_id);
return resolve(gw);
});
}
async registerParticipation(interaction) {
const gw = this.cache.get(interaction.message.id);
if (!gw)
return interaction.deferUpdate();
await interaction.deferReply({ flags: ['Ephemeral'], ephemeral: true }).catch(() => { });
if (gw.participants.includes(interaction.user.id))
return interaction
.editReply({
embeds: [this.embeds.alreadyParticipate(this.getUrl(gw))]
})
.catch(() => { });
const mRoles = interaction.member.roles.cache;
if (gw.required_roles.length > 0) {
const missing = gw.required_roles.filter((x) => mRoles.has(x));
if (missing.length < gw.required_roles.length)
return interaction
.editReply({
embeds: [this.embeds.missingRequiredRoles(missing, this.getUrl(gw))],
})
.catch(() => { });
}
if (gw.denied_roles.length > 0) {
const missing = mRoles
.filter((x) => !gw.denied_roles.includes(x.id))
.toJSON()
.map((x) => x.id);
if (missing.length < mRoles.size)
return interaction
.editReply({
embeds: [this.embeds.hasDeniedRoles(missing, this.getUrl(gw))],
})
.catch(() => { });
}
if (gw.required_servers.length > 0) {
const results = await this.checkServersForUser(interaction.user, gw);
if (results.length > 0)
return interaction.editReply({
embeds: [this.embeds.notInServer(this.getUrl(gw), results.map(x => ({ name: x.name, link: x.invitation })))]
});
}
gw.participants.push(interaction.user.id);
interaction.message.edit({
embeds: [
this.embeds.giveaway({
bonus_roles: gw.bonus_roles,
required_roles: gw.required_roles,
denied_roles: gw.denied_roles,
channel: interaction.channel,
time: gw.endsAt - Date.now(),
hoster_id: gw.hoster_id,
reward: gw.reward,
winnerCount: gw.winnerCount,
guild_id: gw.guild_id,
participants: gw.participants
})
]
});
interaction
.editReply({ embeds: [this.embeds.participationRegistered(this.getUrl(gw))] })
.catch(() => { });
this.cache.set(gw.message_id, gw);
this.updateGiveaway(gw.message_id, gw);
}
/**
* Check if the user is indeed in the required servers
*
* @param user User to check
* @param giveaway
* @returns An array of Guilds where the user is not present
*/
async checkServersForUser(user, giveaway) {
const missing = [];
giveaway.required_servers.forEach(async (x) => {
const guild = this.client.guilds.cache.get(x.id) ?? await this.client.guilds.fetch(x.id).catch(() => { });
if (!guild)
return;
const member = guild.members.cache.get(user.id) ?? await guild.members.fetch(user.id).catch(() => { });
if (!member)
missing.push(x);
});
return missing;
}
unregisterParticipation(interaction) {
const gw = this.cache.get(interaction.message.id);
if (!gw)
return interaction.deferUpdate();
if (!gw.participants.includes(interaction.user.id))
return interaction
.reply({
embeds: [this.embeds.notParticipated(this.getUrl(gw))],
ephemeral: true
})
.catch(() => { });
gw.participants = gw.participants.filter((x) => x !== interaction.user.id);
this.cache.set(gw.message_id, gw);
this.updateGiveaway(gw.message_id, gw);
interaction
.reply({
embeds: [this.embeds.removeParticipation(this.getUrl(gw))],
ephemeral: true
})
.catch(() => { });
interaction.message
.edit({
embeds: [
this.embeds.giveaway({
bonus_roles: gw.bonus_roles,
required_roles: gw.required_roles,
denied_roles: gw.denied_roles,
channel: interaction.channel,
time: gw.endsAt - Date.now(),
hoster_id: gw.hoster_id,
reward: gw.reward,
winnerCount: gw.winnerCount,
guild_id: gw.guild_id,
participants: gw.participants
})
]
})
.catch(() => { });
}
setOnInteraction() {
this.client.on('interactionCreate', (interaction) => {
if (interaction.isButton() && interaction.guild) {
if (interaction.customId === 'gw-participate') {
this.registerParticipation(interaction);
}
if (interaction.customId === 'gw-unparticipate') {
this.unregisterParticipation(interaction);
}
}
});
}
getUrl({ guild_id, channel_id, message_id }) {
return `https://discord.com/channels/${guild_id}/${channel_id}/${message_id}`;
}
async filterGiveawayParticipants(gw) {
if (gw.participants.length === 0)
return true;
if (gw.required_servers.length === 0)
return true;
const guild = this.client.guilds.cache.get(gw.guild_id);
if (!guild)
return true;
const before = gw.participants.length;
gw.participants = gw.participants.filter(async (x) => {
const user = this.client.users.cache.get(x) ?? await this.client.users.fetch(x).catch(() => { });
if (!user)
return false;
const check = await this.checkServersForUser(user, gw);
if (check.length > 0)
return false;
return true;
});
if (gw.participants.length != before) {
this.updateGiveaway(gw.message_id, gw);
}
return true;
}
async roll(gw, guild) {
return new Promise(async (resolve) => {
if (gw.participants.length === 0)
return resolve([]);
if (!guild)
return resolve([]);
await this.filterGiveawayParticipants(gw);
if (gw.participants.length === 0)
return resolve([]);
let participants = [];
for (const id of gw.participants) {
const member = await guild.members.fetch(id).catch(() => { });
if (!member)
return;
participants.push(id);
if (gw.bonus_roles?.length > 0) {
for (const rId of gw.bonus_roles) {
if (member.roles.cache.has(rId))
participants.push(id);
}
}
}
if (participants.length == 0)
return resolve([]);
let winners = [];
const roll = () => {
let winner = participants[Math.floor(Math.random() * participants.length)];
if (winner) {
participants = participants.filter((x) => x !== winner);
}
return winner;
};
let i = 0;
let end = false;
while (end == false) {
i++;
let winner = roll();
if (winner)
winners.push(winner);
if (participants.length == 0 || i == gw.winnerCount)
end = true;
}
return resolve(winners);
});
}
makeQuery(data, exists) {
if (exists === true)
return `UPDATE giveaways SET ${Object.keys(data)
.map((k) => `${k}="${this.getValue(data[k])}"`)
.join(', ')} WHERE message_id='${data.message_id}'`;
return `INSERT INTO giveaways (${Object.keys(data)
.map((k) => k)
.join(', ')}) VALUES (${Object.keys(data)
.map((k) => `"${this.getValue(data[k])}"`)
.join(', ')})`;
}
getValue(x) {
if (typeof x === 'boolean')
return x ? '1' : '0';
if (typeof x === 'string')
return x.replace(/"/g, '\\"');
return JSON.stringify(x).replace(/"/g, '\\"');
}
toObj(x) {
let gw = x;
['participants', 'required_roles', 'winners', 'denied_roles', 'bonus_roles'].forEach((v) => {
gw[v] = JSON.parse(x[v]);
});
gw.ended = gw.ended === 1;
return gw;
}
async fillCache() {
const gws = await this.getDbGiveaways();
for (const gw of gws) {
const g = this.isJSON() ? gw : this.toObj(gw);
if (g.ended === true)
this.ended.set(g.message_id, g);
else
this.cache.set(g.message_id, g);
}
}
databaseQuery(sql) {
return new Promise((resolve, reject) => {
this.database.connection.query(sql, (error, request) => {
if (error)
return reject(error);
return resolve(request);
});
});
}
async getDbGiveaways() {
if (this.isMySQL()) {
return await this.databaseQuery(`SELECT * FROM giveaways`);
}
else if (this.isJSON()) {
return this.database.file.get('giveaways');
}
else if (this.isSequelize()) {
return (await SequelizeModel_1.GiveawaysSequelizeModel.findAll()).map((x) => x.dataValues);
}
}
updateGiveaway(message_id, data) {
if (data.message_id !== message_id) {
throw new Error('Critical: message_id and data.message_id are different. This should never appears, please contact the developper');
}
if (this.isJSON()) {
const array = this.database.file.get('giveaways');
const index = array.indexOf(array.find((x) => x.message_id === message_id));
array[index] = data;
this.database.file.set('giveaways', array);
}
else if (this.isMySQL()) {
this.query(this.makeQuery(data, true));
}
else if (this.isSequelize()) {
SequelizeModel_1.GiveawaysSequelizeModel.update(data, { where: { message_id } }).catch(console.error);
}
else if (this.isMongoDB()) {
const model = this.database.connection.model(this.database.modelName);
model.updateMany({ message_id }, data).catch(console.error);
}
}
insertGiveaway(data) {
if (this.isJSON()) {
const array = this.database.file.get('giveaways');
array.push(data);
this.database.file.set('giveaways', array);
}
else if (this.isMySQL()) {
this.query(this.makeQuery(data, false));
}
else if (this.isSequelize()) {
SequelizeModel_1.GiveawaysSequelizeModel.create(data).catch(console.error);
}
else if (this.isMongoDB()) {
const model = this.database.connection.model(this.database.modelName);
model.create(data).catch(console.error);
}
}
deleteGwDb(message_id) {
if (this.isJSON()) {
const array = this.database.file.get('giveaways');
array.splice(array.indexOf(array.find((x) => x.message_id === message_id)), 1);
this.database.file.set('giveaways', array);
}
else if (this.isMySQL()) {
this.query(`DELETE FROM giveaways WHERE message_id='${message_id}'`);
}
else if (this.isSequelize()) {
SequelizeModel_1.GiveawaysSequelizeModel.destroy({ where: { message_id } }).catch(console.error);
}
else if (this.isMongoDB()) {
const model = this.database.connection.model(this.database.modelName);
model.deleteMany({ message_id }).catch(console.error);
}
}
}
exports.GiveawayManager = GiveawayManager;