UNPKG

discordjs-giveaways

Version:

A customisable giveaways manager for Discord.js v14 and mysql

702 lines (701 loc) 28.8 kB
"use strict"; 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;