diff --git a/CHANGELOG.md b/CHANGELOG.md index 815d527..30b4464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,38 @@ +# [2.5.0](https://github.com/Leyline-gg/leyline-discord/releases/tag/v2.5.0) (2021-10-31) + +## Dev Notes +The custom punishment system introduced previously has been rebranded to Justice System to better reflect the core values of Leyline. All "punish" wording has been changed to either "justice" or "sentence". +A lot of what we're doing at Leyline involves making new paths and breaking away from traditions and creating our own standard. Trial & error is a big component of this process, which means rebrands and revisions are more common than with a well-established company. In addition to the renaming of the Discord justice system, Leyline Points are also being renamed across the platform to Good Points. For more info on any of the changes we make, whether on Discord or not, the weekly AMA on Saturday is a great place to ask your questions in a casual environment and get responses from the core Leyline team. + +## New Features +- Good Acts submissions can now be manually approved by Moderators + - This feature makes use of Discord's context menus + - It can be used to manually approve any message as a Good Act + - When a message is manually approved, the same process for automatic approval occurs (see previous changelogs for details) + - Any users that reacted to the message prior to its approval will receive GP + - Any users that react to the message within 24h of its approval will receive GP + - The approved act will be recorded on the user's Proof of Good ledger +- Good Acts and Kind Words submissions now support custom emoji for moderator reactions +- Good Acts and Kind Words submission approvals & rejections are now logged in a private staff-only channel + - The log information displayed in the embeds has been reformatted + +## Existing Feature Changes +- All `punish` subcommands have been split into their own commands + - Example: `/punish warn` is now `/warn` +- All uses of the phrase "punish" have been renamed to either "justice" or "sentence" +- All front-facing references to LLP have been renamed to GP. This includes: + - `inspect` command + - `profile` command + - all logs and DMs associated with user submissions + - the LLP tag has been changed to GP +- Good Acts and Kind Words submission approvals & rejections are no longer logged in #bot-log +- `awardnft channel` subcommand now only takes voice or stage channels for the `channel` parameter +- `sudosay` command now only takes non-thread text channels for the `channel` parameter +- The green color for success embeds has been darkened slightly + +## Bug Fixes +- `profile` command displaying an incorrect GP value + # [2.4.0](https://github.com/Leyline-gg/leyline-discord/releases/tag/v2.4.0) (2021-10-12) ## Dev Notes diff --git a/api/collectors.js b/api/collectors.js index 2c6101b..c44549d 100644 --- a/api/collectors.js +++ b/api/collectors.js @@ -102,3 +102,17 @@ export const getDiscordReactions = async function (uid) { doc.id === uid && res++; return res; } + +/** + * Get the Firestore document for a collector, if it exists + * @param {String} id The collector's id + * @returns {Promise} JSON collector data or undefined if non-existent + */ +export const fetchCollector = async function (id) { + const collector = await admin + .firestore() + .collection('discord/bot/reaction_collectors') + .doc(id) + .get(); + return collector.data(); +} diff --git a/api/index.js b/api/index.js index 4c46ef6..7f3e06b 100644 --- a/api/index.js +++ b/api/index.js @@ -1,6 +1,6 @@ export * from './polls'; export * from './collectors'; -export * from './llp'; +export * from './points'; export * from './nfts'; export * from './userConnections'; export * from './items'; diff --git a/api/llp.js b/api/points.js similarity index 74% rename from api/llp.js rename to api/points.js index d64dfe7..2636472 100644 --- a/api/llp.js +++ b/api/points.js @@ -1,12 +1,12 @@ import admin from 'firebase-admin'; /** - * Get the latest LLP balance of a Leyline user + * Get the latest GP balance of a Leyline user * Taken from webapp's api package `userService.ts` * @param {String} uid Leyline UID - * @returns {Promise} User's most up-to-date LLP balance + * @returns {Promise} User's most up-to-date GP balance */ -export const getLLPBalance = async function (uid) { +export const getPointsBalance = async function (uid) { const userDoc = await admin.firestore().doc(`users/${uid}`).get(); const userData = userDoc.data(); @@ -28,12 +28,12 @@ export const getLLPBalance = async function (uid) { } /** - * Get a Leyline user's total LLP earned + * Get a Leyline user's total GP earned * Taken from webapp's api `userService.ts` * @param {String} uid Leyline UID - * @returns {Promise} Total LLP earned up until this point + * @returns {Promise} Total GP earned up until this point */ -export const getTotalEarnedLLP = async function (uid) { +export const getTotalEarnedPoints = async function (uid) { const snapshotRef = await admin.firestore() .collection('leaderboards') .orderBy('snapshot_time', 'desc') @@ -51,11 +51,11 @@ export const getTotalEarnedLLP = async function (uid) { } /** - * Get a Leyline user's total LLP earned for volunteering + * Get a Leyline user's total GP earned for volunteering * @param {String} uid Leyline UID - * @returns {Promise} Approximate total LLP earned for volunteering + * @returns {Promise} Approximate total GP earned for volunteering */ -export const getVolunteerLLP = async function (uid) { +export const getVolunteerPoints = async function (uid) { const snapshot = await admin.firestore() .collection('leyline_points') .where('uid', '==', uid) @@ -66,12 +66,12 @@ export const getVolunteerLLP = async function (uid) { } /** - * Award a specific amount of LLP to a user, with an option to include transaction metadata + * Award a specific amount of GP to a user, with an option to include transaction metadata * @param {String} uid Leyline UID - * @param {Number} amount Amount of LLP to award + * @param {Number} amount Amount of GP to award * @param {Object} [metadata] Metadata for transaction. Should contain a `category` property */ -export const awardLLP = async function (uid, amount, metadata = {}) { +export const awardPoints = async function (uid, amount, metadata = {}) { return await admin.firestore().collection('leyline_points').add({ uid: uid, leyline_points: amount, diff --git a/classes/LeylineBot.js b/classes/LeylineBot.js index 555c719..9a34df3 100644 --- a/classes/LeylineBot.js +++ b/classes/LeylineBot.js @@ -1,4 +1,4 @@ -import { Client, Collection } from "discord.js"; +import { Client, Collection, Emoji } from "discord.js"; import config from '../config.js'; import { ConfirmInteraction, EmbedBase, Logger, CloudConfig } from "."; @@ -116,13 +116,26 @@ export class LeylineBot extends Client { } /** - * Sends a discord message on the bot's behalf to a public log channel, specific for punishments + * Sends a discord message on the bot's behalf to a public log channel, specific for sentences * @param {Object} args * @param {EmbedBase} args.embed Singular embed object to be sent in message * @returns {Promise} Promise which resolves to the sent message */ - async logPunishment({embed, ...options}) { - return (await this.channels.fetch(this.config.channels.punishment_log)).send({ + async logSentence({embed, ...options}) { + return (await this.channels.fetch(this.config.channels.mod_log)).send({ + embeds: [embed], + ...options, + }); + } + + /** + * Sends a discord message on the bot's behalf to a private log channel, specific for submissions + * @param {Object} args + * @param {EmbedBase} args.embed Singular embed object to be sent in message + * @returns {Promise} Promise which resolves to the sent message + */ + async logSubmission({embed, ...options}) { + return (await this.channels.fetch(this.config.channels.submission_log)).send({ embeds: [embed], ...options, }); @@ -149,21 +162,21 @@ export class LeylineBot extends Client { * Replies to an interaction * @param {Object} args Destructured arguments * @param {Interaction} args.intr Discord.js `Interaction` - * @param {EmbedBase} [args.embed] Singular embed object to be included in reply + * @param {EmbedBase} [args.embed] Singular embed object to be included in reply. If unspecified, existing embeds are removed * @returns {Promise} The reply that was sent */ - intrReply({intr, embed, ...options}) { + intrReply({intr, embed=null, ...options}) { const payload = { - ...embed && { embeds: [embed] }, + embeds: !!embed ? [embed] : [], fetchReply: true, ...options, }; return (intr.deferred || intr.replied) ? intr.editReply(payload) : intr.reply(payload); } - intrUpdate({intr, embed, ...options}) { + intrUpdate({intr, embed=null, ...options}) { const payload = { - ...embed && { embeds: [embed] }, + embeds: !!embed ? [embed] : [], fetchReply: true, ...options, }; @@ -232,4 +245,16 @@ export class LeylineBot extends Client { return ``; } + /** + * Construct an Discord.js emoji from destructured parameters (such as Firestore data) + * @param {Object} args Destructured arguments, see `Emoji` constructor + * @returns {Emoji} + */ + constructEmoji({name, id, animated=false, ...other} = {}) { + return Object.assign(new Emoji(this, { + name, + id, + animated, + }), other); + } } diff --git a/classes/LeylineUser.js b/classes/LeylineUser.js index fa05a7f..2a6a3b0 100644 --- a/classes/LeylineUser.js +++ b/classes/LeylineUser.js @@ -1,7 +1,7 @@ import * as Firebase from '../api'; export class LeylineUser { - // hours donated, blood donated, llp balance, days slept/exercised + // hours donated, blood donated, gp balance, days slept/exercised // leaderboard positions? for all time // avatar, total items in inventory constructor(uid) { @@ -10,9 +10,9 @@ export class LeylineUser { this.uid = uid; //this.discord_uid = discord_uid || (await Firebase.getDiscordDoc(uid, true)).id; this._username = doc?.username; - this.llp = await Firebase.getLLPBalance(uid); - this.total_llp = await Firebase.getTotalEarnedLLP(uid); - this.volunteer_llp = await Firebase.getVolunteerLLP(uid); + this.gp = await Firebase.getPointsBalance(uid); + this.total_gp = await Firebase.getTotalEarnedPoints(uid); + this.volunteer_gp = await Firebase.getVolunteerPoints(uid); this.rankings = await Firebase.getUserRankings(uid); this.inventory = await Firebase.getInventoryItems(uid); this.profile_url = `https://leyline.gg/profile/${doc?.profile_id}`; diff --git a/classes/collectors/GoodActsReactionCollector.js b/classes/collectors/GoodActsReactionCollector.js index 0525cf8..1667b48 100644 --- a/classes/collectors/GoodActsReactionCollector.js +++ b/classes/collectors/GoodActsReactionCollector.js @@ -5,9 +5,16 @@ const CTA_ROLE = '853414453206188063'; //role to ping when photo is approved export class GoodActsReactionCollector extends ReactionCollectorBase { //override parent properties - get REACTION_LLP() { return CloudConfig.get('ReactionCollector').GoodActs.REACTION_LLP; } - get APPROVAL_LLP() { return CloudConfig.get('ReactionCollector').GoodActs.APPROVAL_LLP; } - get MOD_EMOJIS() { return CloudConfig.get('ReactionCollector').GoodActs.MOD_EMOJIS; } + get REACTION_GP() { return CloudConfig.get('ReactionCollector').GoodActs.REACTION_GP; } + get APPROVAL_GP() { return CloudConfig.get('ReactionCollector').GoodActs.APPROVAL_GP; } + get MOD_EMOJIS() { + return CloudConfig.get('ReactionCollector').GoodActs.MOD_EMOJIS + .map(this.bot.constructEmoji) + .sort((a, b) => ( + {position: Number.MAX_VALUE, ...a}.position - + {position: Number.MAX_VALUE, ...b}.position + )); + } media_placeholder //unfortunately, there is no easy way to extract the thumbnail from a video posted in discord = 'https://cdn1.iconfinder.com/data/icons/growth-marketing/48/marketing_video_marketing-512.png'; @@ -29,14 +36,14 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { //check if user who reacted is msg author if(user.id === msg.author.id) return; - //award LLP to msg author + //award GP to msg author if(!(await Firebase.isUserConnectedToLeyline(msg.author.id))) this.handleUnconnectedAccount(msg.author, { - dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> received a reaction, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> received a reaction, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> recevied a reaction, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> recevied a reaction, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); - else await this.awardAuthorReactionLLP({ + else await this.awardAuthorReactionGP({ user: user, pog: `Discord \ ${msg._activityType} ${this.media_type[0].toUpperCase() + this.media_type.slice(1)} Received Reaction`, @@ -44,11 +51,11 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { return; } - async approveSubmission({user, reaction}) { + async approveSubmission({user, approval_emoji}) { const { bot, msg } = this; try { - //store the activity type for LLP award text both locally and in the cloud - msg._activityType = this.MOD_EMOJIS.find(e => e.unicode === reaction.emoji.name)?.keyword || 'Good Act'; + //store the activity type for GP award text both locally and in the cloud + msg._activityType = this.MOD_EMOJIS.find(e => e.toString() === approval_emoji.toString())?.keyword || 'Good Act'; await Firebase.approveCollector({collector: this, user, metadata: { activity_type: msg._activityType, }}); @@ -64,22 +71,40 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { msg, content: `<@&${CTA_ROLE}> 🚨 **NEW APPROVED ${this.media_type.toUpperCase()}!!** 🚨`, embed: new EmbedBase(bot, { - description: `A new ${this.media_type} was approved! Click [here](${msg.url} 'view message') to view the message.\nBe sure to react within 24 hours to get your LLP!`, + description: `A new ${this.media_type} was approved! Click [here](${msg.url} 'view message') to view the message.\nBe sure to react within 24 hours to get your GP!`, thumbnail: { url: this.media_type === 'photo' ? msg.attachments.first().url : this.media_placeholder }, }), }); - //log approval in bot log - bot.logDiscord({ - embed: new EmbedBase(bot, { + //Privately log approval + this.logApproval({ + user, + embed_data: { fields: [ { - name: `${this.media_type[0].toUpperCase() + this.media_type.slice(1)} Approved`, - value: `${bot.formatUser(user)} approved the [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> by ${bot.formatUser(msg.author)}` + name: 'Channel', + value: `<#${msg.channel.id}>`, + inline: true, + }, + { + name: 'Approved By', + value: bot.formatUser(user), + inline: true, + }, + { name: '\u200b', value: '\u200b', inline: true }, + { + name: 'Category', + value: msg._activityType, + inline: true, }, + { + name: 'Author', + value: bot.formatUser(msg.author), + inline: true, + }, + { name: '\u200b', value: '\u200b', inline: true }, ], - thumbnail: { url: this.media_type === 'photo' ? msg.attachments.first().url : this.media_placeholder }, - }), + }, }); this.setupApprovedCollector(); @@ -88,21 +113,21 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { const is_author_connected = await Firebase.isUserConnectedToLeyline(msg.author.id); if(!is_author_connected) this.handleUnconnectedAccount(msg.author, { - dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); - // I could add some catch statements here and log them to Discord (for the awarding LLP process) + // I could add some catch statements here and log them to Discord (for the awarding GP process) - //award LLP to msg author - else await this.awardApprovalLLP({ + //award GP to msg author + else await this.awardApprovalGP({ user: msg.author, pog: `Discord \ ${msg._activityType} ${this.media_type[0].toUpperCase() + this.media_type.slice(1)} Approved`, }); - // --- Give LLP to the users that have already reacted --- + // --- Give GP to the users that have already reacted --- // --- (this includes the mod that just approved the msg) --- await msg.fetchReactions(); for(const old_reaction of [...msg.reactions.cache.values()]) { @@ -111,11 +136,11 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { //store user's reaction right away, because we do the same in the approved collector await this.storeUserReaction(old_user); - //award LLP to msg author for receiving a reaction (except on his own reaction) + //award GP to msg author for receiving a reaction (except on his own reaction) //(this goes above the continue statement below) is_author_connected && old_user.id !== msg.author.id && - await this.awardAuthorReactionLLP({ + await this.awardAuthorReactionGP({ user: old_user, pog: `Discord \ ${msg._activityType} ${this.media_type[0].toUpperCase() + this.media_type.slice(1)} Received Reaction`, @@ -124,23 +149,26 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { //exit if user is not connected to Leyline if(!(await Firebase.isUserConnectedToLeyline(old_user.id))) { this.handleUnconnectedAccount(old_user, { - dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(old_user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(old_user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); continue; } - //award LLP! - await this.awardReactionLLP({user: old_user}); + //award GP! + await this.awardReactionGP({user: old_user}); } } } //remove all reactions added by the bot msg.reactions.cache.each(reaction => reaction.users.remove(bot.user)); - return; - } catch(err) { return bot.logger.error(err); } + return this; + } catch(err) { + bot.logger.error(err); + return this; + } } // Overwrite of parent method @@ -152,5 +180,3 @@ export class GoodActsReactionCollector extends ReactionCollectorBase { return super.loadMessageCache(doc); } }; - - diff --git a/classes/collectors/KindWordsReactionCollector.js b/classes/collectors/KindWordsReactionCollector.js index 1c9dbac..c1cc368 100644 --- a/classes/collectors/KindWordsReactionCollector.js +++ b/classes/collectors/KindWordsReactionCollector.js @@ -3,9 +3,16 @@ import { EmbedBase, XPService, ReactionCollectorBase, CloudConfig } from '..'; export class KindWordsReactionCollector extends ReactionCollectorBase { //override parent properties - get REACTION_LLP() { return CloudConfig.get('ReactionCollector').KindWords.REACTION_LLP; } - get APPROVAL_LLP() { return CloudConfig.get('ReactionCollector').KindWords.APPROVAL_LLP; } - get MOD_EMOJIS() { return CloudConfig.get('ReactionCollector').KindWords.MOD_EMOJIS; } + get REACTION_GP() { return CloudConfig.get('ReactionCollector').KindWords.REACTION_GP; } + get APPROVAL_GP() { return CloudConfig.get('ReactionCollector').KindWords.APPROVAL_GP; } + get MOD_EMOJIS() { + return CloudConfig.get('ReactionCollector').KindWords.MOD_EMOJIS + .map(this.bot.constructEmoji) + .sort((a, b) => ( + {position: Number.MAX_VALUE, ...a}.position - + {position: Number.MAX_VALUE, ...b}.position + )); + } constructor(bot, { msg, }) { @@ -23,7 +30,7 @@ export class KindWordsReactionCollector extends ReactionCollectorBase { } // Callback specific to this Collector class - async approveSubmission({user, reaction}) { + async approveSubmission({user}) { const { bot, msg } = this; try { await Firebase.approveCollector({collector: this, user}); @@ -34,38 +41,28 @@ export class KindWordsReactionCollector extends ReactionCollectorBase { msg: msg.id, }); - //log approval in bot log - bot.logDiscord({ - embed: new EmbedBase(bot, { - fields: [ - { - name: `${this.media_type[0].toUpperCase() + this.media_type.slice(1)} Approved`, - value: `${bot.formatUser(user)} approved the [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> by ${bot.formatUser(msg.author)}` - }, - ], - }), - }); - - this.setupApprovedCollector(); + //Privately log approval + this.logApproval({user}) + .setupApprovedCollector(); //ensure user is connected to LL const is_author_connected = await Firebase.isUserConnectedToLeyline(msg.author.id); if(!is_author_connected) this.handleUnconnectedAccount(msg.author, { - dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); - // I could add some catch statements here and log them to Discord (for the awarding LLP process) + // I could add some catch statements here and log them to Discord (for the awarding GP process) - //award LLP to msg author - else await this.awardApprovalLLP({ + //award GP to msg author + else await this.awardApprovalGP({ user: msg.author, pog: `Discord Kind Words Shared`, }); - // --- Give LLP to the users that have already reacted --- + // --- Give GP to the users that have already reacted --- // --- (this includes the mod that just approved the msg) --- await msg.fetchReactions(); for (const old_reaction of [...msg.reactions.cache.values()]) { @@ -77,15 +74,15 @@ export class KindWordsReactionCollector extends ReactionCollectorBase { //exit if user is not connected to Leyline if(!(await Firebase.isUserConnectedToLeyline(old_user.id))) { this.handleUnconnectedAccount(old_user, { - dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(old_user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(old_user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); continue; } - //award LLP! - await this.awardReactionLLP({user: old_user}); + //award GP! + await this.awardReactionGP({user: old_user}); } } } @@ -96,5 +93,3 @@ export class KindWordsReactionCollector extends ReactionCollectorBase { } catch(err) { return bot.logger.error(err); } } } - - diff --git a/classes/collectors/ReactionCollectorBase.js b/classes/collectors/ReactionCollectorBase.js index 3d2143d..16041a6 100644 --- a/classes/collectors/ReactionCollectorBase.js +++ b/classes/collectors/ReactionCollectorBase.js @@ -4,15 +4,26 @@ import { EmbedBase, CloudConfig } from '../'; export class ReactionCollectorBase { get APPROVAL_WINDOW() { return CloudConfig.get('ReactionCollector').APPROVAL_WINDOW; } //(hours) how long the mods have to approve a photo get REACTION_WINDOW() { return CloudConfig.get('ReactionCollector').REACTION_WINDOW; } //(hours) how long users have to react after collector approval - get APPROVAL_LLP() { return CloudConfig.get('ReactionCollector').APPROVAL_LLP; } //LLP awarded for approved post - get REACTION_LLP() { return CloudConfig.get('ReactionCollector').REACTION_LLP; } //LLP awarded for reacting + get APPROVAL_GP() { return CloudConfig.get('ReactionCollector').APPROVAL_GP; } //GP awarded for approved post + get REACTION_GP() { return CloudConfig.get('ReactionCollector').REACTION_GP; } //GP awarded for reacting // Emojis allowed in setupModReactionCollector /* Should be of structure { - unicode: String, + name: String, + id?: Snowflake, //for custom emoji + animated?: Boolean, keyword?: String, + position?: Number, //lower numbers get added to msg first add_on_msg?: boolean, } */ - get MOD_EMOJIS() { return CloudConfig.get('ReactionCollector').MOD_EMOJIS; } + get MOD_EMOJIS() { + return CloudConfig.get('ReactionCollector').MOD_EMOJIS + .map(this.bot.constructEmoji) + .sort((a, b) => ( + {position: Number.MAX_VALUE, ...a}.position - + {position: Number.MAX_VALUE, ...b}.position + )); + } + media_type = 'submission'; constructor(bot, { @@ -30,7 +41,7 @@ export class ReactionCollectorBase { /** * !! MUST BE IMPLEMENTED IN ALL SUBCLASSES !! * Method called after a reaction to an approved submission has been received. - * This method should specify actions in addition to reaction storage and reaction user "Moral Support" LLP awardal + * This method should specify actions in addition to reaction storage and reaction user "Moral Support" GP awardal * @param {Object} args Destructured args * @param {Reaction} args.reaction The received reaction * @param {User} args.user The user associated with the incoming reaction @@ -43,10 +54,10 @@ export class ReactionCollectorBase { * !! MUST BE IMPLEMENTED IN ALL SUBCLASSES !! * Method called after a submission has been approved * @param {Object} args Destructured args - * @param {Reaction} args.reaction The reaction that approved the submission + * @param {Emoji} args.approval_emoji The emoji of the reaction that approved the submission * @param {User} args.user The user that approved the submission */ - approveSubmission({reaction, user}) { + approveSubmission({approval_emoji, user}) { throw new Error(`${this.constructor.name} doesn't provide a ${this.reactionReceived.name} method`); } @@ -58,9 +69,9 @@ export class ReactionCollectorBase { //add initial reactions if(!from_firestore) - for (const reaction of this.MOD_EMOJIS) + for (const reaction of this.MOD_EMOJIS) reaction?.add_on_msg !== false && - msg.react(reaction.unicode); + msg.react(reaction.toString()); //setup collector this.collector = msg @@ -74,7 +85,7 @@ export class ReactionCollectorBase { return reaction.users.remove(user); //this takes the place of the reactioncollector filter - if(!(bot.checkMod(user.id) && this.MOD_EMOJIS.some(e => e.unicode === reaction.emoji.name))) + if(!(bot.checkMod(user.id) && this.MOD_EMOJIS.some(e => e.toString() === reaction.emoji.toString()))) return; await msg.fetchReactions(); @@ -86,13 +97,13 @@ export class ReactionCollectorBase { //end this modReactionCollector this.collector.stop(); - this.approveSubmission({reaction, user}); + this.approveSubmission({approval_emoji:reaction.emoji, user}); }); return this; } /** - * Sets up a specific ReactionCollector on an approved message that is designed to last for 24hrs and award LLP to users that react + * Sets up a specific ReactionCollector on an approved message that is designed to last for 24hrs and award GP to users that react * @param {Object} [options] Collector options * @param {Number} [options.duration] How long until the collector expires, in `ms` * @returns {ReactionCollectorBase} This class itself @@ -110,15 +121,15 @@ export class ReactionCollectorBase { //ensure user is connected to LL if(!(await Firebase.isUserConnectedToLeyline(user.id))) this.handleUnconnectedAccount(user, { - dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any LLP! + dm: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but because you have not connected your Discord & Leyline accounts, I couldn't award you any GP! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial`, - log: `${bot.formatUser(user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any LLP because they have not connected their Leyline & Discord accounts`, + log: `${bot.formatUser(user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, but I did not award them any GP because they have not connected their Leyline & Discord accounts`, }); //this handles the whole awarding process - else await this.awardReactionLLP({user}); + else await this.awardReactionGP({user}); - //await in case callback is async + //await in case child method is async await this.reactionReceived({reaction, user}); return; } catch(err) { return bot.logger.error(err); } @@ -126,10 +137,88 @@ export class ReactionCollectorBase { return this; } + /** + * Log an approval in a private log channel + * @param {Object} args Destructured arguments + * @param {User} args.user Discord.js `User` that approved the submission + * @param {Object} [args.embed_data] Embed data to be sent in the approval message + */ + logApproval({user, embed_data} = {}) { + const { bot, msg } = this; + + //log rejection using bot method + bot.logSubmission({ + embed: new EmbedBase(bot, { + title: 'Submission Approved', + url: msg.url, + fields: [ + { + name: 'Channel', + value: `<#${msg.channel.id}>`, + inline: true, + }, + { + name: 'Approved By', + value: bot.formatUser(user), + inline: true, + }, + { + name: 'Author', + value: bot.formatUser(msg.author), + inline: true, + }, + ], + thumbnail: { url: this.media_type === 'photo' ? msg.attachments.first().url : this.media_placeholder }, + ...embed_data, + }).Success(), + }); + + return this; + } + + /** + * Log a rejection in a private log channel + * @param {Object} args Destructured arguments + * @param {User} args.user Discord.js `User` that rejected the submission + * @param {Object} [args.embed_data] Embed data to be sent in the rejection message + */ + logRejection({user, embed_data} = {}) { + const { bot, msg } = this; + + //log rejection using bot method + bot.logSubmission({ + embed: new EmbedBase(bot, { + title: 'Submission Rejected', + url: msg.url, + fields: [ + { + name: 'Channel', + value: `<#${msg.channel.id}>`, + inline: true, + }, + { + name: 'Rejected By', + value: bot.formatUser(user), + inline: true, + }, + { + name: 'Author', + value: bot.formatUser(msg.author), + inline: true, + }, + ], + thumbnail: { url: this.media_type === 'photo' ? msg.attachments.first().url : this.media_placeholder }, + ...embed_data, + }).Error(), + }); + + return this; + } + /** * Soft-rejects a submission and logs actions appropriately - * @param {Object} [args] Destructured arguments - * @param {User} [args.user] Discord.js `User` that rejected the submission + * @param {Object} args Destructured arguments + * @param {User} args.user Discord.js `User` that rejected the submission */ rejectSubmission({user}) { const { bot, msg } = this; @@ -144,17 +233,7 @@ export class ReactionCollectorBase { msg.reactions.cache.each(reaction => reaction.users.remove(bot.user)); //log rejection - bot.logDiscord({ - embed: new EmbedBase(bot, { - fields: [ - { - name: `Submission Rejected`, - value: `${bot.formatUser(user)} rejected the [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> by ${bot.formatUser(msg.author)}` - }, - ], - thumbnail: { url: this.media_type === 'photo' ? msg.attachments.first().url : this.media_placeholder }, - }).Error(), - }); + this.logRejection({user}); return this; } @@ -165,6 +244,7 @@ export class ReactionCollectorBase { * @returns {boolean} `true` if an attachment was detected & stored, `false` otherwise */ processAttachment(url) { + url ||= ''; if (url.endsWith('.png') || url.endsWith('.jpg') || url.endsWith('.jpeg') || url.endsWith('.webp')) { this.media_type = 'photo'; return true; @@ -181,7 +261,7 @@ export class ReactionCollectorBase { * @param {User} user user that has not connected their accounts * @param {Object} args Destructured args * @param {string} args.dm Specific reason why user should connect their accounts - * @param {string} args.log Discord log content to send under the embed title `LLP NOT Awarded` + * @param {string} args.log Discord log content to send under the embed title `GP NOT Awarded` * @returns */ handleUnconnectedAccount(user, {dm, log} = {}) { @@ -199,7 +279,7 @@ export class ReactionCollectorBase { embed: new EmbedBase(bot, { fields: [ { - name: `LLP __NOT__ Awarded`, + name: `GP __NOT__ Awarded`, value: log, }, ], @@ -209,16 +289,16 @@ export class ReactionCollectorBase { } /** - * Award LLP to a user for having a submission approved, and log the transaction appropriately. + * Award GP to a user for having a submission approved, and log the transaction appropriately. * Assumes all checks have been previously applied. * @param {Object} args Destructured arguments * @param {User} args.user Discord user - * @param {string} args.pog "Proof of good" - message to display in LLP history + * @param {string} args.pog "Proof of good" - message to display in GP history */ - async awardApprovalLLP({user, pog}) { + async awardApprovalGP({user, pog}) { const { bot, msg } = this; - await Firebase.awardLLP(await Firebase.getLeylineUID(user.id), this.APPROVAL_LLP, { + await Firebase.awardPoints(await Firebase.getLeylineUID(user.id), this.APPROVAL_GP, { category: pog, comment: `User's Discord ${this.media_type} (${msg.id}) was approved by ${user.tag}`, }); @@ -227,19 +307,19 @@ export class ReactionCollectorBase { bot.sendDM({user, embed: new EmbedBase(bot, { fields: [ { - name: `🎉 You Earned Some LLP!`, - value: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, and you received **+${this.APPROVAL_LLP} LLP**!` + name: `🎉 You Earned Some GP!`, + value: `Your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, and you received **+${this.APPROVAL_GP} GP**!` }, ], })}); - //log LLP change in bot-log + //log GP change in bot-log bot.logDiscord({ embed: new EmbedBase(bot, { fields: [ { - name: `LLP Awarded`, - value: `${bot.formatUser(user)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, and I gave them **+${this.APPROVAL_LLP} LLP**`, + name: `GP Awarded`, + value: `${bot.formatUser(user)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> was approved, and I gave them **+${this.APPROVAL_GP} GP**`, }, ], }), @@ -248,17 +328,17 @@ export class ReactionCollectorBase { } /** - * Award LLP to a user for reacting to an approved submission, and log the transaction appropriately. + * Award GP to a user for reacting to an approved submission, and log the transaction appropriately. * Assumes all checks have been previously applied. * @param {Object} args Destructured arguments * @param {User} args.user Discord user - * @param {string} [args.pog] "Proof of good" - message to display in LLP history + * @param {string} [args.pog] "Proof of good" - message to display in GP history */ - async awardReactionLLP({user, pog=`Discord Moral Support`}) { + async awardReactionGP({user, pog=`Discord Moral Support`}) { const { bot, msg } = this; - //new user reacted, award LLP - await Firebase.awardLLP(await Firebase.getLeylineUID(user.id), this.REACTION_LLP, { + //new user reacted, award GP + await Firebase.awardPoints(await Firebase.getLeylineUID(user.id), this.REACTION_GP, { category: pog, comment: `User reacted to Discord message (${msg.id})`, }); @@ -266,8 +346,8 @@ export class ReactionCollectorBase { bot.sendDM({user, embed: new EmbedBase(bot, { fields: [ { - name: `🎉 You Earned Some LLP!`, - value: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, and received **+${this.REACTION_LLP} LLP**!` + name: `🎉 You Earned Some GP!`, + value: `You reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, and received **+${this.REACTION_GP} GP**!` }, ], })}); @@ -276,8 +356,8 @@ export class ReactionCollectorBase { embed: new EmbedBase(bot, { fields: [ { - name: `LLP Awarded`, - value: `${bot.formatUser(user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, and I gave them **+${this.REACTION_LLP} LLP**`, + name: `GP Awarded`, + value: `${bot.formatUser(user)} reacted to the [${this.media_type}](${msg.url} 'click to view message') posted by ${bot.formatUser(msg.author)} in <#${msg.channel.id}>, and I gave them **+${this.REACTION_GP} GP**`, }, ], }), @@ -286,16 +366,16 @@ export class ReactionCollectorBase { } /** - * Award LLP to the author of an approved submission when someone else reacts, and log the transaction appropriately. + * Award GP to the author of an approved submission when someone else reacts, and log the transaction appropriately. * Assumes all checks have been previously applied. * @param {Object} args Destructured arguments * @param {User} args.user Discord user - * @param {string} args.pog "Proof of good" - message to display in LLP history + * @param {string} args.pog "Proof of good" - message to display in GP history */ - async awardAuthorReactionLLP({user, pog}) { + async awardAuthorReactionGP({user, pog}) { const { bot, msg } = this; - //new user reacted, award LLP - await Firebase.awardLLP(await Firebase.getLeylineUID(msg.author.id), this.REACTION_LLP, { + //new user reacted, award GP + await Firebase.awardPoints(await Firebase.getLeylineUID(msg.author.id), this.REACTION_GP, { category: pog, comment: `User's Discord ${this.media_type} (${msg.id}) received a reaction from ${user.tag}`, }); @@ -305,8 +385,8 @@ export class ReactionCollectorBase { embed: new EmbedBase(bot, { fields: [ { - name: `🎉 You Earned Some LLP!`, - value: `Someone reacted reacted to your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}>, and you received **+${this.REACTION_LLP} LLP**!` + name: `🎉 You Earned Some GP!`, + value: `Someone reacted reacted to your [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}>, and you received **+${this.REACTION_GP} GP**!` }, ], })}); @@ -315,8 +395,8 @@ export class ReactionCollectorBase { embed: new EmbedBase(bot, { fields: [ { - name: `LLP Awarded`, - value: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> received a reaction, and I gave them **+${this.REACTION_LLP} LLP**`, + name: `GP Awarded`, + value: `${bot.formatUser(msg.author)}'s [${this.media_type}](${msg.url} 'click to view message') posted in <#${msg.channel.id}> received a reaction, and I gave them **+${this.REACTION_GP} GP**`, }, ], }), @@ -404,5 +484,3 @@ export class ReactionCollectorBase { return this; } } - - diff --git a/classes/Command.js b/classes/commands/Command.js similarity index 79% rename from classes/Command.js rename to classes/commands/Command.js index c7b1a6b..ec2e27e 100644 --- a/classes/Command.js +++ b/classes/commands/Command.js @@ -1,10 +1,11 @@ export class Command { constructor(bot, { name = null, - description = "No description provided.", + description = '', //cannot be empty for chat commands options = [], category, deferResponse = false, //for commands that take longer to run + type = 'CHAT_INPUT', ...other }) { this.bot = bot; @@ -12,8 +13,9 @@ export class Command { this.description = description; this.options = options; this.category = category; - this.defaultPermission = (this.category !== 'admin'); //lock admin cmds + this.defaultPermission = (this.category !== 'admin' && this.category !== 'moderator'); //lock admin cmds this.deferResponse = deferResponse; + this.type = type; Object.assign(this, other); } diff --git a/classes/commands/JusticeCommand.js b/classes/commands/JusticeCommand.js new file mode 100644 index 0000000..ab0689e --- /dev/null +++ b/classes/commands/JusticeCommand.js @@ -0,0 +1,93 @@ +import { Command, EmbedBase } from ".."; +import parse from 'parse-duration'; + +export class JusticeCommand extends Command { + constructor(bot, { + name, + description, + sentence_type, //uppercase string, see SENTENCE_TYPES + options: { + target=true, + duration=true, + reason=true, + } = {}, + } = {}) { + super(bot, { + name, + description, + options: [ + ...(target ? [{ + type: 'USER', + name: 'target', + description: 'The target user to sentence', + required: true, + }] : []), + ...(duration ? [{ + type: 'STRING', + name: 'duration', + description: 'The duration of the sentence, eg: 7d. No duration means indefinite', + required: false, + }] : []), + ...(reason ? [{ + type: 'STRING', + name: 'reason', + description: 'The reason the sentence was issued', + required: false, + }] : []), + ], + category: 'admin', + }); + this.sentence_type = sentence_type; + } + + parseInput(opts) { + const [user, duration, reason] = [ + opts.getUser('target'), + opts.getString('duration'), + opts.getString('reason'), + ]; + + const parsed_dur = parse(duration); //ms + if(!!duration && !parsed_dur) + throw new Error(`That's not a valid duration`); + + //convert duration to epoch timestamp + const expires = !!duration + ? Date.now() + parsed_dur + : null; + + return {user, duration, reason, expires}; + } + + getModConfirmation({intr, user, reason}) { + const { bot, sentence_type } = this; + return bot.intrConfirm({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: ` + ⚠ **Are you sure you want to ${sentence_type} ${bot.formatUser(user)} for \`${reason ?? 'No reason given'}\`?** + + Is this sentence consistent with the official rules & moderation protocol? + Is this sentence consistent with the other sentences you've issued this past month? + `, + }).Warn(), + }) + } + + checkEasterEgg({user, intr}) { + const { bot, sentence_type } = this; + return (sentence_type !== 'HISTORY' && user.id === '139120967208271872') + ? bot.intrReply({intr, embed: new EmbedBase(bot, { + title: 'Nice try!', + image: { + url: 'https://i.imgur.com/kAVql0f.jpg', + }, + }).Warn()}) + : false; + } + + executeSentence() { + throw new Error(`command ${this.constructor.name} does not have an execution implemented`); + } +} \ No newline at end of file diff --git a/classes/components/EmbedBase.js b/classes/components/EmbedBase.js index 54cbaaa..eb68f84 100644 --- a/classes/components/EmbedBase.js +++ b/classes/components/EmbedBase.js @@ -50,11 +50,11 @@ export class EmbedBase extends MessageEmbed { } Success() { - this.color = 0x35de2f; + this.color = 0x31d64d; return this; } - Punish() { + Sentence() { this.color = 0xe3da32; return this; } diff --git a/classes/index.js b/classes/index.js index 101fdd2..8d99317 100644 --- a/classes/index.js +++ b/classes/index.js @@ -1,4 +1,3 @@ -export * from './Command.js'; export * from './CommunityPoll.js'; export * from './LeylineUser.js'; export * from './Logger.js'; @@ -7,11 +6,13 @@ export * from './collectors/ReactionCollectorBase.js'; export * from './collectors/GoodActsReactionCollector.js'; export * from './collectors/KindWordsReactionCollector.js'; export * from './collectors/ReactionCollector.js'; +export * from './commands/Command.js'; +export * from './commands/JusticeCommand.js'; export * from './components/ConfirmInteraction.js'; export * from './components/EmbedBase.js'; export * from './events/DiscordEvent.js'; export * from './events/FirebaseEvent.js'; export * from './services/XPService.js'; -export * from './services/PunishmentService'; +export * from './services/SentenceService'; export * from './CloudConfig'; export * from './LeylineBot'; diff --git a/classes/services/PunishmentService.js b/classes/services/SentenceService.js similarity index 69% rename from classes/services/PunishmentService.js rename to classes/services/SentenceService.js index bf75e97..0bca12a 100644 --- a/classes/services/PunishmentService.js +++ b/classes/services/SentenceService.js @@ -2,9 +2,9 @@ import admin from 'firebase-admin'; import { scheduleJob } from 'node-schedule'; import { EmbedBase } from '..'; -export class PunishmentService { - static COLLECTION_PATH = 'discord/bot/punishments'; - static PUNISHMENT_TYPES = { +export class SentenceService { + static COLLECTION_PATH = 'discord/bot/sentences'; + static SENTENCE_TYPES = { WARN: 'WARN', MUTE: 'MUTE', KICK: 'KICK', @@ -12,18 +12,18 @@ export class PunishmentService { }; /** - * Record a punishment in firestore + * Record a sentence in firestore * @param {Object} args Destructured args * @param {string} args.uid Target user id - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {string} args.type See `PUNISHMENT_TYPES`. Type of punishment - * @param {number} [args.expires] Unix timestamp for when punishment should expire. `null` if no expiration - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason - * @param {number} [args.timestamp] Unix timestamp of when punishment was issued. Defaults to `Date.now()` + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {string} args.type See `SENTENCE_TYPES`. Type of sentence + * @param {number} [args.expires] Unix timestamp for when sentence should expire. `null` if no expiration + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason + * @param {number} [args.timestamp] Unix timestamp of when sentence was issued. Defaults to `Date.now()` * @param {Object} [args.metadata] Metadata to be included in Firestore doc * @returns Resolves to added doc */ - static async recordPunishment({uid, mod, type, expires=null, reason=null, timestamp=Date.now(), metadata=null} = {}) { + static async recordSentence({uid, mod, type, expires=null, reason=null, timestamp=Date.now(), metadata=null} = {}) { return await admin.firestore() .collection(this.COLLECTION_PATH) .add({ @@ -39,22 +39,22 @@ export class PunishmentService { } /** - * Log a punishment in the appropriate Discord channels + * Log a sentence in the appropriate Discord channels * AND send a message to the end user * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {User} args.user `User` object of user that is being punished - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {string} args.type See `PUNISHMENT_TYPES`. Type of punishment - * @param {number} [args.expires] Unix timestamp for when punishment should expire. `null` if no expiration - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason - * @param {number} [args.timestamp] Unix timestamp of when punishment was issued. Defaults to `Date.now()` + * @param {User} args.user `User` object of user that is being sentenced + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {string} args.type See `SENTENCE_TYPES`. Type of sentence + * @param {number} [args.expires] Unix timestamp for when sentence should expire. `null` if no expiration + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason + * @param {number} [args.timestamp] Unix timestamp of when sentence was issued. Defaults to `Date.now()` * @returns Resolves to added doc */ - static async logPunishment({bot, user, mod, type, expires=null, reason=null, timestamp=Date.now()} = {}) { - const { PUNISHMENT_TYPES } = this; + static async logSentence({bot, user, mod, sentence_type: type, expires=null, reason=null, timestamp=Date.now()} = {}) { + const { SENTENCE_TYPES } = this; const embed = new EmbedBase(bot, { - title: 'Punishment Issued', + title: 'Justice Served', fields: [ { name: 'Type', @@ -68,13 +68,13 @@ export class PunishmentService { }, { name: '\u200b', value: '\u200b', inline: true }, { - name: 'Reason', - value: reason ?? 'No reason given', + name: 'Expires', + value: !!expires ? bot.formatTimestamp(expires) : 'No expiration', inline: true, }, { - name: 'Expires', - value: !!expires ? bot.formatTimestamp(expires) : 'No expiration', + name: 'Reason', + value: reason ?? 'No reason given', inline: true, }, { name: '\u200b', value: '\u200b', inline: true }, @@ -82,7 +82,7 @@ export class PunishmentService { name: 'Current Warnings', value: `${(await this.getHistory({ user, - types: [PUNISHMENT_TYPES.WARN], + types: [SENTENCE_TYPES.WARN], })).length} Warnings`, inline: true, }, @@ -94,42 +94,42 @@ export class PunishmentService { { name: '\u200b', value: '\u200b', inline: true } ], timestamp, - }).Punish(); + }).Sentence(); //Message user await bot.sendDM({send_disabled_msg: false, user, embed}); //log publicly - if(type === PUNISHMENT_TYPES.BAN) - await bot.logPunishment({embed}); + if(type === SENTENCE_TYPES.BAN) + await bot.logDiscord({embed}); //log privately - await bot.logDiscord({embed}); + await bot.logSentence({embed}); } /** - * Schedule removal of a temporary punishment. + * Schedule removal of a temporary sentence. * * Data should be pre-fetched Firestore doc data, * or locally cached data reconstructed to match params * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {string} args.id Punishment ID retrieved from the Firestore `DocumentSnapshot` - * @param {Object} args.data Pre-fetched punishment data - * @param {string} args.data.type See `PUNISHMENT_TYPES`. Type of punishment + * @param {string} args.id Sentence ID retrieved from the Firestore `DocumentSnapshot` + * @param {Object} args.data Pre-fetched sentence data + * @param {string} args.data.type See `SENTENCE_TYPES`. Type of sentence * @param {string} args.data.uid Target Discord user ID - * @param {string} args.data.issued_by ID of Discord Mod that issued the punishment - * @param {number} args.data.expires Unix timestamp of punishment expiration - * @param {string} [args.data.reason] Reason punishment was issued - * @returns {PunishmentService} Resolves to this class + * @param {string} args.data.issued_by ID of Discord Mod that issued the sentence + * @param {number} args.data.expires Unix timestamp of sentence expiration + * @param {string} [args.data.reason] Reason sentence was issued + * @returns {SentenceService} Resolves to this class */ static scheduleRemoval({bot, id, data}) { - const { PUNISHMENT_TYPES: types } = this; + const { SENTENCE_TYPES: types } = this; // If no expiration, exit - // Check punishment type + // Check sentence type // Create scheduled job if(!data.expires) return; const job = scheduleJob(new Date(data.expires), (fire_date) => { - bot.logger.debug(`un${data.type} for punishment ${id} scheduled for ${fire_date} triggered at ${new Date()}`); + bot.logger.debug(`un${data.type} for sentence ${id} scheduled for ${fire_date} triggered at ${new Date()}`); switch(data.type) { case types.BAN: { this.unbanUser({bot, id, ...data}); @@ -141,32 +141,32 @@ export class PunishmentService { } } }); - bot.logger.log(`un${data.type} for punishment ${id} scheduled for ${job.nextInvocation()}`); + bot.logger.log(`un${data.type} for sentence ${id} scheduled for ${job.nextInvocation()}`); return this; } /** - * Issue a WARN punishment to a user. + * Issue a WARN sentence to a user. * Discord logging should be handeled separately * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {User} args.user `User` object of user that is being punished - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason + * @param {User} args.user `User` object of user that is being sentenced + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason * @returns {Promise} Resolves to true if successfully executed */ static async warnUser({bot, user, mod, reason=null} = {}) { - const type = this.PUNISHMENT_TYPES.WARN; + const type = this.SENTENCE_TYPES.WARN; const member = await bot.leyline_guild.members.fetch({ user, force: true, }); if(!member) throw new Error(`I could not find member ${user.id} in the server!`); - //issue punishment (log in cloud) - //store punishment in cloud - await this.recordPunishment({ + //issue sentence (log in cloud) + //store sentence in cloud + await this.recordSentence({ uid: user.id, mod, type, @@ -179,19 +179,19 @@ export class PunishmentService { } /** - * Issue a MUTE punishment to a user. + * Issue a MUTE sentence to a user. * Discord logging should be handeled separately * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {User} args.user `User` object of user that is being punished - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {number} [args.expires] Unix timestamp for when punishment should expire. `null` if no expiration - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason + * @param {User} args.user `User` object of user that is being sentenced + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {number} [args.expires] Unix timestamp for when sentence should expire. `null` if no expiration + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason * @returns {Promise} Resolves to true if successfully executed */ static async muteUser({bot, user, mod, expires=null, reason=null} = {}) { const MUTED_ROLE = bot.config.muted_role; - const type = this.PUNISHMENT_TYPES.MUTE; + const type = this.SENTENCE_TYPES.MUTE; const member = await bot.leyline_guild.members.fetch({ user, @@ -199,13 +199,13 @@ export class PunishmentService { }); if(!member) throw new Error(`I could not find member ${user.id} in the server!`); - //issue punishment + //issue sentence if(member.roles.cache.has(MUTED_ROLE)) throw new Error(`Member ${member.id} is already muted!`); - await member.roles.add(MUTED_ROLE, `Punishment issued by ${mod.tag}`); + await member.roles.add(MUTED_ROLE, `Justice Served by ${mod.tag}`); - //store punishment in cloud - const doc = await this.recordPunishment({ + //store sentence in cloud + const doc = await this.recordSentence({ uid: user.id, mod, type, @@ -230,28 +230,28 @@ export class PunishmentService { } /** - * Issue a KICK punishment to a user. + * Issue a KICK sentence to a user. * Discord logging should be handeled separately * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {User} args.user `User` object of user that is being punished - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason + * @param {User} args.user `User` object of user that is being sentenced + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason * @returns {Promise} Resolves to true if successfully executed */ static async kickUser({bot, user, mod, reason=null} = {}) { - const type = this.PUNISHMENT_TYPES.KICK; + const type = this.SENTENCE_TYPES.KICK; const member = await bot.leyline_guild.members.fetch({ user, force: true, }); if(!member) throw new Error(`I could not find member ${user.id} in the server!`); - //issue punishment - await member.kick(`Punishment issued by ${mod.tag}`); + //issue sentence + await member.kick(`Justice Served by ${mod.tag}`); - //store punishment in cloud - await this.recordPunishment({ + //store sentence in cloud + await this.recordSentence({ uid: user.id, mod, type, @@ -264,31 +264,32 @@ export class PunishmentService { } /** - * Issue a BAN punishment to a user. + * Issue a BAN sentence to a user. * Discord logging should be handeled separately * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {User} args.user `User` object of user that is being punished - * @param {User} args.mod `User` object of staff that is issuing punishment - * @param {number} [args.expires] Unix timestamp for when punishment should expire. `null` if no expiration - * @param {string} [args.reason] Mod-provided reason for why punishment was issued. `null` if no reason + * @param {User} args.user `User` object of user that is being sentenced + * @param {User} args.mod `User` object of staff that is issuing sentence + * @param {number} [args.expires] Unix timestamp for when sentence should expire. `null` if no expiration + * @param {string} [args.reason] Mod-provided reason for why sentence was issued. `null` if no reason * @returns {Promise} Resolves to true if successfully executed */ static async banUser({bot, user, mod, expires=null, reason=null} = {}) { - const type = this.PUNISHMENT_TYPES.BAN; + const type = this.SENTENCE_TYPES.BAN; const member = await bot.leyline_guild.members.fetch({ user, force: true, }); if(!member) throw new Error(`I could not find member ${user.id} in the server!`); - //issue punishment + //issue sentence await member.ban({ - reason: `Punishment issued by ${mod.tag}`, + reason: `Justice Served by ${mod.tag}`, + days: 1, }); - //store punishment in cloud - const doc = await this.recordPunishment({ + //store sentence in cloud + const doc = await this.recordSentence({ uid: user.id, mod, type, @@ -313,14 +314,14 @@ export class PunishmentService { } /** - * Retrieve all the punishments issued for a given user + * Retrieve all the sentences issued for a given user * @param {Object} args Destructured args - * @param {User} args.user user to query punishment history for - * @param {Array} [args.types] Array of valid `PUNISHMENT_TYPE`s to be filtered. Defaults to all types + * @param {User} args.user user to query sentence history for + * @param {Array} [args.types] Array of valid `SENTENCE_TYPE`s to be filtered. Defaults to all types * @returns {Promise[]>} - * Array of documents for the user's punishment history, sorted by date issued, descending + * Array of documents for the user's sentence history, sorted by date issued, descending */ - static async getHistory({user, types=Object.values(this.PUNISHMENT_TYPES)}) { + static async getHistory({user, types=Object.values(this.SENTENCE_TYPES)}) { return (await admin.firestore() .collection(this.COLLECTION_PATH) .where('uid', '==', user.id) @@ -329,14 +330,14 @@ export class PunishmentService { } /** - * Retrieve all the punishments issued by a moderator + * Retrieve all the sentences issued by a moderator * @param {Object} args Destructured args - * @param {User} args.user moderator to query punishment history for - * @param {Array} [args.types] Array of valid `PUNISHMENT_TYPE`s to be filtered. Defaults to all types + * @param {User} args.user moderator to query sentence history for + * @param {Array} [args.types] Array of valid `SENTENCE_TYPE`s to be filtered. Defaults to all types * @returns {Promise[]>} - * Array of documents for the moderator's punishment history, sorted by date issued, descending + * Array of documents for the moderator's sentence history, sorted by date issued, descending */ - static async getModHistory({user, types=Object.values(this.PUNISHMENT_TYPES)}) { + static async getModHistory({user, types=Object.values(this.SENTENCE_TYPES)}) { return (await admin.firestore() .collection(this.COLLECTION_PATH) .where('issued_by', '==', user.id) @@ -345,23 +346,23 @@ export class PunishmentService { } /** - * Generate a punishment history embed for a given user + * Generate a sentence history embed for a given user * @param {Object} args Destructured args * @param {LeylineBot} args.bot Leyline Bot class - * @param {User} args.user user to query punishment history for + * @param {User} args.user user to query sentence history for * @param {Array} args.history_docs Documents retrieved from `getHistory()` * @param {boolean} [args.mod] Whether the embed is for a mod issued history - * @returns {EmbedBase} embed with punishment history + * @returns {EmbedBase} embed with sentence history */ static generateHistoryEmbed({bot, user, history_docs, mod=false}) { const embed = new EmbedBase(bot, { title: mod - ? `Punishments Issued by ${user.tag} (${user.id})` - : `Punishment History for ${user.tag} (${user.id})`, - description: `**Total punishments: ${history_docs.length}**`, - }).Punish(); + ? `Sentences Issued by ${user.tag} (${user.id})` + : `Sentence History for ${user.tag} (${user.id})`, + description: `**Total sentences: ${history_docs.length}**`, + }).Sentence(); - //add each individual punishment to embed (25 fields max) + //add each individual sentence to embed (25 fields max) for(const doc of history_docs.slice(0, 25)) { const data = doc.data(); embed.fields.push({ @@ -382,14 +383,14 @@ export class PunishmentService { } /** - * Reverse a MUTE punishment to a user. + * Reverse a MUTE sentence to a user. * Discord logging is handeled in this function * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {string} args.id Punishment ID (Firestore doc id) + * @param {string} args.id Sentence ID (Firestore doc id) * @param {string} args.uid Target Discord user ID - * @param {string} args.issued_by ID of Discord Mod that issued the punishment - * @param {string} [args.reason] Reason punishment was issued + * @param {string} args.issued_by ID of Discord Mod that issued the sentence + * @param {string} [args.reason] Reason sentence was issued * @returns {Promise} Resolves to true if successfully executed */ static async unmuteUser({bot, id, uid, issued_by, reason=null}= {}) { @@ -404,11 +405,11 @@ export class PunishmentService { //generate embed to modify it later const embed = new EmbedBase(bot, { - title: 'Punishment Expired', + title: 'Sentence Expired', fields: [ { name: 'Type', - value: this.PUNISHMENT_TYPES.MUTE, + value: this.SENTENCE_TYPES.MUTE, inline: true, }, { @@ -418,18 +419,18 @@ export class PunishmentService { }, { name: '\u200b', value: '\u200b', inline: true }, { - name: 'Reason', - value: reason ?? 'No reason given', + name: 'Target', + value: bot.formatUser(member?.user), inline: true, }, { - name: 'Target', - value: bot.formatUser(member?.user), + name: 'Reason', + value: reason ?? 'No reason given', inline: true, }, { name: '\u200b', value: '\u200b', inline: true }, ], - }).Punish(); + }).Sentence(); if(!member) { embed.description = `⚠ I was unable to find the user in the server`; @@ -443,10 +444,10 @@ export class PunishmentService { await bot.logDiscord({embed}); return false; } - //remove punishment + //remove sentence await member.roles.remove( MUTED_ROLE, - `Scheduled unmute for punishment ${id} issued by ${issuer?.tag || 'Unknown User'}` + `Scheduled unmute for sentence ${id} issued by ${issuer?.tag || 'Unknown User'}` ); //log removal @@ -456,32 +457,32 @@ export class PunishmentService { } /** - * Reverse a BAN punishment to a user. + * Reverse a BAN sentence to a user. * Discord logging is handeled in this function * @param {Object} args Destructured args * @param {LeylineBot} args.bot bot - * @param {string} args.id Punishment ID (Firestore doc id) + * @param {string} args.id Sentence ID (Firestore doc id) * @param {string} args.uid Target Discord user ID - * @param {string} args.issued_by ID of Discord Mod that issued the punishment - * @param {string} [args.reason] Reason punishment was issued + * @param {string} args.issued_by ID of Discord Mod that issued the sentence + * @param {string} [args.reason] Reason sentence was issued * @returns {Promise} Resolves to true if successfully executed */ static async unbanUser({bot, id, uid, issued_by, reason=null} = {}) { const issuer = await bot.users.fetch(issued_by); - //remove punishment, store resolved user + //remove sentence, store resolved user const user = await bot.leyline_guild.bans.remove( uid, - `Scheduled unban for punishment ${id} issued by ${issuer?.tag || 'Unknown User'}` + `Scheduled unban for sentence ${id} issued by ${issuer?.tag || 'Unknown User'}` ) || await bot.users.fetch(uid); //log removal await bot.logDiscord({embed: new EmbedBase(bot, { - title: 'Punishment Expired', + title: 'Sentence Expired', fields: [ { name: 'Type', - value: this.PUNISHMENT_TYPES.BAN, + value: this.SENTENCE_TYPES.BAN, inline: true, }, { @@ -491,18 +492,18 @@ export class PunishmentService { }, { name: '\u200b', value: '\u200b', inline: true }, { - name: 'Reason', - value: reason ?? 'No reason given', + name: 'Target', + value: bot.formatUser(user), inline: true, }, { - name: 'Target', - value: bot.formatUser(user), + name: 'Reason', + value: reason ?? 'No reason given', inline: true, }, { name: '\u200b', value: '\u200b', inline: true }, ], - }).Punish()}); + }).Sentence()}); return true; } diff --git a/commands/admin/awardnft.js b/commands/admin/awardnft.js index 55d50b3..616c831 100644 --- a/commands/admin/awardnft.js +++ b/commands/admin/awardnft.js @@ -42,6 +42,10 @@ class awardnft extends Command { name: 'channel', description: 'The voice channel where all members inside it will receive an NFT', required: true, + channelTypes: [ + 'GUILD_VOICE', + 'GUILD_STAGE_VOICE', + ], }, ], }, diff --git a/commands/admin/inspect.js b/commands/admin/inspect.js index 326e69b..2a451dd 100644 --- a/commands/admin/inspect.js +++ b/commands/admin/inspect.js @@ -53,8 +53,8 @@ class inspect extends Command { inline: true }, { - name: 'LLP Balance', - value: `${!!llid ? await Firebase.getLLPBalance(llid) : 'N/A'}`, + name: 'GP Balance', + value: `${!!llid ? await Firebase.getPointsBalance(llid) : 'N/A'}`, inline: true }, ], diff --git a/commands/admin/justice/ban.js b/commands/admin/justice/ban.js new file mode 100644 index 0000000..fc7cc67 --- /dev/null +++ b/commands/admin/justice/ban.js @@ -0,0 +1,66 @@ +import { JusticeCommand, SentenceService, EmbedBase } from '../../../classes'; + +class ban extends JusticeCommand { + constructor(bot) { + super(bot, { + name: 'ban', + sentence_type: SentenceService.SENTENCE_TYPES.BAN, + description: 'Issue a temporary or permanent ban to a Discord user', + }); + } + + //Override parent + async executeSentence({intr, user, expires, reason}) { + const { bot, sentence_type } = this; + //issue sentence + await SentenceService.banUser({ + bot, + user, + mod: intr.user, + expires, + reason, + }); + //log sentence + /*await*/ SentenceService.logSentence({ + bot, + user, + mod: intr.user, + sentence_type, + expires, + reason, + }); + return bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `⚖ **Sentence Successfully Issued**`, + }).Sentence(), ephemeral: true}); + } + + async run({intr, opts}) { + const { bot, sentence_type } = this; + const { SENTENCE_TYPES } = SentenceService; + + const { user, reason, expires } = super.parseInput(opts); + + //send confirm prompt if this is a sentence in SENTENCE_TYPES + if (Object.keys(SENTENCE_TYPES).includes(sentence_type)) + if (!(await super.getModConfirmation({intr, user, reason}))) + return bot.intrReply({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: `❌ **Sentence canceled**`, + }).Error(), + }); + + + //easter egg + if(!!super.checkEasterEgg({intr, user})) return; + + this.executeSentence({intr, user, reason, expires}) + .catch(err => { + bot.logger.error(err); + bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); + }); + } +} + +export default ban; diff --git a/commands/admin/justice/history.js b/commands/admin/justice/history.js new file mode 100644 index 0000000..44e3c39 --- /dev/null +++ b/commands/admin/justice/history.js @@ -0,0 +1,63 @@ +import { JusticeCommand, SentenceService, EmbedBase } from '../../../classes'; + +class history extends JusticeCommand { + constructor(bot) { + super(bot, { + name: 'history', + sentence_type: 'HISTORY', + description: 'View the entire recorded sentence history for a Discord user', + options: { + duration: false, + reason: false, + }, + }); + } + + //Override parent + async executeSentence({intr, user}) { + const { bot } = this; + const mod = bot.checkMod(user.id); //we use this twice below + return bot.intrReply({ + intr, + embed: SentenceService.generateHistoryEmbed({ + bot, + user, + mod, + history_docs: await (mod + ? SentenceService.getModHistory({user}) + : SentenceService.getHistory({user})), + }), + ephemeral: true + }); + } + + async run({intr, opts}) { + const { bot, sentence_type } = this; + const { SENTENCE_TYPES } = SentenceService; + + const { user } = super.parseInput(opts); + + //send confirm prompt if this is a sentence in SENTENCE_TYPES + if (Object.keys(SENTENCE_TYPES).includes(sentence_type)) + if (!(await super.getModConfirmation({intr, user}))) + return bot.intrReply({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: `❌ **Sentence canceled**`, + }).Error(), + }); + + + //easter egg + if(!!super.checkEasterEgg({intr, user})) return; + + this.executeSentence({intr, user}) + .catch(err => { + bot.logger.error(err); + bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); + }); + } +} + +export default history; diff --git a/commands/admin/justice/kick.js b/commands/admin/justice/kick.js new file mode 100644 index 0000000..cd5e190 --- /dev/null +++ b/commands/admin/justice/kick.js @@ -0,0 +1,67 @@ +import { JusticeCommand, SentenceService, EmbedBase } from '../../../classes'; + +class kick extends JusticeCommand { + constructor(bot) { + super(bot, { + name: 'kick', + sentence_type: SentenceService.SENTENCE_TYPES.KICK, + description: 'Remove a Discord user from the server', + options: { + duration: false, + }, + }); + } + + //Override parent + async executeSentence({intr, user, reason}) { + const { bot, sentence_type } = this; + //issue sentence + await SentenceService.kickUser({ + bot, + user, + mod: intr.user, + reason, + }); + //log sentence + /*await*/ SentenceService.logSentence({ + bot, + user, + mod: intr.user, + sentence_type, + reason, + }); + return bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `⚖ **Sentence Successfully Issued**`, + }).Sentence(), ephemeral: true}); + } + + async run({intr, opts}) { + const { bot, sentence_type } = this; + const { SENTENCE_TYPES } = SentenceService; + + const { user, reason } = super.parseInput(opts); + + //send confirm prompt if this is a sentence in SENTENCE_TYPES + if (Object.keys(SENTENCE_TYPES).includes(sentence_type)) + if (!(await super.getModConfirmation({intr, user, reason}))) + return bot.intrReply({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: `❌ **Sentence canceled**`, + }).Error(), + }); + + + //easter egg + if(!!super.checkEasterEgg({intr, user})) return; + + this.executeSentence({intr, user, reason}) + .catch(err => { + bot.logger.error(err); + bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); + }); + } +} + +export default kick; diff --git a/commands/admin/justice/mute.js b/commands/admin/justice/mute.js new file mode 100644 index 0000000..46fd499 --- /dev/null +++ b/commands/admin/justice/mute.js @@ -0,0 +1,66 @@ +import { JusticeCommand, SentenceService, EmbedBase } from '../../../classes'; + +class mute extends JusticeCommand { + constructor(bot) { + super(bot, { + name: 'mute', + sentence_type: SentenceService.SENTENCE_TYPES.MUTE, + description: 'Issue a server mute to a Discord user', + }); + } + + //Override parent + async executeSentence({intr, user, expires, reason}) { + const { bot, sentence_type } = this; + //issue sentence + await SentenceService.muteUser({ + bot, + user, + mod: intr.user, + expires, + reason, + }); + //log sentence + /*await*/ SentenceService.logSentence({ + bot, + user, + mod: intr.user, + sentence_type, + expires, + reason, + }); + return bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `⚖ **Sentence Successfully Issued**`, + }).Sentence(), ephemeral: true}); + } + + async run({intr, opts}) { + const { bot, sentence_type } = this; + const { SENTENCE_TYPES } = SentenceService; + + const { user, reason, expires } = super.parseInput(opts); + + //send confirm prompt if this is a sentence in SENTENCE_TYPES + if (Object.keys(SENTENCE_TYPES).includes(sentence_type)) + if (!(await super.getModConfirmation({intr, user, reason}))) + return bot.intrReply({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: `❌ **Sentence canceled**`, + }).Error(), + }); + + + //easter egg + if(!!super.checkEasterEgg({intr, user})) return; + + this.executeSentence({intr, user, reason, expires}) + .catch(err => { + bot.logger.error(err); + bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); + }); + } +} + +export default mute; diff --git a/commands/admin/justice/warn.js b/commands/admin/justice/warn.js new file mode 100644 index 0000000..168dbf4 --- /dev/null +++ b/commands/admin/justice/warn.js @@ -0,0 +1,67 @@ +import { JusticeCommand, SentenceService, EmbedBase } from '../../../classes'; + +class warn extends JusticeCommand { + constructor(bot) { + super(bot, { + name: 'warn', + sentence_type: SentenceService.SENTENCE_TYPES.WARN, + description: 'Issue a written warning to a Discord user', + options: { + duration: false, + }, + }); + } + + //Override parent + async executeSentence({intr, user, reason}) { + const { bot, sentence_type } = this; + //issue sentence + await SentenceService.warnUser({ + bot, + user, + mod: intr.user, + reason, + }); + //log sentence + /*await*/ SentenceService.logSentence({ + bot, + user, + mod: intr.user, + sentence_type, + reason, + }); + return bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `⚖ **Sentence Successfully Issued**`, + }).Sentence(), ephemeral: true}); + } + + async run({intr, opts}) { + const { bot, sentence_type } = this; + const { SENTENCE_TYPES } = SentenceService; + + const { user, reason } = super.parseInput(opts); + + //send confirm prompt if this is a sentence in SENTENCE_TYPES + if (Object.keys(SENTENCE_TYPES).includes(sentence_type)) + if (!(await super.getModConfirmation({intr, user, reason}))) + return bot.intrReply({ + intr, + ephemeral: true, + embed: new EmbedBase(bot, { + description: `❌ **Sentence canceled**`, + }).Error(), + }); + + + //easter egg + if(!!super.checkEasterEgg({intr, user})) return; + + this.executeSentence({intr, user, reason}) + .catch(err => { + bot.logger.error(err); + bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); + }); + } +} + +export default warn; diff --git a/commands/admin/punish.js b/commands/admin/punish.js deleted file mode 100644 index eba00d7..0000000 --- a/commands/admin/punish.js +++ /dev/null @@ -1,258 +0,0 @@ -import { Command, EmbedBase, PunishmentService } from '../../classes'; -import parse from 'parse-duration'; - -class PunishmentSubCommand { - constructor({ - name, - description, - options: { - target=true, - duration=true, - reason=true, - } = {}, - } = {}) { - return { - type: 'SUB_COMMAND', - name, - description, - options: [ - ...(target ? [{ - type: 'USER', - name: 'target', - description: 'The target user to punish', - required: true, - }] : []), - ...(duration ? [{ - type: 'STRING', - name: 'duration', - description: 'The duration of the punishment, eg: 7d. No duration means indefinite', - required: false, - }] : []), - ...(reason ? [{ - type: 'STRING', - name: 'reason', - description: 'The reason the punishment was issued', - required: false, - }] : []), - ], - } - } -} - -class punish extends Command { - constructor(bot) { - super(bot, { - name: 'punish', - description: "Punishment utilities", - options: [ - new PunishmentSubCommand({ - name: 'warn', - description: 'Issue a written warning to a Discord user', - options: { - duration: false, - }, - }), - new PunishmentSubCommand({ - name: 'mute', - description: 'Issue a server mute to a Discord user', - }), - new PunishmentSubCommand({ - name: 'kick', - description: 'Remove a Discord user from the server', - options: { - duration: false, - }, - }), - new PunishmentSubCommand({ - name: 'ban', - description: 'Issue a temporary or permanent ban to a Discord user', - }), - new PunishmentSubCommand({ - name: 'history', - description: 'View the entire recorded punishment history for a Discord user', - options: { - duration: false, - reason: false, - } - }), - ], - category: 'admin', - //deferResponse: true, - }); - } - - subcommands = { - warn: async ({intr, type, user, reason}) => { - const { bot } = this; - //issue punishment - await PunishmentService.warnUser({ - bot, - mod: intr.user, - user, - reason, - }); - //log punishment - /*await*/ PunishmentService.logPunishment({ - bot, - user, - mod: intr.user, - type, - reason, - }); - return bot.intrReply({intr, embed: new EmbedBase(bot, { - description: `⚖ **Punishment Successfully Issued**`, - }).Punish(), ephemeral: true}); - }, - mute: async ({intr, type, user, expires, reason}) => { - const { bot } = this; - //issue punishment - await PunishmentService.muteUser({ - bot, - user, - mod: intr.user, - expires, - reason, - }); - //log punishment - /*await*/ PunishmentService.logPunishment({ - bot, - user, - mod: intr.user, - type, - expires, - reason, - }); - return bot.intrReply({intr, embed: new EmbedBase(bot, { - description: `⚖ **Punishment Successfully Issued**`, - }).Punish(), ephemeral: true}); - }, - kick: async ({intr, type, user, reason}) => { - const { bot } = this; - //issue punishment - await PunishmentService.kickUser({ - bot, - mod: intr.user, - user, - reason, - }); - //log punishment - /*await*/ PunishmentService.logPunishment({ - bot, - user, - mod: intr.user, - type, - reason, - }); - return bot.intrReply({intr, embed: new EmbedBase(bot, { - description: `⚖ **Punishment Successfully Issued**`, - }).Punish(), ephemeral: true}); - }, - ban: async ({intr, type, user, expires, reason}) => { - const { bot } = this; - //issue punishment - await PunishmentService.banUser({ - bot, - user, - mod: intr.user, - expires, - reason, - }); - //log punishment - /*await*/ PunishmentService.logPunishment({ - bot, - user, - mod: intr.user, - type, - expires, - reason, - }); - return bot.intrReply({intr, embed: new EmbedBase(bot, { - description: `⚖ **Punishment Successfully Issued**`, - }).Punish(), ephemeral: true}); - }, - history: async ({intr, user}) => { - const { bot } = this; - const mod = bot.checkMod(user.id); - return bot.intrReply({ - intr, - embed: PunishmentService.generateHistoryEmbed({ - bot, - user, - mod, - history_docs: await (mod - ? PunishmentService.getModHistory({user}) - : PunishmentService.getHistory({user})), - }), - ephemeral: true - }); - }, - }; - - async run({intr, opts}) { - const { bot } = this; - const { PUNISHMENT_TYPES } = PunishmentService; - - //gotta store `type` separately to pass into subcmd, thanks 'use strict' :/ - const [type, user, duration, reason] = [ - opts.getSubcommand().toUpperCase(), - opts.getUser('target'), - opts.getString('duration'), - opts.getString('reason'), - ]; - const parsed_dur = parse(duration); //ms - - if(!!duration && !parsed_dur) - return bot.intrReply({intr, embed: new EmbedBase(bot, { - description: `❌ **That's not a valid duration**`, - }).Error()}); - - //send confirm prompt - if (Object.keys(PUNISHMENT_TYPES).includes(type)) - if (!(await bot.intrConfirm({ - intr, - ephemeral: true, - embed: new EmbedBase(bot, { - description: ` - ⚠ **Are you sure you want to ${type} ${bot.formatUser(user)} for \`${reason ?? 'No reason given'}\`?** - - Is this punishment consistent with the official rules & moderation protocol? - Is this punishment consistent with the other punishments you've issued this past month? - `, - }).Warn(), - }))) - return bot.intrReply({ - intr, - ephemeral: true, - embed: new EmbedBase(bot, { - description: `❌ **Punishment canceled**`, - }).Error(), - }); - - //convert duration to epoch timestamp - const expires = !!duration - ? Date.now() + parsed_dur - : null; - - //easter egg - if(type !== 'HISTORY' && user.id === '139120967208271872') - return bot.intrReply({intr, embed: new EmbedBase(bot, { - title: 'Nice try!', - image: { - url: '', //to be updated later - }, - }).Warn()}); - - this.subcommands[type.toLowerCase()]({ - intr, - user, - expires, - reason, - type: PUNISHMENT_TYPES[type], - }).catch(err => { - bot.logger.error(err); - bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc(err.message), ephemeral: true}); - }); - } -} - -export default punish; diff --git a/commands/admin/role.js b/commands/admin/role.js index 15a88fe..92fe3d2 100644 --- a/commands/admin/role.js +++ b/commands/admin/role.js @@ -74,7 +74,7 @@ class role extends Command { async run({intr, opts}) { const { bot } = this; - const mem = (await bot.leyline_guild.members.fetch()).get(opts.getUser('user').id); + const mem = await bot.leyline_guild.members.fetch(opts.getUser('user')); if(!mem) return bot.intrReply({intr, embed: new EmbedBase(bot, { description: `❌ **I couldn't find that user**`, }).Error()}); diff --git a/commands/admin/sudosay.js b/commands/admin/sudosay.js index 63b94de..3f5610e 100644 --- a/commands/admin/sudosay.js +++ b/commands/admin/sudosay.js @@ -11,6 +11,10 @@ class sudosay extends Command { name: 'channel', description: 'The text channel where the bot will send the message', required: true, + channelTypes: [ + 'GUILD_TEXT', + 'GUILD_NEWS', + ], }, { type: 'STRING', diff --git a/commands/development/embedtest.js b/commands/development/embedtest.js index 609a8a1..2398098 100644 --- a/commands/development/embedtest.js +++ b/commands/development/embedtest.js @@ -12,27 +12,27 @@ class embedtest extends Command { async run({intr, opts}) { const { bot } = this; let expires = false; - bot.intrReply({intr, embed: new EmbedBase(bot).ErrorDesc('I ran into an error!')}); - //bot.intrReply({intr, embed: new EmbedBase(bot, { - // title: 'Punishment Issued', - // fields: [ - // { - // name: 'Issued By', - // value: bot.formatUser(intr.user), - // inline: true, - // }, - // { - // name: 'Reason', - // value: null ?? 'No reason given', - // inline: true, - // }, - // { - // name: 'Expires', - // value: !!expires ? bot.formatTimestamp(expires) : 'No expiration', - // inline: true, - // }, - // ], - //}).Punish(), files: ['./images/avatar-default.png']}); + bot.intrReply({intr, embed: new EmbedBase(bot, { + title: 'Justice Served', + fields: [ + { + name: 'Issued By', + value: bot.formatUser(intr.user), + inline: true, + }, + { + name: 'Reason', + value: null ?? 'No reason given', + inline: true, + }, + { + name: 'Expires', + value: !!expires ? bot.formatTimestamp(expires) : 'No expiration', + inline: true, + }, + ], + color: 0x31d64d, + })}); } } diff --git a/commands/moderator/ApproveGoodAct.js b/commands/moderator/ApproveGoodAct.js new file mode 100644 index 0000000..9b9b1be --- /dev/null +++ b/commands/moderator/ApproveGoodAct.js @@ -0,0 +1,98 @@ +import { Command, EmbedBase, ReactionCollector } from '../../classes'; +import * as Firebase from '../../api'; + +class ApproveGoodAct extends Command { + constructor(bot) { + super(bot, { + name: 'Approve Good Act', + category: 'moderator', + type: 'MESSAGE', + }); + } + + async run({intr, msg}) { + const { bot } = this; + + //Input Validations + const cloud_collector = await Firebase.fetchCollector(msg.id); + if(cloud_collector?.approved) + return bot.intrReply({ + intr, + embed: new EmbedBase(bot).ErrorDesc('This submission has already been approved'), + ephemeral: true, + }); + if(!!cloud_collector?.rejected_by) + return bot.intrReply({ + intr, + embed: new EmbedBase(bot).ErrorDesc('This submission has already been rejected'), + ephemeral: true, + }); + if(cloud_collector?.expires < Date.now()) + return bot.intrReply({ + intr, + embed: new EmbedBase(bot).ErrorDesc('The approval window for this submission has expired'), + ephemeral: true, + }); + + //Send confirmation prompt + if (!(await bot.intrConfirm({ + intr, + embed: new EmbedBase(bot, { + description: `⚠ **Are you sure you want to approve this Good Act?\nIt will permanently be on this user's Proof of Good ledger.**`, + }), + ephemeral: true, + }))) + return bot.intrReply({ + intr, + embed: new EmbedBase(bot, { + description: `❌ **Approval canceled**`, + }).Error(), + ephemeral: true, + }); + + //instantiate collector to get mod_emojis + const collector = new ReactionCollector(bot, { type: ReactionCollector.Collectors.GOOD_ACTS, msg }); + const response_intr = await (await bot.intrReply({ + intr, + content: `Select an approval category`, + components: [{ + components: [ + { + custom_id: 'category-menu', + disabled: false, + placeholder: 'Select a category...', + min_values: 1, + max_values: 1, + options: collector.MOD_EMOJIS + .filter(e => e.name !== '❌') + .map(emoji => ({ + label: emoji.keyword, + value: emoji?.id || emoji.name, + emoji: { + name: emoji.name, + id: emoji?.id, + animated: emoji?.animated, + }, + }) + ), + type: 3, + }, + ], + type: 1, + }], + })).awaitInteractionFromUser({user: intr.user}); + + //parse emoji and setup collector + const emoji = bot.emojis.resolve(response_intr.values[0]) ?? response_intr.values[0]; + await Firebase.createCollector(collector); //needs to be performed first + await collector.approveSubmission({user: intr.user, approval_emoji: emoji}); + collector.createThread(); + msg.react(emoji.toString()); + + return bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `✅ **Good Act Approved**`, + }).Success(), content: '\u200b', components: [], ephemeral: true}); + } +} + +export default ApproveGoodAct; diff --git a/commands/user/profile.js b/commands/user/profile.js index efffc4c..0160e0a 100644 --- a/commands/user/profile.js +++ b/commands/user/profile.js @@ -54,8 +54,8 @@ class profile extends Command { }, fields: [ { - name: `${bot.config.emoji.leyline_logo} Lifetime LLP`, - value: `**${user.total_llp}** Leyline Points\n\u200b`, /*newline for spacing*/ + name: `${bot.config.emoji.leyline_logo} Lifetime GP`, + value: `**${user.total_gp}** Good Points\n\u200b`, /*newline for spacing*/ inline: true, }, { @@ -118,7 +118,7 @@ class profile extends Command { }, { name: '👤 Leyline Volunteering', - value: `**${user.volunteer_llp || 0}** Leyline Points\n\u200b`, + value: `**${user.volunteer_gp || 0}** Good Points\n\u200b`, inline: true, }, { diff --git a/config.js b/config.js index e982820..37e38c4 100644 --- a/config.js +++ b/config.js @@ -3,35 +3,56 @@ export default { get production() { return { // Which users/roles get access to all commands - command_perms: [ - { // Admin - id: '784875278593818694', - type: 'ROLE', - permission: true, - }, - { // Moderator - id: '752363863441145866', - type: 'ROLE', - permission: true, - }, - { // Leyline staff - id: '751919243062411385', - type: 'ROLE', - permission: true, - }, - { - // Ollog10 - id: '139120967208271872', - type: 'USER', - permission: true, - }, - ], + command_perms: { + moderator: [ + { // Admin + id: '784875278593818694', + type: 'ROLE', + permission: true, + }, + { // Moderator + id: '752363863441145866', + type: 'ROLE', + permission: true, + }, + { + // Ollog10 + id: '139120967208271872', + type: 'USER', + permission: true, + }, + ], + admin: [ + { // Admin + id: '784875278593818694', + type: 'ROLE', + permission: true, + }, + { // Moderator + id: '752363863441145866', + type: 'ROLE', + permission: true, + }, + { + // Ollog10 + id: '139120967208271872', + type: 'USER', + permission: true, + }, + { // Leyline staff + id: '751919243062411385', + type: 'ROLE', + permission: true, + }, + ], + }, leyline_guild_id: '751913089271726160', channels: { private_log: '843892751276048394', public_log: '810265135419490314', reward_log: '872724760805654538', - punishment_log: '896539306800328734', //public punishment log + mod_log: '896539306800328734', //private mod log + submission_log: '903056355311644732', //private submission log ama_vc: '794283967460147221', polls: '790063418898907166', }, @@ -65,25 +86,36 @@ export default { get development() { return { // Which users/roles get access to all commands - command_perms: [ - { // Leyline staff - id: '858144532318519326', - type: 'ROLE', - permission: true, - }, - { - // Ollog10 - id: '139120967208271872', - type: 'USER', - permission: true, - }, - ], + command_perms: { + moderator: [ + { + // Moderator + id: '904095889558212660', + type: 'ROLE', + permission: true, + }, + ], + admin: [ + { + // Moderator + id: '904095889558212660', + type: 'ROLE', + permission: true, + }, + { // Leyline staff + id: '858144532318519326', + type: 'ROLE', + permission: true, + }, + ], + }, leyline_guild_id: '857839180608307210', channels: { private_log: '858141871788392448', public_log: '858141914841481246', - reward_log: '858141836513771550', - punishment_log: '892268882285457439', //public punishment log + reward_log: '904081593029759006', + mod_log: '892268882285457439', //private mod log + submission_log: '903055896173764659', //private submission log ama_vc: '869993145499287604', polls: '877229054456107069', }, diff --git a/events/discord/interactionCreate/contextMenu.js b/events/discord/interactionCreate/contextMenu.js new file mode 100644 index 0000000..7f33cdb --- /dev/null +++ b/events/discord/interactionCreate/contextMenu.js @@ -0,0 +1,35 @@ +import { DiscordEvent, EmbedBase } from "../../../classes"; + +export default class extends DiscordEvent { + constructor(bot) { + super(bot, { + name: 'contextMenu', + description: 'Receive, parse, and execute context menu commands', + event_type: 'interactionCreate', + }); + } + + async run(intr) { + const { bot } = this; + + if(!intr.isContextMenu()) return; + // Ignore commands sent by other bots or sent in DM + if(intr.user.bot || !intr.inGuild()) return; + + const command = bot.commands.get(intr.commandName.replaceAll(' ', '')); + + //defer reply because some commands take > 3 secs to run + command.deferResponse && + await intr.deferReply({fetchReply: true}); + + try { + bot.logger.cmd(`${intr.user.tag} (${intr.user.id}) ran context menu option ${intr.commandName}`); + await command.run({intr, user: intr.options.getMember('user'), msg: intr.options.getMessage('message')}); + } catch (err) { + bot.logger.error(`Error with ctx menu cmd ${intr.commandName}: ${err}`); + bot.intrReply({intr, embed: new EmbedBase(bot, { + description: `❌ **I ran into an error while trying to run that command**`, + }).Error()}); + } + } +}; diff --git a/events/discord/messageCreate/goodActs.js b/events/discord/messageCreate/goodActs.js index d065458..3cfe301 100644 --- a/events/discord/messageCreate/goodActs.js +++ b/events/discord/messageCreate/goodActs.js @@ -25,13 +25,31 @@ export default class extends DiscordEvent { rejectSubmission(msg) { const { bot } = this; - bot.logDiscord({embed: new EmbedBase(bot, { - fields:[{ - name: `Submission Auto-Rejected`, - value: `The [submission](${msg.url} 'click to view message') posted in <#${msg.channel.id}> by ${bot.formatUser(msg.author)} was automatically rejected because it did not contain a description.`, - }], - thumbnail: { url: msg.attachments.first().url }, - }).Error()}); + bot.logSubmission({ + embed: new EmbedBase(bot, { + title: 'Submission Auto-Rejected', + description: 'The submission did not contain a description', + url: msg.url, + fields: [ + { + name: 'Channel', + value: `<#${msg.channel.id}>`, + inline: true, + }, + { + name: 'Rejected By', + value: bot.formatUser(bot.user), + inline: true, + }, + { + name: 'Author', + value: bot.formatUser(msg.author), + inline: true, + }, + ], + thumbnail: { url: msg.attachments.first().url }, + }).Error(), + }); bot.sendDM({ user: msg.author, @@ -70,7 +88,7 @@ export default class extends DiscordEvent { embed: new EmbedBase(bot, { fields:[{ name: `Thank you for your submission!`, - value: `Please remember to connect your Leyline & Discord accounts so you can receive LLP if your [submission](${msg.url}) is approved! + value: `Please remember to connect your Leyline & Discord accounts so you can receive GP if your [submission](${msg.url}) is approved! [Click here](${bot.connection_tutorial} 'How to connect your accounts') to view the account connection tutorial.`, }], }), diff --git a/index.js b/index.js index 3238c33..d42a4c0 100644 --- a/index.js +++ b/index.js @@ -7,7 +7,7 @@ import { Intents, Message } from 'discord.js'; import admin from 'firebase-admin'; import klaw from 'klaw'; import path from 'path'; -import { LeylineBot, EmbedBase, CommunityPoll, ReactionCollector, PunishmentService, CloudConfig } from './classes'; +import { LeylineBot, EmbedBase, CommunityPoll, ReactionCollector, SentenceService, CloudConfig } from './classes'; //formally, dotenv shouldn't be used in prod, but because staging and prod share a VM, it's an option I elected to go with for convenience import { config as dotenv_config } from 'dotenv'; dotenv_config(); @@ -171,13 +171,14 @@ const postInit = async function () { const cmds = await bot.leyline_guild.commands.set(bot.commands.map(({ run, ...data }) => data)) .catch(err => bot.logger.error(`registerCommands err: ${err}`)); //turn each Command into an ApplicationCommand - cmds.forEach(cmd => bot.commands.get(cmd.name).setApplicationCommand(cmd)); + cmds.forEach(cmd => bot.commands.get(cmd.name.replaceAll(' ', '')).setApplicationCommand(cmd)); //Register command permissions await bot.leyline_guild.commands.permissions.set({ - fullPermissions: bot.commands.filter(c => c.category === 'admin') - .map(cmd => ({ - id: cmd.id, - permissions: bot.config.command_perms, + fullPermissions: bot.commands + .filter(c => Object.keys(bot.config.command_perms).includes(c.category)) + .map(({id, category}) => ({ + id, + permissions: bot.config.command_perms[category], })), }).catch(err => bot.logger.error(`registerCommands err: ${err}`)); bot.logger.log(`Registered ${cmds.size} out of ${bot.commands.size} commands to Discord`); @@ -241,27 +242,27 @@ const postInit = async function () { return; })(); - //import punishments - await (async function importPunishments() { + //import sentences + await (async function importSentences() { let succesfully_imported = 0; - const punishments = await admin + const sentences = await admin .firestore() - .collection(PunishmentService.COLLECTION_PATH) + .collection(SentenceService.COLLECTION_PATH) .where('expires', '>', Date.now()) .get(); - for (const doc of punishments.docs) { + for (const doc of sentences.docs) { try { - PunishmentService.scheduleRemoval({ + SentenceService.scheduleRemoval({ bot, id: doc.id, data: { ...doc.data() }, }); succesfully_imported++; } catch (err) { - bot.logger.error(`importPunishments error with doc id ${doc.id}: ${err}`); + bot.logger.error(`importSentences error with doc id ${doc.id}: ${err}`); } } - bot.logger.log(`Imported ${succesfully_imported} punishments from Firestore`); + bot.logger.log(`Imported ${succesfully_imported} sentences from Firestore`); return; })(); diff --git a/package-lock.json b/package-lock.json index f83574c..85bfff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "leyline-discord", - "version": "2.4.0", + "version": "2.5.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "version": "2.4.0", + "version": "2.5.0", "license": "ISC", "dependencies": { "@google-cloud/pubsub": "^2.16.1", "chalk": "^4.1.1", "dedent": "^0.7.0", - "discord.js": "^13.2.0-dev.1629202082.9a833b1", + "discord.js": "^13.3.0", "dotenv": "^10.0.0", "firebase-admin": "^9.8.0", "klaw": "^3.0.0", @@ -25,27 +25,28 @@ } }, "node_modules/@discordjs/builders": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.5.0.tgz", - "integrity": "sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.1.tgz", + "integrity": "sha512-kYJMvZ/BjRD1/6G2t1pQop2yoJNUmYvvKeG4mOBUCHFmfb7WIeBFmN/eSiP3cVSfRx3lbNiyxkdd5JzhjQnGbg==", "dependencies": { - "@sindresorhus/is": "^4.0.1", - "discord-api-types": "^0.22.0", - "ow": "^0.27.0", + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.24.0", + "ow": "^0.28.1", "ts-mixer": "^6.0.0", - "tslib": "^2.3.0" + "tslib": "^2.3.1" }, "engines": { - "node": ">=14.0.0", + "node": ">=16.0.0", "npm": ">=7.0.0" } }, "node_modules/@discordjs/collection": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz", - "integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==", + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.3.2.tgz", + "integrity": "sha512-dMjLl60b2DMqObbH1MQZKePgWhsNe49XkKBZ0W5Acl5uVV43SN414i2QfZwRI7dXAqIn8pEWD2+XXQFn9KWxqg==", "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0", + "npm": ">=7.0.0" } }, "node_modules/@discordjs/form-data": { @@ -399,18 +400,18 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "node_modules/@sapphire/async-queue": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz", - "integrity": "sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA==", + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.8.tgz", + "integrity": "sha512-Oi4EEi8vOne8RM1tCdQ3kYAtl/J6ztak3Th6wwGFqA2SVNJtedw196LjsLX0bK8Li8cwaljbFf08N+0zeqhkWQ==", "engines": { - "node": ">=14", - "npm": ">=6" + "node": ">=v14.0.0", + "npm": ">=7.0.0" } }, "node_modules/@sindresorhus/is": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", - "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==", "engines": { "node": ">=10" }, @@ -505,6 +506,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/qs": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", @@ -525,9 +535,9 @@ } }, "node_modules/@types/ws": { - "version": "7.4.7", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", - "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", "dependencies": { "@types/node": "*" } @@ -810,26 +820,27 @@ } }, "node_modules/discord-api-types": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz", - "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==", "engines": { "node": ">=12" } }, "node_modules/discord.js": { - "version": "13.2.0-dev.1629202082.9a833b1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.2.0-dev.1629202082.9a833b1.tgz", - "integrity": "sha512-EXghzG7XVd/gZy0+Zj3Irz2kw7wwuWOT9WV2Nv7E9ihdJaPniXEf+HKwG2DDKZGttRxvYkloEu0Z5Ky9IS6FPw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.3.0.tgz", + "integrity": "sha512-kZcDVrQRTuzjRx99/Xl9HF1Kt7xNkiN4Gwvk1hNmLRAn+7Syzw9XTkQZdOPXLpijhbTNsZcdAaMxgvTmtyNdyA==", "dependencies": { - "@discordjs/builders": "^0.5.0", - "@discordjs/collection": "^0.2.1", + "@discordjs/builders": "^0.8.1", + "@discordjs/collection": "^0.3.2", "@discordjs/form-data": "^3.0.1", - "@sapphire/async-queue": "^1.1.4", - "@types/ws": "^7.4.7", - "discord-api-types": "^0.22.0", + "@sapphire/async-queue": "^1.1.8", + "@types/node-fetch": "^2.5.12", + "@types/ws": "^8.2.0", + "discord-api-types": "^0.24.0", "node-fetch": "^2.6.1", - "ws": "^7.5.1" + "ws": "^8.2.3" }, "engines": { "node": ">=16.6.0", @@ -958,6 +969,19 @@ "@google-cloud/storage": "^5.3.0" } }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -1619,15 +1643,15 @@ } }, "node_modules/ow": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", - "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.1.tgz", + "integrity": "sha512-1EZTywPZeUKac9gD7q8np3Aj+V54kvfIcjNEVNDSbG2Ys5xA5foW2HquvMMqgyWGLqIFMlc0Iq/HmyMHqN48sA==", "dependencies": { - "@sindresorhus/is": "^4.0.1", + "@sindresorhus/is": "^4.2.0", "callsites": "^3.1.0", "dot-prop": "^6.0.1", "lodash.isequal": "^4.5.0", - "type-fest": "^1.2.1", + "type-fest": "^2.3.4", "vali-date": "^1.0.0" }, "engines": { @@ -1917,16 +1941,16 @@ "integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ==" }, "node_modules/tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.2.tgz", + "integrity": "sha512-WMbytmAs5PUTqwGJRE+WoRrD2S0bYFtHX8k4Y/1l18CG5kqA3keJud9pPQ/r30FE9n8XRFCXF9BbccHIZzRYJw==", "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2030,11 +2054,11 @@ } }, "node_modules/ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -2111,21 +2135,21 @@ }, "dependencies": { "@discordjs/builders": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.5.0.tgz", - "integrity": "sha512-HP5y4Rqw68o61Qv4qM5tVmDbWi4mdTFftqIOGRo33SNPpLJ1Ga3KEIR2ibKofkmsoQhEpLmopD1AZDs3cKpHuw==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.8.1.tgz", + "integrity": "sha512-kYJMvZ/BjRD1/6G2t1pQop2yoJNUmYvvKeG4mOBUCHFmfb7WIeBFmN/eSiP3cVSfRx3lbNiyxkdd5JzhjQnGbg==", "requires": { - "@sindresorhus/is": "^4.0.1", - "discord-api-types": "^0.22.0", - "ow": "^0.27.0", + "@sindresorhus/is": "^4.2.0", + "discord-api-types": "^0.24.0", + "ow": "^0.28.1", "ts-mixer": "^6.0.0", - "tslib": "^2.3.0" + "tslib": "^2.3.1" } }, "@discordjs/collection": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.2.1.tgz", - "integrity": "sha512-vhxqzzM8gkomw0TYRF3tgx7SwElzUlXT/Aa41O7mOcyN6wIJfj5JmDWaO5XGKsGSsNx7F3i5oIlrucCCWV1Nog==" + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.3.2.tgz", + "integrity": "sha512-dMjLl60b2DMqObbH1MQZKePgWhsNe49XkKBZ0W5Acl5uVV43SN414i2QfZwRI7dXAqIn8pEWD2+XXQFn9KWxqg==" }, "@discordjs/form-data": { "version": "3.0.1", @@ -2425,14 +2449,14 @@ "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" }, "@sapphire/async-queue": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.4.tgz", - "integrity": "sha512-fFrlF/uWpGOX5djw5Mu2Hnnrunao75WGey0sP0J3jnhmrJ5TAPzHYOmytD5iN/+pMxS+f+u/gezqHa9tPhRHEA==" + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.1.8.tgz", + "integrity": "sha512-Oi4EEi8vOne8RM1tCdQ3kYAtl/J6ztak3Th6wwGFqA2SVNJtedw196LjsLX0bK8Li8cwaljbFf08N+0zeqhkWQ==" }, "@sindresorhus/is": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.0.1.tgz", - "integrity": "sha512-Qm9hBEBu18wt1PO2flE7LPb30BHMQt1eQgbV76YntdNk73XZGpn3izvGTYxbGgzXKgbCjiia0uxTd3aTNQrY/g==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.2.0.tgz", + "integrity": "sha512-VkE3KLBmJwcCaVARtQpfuKcKv8gcBmUubrfHGF84dXuuW6jgsRYxPtzcIhPyK9WAPpRt2/xY6zkD9MnRaJzSyw==" }, "@tootallnate/once": { "version": "1.1.2", @@ -2518,6 +2542,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-15.0.3.tgz", "integrity": "sha512-/WbxFeBU+0F79z9RdEOXH4CsDga+ibi5M8uEYr91u3CkT/pdWcV8MCook+4wDPnZBexRdwWS+PiVZ2xJviAzcQ==" }, + "@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/qs": { "version": "6.9.6", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.6.tgz", @@ -2538,9 +2571,9 @@ } }, "@types/ws": { - "version": "7.4.7", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", - "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.2.0.tgz", + "integrity": "sha512-cyeefcUCgJlEk+hk2h3N+MqKKsPViQgF5boi9TTHSK+PoR9KWBb/C5ccPcDyAqgsbAYHTwulch725DV84+pSpg==", "requires": { "@types/node": "*" } @@ -2741,23 +2774,24 @@ } }, "discord-api-types": { - "version": "0.22.0", - "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.22.0.tgz", - "integrity": "sha512-l8yD/2zRbZItUQpy7ZxBJwaLX/Bs2TGaCthRppk8Sw24LOIWg12t9JEreezPoYD0SQcC2htNNo27kYEpYW/Srg==" + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.24.0.tgz", + "integrity": "sha512-X0uA2a92cRjowUEXpLZIHWl4jiX1NsUpDhcEOpa1/hpO1vkaokgZ8kkPtPih9hHth5UVQ3mHBu/PpB4qjyfJ4A==" }, "discord.js": { - "version": "13.2.0-dev.1629202082.9a833b1", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.2.0-dev.1629202082.9a833b1.tgz", - "integrity": "sha512-EXghzG7XVd/gZy0+Zj3Irz2kw7wwuWOT9WV2Nv7E9ihdJaPniXEf+HKwG2DDKZGttRxvYkloEu0Z5Ky9IS6FPw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.3.0.tgz", + "integrity": "sha512-kZcDVrQRTuzjRx99/Xl9HF1Kt7xNkiN4Gwvk1hNmLRAn+7Syzw9XTkQZdOPXLpijhbTNsZcdAaMxgvTmtyNdyA==", "requires": { - "@discordjs/builders": "^0.5.0", - "@discordjs/collection": "^0.2.1", + "@discordjs/builders": "^0.8.1", + "@discordjs/collection": "^0.3.2", "@discordjs/form-data": "^3.0.1", - "@sapphire/async-queue": "^1.1.4", - "@types/ws": "^7.4.7", - "discord-api-types": "^0.22.0", + "@sapphire/async-queue": "^1.1.8", + "@types/node-fetch": "^2.5.12", + "@types/ws": "^8.2.0", + "discord-api-types": "^0.24.0", "node-fetch": "^2.6.1", - "ws": "^7.5.1" + "ws": "^8.2.3" } }, "dot-prop": { @@ -2862,6 +2896,16 @@ "node-forge": "^0.10.0" } }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -3382,15 +3426,15 @@ } }, "ow": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.27.0.tgz", - "integrity": "sha512-SGnrGUbhn4VaUGdU0EJLMwZWSupPmF46hnTRII7aCLCrqixTAC5eKo8kI4/XXf1eaaI8YEVT+3FeGNJI9himAQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.1.tgz", + "integrity": "sha512-1EZTywPZeUKac9gD7q8np3Aj+V54kvfIcjNEVNDSbG2Ys5xA5foW2HquvMMqgyWGLqIFMlc0Iq/HmyMHqN48sA==", "requires": { - "@sindresorhus/is": "^4.0.1", + "@sindresorhus/is": "^4.2.0", "callsites": "^3.1.0", "dot-prop": "^6.0.1", "lodash.isequal": "^4.5.0", - "type-fest": "^1.2.1", + "type-fest": "^2.3.4", "vali-date": "^1.0.0" }, "dependencies": { @@ -3609,14 +3653,14 @@ "integrity": "sha512-nXIb1fvdY5CBSrDIblLn73NW0qRDk5yJ0Sk1qPBF560OdJfQp9jhl+0tzcY09OZ9U+6GpeoI9RjwoIKFIoB9MQ==" }, "tslib": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", - "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", + "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" }, "type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==" + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.5.2.tgz", + "integrity": "sha512-WMbytmAs5PUTqwGJRE+WoRrD2S0bYFtHX8k4Y/1l18CG5kqA3keJud9pPQ/r30FE9n8XRFCXF9BbccHIZzRYJw==" }, "typedarray-to-buffer": { "version": "3.1.5", @@ -3695,9 +3739,9 @@ } }, "ws": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.3.tgz", - "integrity": "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg==", + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", "requires": {} }, "xdg-basedir": { diff --git a/package.json b/package.json index 863ee94..c3da30f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "leyline-discord", - "version": "2.4.0", + "version": "2.5.0", "description": "Leyline Discord bot", "main": "index.js", "type": "module", @@ -19,7 +19,7 @@ "@google-cloud/pubsub": "^2.16.1", "chalk": "^4.1.1", "dedent": "^0.7.0", - "discord.js": "^13.2.0-dev.1629202082.9a833b1", + "discord.js": "^13.3.0", "dotenv": "^10.0.0", "firebase-admin": "^9.8.0", "klaw": "^3.0.0",