From 41d5962345e60eaf196a477f5ebbbcceb4de3dac Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Mon, 27 Feb 2023 21:40:04 -0800 Subject: [PATCH 01/67] add members functionality --- commands/verification/add-members.js | 204 ++++++++++++++++++--------- db/firebase/firebase-services.js | 52 ++++--- 2 files changed, 166 insertions(+), 90 deletions(-) diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index bfa94a75..482f06ff 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -1,86 +1,154 @@ -const csvParser = require('csv-parser'); -const { Message } = require('discord.js'); -const https = require('https'); -const PermissionCommand = require('../../classes/permission-command'); +const { Command } = require('@sapphire/framework'); +const BotGuild = require('../../db/mongo/BotGuild'); +const { Modal, MessageActionRow, TextInputComponent } = require('discord.js'); const { addUserData } = require('../../db/firebase/firebase-services'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); -const { sendMsgToChannel } = require('../../discord-services'); -const winston = require('winston'); -const { MessagePrompt } = require('advanced-discord.js-prompts'); -/** - * Will prompt the user for a csv file to add members to firebase. The csv file must have the following columns with exactly those names: - * * email -> the user's email, must be a string - * * firstName -> the user's first name, must be a string - * * lastName -> the user's last name, must be a string - * * types -> the types the user will get, must be a list of strings separated by a comma, spaces are okay, types must be the same ones used when setting up verification - * @category Commands - * @subcategory Verification - * @extends PermissionCommand - * @guildOnly - */ -class AddMembers extends PermissionCommand { - constructor(client) { - super(client, { - name: 'add-members', - group: 'verification', - memberName: 'add members to verification', - description: 'adds members to the verification system using via file', - guildOnly: true, - }, - { - role: PermissionCommand.FLAGS.STAFF_ROLE, - roleMessage: 'Hey there, the !add-members command is only for staff!', - channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, - channelMessage: 'Hey there, the !add-members command is only available on the admin console!', +class AddMembers extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Start verification prompt in landing channel.' }); } + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addStringOption(option => + option.setName('participantstype') + .setDescription('Type of role of the added participants (i.e. hacker, sponsor)') + .setRequired(true)) + .addBooleanOption(option => + option.setName('overwrite') + .setDescription('Overwrite existing role?') + .setRequired(false)) + ) + } + /** - * @param {BotGuildModel} botGuild + * @param {BotGuildModel} botGuild * @param {Message} message */ - async runCommand(botGuild, message) { + async chatInputRun(interaction) { + this.botGuild = await BotGuild.findById(interaction.guild.id); + const userId = interaction.user.id; + const guild = interaction.guild; + const participantsType = interaction.options.getString('participantstype'); + const overwrite = interaction.options.getBoolean('overwrite') ?? false; + + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + } + + if (!this.botGuild.verification.verificationRoles.has(participantsType)) { + await interaction.reply({ content: 'The role you entered does not exist!', ephemeral: true }); + return; + } - try { - // request file - let msg = await MessagePrompt.prompt({ prompt: 'Please send the csv file!', channel: message.channel, userId: message.author.id}); + const modal = new Modal() + .setCustomId('emailsModal') + .setTitle('Enter all emails to be added as ' + participantsType) + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('emails') + .setLabel('Newline-separated Emails') + .setStyle('PARAGRAPH') + .setRequired(true), + ), + ]); + await interaction.showModal(modal); - let fileUrl = msg.attachments.first().url; + const submitted = await interaction.awaitModalSubmit({ time: 300000, filter: j => j.user.id === interaction.user.id }) + .catch(error => { + }); - https.get(fileUrl).on('error', (error) => winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' })); + if (submitted) { + const emailsRaw = submitted.fields.getTextInputValue('emails'); + const emails = emailsRaw.split(/\r?\n|\r|\n/g); + emails.forEach(email => { + addUserData(email, participantsType, interaction.guild.id, overwrite); + }); + submitted.reply({ content: emails.length + ' emails have been added as ' + participantsType, ephemeral: true }) + } +} +} +module.exports = AddMembers; +// /** +// * Will prompt the user for a csv file to add members to firebase. The csv file must have the following columns with exactly those names: +// * * email -> the user's email, must be a string +// * * firstName -> the user's first name, must be a string +// * * lastName -> the user's last name, must be a string +// * * types -> the types the user will get, must be a list of strings separated by a comma, spaces are okay, types must be the same ones used when setting up verification +// * @category Commands +// * @subcategory Verification +// * @extends PermissionCommand +// * @guildOnly +// */ +// class AddMembers extends PermissionCommand { +// constructor(client) { +// super(client, { +// name: 'add-members', +// group: 'verification', +// memberName: 'add members to verification', +// description: 'adds members to the verification system using via file', +// guildOnly: true, +// }, +// { +// role: PermissionCommand.FLAGS.STAFF_ROLE, +// roleMessage: 'Hey there, the !add-members command is only for staff!', +// channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, +// channelMessage: 'Hey there, the !add-members command is only available on the admin console!', +// }); +// } - var holdMsg = await sendMsgToChannel(message.channel, message.author.id, 'Adding data please hold ...'); +// /** +// * @param {BotGuildModel} botGuild +// * @param {Message} message +// */ +// async runCommand(botGuild, message) { + +// try { +// // request file +// let msg = await MessagePrompt.prompt({ prompt: 'Please send the csv file!', channel: message.channel, userId: message.author.id}); + +// let fileUrl = msg.attachments.first().url; + +// https.get(fileUrl).on('error', (error) => winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' })); + +// var holdMsg = await sendMsgToChannel(message.channel, message.author.id, 'Adding data please hold ...'); - https.get(fileUrl, (response) => { - response.pipe(csvParser()).on('data', async (data) => { +// https.get(fileUrl, (response) => { +// response.pipe(csvParser()).on('data', async (data) => { - if (!data.email || !data.firstName || !data.lastName || !data.types) { - sendMsgToChannel(message.channel, message.author.id, 'The excel data is incomplete or the file type is not CSV (might be CSV UTF-8). Try again!', 10); - return; - } +// if (!data.email || !data.firstName || !data.lastName || !data.types) { +// sendMsgToChannel(message.channel, message.author.id, 'The excel data is incomplete or the file type is not CSV (might be CSV UTF-8). Try again!', 10); +// return; +// } - /** @type {String} */ - let typesString = data.types; +// /** @type {String} */ +// let typesString = data.types; - let typesList = typesString.split(',').map(string => string.trim().toLowerCase()); +// let typesList = typesString.split(',').map(string => string.trim().toLowerCase()); - typesList = typesList.filter(type => botGuild.verification.verificationRoles.has(type)); +// typesList = typesList.filter(type => botGuild.verification.verificationRoles.has(type)); - if (typesList.length > 0) await addUserData(data.email, typesList, message.guild.id, undefined, data.firstName, data.lastName); - }).on('end', () => { - holdMsg.delete(); - sendMsgToChannel(message.channel, message.author.id, 'The members have been added to the database!', 10); - winston.loggers.get(message.guild.id).verbose(`Members have been added to the database by ${message.author.id}.`, { event: 'Add Member Command' }); - }); - }).on('error', (error) => { - holdMsg.delete(); - sendMsgToChannel(message.channel, message.author.id, `There was an error, please try again! Error: ${error}`, 10); - winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' }); - }); - } catch (error) { - winston.loggers.get(message.guild.id).warning(`There was an error when adding members. Error: ${error}`, { event: 'Add Member Command' }); - } - } -} -module.exports = AddMembers; \ No newline at end of file +// if (typesList.length > 0) await addUserData(data.email, typesList, message.guild.id, undefined, data.firstName, data.lastName); +// }).on('end', () => { +// holdMsg.delete(); +// sendMsgToChannel(message.channel, message.author.id, 'The members have been added to the database!', 10); +// winston.loggers.get(message.guild.id).verbose(`Members have been added to the database by ${message.author.id}.`, { event: 'Add Member Command' }); +// }); +// }).on('error', (error) => { +// holdMsg.delete(); +// sendMsgToChannel(message.channel, message.author.id, `There was an error, please try again! Error: ${error}`, 10); +// winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' }); +// }); +// } catch (error) { +// winston.loggers.get(message.guild.id).warning(`There was an error when adding members. Error: ${error}`, { event: 'Add Member Command' }); +// } +// } +// } +// module.exports = AddMembers; \ No newline at end of file diff --git a/db/firebase/firebase-services.js b/db/firebase/firebase-services.js index 8dfac48c..5342ba82 100644 --- a/db/firebase/firebase-services.js +++ b/db/firebase/firebase-services.js @@ -80,7 +80,7 @@ async function getReminder(guildId) { //if there reminder unsent, change its status to asked if (reminder != undefined) { reminder.ref.update({ - 'sent' : true, + 'sent': true, }); return reminder.data(); } @@ -197,26 +197,34 @@ module.exports.checkName = checkName; * @param {String} [lastName=''] - users last name * @async */ -async function addUserData(email, types, guildId, member = {}, firstName = '', lastName = '') { - var newDocument = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').doc(email.toLowerCase()); - - /** @type {FirebaseUser} */ - let data = { - email: email.toLowerCase(), - discordId: member?.id || null, - types: types.map((type, index, array) => { - /** @type {UserType} */ - let userType = { - type: type, +async function addUserData(email, type, guildId, overwrite) { + const cleanEmail = email.trim().toLowerCase(); + var documentRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + const doc = await documentRef.get(); + + if (doc.exists && !overwrite) { + var types = await doc.data().types; + var containsType = false; + types.forEach(existingType => { + if (existingType.type === type) { + containsType = true; + return; + } + }); + if (!containsType) { + types.push({ type: type, isVerified: false }) + } + await documentRef.update({ types: types }); + } else { + let data = { + email: cleanEmail, + types: [{ isVerified: false, - }; - return userType; - }), - firstName: firstName, - lastName: lastName, - }; - - await newDocument.set(data); + type: type + }] + }; + await documentRef.set(data); + } } module.exports.addUserData = addUserData; @@ -332,7 +340,7 @@ async function lookupById(guildId, memberId) { } else { return undefined; } - + } module.exports.lookupById = lookupById; @@ -360,7 +368,7 @@ async function retrieveLeaderboard(guildId) { snapshot.forEach(doc => { winners.push(doc.data()); }) - winners.sort((a,b) => parseFloat(b.points) - parseFloat(a.points)); + winners.sort((a, b) => parseFloat(b.points) - parseFloat(a.points)); return winners; } module.exports.retrieveLeaderboard = retrieveLeaderboard; \ No newline at end of file From 70cfc1e550d5aa01e09d60922edb98d2360336cb Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Mon, 27 Feb 2023 22:52:39 -0800 Subject: [PATCH 02/67] get emails in user context menu, add bot spam channel --- commands/verification/add-members.js | 78 +---------------------- commands/verification/get-email.js | 46 ++++++++++++++ commands/verification/manual-verify.js | 86 -------------------------- db/mongo/BotGuild.d.ts | 2 + db/mongo/BotGuild.js | 3 + 5 files changed, 52 insertions(+), 163 deletions(-) create mode 100644 commands/verification/get-email.js delete mode 100644 commands/verification/manual-verify.js diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index 482f06ff..133cc240 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -75,80 +75,4 @@ class AddMembers extends Command { } } } -module.exports = AddMembers; -// /** -// * Will prompt the user for a csv file to add members to firebase. The csv file must have the following columns with exactly those names: -// * * email -> the user's email, must be a string -// * * firstName -> the user's first name, must be a string -// * * lastName -> the user's last name, must be a string -// * * types -> the types the user will get, must be a list of strings separated by a comma, spaces are okay, types must be the same ones used when setting up verification -// * @category Commands -// * @subcategory Verification -// * @extends PermissionCommand -// * @guildOnly -// */ -// class AddMembers extends PermissionCommand { -// constructor(client) { -// super(client, { -// name: 'add-members', -// group: 'verification', -// memberName: 'add members to verification', -// description: 'adds members to the verification system using via file', -// guildOnly: true, -// }, -// { -// role: PermissionCommand.FLAGS.STAFF_ROLE, -// roleMessage: 'Hey there, the !add-members command is only for staff!', -// channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, -// channelMessage: 'Hey there, the !add-members command is only available on the admin console!', -// }); -// } - -// /** -// * @param {BotGuildModel} botGuild -// * @param {Message} message -// */ -// async runCommand(botGuild, message) { - -// try { -// // request file -// let msg = await MessagePrompt.prompt({ prompt: 'Please send the csv file!', channel: message.channel, userId: message.author.id}); - -// let fileUrl = msg.attachments.first().url; - -// https.get(fileUrl).on('error', (error) => winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' })); - -// var holdMsg = await sendMsgToChannel(message.channel, message.author.id, 'Adding data please hold ...'); - -// https.get(fileUrl, (response) => { -// response.pipe(csvParser()).on('data', async (data) => { - -// if (!data.email || !data.firstName || !data.lastName || !data.types) { -// sendMsgToChannel(message.channel, message.author.id, 'The excel data is incomplete or the file type is not CSV (might be CSV UTF-8). Try again!', 10); -// return; -// } - -// /** @type {String} */ -// let typesString = data.types; - -// let typesList = typesString.split(',').map(string => string.trim().toLowerCase()); - -// typesList = typesList.filter(type => botGuild.verification.verificationRoles.has(type)); - -// if (typesList.length > 0) await addUserData(data.email, typesList, message.guild.id, undefined, data.firstName, data.lastName); -// }).on('end', () => { -// holdMsg.delete(); -// sendMsgToChannel(message.channel, message.author.id, 'The members have been added to the database!', 10); -// winston.loggers.get(message.guild.id).verbose(`Members have been added to the database by ${message.author.id}.`, { event: 'Add Member Command' }); -// }); -// }).on('error', (error) => { -// holdMsg.delete(); -// sendMsgToChannel(message.channel, message.author.id, `There was an error, please try again! Error: ${error}`, 10); -// winston.loggers.get(message.guild.id).warning(`There was an error while adding members- Error: ${error}`, { event: 'Add Member Command' }); -// }); -// } catch (error) { -// winston.loggers.get(message.guild.id).warning(`There was an error when adding members. Error: ${error}`, { event: 'Add Member Command' }); -// } -// } -// } -// module.exports = AddMembers; \ No newline at end of file +module.exports = AddMembers; \ No newline at end of file diff --git a/commands/verification/get-email.js b/commands/verification/get-email.js new file mode 100644 index 00000000..c7f21b2b --- /dev/null +++ b/commands/verification/get-email.js @@ -0,0 +1,46 @@ +const { Command } = require('@sapphire/framework'); +const BotGuild = require('../../db/mongo/BotGuild') +const { lookupById } = require('../../db/firebase/firebase-services'); + +class GetEmails extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Check the email of a given user if they have it linked to their Discord ID in our database.' + }); + } + + registerApplicationCommands(registry) { + registry.registerContextMenuCommand((builder) => + builder + .setName(this.name) + .setType(2) + ); + } + async contextMenuRun(interaction) { + const guild = interaction.guild; + const userId = interaction.user.id; + this.botGuild = this.botGuild = await BotGuild.findById(guild.id); + + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + } + + let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + + const email = await lookupById(guild.id, interaction.targetUser.id) + if (email) { + interaction.reply('Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results'); + botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is: ' + email); + return; + } else { + interaction.reply('Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results'); + botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is not in our database!'); + return; + } + } +} + +module.exports = { + GetEmails +}; diff --git a/commands/verification/manual-verify.js b/commands/verification/manual-verify.js deleted file mode 100644 index 2daac571..00000000 --- a/commands/verification/manual-verify.js +++ /dev/null @@ -1,86 +0,0 @@ -// Discord.js commando requirements -const PermissionCommand = require('../../classes/permission-command'); -const { addUserData } = require('../../db/firebase/firebase-services'); -const { sendMsgToChannel, checkForRole, validateEmail } = require('../../discord-services'); -const { Message } = require('discord.js'); -const Verification = require('../../classes/Bot/Features/Verification/verification'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); -const winston = require('winston'); -const { StringPrompt, NumberPrompt } = require('advanced-discord.js-prompts'); - -/** - * Will manually verify a user to the server and the database. Asks the user for a user ID, email, and type(s) to add with. - * @category Commands - * @subcategory Verification - * @extends PermissionCommand - * @guildonly - */ -class ManualVerify extends PermissionCommand { - constructor(client) { - super(client, { - name: 'manual-verify', - group: 'verification', - memberName: 'manual hacker verification', - description: 'Will verify a guest to the specified role.', - guildOnly: true, - }, - { - role: PermissionCommand.FLAGS.STAFF_ROLE, - roleMessage: 'Hey there, the !manual-verify command is only for staff!', - channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, - channelMessage: 'The !manual-verify command is only available in the admin console!' - }); - } - - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - */ - async runCommand(botGuild, message) { - try { - // helpful vars - let channel = message.channel; - let userId = message.author.id; - - let guestId = await NumberPrompt.single({ prompt: 'What is the ID of the member you would like to verify?', channel, userId, cancelable: true}); - var member = message.guild.member(guestId); // get member object by id - - // check for valid ID - if (!member) { - sendMsgToChannel(channel, userId, `${guestId.toString()} is an invalid ID!`, 5); - return; - } - // check for member to have guest role - if (!checkForRole(member, botGuild.verification.guestRoleID)) { - sendMsgToChannel(channel, userId, `<@${guestId.toString()}> does not have the guest role! Cant verify!`, 5); - return; - } - - let availableTypes = Array.from(botGuild.verification.verificationRoles.keys()).join(); - - let types = StringPrompt.multiRestricted({ prompt: 'Please respond with the types you want this user to verify.', channel, userId}, availableTypes); - - let email = await StringPrompt.single({ prompt: 'What is their email?', channel, userId, cancelable: true}); - - // validate the email - if(!validateEmail(email)) { - sendMsgToChannel(channel, userId, 'The email is not valid!', 5); - return; - } - - await addUserData(email, types, member.guild.id, member); - try { - await Verification.verify(member, email, message.guild, botGuild); - } catch (error) { - sendMsgToChannel(channel, userId, 'Email provided is not valid!', 5); - } - - message.channel.send('Verification complete!').then(msg => msg.delete({ timeout: 3000 })); - - } catch (error) { - winston.loggers.get(botGuild._id).warning(`While manually verifying, there was an error but it was handled. Error: ${error}.`, { event: 'Manual Verify Command' }); - return; - } - } -} -module.exports = ManualVerify; diff --git a/db/mongo/BotGuild.d.ts b/db/mongo/BotGuild.d.ts index 7ebffd5e..a67bf419 100644 --- a/db/mongo/BotGuild.d.ts +++ b/db/mongo/BotGuild.d.ts @@ -13,6 +13,7 @@ interface BotGuild extends Document { staffRole: String, adminRole: String, everyoneRole: String, + mentorRole: String }, channelIDs: { @@ -20,6 +21,7 @@ interface BotGuild extends Document { adminLog: String, botSupportChannel: String, archiveCategory: String, + botSpamChannel: String, }, verification: { diff --git a/db/mongo/BotGuild.js b/db/mongo/BotGuild.js index 992c3a2e..c5198fc1 100644 --- a/db/mongo/BotGuild.js +++ b/db/mongo/BotGuild.js @@ -32,6 +32,9 @@ const BotGuildSchema = new Schema({ adminLog: { type: String, }, + botSpamChannel: { + type: String, + }, botSupportChannel: { type: String, }, From b8f606e933bf294be80efa2d69d4db806cf3f23c Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 28 Feb 2023 00:35:16 -0800 Subject: [PATCH 03/67] add feature to check user associated with email --- classes/Bot/bot-guild.d.ts | 1 + commands/a_activity/discord-contests.js | 8 +- commands/verification/check-email.js | 123 ++++++++++++++++++++++++ commands/verification/check-member.js | 69 ------------- commands/verification/get-email.js | 4 +- db/firebase/firebase-services.js | 33 ++++--- 6 files changed, 149 insertions(+), 89 deletions(-) create mode 100644 commands/verification/check-email.js delete mode 100644 commands/verification/check-member.js diff --git a/classes/Bot/bot-guild.d.ts b/classes/Bot/bot-guild.d.ts index 3705a85c..c8be6dd7 100644 --- a/classes/Bot/bot-guild.d.ts +++ b/classes/Bot/bot-guild.d.ts @@ -20,6 +20,7 @@ interface BotGuild extends Document { adminLog: String, botSupportChannel: String, archiveCategory: String, + botSpamChannel: String, }, verification: { diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index 57d95802..a11c0d15 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -58,7 +58,7 @@ class DiscordContests extends Command { // this.botGuild = this.botGuild; let guild = interaction.guild; this.botGuild = await BotGuild.findById(guild.id); - let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); var interval; @@ -154,7 +154,7 @@ class DiscordContests extends Command { } }) - const controlPanel = await adminConsole.send({ content: 'Discord contests started by <@' + userId + '>', components: [row] }); + const controlPanel = await botSpamChannel.send({ content: 'Discord contests started by <@' + userId + '>', components: [row] }); const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); const collector = controlPanel.createMessageComponentCollector({filter}); collector.on('collect', async i => { @@ -238,7 +238,7 @@ class DiscordContests extends Command { channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }).then(async (msg) => { if (answers.length === 0) { //send message to console - const questionMsg = await adminConsole.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }) + const questionMsg = await botSpamChannel.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }) const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.adminRole)); const collector = await questionMsg.createMessageComponentCollector({ filter }); @@ -247,7 +247,7 @@ class DiscordContests extends Command { const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); const winnerFilter = message => message.user.id === i.user.id; // error? - const winnerCollector = adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); + const winnerCollector = botSpamChannel.createMessageCollector({ filter: winnerFilter, max: 1 }); winnerCollector.on('collect', async m => { if (m.mentions.members.size > 0) { const member = await m.mentions.members.first(); diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js new file mode 100644 index 00000000..c33262a2 --- /dev/null +++ b/commands/verification/check-email.js @@ -0,0 +1,123 @@ +const { checkEmail } = require("../../db/firebase/firebase-services"); +const { Command } = require('@sapphire/framework'); +const BotGuild = require('../../db/mongo/BotGuild'); + +class CheckEmail extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Return user information given an email.' + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addStringOption(option => + option.setName('email') + .setDescription('Email to be checked') + .setRequired(true)) + ) + } + + async chatInputRun(interaction) { + this.botGuild = await BotGuild.findById(interaction.guild.id); + const guild = interaction.guild; + const email = interaction.options.getString('email'); + let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + const userData = await checkEmail(email, guild.id); + interaction.reply({content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); + + if (userData) { + if (userData.discordId && userData.types) { + const roleToString = await Promise.all(userData.types.map(type => (type.type + ': ' + (type.isVerified ? 'verified' : 'not verified')))); + botSpamChannel.send('The user associated with the email "' + email + '" is <@' + userData.discordId + '>. \n' + + 'Their role is:\n' + roleToString.join('\n')) + return; + } else if (userData.discordId) { + botSpamChannel.send('The user associated with the email "' + email + '" is <@' + userData.discordId + '>.'); + return; + } else { + botSpamChannel.send('Hmm. No Discord user is associated with the email "' + email + '" but we do have their email on file.'); + return; + } + } else { + botSpamChannel.send('The email "' + email +'" does not exist in our database'); + return; + } + } +} + +module.exports = CheckEmail; + +// // Discord.js commando requirements +// const PermissionCommand = require('../../classes/permission-command'); +// const firebaseServices = require('../../db/firebase/firebase-services'); +// const BotGuildModel = require('../../classes/Bot/bot-guild'); +// const { Message } = require('discord.js'); + +// /** +// * User can check if a member is in the database by email or name. +// * @category Commands +// * @subcategory Verification +// * @extends PermissionCommand +// */ +// class CheckMember extends PermissionCommand { +// constructor(client) { +// super(client, { +// name: 'check-member', +// group: 'verification', +// memberName: 'check if member is in database', +// description: 'If given email, will tell user if email is valid, or suggest similar emails if not. If given name, returns corresponding email.', +// args: [ +// { +// key: 'emailOrName', +// prompt: 'Please provide the email address or name to check (write names in the format firstName-lastName)', +// type: 'string', +// default: '', +// }, + +// ], +// }, +// { +// role: PermissionCommand.FLAGS.STAFF_ROLE, +// roleMessage: 'Hey there, the !check-member command is only for staff!', +// channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, +// channelMessage: 'Hey there, the !check-member command is only available in the admin console channel.', +// }); +// } + +// /** +// * @param {BotGuildModel} botGuild +// * @param {Message} message +// * @param {Object} args +// * @param {String} args.emailOrName +// */ +// async runCommand(botGuild, message, { emailOrName }) { +// if (emailOrName.split('-').length === 1) { // check for similar emails if given argument is an email +// let result = await firebaseServices.checkEmail(emailOrName, message.guild.id); +// if (result.length > 0) { // if similar emails were found, print them +// let listMembers = ''; +// result.forEach(member => { +// let listMember = member.email + ' (' + member.types.join(', ') + ') '; +// listMembers += listMember; +// }); +// message.channel.send('Here are the results I found similar to ' + emailOrName + ': ' + listMembers); +// } else { // message if no similar emails found +// message.channel.send('No matches to this email were found').then(msg => msg.delete({ timeout: 8000 })); +// } +// } else { // check for members of the given name if argument was a name +// let firstName = emailOrName.split('-')[0]; +// let lastName = emailOrName.split('-')[1]; +// let result = await firebaseServices.checkName(firstName, lastName, message.guild.id); +// if (result != null) { // print email if member was found +// message.channel.send('Email found for ' + firstName + ' ' + lastName + ' is: ' + result); +// } else { // message if member was not found +// message.channel.send('The name does not exist in our database!').then(msg => msg.delete({ timeout: 8000 })); +// } +// } +// } +// } +// module.exports = CheckMember; diff --git a/commands/verification/check-member.js b/commands/verification/check-member.js deleted file mode 100644 index 5bafcb2c..00000000 --- a/commands/verification/check-member.js +++ /dev/null @@ -1,69 +0,0 @@ -// Discord.js commando requirements -const PermissionCommand = require('../../classes/permission-command'); -const firebaseServices = require('../../db/firebase/firebase-services'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); -const { Message } = require('discord.js'); - -/** - * User can check if a member is in the database by email or name. - * @category Commands - * @subcategory Verification - * @extends PermissionCommand - */ -class CheckMember extends PermissionCommand { - constructor(client) { - super(client, { - name: 'check-member', - group: 'verification', - memberName: 'check if member is in database', - description: 'If given email, will tell user if email is valid, or suggest similar emails if not. If given name, returns corresponding email.', - args: [ - { - key: 'emailOrName', - prompt: 'Please provide the email address or name to check (write names in the format firstName-lastName)', - type: 'string', - default: '', - }, - - ], - }, - { - role: PermissionCommand.FLAGS.STAFF_ROLE, - roleMessage: 'Hey there, the !check-member command is only for staff!', - channel: PermissionCommand.FLAGS.ADMIN_CONSOLE, - channelMessage: 'Hey there, the !check-member command is only available in the admin console channel.', - }); - } - - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - * @param {Object} args - * @param {String} args.emailOrName - */ - async runCommand(botGuild, message, { emailOrName }) { - if (emailOrName.split('-').length === 1) { // check for similar emails if given argument is an email - let result = await firebaseServices.checkEmail(emailOrName, message.guild.id); - if (result.length > 0) { // if similar emails were found, print them - let listMembers = ''; - result.forEach(member => { - let listMember = member.email + ' (' + member.types.join(', ') + ') '; - listMembers += listMember; - }); - message.channel.send('Here are the results I found similar to ' + emailOrName + ': ' + listMembers); - } else { // message if no similar emails found - message.channel.send('No matches to this email were found').then(msg => msg.delete({ timeout: 8000 })); - } - } else { // check for members of the given name if argument was a name - let firstName = emailOrName.split('-')[0]; - let lastName = emailOrName.split('-')[1]; - let result = await firebaseServices.checkName(firstName, lastName, message.guild.id); - if (result != null) { // print email if member was found - message.channel.send('Email found for ' + firstName + ' ' + lastName + ' is: ' + result); - } else { // message if member was not found - message.channel.send('The name does not exist in our database!').then(msg => msg.delete({ timeout: 8000 })); - } - } - } -} -module.exports = CheckMember; diff --git a/commands/verification/get-email.js b/commands/verification/get-email.js index c7f21b2b..d65045a9 100644 --- a/commands/verification/get-email.js +++ b/commands/verification/get-email.js @@ -30,11 +30,11 @@ class GetEmails extends Command { const email = await lookupById(guild.id, interaction.targetUser.id) if (email) { - interaction.reply('Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results'); + interaction.reply({ content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is: ' + email); return; } else { - interaction.reply('Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results'); + interaction.reply({ content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is not in our database!'); return; } diff --git a/db/firebase/firebase-services.js b/db/firebase/firebase-services.js index 5342ba82..594fbb1d 100644 --- a/db/firebase/firebase-services.js +++ b/db/firebase/firebase-services.js @@ -105,23 +105,28 @@ module.exports.getReminder = getReminder; * @returns {Promise>} - array of members with similar emails to parameter email */ async function checkEmail(email, guildId) { - const snapshot = (await apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').get()).docs; // retrieve snapshot as an array of documents in the Firestore - var foundEmails = []; - snapshot.forEach(memberDoc => { + const cleanEmail = email.trim().toLowerCase(); + const docRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + const doc = await docRef.get(); + return doc.data(); + // var foundEmails = []; + // snapshot.forEach(memberDoc => { // compare each member's email with the given email - if (memberDoc.get('email') != null) { - let compare = memberDoc.get('email'); + // if (memberDoc.get('email') != null) { + // let compare = memberDoc.get('email'); // if the member's emails is similar to the given email, retrieve and add the email, verification status, and member type of // the member as an object to the array - if (compareEmails(email.split('@')[0], compare.split('@')[0])) { - foundEmails.push({ - email: compare, - types: memberDoc.get('types').map(type => type.type), - }); - } - } - }); - return foundEmails; + // if (compareEmails(email.split('@')[0], compare.split('@')[0])) { + // foundEmails.push({ + // email: compare, + // types: memberDoc.get('types').map(type => type.type), + // }); + // } + + // } + + // }); + // return foundEmails; } module.exports.checkEmail = checkEmail; From cc37f2329ced42d1fa2d8d551c98cc0e5fc739d2 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 28 Feb 2023 00:37:24 -0800 Subject: [PATCH 04/67] add permission control --- commands/verification/check-email.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index c33262a2..93a72fac 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -26,6 +26,11 @@ class CheckEmail extends Command { this.botGuild = await BotGuild.findById(interaction.guild.id); const guild = interaction.guild; const email = interaction.options.getString('email'); + + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + } + let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); const userData = await checkEmail(email, guild.id); interaction.reply({content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); From 353d2a451c78923e4e39acb361bcc4e5cd126af8 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 02:12:18 -0700 Subject: [PATCH 05/67] update discord.js and maintain mentor cave --- .../a_start_commands/start-mentor-cave.js | 150 +++++++++--------- package-lock.json | 93 ++++------- package.json | 2 +- 3 files changed, 114 insertions(+), 131 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index d185619c..d898d4d5 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -2,7 +2,7 @@ const { Command } = require('@sapphire/framework'); const { Interaction, MessageEmbed } = require('discord.js'); const { randomColor, discordLog } = require('../../discord-services'); const { Message, Collection } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild') +const BotGuild = require('../../db/mongo/BotGuild'); const winston = require('winston'); const BotGuildModel = require('../../classes/Bot/bot-guild'); const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); @@ -45,7 +45,7 @@ class StartMentorCave extends Command { option.setName('additional_mentor_role') .setDescription('Tag up to one additional role **aside from mentors and staff** that is allowed to help with tickets') .setRequired(false)) - ) + ); } /** @@ -60,19 +60,21 @@ class StartMentorCave extends Command { let guild = interaction.guild; this.botGuild = await BotGuild.findById(guild.id); let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + let adminLog = guild.channels.resolve(this.botGuild.channelIDs.adminLog); this.ticketCount = 0; const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); const publicRole = interaction.options.getRole('request_ticket_role'); const inactivePeriod = interaction.options.getInteger('inactivity_time'); const bufferTime = inactivePeriod / 2; - const reminderTime = interaction.options.getInteger('unanswered_ticket_time') + const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }); } - interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }) + interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); + adminLog.send('Mentor cave started by <@' + userId + '>'); // create channels let overwrites = @@ -87,18 +89,18 @@ class StartMentorCave extends Command { { id: this.botGuild.roleIDs.staffRole, allow: ['VIEW_CHANNEL'], - }] + }]; if (additionalMentorRole) { overwrites.push({ id: additionalMentorRole, allow: ['VIEW_CHANNEL'] - }) + }); } let mentorCategory = await channel.guild.channels.create('Mentors', { - type: "GUILD_CATEGORY", + type: 'GUILD_CATEGORY', permissionOverwrites: overwrites } ); @@ -113,15 +115,15 @@ class StartMentorCave extends Command { await channel.guild.channels.create('mentors-announcements', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', parent: mentorCategory, permissionOverwrites: announcementsOverwrites } - ) + ); const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', parent: mentorCategory } @@ -182,7 +184,7 @@ class StartMentorCave extends Command { const roleSelection = new MessageEmbed() .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') - .addFields(fields) + .addFields(fields); const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); for (let key of emojisMap.keys()) { @@ -206,11 +208,11 @@ class StartMentorCave extends Command { const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); } - }) + }); channel.guild.channels.create('mentors-general', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', topic: 'Private chat between all mentors and organizers', parent: mentorCategory } @@ -218,7 +220,7 @@ class StartMentorCave extends Command { const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', topic: 'Tickets from hackers will come in here!', parent: mentorCategory } @@ -226,7 +228,7 @@ class StartMentorCave extends Command { const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', { - type: "GUILD_CATEGORY", + type: 'GUILD_CATEGORY', permissionOverwrites: [ { id: this.botGuild.verification.guestRoleID, @@ -238,7 +240,7 @@ class StartMentorCave extends Command { channel.guild.channels.create('quick-questions', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', topic: 'ask questions for mentors here!', parent: mentorHelpCategory } @@ -246,7 +248,7 @@ class StartMentorCave extends Command { const requestTicketChannel = await channel.guild.channels.create('request-ticket', { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', topic: 'request 1-on-1 help from mentors here!', parent: mentorHelpCategory, permissionOverwrites: [ @@ -269,28 +271,31 @@ class StartMentorCave extends Command { const requestTicketEmbed = new MessageEmbed() .setTitle('Need 1:1 mentor help?') - .setDescription('Select a technology you need help with and follow the instructions!') + .setDescription('Select a technology you need help with and follow the instructions!'); var options = []; for (let value of emojisMap.values()) { options.push({ label: value, value: value }); } - options.push({ label: 'None of the above', value: 'None of the above' }) + options.push({ label: 'None of the above', value: 'None of the above' }); const selectMenuRow = new MessageActionRow() .addComponents( new MessageSelectMenu() .setCustomId('ticketType') .addOptions(options) - ) + ); const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - const selectMenuFilter = i => !i.user.bot && guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id); + const selectMenuFilter = i => !i.user.bot; const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); selectMenuCollector.on('collect', async i => { if (i.customId === 'ticketType') { requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + // if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { + // return this.error({ message: 'You do not have permissions to request tickets!', ephemeral: true }); + // } const modal = new Modal() .setCustomId('ticketSubmitModal') .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) @@ -333,7 +338,7 @@ class StartMentorCave extends Command { const role = i.values[0] === 'None of the above' ? this.botGuild.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; const description = submitted.fields.getTextInputValue('ticketDescription'); const location = submitted.fields.getTextInputValue('location'); - const helpFormat = submitted.fields.getTextInputValue('helpFormat') + const helpFormat = submitted.fields.getTextInputValue('helpFormat'); const ticketNumber = this.ticketCount; this.ticketCount++; const newTicketEmbed = new MessageEmbed() @@ -352,7 +357,7 @@ class StartMentorCave extends Command { name: 'OK with being helped online?', value: helpFormat } - ]) + ]); const ticketAcceptanceRow = new MessageActionRow() .addComponents( new MessageButton() @@ -368,47 +373,47 @@ class StartMentorCave extends Command { ); const ticketMsg = await incomingTicketChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); - submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }) + submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); const ticketReminder = setTimeout(() => { ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); - }, reminderTime * 60000) + }, reminderTime * 60000); const confirmationEmbed = new MessageEmbed() .setTitle('Your ticket is number ' + ticketNumber) - .setDescription(description) + .setDescription(description); const deleteTicketRow = new MessageActionRow() .addComponents( new MessageButton() .setCustomId('deleteTicket') .setLabel('Delete ticket') .setStyle('DANGER'), - ) + ); const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); deleteTicketCollector.on('collect', async deleteInteraction => { await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); clearTimeout(ticketReminder); deleteInteraction.reply('Ticket deleted!'); - ticketReceipt.edit({ components: [] }) - }) + ticketReceipt.edit({ components: [] }); + }); const ticketAcceptFilter = i => !i.user.bot && i.isButton(); const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); ticketAcceptanceCollector.on('collect', async acceptInteraction => { const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { - await ticketReceipt.edit({ components: [] }) + await ticketReceipt.edit({ components: [] }); clearTimeout(ticketReminder); ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); } if (acceptInteraction.customId === 'acceptIrl') { // TODO: mark as complete? submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }) + acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); } if (acceptInteraction.customId === 'acceptOnline') { submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }) + acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); let ticketChannelOverwrites = [{ id: this.botGuild.roleIDs.everyoneRole, @@ -421,25 +426,25 @@ class StartMentorCave extends Command { { id: submitted.user.id, allow: ['VIEW_CHANNEL'], - }] + }]; let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, { - type: "GUILD_CATEGORY", + type: 'GUILD_CATEGORY', permissionOverwrites: ticketChannelOverwrites } ); const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, { - type: "GUILD_TEXT", + type: 'GUILD_TEXT', parent: ticketCategory } ); const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', { - type: "GUILD_VOICE", + type: 'GUILD_VOICE', parent: ticketCategory } ); @@ -447,7 +452,7 @@ class StartMentorCave extends Command { const ticketChannelEmbed = new MessageEmbed() .setColor(this.botGuild.colors.embedColor) .setTitle('Ticket description') - .setDescription(submitted.fields.getTextInputValue('ticketDescription')) + .setDescription(submitted.fields.getTextInputValue('ticketDescription')); const ticketChannelButtons = new MessageActionRow() .addComponents( @@ -470,7 +475,7 @@ class StartMentorCave extends Command { if (ticketInteraction.customId === 'addMembers') { ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) .then(() => { - const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id + const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) .then(async collected => { if (collected.first().mentions.members.size === 0) { @@ -480,17 +485,17 @@ class StartMentorCave extends Command { collected.first().mentions.members.forEach(member => { ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); newMembersArray.push(member.id); - }) - ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.') + }); + ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); } }) .catch(collected => { - ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }) - }) - }) + ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); + }); + }); } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); - ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }) + ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); @@ -505,11 +510,11 @@ class StartMentorCave extends Command { } } - }) + }); const adminEmbed = new MessageEmbed() .setTitle('Mentor Cave Console') - .setColor(this.botGuild.colors.embedColor) + .setColor(this.botGuild.colors.embedColor); const adminRow = new MessageActionRow() .addComponents( @@ -518,19 +523,19 @@ class StartMentorCave extends Command { .setLabel('Add Mentor Role') .setStyle('PRIMARY'), ) - // .addComponents( - // new MessageButton() - // .setCustomId('deleteCave') - // .setLabel('Delete Cave') - // .setStyle('DANGER'), - // ); + .addComponents( + new MessageButton() + .setCustomId('deleteCave') + .setLabel('Delete Cave') + .setStyle('DANGER'), + ); const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(this.botGuild.roleIDs.adminRole) }); adminCollector.on('collect', async adminInteraction => { if (adminInteraction.customId === 'addRole') { - const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }) - const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }) + const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); + const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); let roleName; roleNameCollector.on('collect', async collected => { if (collected.content.toLowerCase() != 'cancel') { @@ -552,8 +557,8 @@ class StartMentorCave extends Command { emojiCollector.on('collect', collected => { if (emojisMap.has(collected.emoji.name)) { adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { - setTimeout(() => msg.delete(), 5000) - }) + setTimeout(() => msg.delete(), 5000); + }); } else { emojiCollector.stop(); emojisMap.set(collected.emoji.name, roleName); @@ -568,11 +573,11 @@ class StartMentorCave extends Command { new MessageSelectMenu() .setCustomId('ticketType') .addOptions(newOptions) - ) + ); requestTicketConsole.edit({ components: [newSelectMenuRow] }); askForEmoji.delete(); } - }) + }); } askForRoleName.delete(); collected.delete(); @@ -580,15 +585,18 @@ class StartMentorCave extends Command { }); - } - // else if (adminInteraction.customId === 'deleteCave') { - // mentorCategory.delete(); - // mentorHelpCategory.delete(); - // mentorRoleSelectionChannel.delete(); - // incomingTicketChannel.delete(); - // requestTicketChannel.delete(); - // } - }) + } else if (adminInteraction.customId === 'deleteCave') { + adminInteraction.reply({ content: 'Mentor cave deleted!', ephemeral: true }); + adminLog.send('Mentor cave deleted by <@' + adminInteraction.user.id + '>'); + // unknown channel error + await mentorCategory.delete(); + await mentorHelpCategory.delete(); + await mentorRoleSelectionChannel.delete(); + await incomingTicketChannel.delete(); + await requestTicketChannel.delete(); + await adminControls.delete(); + } + }); } catch (error) { // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); } @@ -616,7 +624,7 @@ class StartMentorCave extends Command { .setCustomId('keepChannels') .setLabel('Keep Channels') .setStyle('PRIMARY'), - ) + ); const warning = await ticketText.send({ content: msgText, components: [row] }); warning.awaitMessageComponent({ filter: i => !i.user.bot, time: bufferTime * 60 * 1000 }) @@ -629,8 +637,8 @@ class StartMentorCave extends Command { .setLabel('Keep Channels') .setDisabled(true) .setStyle('PRIMARY'), - ) - warning.edit({ components: [disabledButtonRow] }) + ); + warning.edit({ components: [disabledButtonRow] }); this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); }) .catch(error => { diff --git a/package-lock.json b/package-lock.json index 9aa076f2..0f904417 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@sentry/tracing": "^6.14.3", "advanced-discord.js-prompts": "^1.7.0", "csv-parser": "^3.0.0", - "discord.js": "^13.12.0", + "discord.js": "^13.16.0", "dotenv-flow": "^3.2.0", "firebase-admin": "^9.11.1", "fs": "0.0.1-security", @@ -820,25 +820,12 @@ "integrity": "sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA==" }, "node_modules/@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.8.tgz", + "integrity": "sha512-nnH5lV9QCMPsbEVdTb5Y+F3GQxLSw1xQgIydrb2gSfEavRPs50FnMr+KUaa+LoPSqibm2N+ZZxH7lavZlAT4GA==", "dependencies": { "@types/node": "*", - "form-data": "^3.0.0" - } - }, - "node_modules/@types/node-fetch/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" + "form-data": "^4.0.0" } }, "node_modules/@types/qs": { @@ -861,9 +848,9 @@ } }, "node_modules/@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", + "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", "dependencies": { "@types/node": "*" } @@ -1440,19 +1427,19 @@ "integrity": "sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==" }, "node_modules/discord.js": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.12.0.tgz", - "integrity": "sha512-K5qhREsYcTHkEqt7+7LcSoXTeQYZpI+SQRs9ei/FhbhUpirmjqFtN99P8W2mrKUyhhy7WXWm7rnna0AooKtIpw==", + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.16.0.tgz", + "integrity": "sha512-bOoCs1Ilojd/UshZVxmEcpxVmHcYOv2fPVZOVq3aFV8xrKLJfaF9mxlvGZ1D1z9aIqf2NkptDr+QndeNuQBTxQ==", "dependencies": { "@discordjs/builders": "^0.16.0", "@discordjs/collection": "^0.7.0", "@sapphire/async-queue": "^1.5.0", - "@types/node-fetch": "^2.6.2", - "@types/ws": "^8.5.3", + "@types/node-fetch": "^2.6.3", + "@types/ws": "^8.5.4", "discord-api-types": "^0.33.5", "form-data": "^4.0.0", "node-fetch": "^2.6.7", - "ws": "^8.9.0" + "ws": "^8.13.0" }, "engines": { "node": ">=16.6.0", @@ -3994,15 +3981,15 @@ } }, "node_modules/ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -4736,24 +4723,12 @@ "integrity": "sha512-KDazLNYAGIuJugdbULwFZULF9qQ13yNWEBFnfVpqlpgAAo6H/qnM9RjBgh0A0kmHf3XxAKLdN5mTIng9iUvVLA==" }, "@types/node-fetch": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", - "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.8.tgz", + "integrity": "sha512-nnH5lV9QCMPsbEVdTb5Y+F3GQxLSw1xQgIydrb2gSfEavRPs50FnMr+KUaa+LoPSqibm2N+ZZxH7lavZlAT4GA==", "requires": { "@types/node": "*", - "form-data": "^3.0.0" - }, - "dependencies": { - "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" - } - } + "form-data": "^4.0.0" } }, "@types/qs": { @@ -4776,9 +4751,9 @@ } }, "@types/ws": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", - "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.8.tgz", + "integrity": "sha512-flUksGIQCnJd6sZ1l5dqCEG/ksaoAg/eUwiLAGTJQcfgvZJKF++Ta4bJA6A5aPSJmsr+xlseHn4KLgVlNnvPTg==", "requires": { "@types/node": "*" } @@ -5213,19 +5188,19 @@ "integrity": "sha512-bz/NDyG0KBo/tY14vSkrwQ/n3HKPf87a0WFW/1M9+tXYK+vp5Z5EksawfCWo2zkAc6o7CClc0eff1Pjrqznlwg==" }, "discord.js": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.12.0.tgz", - "integrity": "sha512-K5qhREsYcTHkEqt7+7LcSoXTeQYZpI+SQRs9ei/FhbhUpirmjqFtN99P8W2mrKUyhhy7WXWm7rnna0AooKtIpw==", + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.16.0.tgz", + "integrity": "sha512-bOoCs1Ilojd/UshZVxmEcpxVmHcYOv2fPVZOVq3aFV8xrKLJfaF9mxlvGZ1D1z9aIqf2NkptDr+QndeNuQBTxQ==", "requires": { "@discordjs/builders": "^0.16.0", "@discordjs/collection": "^0.7.0", "@sapphire/async-queue": "^1.5.0", - "@types/node-fetch": "^2.6.2", - "@types/ws": "^8.5.3", + "@types/node-fetch": "^2.6.3", + "@types/ws": "^8.5.4", "discord-api-types": "^0.33.5", "form-data": "^4.0.0", "node-fetch": "^2.6.7", - "ws": "^8.9.0" + "ws": "^8.13.0" }, "dependencies": { "@discordjs/collection": { @@ -7218,9 +7193,9 @@ } }, "ws": { - "version": "8.9.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.9.0.tgz", - "integrity": "sha512-Ja7nszREasGaYUYCI2k4lCKIRTt+y7XuqVoHR44YpI49TtryyqbqvDMn5eqfW7e6HzTukDRIsXqzVHScqRcafg==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "requires": {} }, "xdg-basedir": { diff --git a/package.json b/package.json index 6049c158..ee1feedc 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@sentry/tracing": "^6.14.3", "advanced-discord.js-prompts": "^1.7.0", "csv-parser": "^3.0.0", - "discord.js": "^13.12.0", + "discord.js": "^13.16.0", "dotenv-flow": "^3.2.0", "firebase-admin": "^9.11.1", "fs": "0.0.1-security", From 9fce921b47f1e537b3809665f8200da2200b7bb7 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 03:08:47 -0700 Subject: [PATCH 06/67] add permissions feedback to verification, fix mentor cave perms --- .../a_start_commands/start-mentor-cave.js | 11 +++---- commands/verification/start-verification.js | 29 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index d898d4d5..874f5f78 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -60,7 +60,6 @@ class StartMentorCave extends Command { let guild = interaction.guild; this.botGuild = await BotGuild.findById(guild.id); let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); - let adminLog = guild.channels.resolve(this.botGuild.channelIDs.adminLog); this.ticketCount = 0; const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); @@ -70,11 +69,12 @@ class StartMentorCave extends Command { const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }); + await interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); + return; } interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); - adminLog.send('Mentor cave started by <@' + userId + '>'); + discordLog('Mentor cave started by <@' + userId + '>'); // create channels let overwrites = @@ -294,7 +294,8 @@ class StartMentorCave extends Command { if (i.customId === 'ticketType') { requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); // if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { - // return this.error({ message: 'You do not have permissions to request tickets!', ephemeral: true }); + // await i.reply({ message: 'You do not have permissions to request tickets!', ephemeral: true }); + // return // } const modal = new Modal() .setCustomId('ticketSubmitModal') @@ -587,7 +588,7 @@ class StartMentorCave extends Command { } else if (adminInteraction.customId === 'deleteCave') { adminInteraction.reply({ content: 'Mentor cave deleted!', ephemeral: true }); - adminLog.send('Mentor cave deleted by <@' + adminInteraction.user.id + '>'); + discordLog('Mentor cave deleted by <@' + adminInteraction.user.id + '>'); // unknown channel error await mentorCategory.delete(); await mentorHelpCategory.delete(); diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 1eb4638d..3ec09c87 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -1,5 +1,5 @@ const { Command } = require('@sapphire/framework'); -const BotGuild = require('../../db/mongo/BotGuild') +const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); const firebaseServices = require('../../db/firebase/firebase-services'); @@ -18,7 +18,7 @@ class StartVerification extends Command { builder .setName(this.name) .setDescription(this.description) - ) + ); } /** @@ -29,21 +29,24 @@ class StartVerification extends Command { this.botGuild = await BotGuild.findById(interaction.guild.id); const embed = new MessageEmbed() - .setTitle(`Please click the button below to check-in to the ${interaction.guild.name} server! Make sure you know which email you used to apply to nwHacks!`) - .setDescription('If you have not already, make sure to enable emojis and embeds/link previews in your personal Discord settings! If you have any issues, please find an organizer!') -// modal timeout warning? + .setTitle(`Please click the button below to check-in to the ${interaction.guild.name} server! Make sure you know which email you used to apply to ${interaction.guild.name}!`); + // modal timeout warning? const row = new MessageActionRow() .addComponents( new MessageButton() .setCustomId('verify') .setLabel('Check-in') .setStyle('PRIMARY'), - ) + ); interaction.reply({ content: 'Verification started!', ephemeral: true }); - const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); + const msg = await interaction.channel.send({ content: 'If you have not already, make sure to enable emojis and embeds/link previews in your personal Discord settings! If you have any issues, please find an organizer!', embeds: [embed], components: [row] }); - const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot && interaction.guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.verification.guestRoleID) }) + const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); checkInCollector.on('collect', async i => { + if (!interaction.guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.verification.guestRoleID)) { + await i.reply({ content: 'You are not eligible to be checked in! If you don\'t have correct access to the server, please contact an organizer.', ephemeral: true}); + return; + } const modal = new Modal() .setCustomId('verifyModal') .setTitle('Check-in to gain access to the server!') @@ -71,7 +74,7 @@ class StartVerification extends Command { try { types = await firebaseServices.verify(email, submitted.user.id, submitted.guild.id); } catch { - submitted.reply({ content: 'Your email could not be found! Please try again or ask an admin for help.', ephemeral: true }) + submitted.reply({ content: 'Your email could not be found! Please try again or ask an admin for help.', ephemeral: true }); discordLog(interaction.guild, `VERIFY FAILURE : <@${submitted.user.id}> Verified email: ${email} but was a failure, I could not find that email!`); return; } @@ -91,15 +94,15 @@ class StartVerification extends Command { if (correctTypes.length === 0) member.roles.remove(this.botGuild.verification.guestRoleID); correctTypes.push(type); } else { - discordLog(`VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`) + discordLog(`VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); } - }) + }); if (correctTypes.length > 0) { - submitted.reply({ content: 'You have successfully verified as a ' + correctTypes.join(', ') + '!', ephemeral: true }) + submitted.reply({ content: 'You have successfully verified as a ' + correctTypes.join(', ') + '!', ephemeral: true }); } } - }) + }); } } module.exports = StartVerification; \ No newline at end of file From 93e22bf20a45d94d3a0d3effae0f677cc73b4a0c Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 03:15:24 -0700 Subject: [PATCH 07/67] linter changes + perm checks for discord contests --- commands/a_activity/discord-contests.js | 31 +++++++++++++------------ 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index a11c0d15..781e7de1 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -2,7 +2,7 @@ const { Command } = require('@sapphire/framework'); const { discordLog, checkForRole } = require('../../discord-services'); const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebase-services'); -const BotGuild = require('../../db/mongo/BotGuild') +const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); /** @@ -41,7 +41,7 @@ class DiscordContests extends Command { option.setName('start_question_now') .setDescription('True to start first question now, false to start it after one interval') .setRequired(false)) - ) + ); } /** @@ -68,7 +68,8 @@ class DiscordContests extends Command { var roleId = interaction.options.getRole('notify'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; } // try { // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); @@ -93,7 +94,7 @@ class DiscordContests extends Command { */ const winners = []; - var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!' + var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!'; const row = new MessageActionRow() .addComponents( @@ -133,10 +134,10 @@ class DiscordContests extends Command { .setColor(this.botGuild.colors.embedColor) .setTitle(string) .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') - .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]) + .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); const leaderboard = new MessageEmbed() - .setTitle('Leaderboard') + .setTitle('Leaderboard'); let pinnedMessage = await channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed, leaderboard] }); pinnedMessage.pin(); @@ -152,19 +153,19 @@ class DiscordContests extends Command { if (reaction.emoji.name === '🍀') { guild.members.cache.get(user.id).roles.remove(roleId); } - }) + }); const controlPanel = await botSpamChannel.send({ content: 'Discord contests started by <@' + userId + '>', components: [row] }); const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); const collector = controlPanel.createMessageComponentCollector({filter}); collector.on('collect', async i => { if (i.customId == 'refresh') { - await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }) + await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }); await updateLeaderboard(null); } else if (interval != null && !paused && i.customId == 'pause') { clearInterval(interval); paused = true; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest paused by <@' + i.user.id + '>!') + await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest paused by <@' + i.user.id + '>!'); await i.reply({ content: 'Discord contest has been paused!', ephemeral: true }); } else if (paused && i.customId == 'play') { await sendQuestion(this.botGuild); @@ -173,7 +174,7 @@ class DiscordContests extends Command { await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); await i.reply({ content: 'Discord contest has been un-paused!', ephemeral: true }); } else { - await i.reply({ content: `Wrong button or wrong permissions!`, ephemeral: true }); + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); } }); @@ -238,7 +239,7 @@ class DiscordContests extends Command { channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }).then(async (msg) => { if (answers.length === 0) { //send message to console - const questionMsg = await botSpamChannel.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }) + const questionMsg = await botSpamChannel.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.adminRole)); const collector = await questionMsg.createMessageComponentCollector({ filter }); @@ -246,7 +247,7 @@ class DiscordContests extends Command { collector.on('collect', async i => { const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); - const winnerFilter = message => message.user.id === i.user.id; // error? + const winnerFilter = message => message.author.id === i.user.id; // error? const winnerCollector = botSpamChannel.createMessageCollector({ filter: winnerFilter, max: 1 }); winnerCollector.on('collect', async m => { if (m.mentions.members.size > 0) { @@ -255,7 +256,7 @@ class DiscordContests extends Command { await m.delete(); await questionMsg.delete(); await i.editReply('<@' + memberId + '> has been recorded!'); - row.components[0].setDisabled(true) + row.components[0].setDisabled(true); // row.components[0].setDisabled(); await channel.send('Congrats <@' + memberId + '> for the best answer to the last question!'); // winners.push(memberId); @@ -270,7 +271,7 @@ class DiscordContests extends Command { errorMsg.delete(); }, 5000); } - }) + }); }); } else { //automatically mark answers @@ -317,7 +318,7 @@ class DiscordContests extends Command { async function recordWinner(member) { try { - let email = await lookupById(guild.id, member.id) + let email = await lookupById(guild.id, member.id); discordLog(`Discord contest winner: ${member.id} - ${email}`); } catch (error) { console.log(error); From 0bb1169225a261045051e5be5494f5fbede42176 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 03:19:40 -0700 Subject: [PATCH 08/67] linter changes + perm checks for self-care reminders --- commands/a_utility/self-care.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index f3a1dbe5..60a9fdd3 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -3,7 +3,7 @@ const PermissionCommand = require('../../classes/permission-command'); const { discordLog } = require('../../discord-services'); const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); const { getReminder } = require('../../db/firebase/firebase-services'); -const BotGuild = require('../../db/mongo/BotGuild') +const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); @@ -39,7 +39,7 @@ class SelfCareReminders extends Command { option.setName('start_reminder_now') .setDescription('True to start first reminder now, false to start it after one interval') .setRequired(false)) - ) + ); } async chatInputRun(interaction) { @@ -57,7 +57,8 @@ class SelfCareReminders extends Command { var roleId = interaction.options.getRole('notify'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); + return; } // keeps track of whether it has been paused @@ -79,11 +80,11 @@ class SelfCareReminders extends Command { const startEmbed = new MessageEmbed() .setColor(this.botGuild.colors.embedColor) - .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!') + .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); - roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }) + roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); @@ -92,7 +93,7 @@ class SelfCareReminders extends Command { if (interval != null && !paused && i.customId == 'pause') { clearInterval(interval); paused = true; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Self care reminders paused by <@' + i.user.id + '>!') + await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Self care reminders paused by <@' + i.user.id + '>!'); await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); } else if (paused && i.customId == 'play') { await sendReminder(this.botGuild); @@ -101,7 +102,7 @@ class SelfCareReminders extends Command { await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Self care reminders restarted by <@' + i.user.id + '>!'); await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); } else { - await i.reply({ content: `Wrong button or wrong permissions!`, ephemeral: true }); + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); } }); From da4e5044454eb7272f33db8b7457013826d4f99d Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 04:31:36 -0700 Subject: [PATCH 09/67] formatting pronouns command --- commands/a_utility/pronouns.js | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index e147dc5f..119347ef 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -15,24 +15,24 @@ const { Interaction, MessageEmbed } = require('discord.js'); class Pronouns extends Command { constructor(context, options) { super(context, { - ...options, - description: 'Start pronoun selector.' + ...options, + description: 'Start pronoun selector.' }); } registerApplicationCommands(registry) { registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - ) + builder + .setName(this.name) + .setDescription(this.description) + ); } /** * * @param {Interaction} interaction */ - async chatInputRun(interaction) { + async chatInputRun(interaction) { const sheRole = interaction.guild.roles.cache.find(role => role.name === 'she/her'); const heRole = interaction.guild.roles.cache.find(role => role.name === 'he/him'); const theyRole = interaction.guild.roles.cache.find(role => role.name === 'they/them'); @@ -57,11 +57,11 @@ class Pronouns extends Command { let messageEmbed = await interaction.channel.send({embeds: [embed]}); emojis.forEach(emoji => messageEmbed.react(emoji)); - interaction.reply({content: 'Pronouns selector started!', ephemeral: true}) + interaction.reply({content: 'Pronouns selector started!', ephemeral: true}); let filter = (reaction, user) => { return user.bot != true && emojis.includes(reaction.emoji.name); - } + }; // create collector const reactionCollector = messageEmbed.createReactionCollector({filter, dispose: true}); @@ -69,32 +69,32 @@ class Pronouns extends Command { // on emoji reaction reactionCollector.on('collect', async (reaction, user) => { if (reaction.emoji.name === emojis[0]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.add(sheRole); } if (reaction.emoji.name === emojis[1]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.add(heRole); } if (reaction.emoji.name === emojis[2]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.add(theyRole); } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.add(otherRole); } }); reactionCollector.on('remove', async (reaction, user) => { if (reaction.emoji.name === emojis[0]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(sheRole); } if (reaction.emoji.name === emojis[1]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(heRole); } if (reaction.emoji.name === emojis[2]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(theyRole); } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id) + const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(otherRole); } }); From 18471029edc37538369a4b2b1f371dcf66f93e11 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 04:42:39 -0700 Subject: [PATCH 10/67] add idHints to hc commands --- commands/a_activity/discord-contests.js | 5 ++++- commands/a_start_commands/start-mentor-cave.js | 5 ++++- commands/a_utility/pronouns.js | 5 ++++- commands/a_utility/self-care.js | 5 ++++- commands/verification/start-verification.js | 5 ++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index 781e7de1..cf02fdea 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -41,7 +41,10 @@ class DiscordContests extends Command { option.setName('start_question_now') .setDescription('True to start first question now, false to start it after one interval') .setRequired(false)) - ); + ), + { + idHints: 1051737343729610812 + }; } /** diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 874f5f78..ca1fd901 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -45,7 +45,10 @@ class StartMentorCave extends Command { option.setName('additional_mentor_role') .setDescription('Tag up to one additional role **aside from mentors and staff** that is allowed to help with tickets') .setRequired(false)) - ); + ), + { + idHints: 1051737344937566229 + }; } /** diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 119347ef..856484ec 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -25,7 +25,10 @@ class Pronouns extends Command { builder .setName(this.name) .setDescription(this.description) - ); + ), + { + idHints: 1051737347441569813 + }; } /** diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 60a9fdd3..21987417 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -39,7 +39,10 @@ class SelfCareReminders extends Command { option.setName('start_reminder_now') .setDescription('True to start first reminder now, false to start it after one interval') .setRequired(false)) - ); + ), + { + idHints: 1052699103143927859 + }; } async chatInputRun(interaction) { diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 3ec09c87..397fa105 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -18,7 +18,10 @@ class StartVerification extends Command { builder .setName(this.name) .setDescription(this.description) - ); + ), + { + idHints: 1060545714133938309 + }; } /** From 402ff32d6036b10f361a068de270e7865b01511b Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 22:38:15 -0700 Subject: [PATCH 11/67] fix discordLog bug and change mentor cave to use existing channels --- classes/Bot/bot-guild.d.ts | 3 + commands/a_activity/discord-contests.js | 2 +- .../a_start_commands/start-mentor-cave.js | 276 ++++++++---------- commands/verification/start-verification.js | 2 +- db/mongo/BotGuild.d.ts | 3 + db/mongo/BotGuild.js | 9 + discord-services.js | 4 +- 7 files changed, 145 insertions(+), 154 deletions(-) diff --git a/classes/Bot/bot-guild.d.ts b/classes/Bot/bot-guild.d.ts index c8be6dd7..92f56a01 100644 --- a/classes/Bot/bot-guild.d.ts +++ b/classes/Bot/bot-guild.d.ts @@ -21,6 +21,9 @@ interface BotGuild extends Document { botSupportChannel: String, archiveCategory: String, botSpamChannel: String, + incomingTicketsChannel: String, + mentorRoleSelectionChannel: String, + requestTicketChannel: String }, verification: { diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index cf02fdea..0b5fc170 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -322,7 +322,7 @@ class DiscordContests extends Command { async function recordWinner(member) { try { let email = await lookupById(guild.id, member.id); - discordLog(`Discord contest winner: ${member.id} - ${email}`); + discordLog(guild, `Discord contest winner: ${member.id} - ${email}`); } catch (error) { console.log(error); } diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index ca1fd901..35b11cba 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -41,10 +41,10 @@ class StartMentorCave extends Command { option.setName('request_ticket_role') .setDescription('Tag the role that is allowed to request tickets') .setRequired(true)) - .addRoleOption(option => - option.setName('additional_mentor_role') - .setDescription('Tag up to one additional role **aside from mentors and staff** that is allowed to help with tickets') - .setRequired(false)) + // .addRoleOption(option => + // option.setName('additional_mentor_role') + // .setDescription('Tag up to one additional role **aside from mentors and staff** that is allowed to help with tickets') + // .setRequired(false)) ), { idHints: 1051737344937566229 @@ -65,72 +65,75 @@ class StartMentorCave extends Command { let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); this.ticketCount = 0; - const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); + // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); const publicRole = interaction.options.getRole('request_ticket_role'); const inactivePeriod = interaction.options.getInteger('inactivity_time'); const bufferTime = inactivePeriod / 2; const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - await interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); - discordLog('Mentor cave started by <@' + userId + '>'); - - // create channels - let overwrites = - [{ - id: this.botGuild.roleIDs.everyoneRole, - deny: ['VIEW_CHANNEL'], - }, - { - id: this.botGuild.roleIDs.mentorRole, - allow: ['VIEW_CHANNEL'], - }, - { - id: this.botGuild.roleIDs.staffRole, - allow: ['VIEW_CHANNEL'], - }]; - - if (additionalMentorRole) { - overwrites.push({ - id: additionalMentorRole, - allow: ['VIEW_CHANNEL'] - }); - } - - let mentorCategory = await channel.guild.channels.create('Mentors', - { - type: 'GUILD_CATEGORY', - permissionOverwrites: overwrites - } - ); - - let announcementsOverwrites = overwrites; - announcementsOverwrites.push( - { - id: this.botGuild.roleIDs.mentorRole, - deny: ['SEND_MESSAGES'], - allow: ['VIEW_CHANNEL'] - }); - - await channel.guild.channels.create('mentors-announcements', - { - type: 'GUILD_TEXT', - parent: mentorCategory, - permissionOverwrites: announcementsOverwrites - } - ); - - const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', - { - type: 'GUILD_TEXT', - topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', - parent: mentorCategory - } - ); + discordLog(guild, 'Mentor cave started by <@' + userId + '>'); + + // these are all old code that create channels rather than using existing channels + // let overwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.mentorRole, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'], + // }]; + + // if (additionalMentorRole) { + // overwrites.push({ + // id: additionalMentorRole, + // allow: ['VIEW_CHANNEL'] + // }); + // } + + // let mentorCategory = await channel.guild.channels.create('Mentors', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: overwrites + // } + // ); + + // let announcementsOverwrites = overwrites; + // announcementsOverwrites.push( + // { + // id: this.botGuild.roleIDs.mentorRole, + // deny: ['SEND_MESSAGES'], + // allow: ['VIEW_CHANNEL'] + // }); + + // await channel.guild.channels.create('mentors-announcements', + // { + // type: 'GUILD_TEXT', + // parent: mentorCategory, + // permissionOverwrites: announcementsOverwrites + // } + // ); + + // const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', + // { + // type: 'GUILD_TEXT', + // topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', + // parent: mentorCategory + // } + // ); + const mentorRoleSelectionChannel = guild.channels.resolve(this.botGuild.channelIDs.mentorRoleSelectionChannel); + const incomingTicketChannel = guild.channels.resolve(this.botGuild.channelIDs.incomingTicketChannel); + const requestTicketChannel = guild.channels.resolve(this.botGuild.channelIDs.requestTicketChannel); //TODO: allow staff to add more roles const htmlCssEmoji = '💻'; @@ -213,64 +216,64 @@ class StartMentorCave extends Command { } }); - channel.guild.channels.create('mentors-general', - { - type: 'GUILD_TEXT', - topic: 'Private chat between all mentors and organizers', - parent: mentorCategory - } - ); - - const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', - { - type: 'GUILD_TEXT', - topic: 'Tickets from hackers will come in here!', - parent: mentorCategory - } - ); - - const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', - { - type: 'GUILD_CATEGORY', - permissionOverwrites: [ - { - id: this.botGuild.verification.guestRoleID, - deny: ['VIEW_CHANNEL'], - }, - ] - } - ); - - channel.guild.channels.create('quick-questions', - { - type: 'GUILD_TEXT', - topic: 'ask questions for mentors here!', - parent: mentorHelpCategory - } - ); - - const requestTicketChannel = await channel.guild.channels.create('request-ticket', - { - type: 'GUILD_TEXT', - topic: 'request 1-on-1 help from mentors here!', - parent: mentorHelpCategory, - permissionOverwrites: [ - { - id: publicRole, - allow: ['VIEW_CHANNEL'], - deny: ['SEND_MESSAGES'] - }, - { - id: this.botGuild.roleIDs.staffRole, - allow: ['VIEW_CHANNEL'] - }, - { - id: this.botGuild.roleIDs.everyoneRole, - deny: ['VIEW_CHANNEL'] - } - ] - } - ); + // channel.guild.channels.create('mentors-general', + // { + // type: 'GUILD_TEXT', + // topic: 'Private chat between all mentors and organizers', + // parent: mentorCategory + // } + // ); + + // const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', + // { + // type: 'GUILD_TEXT', + // topic: 'Tickets from hackers will come in here!', + // parent: mentorCategory + // } + // ); + + // const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: [ + // { + // id: this.botGuild.verification.guestRoleID, + // deny: ['VIEW_CHANNEL'], + // }, + // ] + // } + // ); + + // channel.guild.channels.create('quick-questions', + // { + // type: 'GUILD_TEXT', + // topic: 'ask questions for mentors here!', + // parent: mentorHelpCategory + // } + // ); + + // const requestTicketChannel = await channel.guild.channels.create('request-ticket', + // { + // type: 'GUILD_TEXT', + // topic: 'request 1-on-1 help from mentors here!', + // parent: mentorHelpCategory, + // permissionOverwrites: [ + // { + // id: publicRole, + // allow: ['VIEW_CHANNEL'], + // deny: ['SEND_MESSAGES'] + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'] + // }, + // { + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'] + // } + // ] + // } + // ); const requestTicketEmbed = new MessageEmbed() .setTitle('Need 1:1 mentor help?') @@ -296,10 +299,10 @@ class StartMentorCave extends Command { selectMenuCollector.on('collect', async i => { if (i.customId === 'ticketType') { requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - // if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { - // await i.reply({ message: 'You do not have permissions to request tickets!', ephemeral: true }); - // return - // } + if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { + await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); + return; + } const modal = new Modal() .setCustomId('ticketSubmitModal') .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) @@ -321,16 +324,7 @@ class StartMentorCave extends Command { .setMaxLength(300) .setStyle('PARAGRAPH') .setRequired(true), - ), - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('helpFormat') - .setLabel('Would you be OK with an online mentor?') - .setMaxLength(20) - .setPlaceholder('Enter yes or no') - .setStyle('SHORT') - .setRequired(true), - ), + ) ]); await i.showModal(modal); @@ -526,12 +520,6 @@ class StartMentorCave extends Command { .setCustomId('addRole') .setLabel('Add Mentor Role') .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('deleteCave') - .setLabel('Delete Cave') - .setStyle('DANGER'), ); const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); @@ -587,18 +575,6 @@ class StartMentorCave extends Command { collected.delete(); }); - - - } else if (adminInteraction.customId === 'deleteCave') { - adminInteraction.reply({ content: 'Mentor cave deleted!', ephemeral: true }); - discordLog('Mentor cave deleted by <@' + adminInteraction.user.id + '>'); - // unknown channel error - await mentorCategory.delete(); - await mentorHelpCategory.delete(); - await mentorRoleSelectionChannel.delete(); - await incomingTicketChannel.delete(); - await requestTicketChannel.delete(); - await adminControls.delete(); } }); } catch (error) { diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 397fa105..75435f05 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -97,7 +97,7 @@ class StartVerification extends Command { if (correctTypes.length === 0) member.roles.remove(this.botGuild.verification.guestRoleID); correctTypes.push(type); } else { - discordLog(`VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); + discordLog(interaction.guild, `VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); } }); diff --git a/db/mongo/BotGuild.d.ts b/db/mongo/BotGuild.d.ts index a67bf419..828ae393 100644 --- a/db/mongo/BotGuild.d.ts +++ b/db/mongo/BotGuild.d.ts @@ -22,6 +22,9 @@ interface BotGuild extends Document { botSupportChannel: String, archiveCategory: String, botSpamChannel: String, + incomingTicketsChannel: String, + mentorRoleSelectionChannel: String, + requestTicketChannel: String }, verification: { diff --git a/db/mongo/BotGuild.js b/db/mongo/BotGuild.js index c5198fc1..f38f03ce 100644 --- a/db/mongo/BotGuild.js +++ b/db/mongo/BotGuild.js @@ -41,6 +41,15 @@ const BotGuildSchema = new Schema({ archiveCategory: { type: String, }, + incomingTicketsChannel: { + type: String, + }, + mentorRoleSelectionChannel: { + type: String, + }, + requestTicketChannel: { + type: String, + } }, verification: { diff --git a/discord-services.js b/discord-services.js index a2cba142..f91f191b 100644 --- a/discord-services.js +++ b/discord-services.js @@ -175,7 +175,7 @@ module.exports.replaceRoleToMember = replaceRoleToMember; async function discordLog(guild, message) { let botGuild = await BotGuild.findById(guild.id); if (botGuild?.channelIDs?.adminLog) { - guild.channels.cache.get(botGuild.channelIDs.adminLog)?.send(message); + guild.channels.resolve(botGuild.channelIDs.adminLog)?.send(message); winston.loggers.get(guild.id).silly(`The following was logged to discord: ${message}`); } else winston.loggers.get(guild.id).error('I was not able to log something to discord!! I could not find the botGuild or the adminLog channel!'); @@ -292,7 +292,7 @@ async function askBoolQuestion(member, botGuild, title, description, thankYouMes await message.react('👍'); const filter = (reaction, user) => { return reaction.emoji.name === '👍' && user.id != message.author.id; - } + }; const collector = message.createReactionCollector(filter, { max: 1 }); collector.on('collect', async (reaction, user) => { sendMessageToMember(member, thankYouMessage, false); From bd1a33bd52362d86ce3014429cd5069a0bb5ea4a Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 31 Oct 2023 22:50:01 -0700 Subject: [PATCH 12/67] comment out all online mentoring ticket things --- .../a_start_commands/start-mentor-cave.js | 232 +++++++++--------- 1 file changed, 122 insertions(+), 110 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 35b11cba..af3bd670 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -132,7 +132,7 @@ class StartMentorCave extends Command { // } // ); const mentorRoleSelectionChannel = guild.channels.resolve(this.botGuild.channelIDs.mentorRoleSelectionChannel); - const incomingTicketChannel = guild.channels.resolve(this.botGuild.channelIDs.incomingTicketChannel); + const incomingTicketsChannel = guild.channels.resolve(this.botGuild.channelIDs.incomingTicketsChannel); const requestTicketChannel = guild.channels.resolve(this.botGuild.channelIDs.requestTicketChannel); //TODO: allow staff to add more roles @@ -336,7 +336,7 @@ class StartMentorCave extends Command { const role = i.values[0] === 'None of the above' ? this.botGuild.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; const description = submitted.fields.getTextInputValue('ticketDescription'); const location = submitted.fields.getTextInputValue('location'); - const helpFormat = submitted.fields.getTextInputValue('helpFormat'); + // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); const ticketNumber = this.ticketCount; this.ticketCount++; const newTicketEmbed = new MessageEmbed() @@ -351,10 +351,10 @@ class StartMentorCave extends Command { name: 'Where to meet', value: location }, - { - name: 'OK with being helped online?', - value: helpFormat - } + // { + // name: 'OK with being helped online?', + // value: helpFormat + // } ]); const ticketAcceptanceRow = new MessageActionRow() .addComponents( @@ -362,15 +362,15 @@ class StartMentorCave extends Command { .setCustomId('acceptIrl') .setLabel('Accept ticket (in-person)') .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('acceptOnline') - .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') - .setStyle('PRIMARY'), ); - - const ticketMsg = await incomingTicketChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); + // .addComponents( + // new MessageButton() + // .setCustomId('acceptOnline') + // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') + // .setStyle('PRIMARY'), + // ); + + const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); const ticketReminder = setTimeout(() => { ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); @@ -378,7 +378,16 @@ class StartMentorCave extends Command { const confirmationEmbed = new MessageEmbed() .setTitle('Your ticket is number ' + ticketNumber) - .setDescription(description); + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + } + ]); const deleteTicketRow = new MessageActionRow() .addComponents( new MessageButton() @@ -409,101 +418,101 @@ class StartMentorCave extends Command { submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); } - if (acceptInteraction.customId === 'acceptOnline') { - submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); - let ticketChannelOverwrites = - [{ - id: this.botGuild.roleIDs.everyoneRole, - deny: ['VIEW_CHANNEL'], - }, - { - id: acceptInteraction.user.id, - allow: ['VIEW_CHANNEL'], - }, - { - id: submitted.user.id, - allow: ['VIEW_CHANNEL'], - }]; - - let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, - { - type: 'GUILD_CATEGORY', - permissionOverwrites: ticketChannelOverwrites - } - ); - - const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, - { - type: 'GUILD_TEXT', - parent: ticketCategory - } - ); - - const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', - { - type: 'GUILD_VOICE', - parent: ticketCategory - } - ); - - const ticketChannelEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) - .setTitle('Ticket description') - .setDescription(submitted.fields.getTextInputValue('ticketDescription')); - - const ticketChannelButtons = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('addMembers') - .setLabel('Add Members to Channels') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('leaveTicket') - .setLabel('Leave') - .setStyle('DANGER'), - ); - const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); - ticketChannelInfoMsg.pin(); - - const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); - ticketChannelCollector.on('collect', async ticketInteraction => { - if (ticketInteraction.customId === 'addMembers') { - ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) - .then(() => { - const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; - ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) - .then(async collected => { - if (collected.first().mentions.members.size === 0) { - await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); - } else { - var newMembersArray = []; - collected.first().mentions.members.forEach(member => { - ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); - newMembersArray.push(member.id); - }); - ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); - } - }) - .catch(collected => { - ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); - }); - }); - } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { - await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); - ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); - if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { - const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); - await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); - } - } else { - ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); - } - }); - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - } + // if (acceptInteraction.customId === 'acceptOnline') { + // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); + // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); + // let ticketChannelOverwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: acceptInteraction.user.id, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: submitted.user.id, + // allow: ['VIEW_CHANNEL'], + // }]; + + // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: ticketChannelOverwrites + // } + // ); + + // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, + // { + // type: 'GUILD_TEXT', + // parent: ticketCategory + // } + // ); + + // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', + // { + // type: 'GUILD_VOICE', + // parent: ticketCategory + // } + // ); + + // const ticketChannelEmbed = new MessageEmbed() + // .setColor(this.botGuild.colors.embedColor) + // .setTitle('Ticket description') + // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); + + // const ticketChannelButtons = new MessageActionRow() + // .addComponents( + // new MessageButton() + // .setCustomId('addMembers') + // .setLabel('Add Members to Channels') + // .setStyle('PRIMARY'), + // ) + // .addComponents( + // new MessageButton() + // .setCustomId('leaveTicket') + // .setLabel('Leave') + // .setStyle('DANGER'), + // ); + // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); + // ticketChannelInfoMsg.pin(); + + // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); + // ticketChannelCollector.on('collect', async ticketInteraction => { + // if (ticketInteraction.customId === 'addMembers') { + // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) + // .then(() => { + // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; + // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) + // .then(async collected => { + // if (collected.first().mentions.members.size === 0) { + // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); + // } else { + // var newMembersArray = []; + // collected.first().mentions.members.forEach(member => { + // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); + // newMembersArray.push(member.id); + // }); + // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); + // } + // }) + // .catch(collected => { + // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); + // }); + // }); + // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { + // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); + // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); + // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { + // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); + // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); + // } + // } else { + // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); + // } + // }); + // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + // } }); } @@ -554,6 +563,9 @@ class StartMentorCave extends Command { } else { emojiCollector.stop(); emojisMap.set(collected.emoji.name, roleName); + adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); roleSelectionMsg.react(collected.emoji.name); From 38cbf7e752858c5af70dde274df6e6123d879be5 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Wed, 1 Nov 2023 07:20:11 -0700 Subject: [PATCH 13/67] schema overhaul and minor improvements --- classes/Bot/bot-guild.d.ts | 61 +--- classes/Bot/bot-guild.js | 343 +++--------------- .../a_start_commands/start-mentor-cave.js | 2 +- commands/a_utility/pronouns.js | 10 +- commands/verification/start-verification.js | 21 +- db/mongo/BotGuild.d.ts | 79 +--- db/mongo/BotGuild.js | 126 ++----- 7 files changed, 122 insertions(+), 520 deletions(-) diff --git a/classes/Bot/bot-guild.d.ts b/classes/Bot/bot-guild.d.ts index 92f56a01..faea5de5 100644 --- a/classes/Bot/bot-guild.d.ts +++ b/classes/Bot/bot-guild.d.ts @@ -18,8 +18,6 @@ interface BotGuild extends Document { channelIDs: { adminConsole: String, adminLog: String, - botSupportChannel: String, - archiveCategory: String, botSpamChannel: String, incomingTicketsChannel: String, mentorRoleSelectionChannel: String, @@ -30,18 +28,11 @@ interface BotGuild extends Document { /** @required */ isEnabled: Boolean, guestRoleID: String, - welcomeChannelID: String, welcomeSupportChannelID: String, /** */ verificationRoles: Map }, - attendance: { - /** @required */ - isEnabled: Boolean, - attendeeRoleID: String, - }, - stamps: { /** @required */ isEnabled: Boolean, @@ -52,51 +43,8 @@ interface BotGuild extends Document { stampCollectionTime: Number, }, - report: { - /** @required */ - isEnabled: Boolean, - incomingReportChannelID: String, - }, - - announcement: { - /** @required */ - isEnabled: Boolean, - announcementChannelID: String, - }, - - ask: { - /** @required */ - isEnabled: Boolean, - }, - - /** - * This is some text - */ - blackList: Map, - - /** - * The caves created in this guild. - */ - caves: Map, - - /** - * An object with some nice colors to use! - */ - colors: { - /** @hexColor */ - embedColor: String, - /** @hexColor */ - questionEmbedColor: String, - /** @hexColor */ - announcementEmbedColor: String, - /** @hexColor */ - tfTeamEmbedColor: String, - /** @hexColor */ - tfHackerEmbedColor: String, - /** @hexColor */ - specialDMEmbedColor: String, - }, - + /** @hexColor */ + embedColor: String, /** * The botGuild id must equal the guild id * @required @@ -108,11 +56,6 @@ interface BotGuild extends Document { */ isSetUpComplete: Boolean, - /** - * The prefix used by the bot in this guild. - */ - prefix: String, - /** * Will set the minimum required information for the bot to work on this guild. * @param {BotGuildInfo} botGuildInfo diff --git a/classes/Bot/bot-guild.js b/classes/Bot/bot-guild.js index 1bc1df76..600a2152 100644 --- a/classes/Bot/bot-guild.js +++ b/classes/Bot/bot-guild.js @@ -14,7 +14,7 @@ class BotGuild { * Staff role permissions. * @type {String[]} */ - static staffPermissions = ['VIEW_CHANNEL', 'MANAGE_EMOJIS', 'CHANGE_NICKNAME', 'MANAGE_NICKNAMES', + static staffPermissions = ['VIEW_CHANNEL', 'CHANGE_NICKNAME', 'MANAGE_NICKNAMES', 'KICK_MEMBERS', 'BAN_MEMBERS', 'SEND_MESSAGES', 'EMBED_LINKS', 'ATTACH_FILES', 'ADD_REACTIONS', 'USE_EXTERNAL_EMOJIS', 'MANAGE_MESSAGES', 'READ_MESSAGE_HISTORY', 'CONNECT', 'STREAM', 'SPEAK', 'PRIORITY_SPEAKER', 'USE_VAD', 'MUTE_MEMBERS', 'DEAFEN_MEMBERS', 'MOVE_MEMBERS']; @@ -87,19 +87,6 @@ class BotGuild { * @property {ChannelIDs} channelIDs */ - /** - * Validate the information. - * @param {BotGuildInfo} botGuildInfo - the information to validate - * @throws Error if the botGuildInfo is incomplete - */ - validateBotGuildInfo(botGuildInfo) { - if (typeof botGuildInfo != 'object') throw new Error('The bot guild information is required!'); - if (!botGuildInfo?.roleIDs || !botGuildInfo?.roleIDs?.adminRole || !botGuildInfo?.roleIDs?.everyoneRole - || !botGuildInfo?.roleIDs?.memberRole || !botGuildInfo?.roleIDs?.staffRole) throw new Error('All the role IDs are required!'); - if (!botGuildInfo?.channelIDs || !botGuildInfo?.channelIDs?.adminConsole || !botGuildInfo?.channelIDs?.adminLog - || !botGuildInfo?.channelIDs?.botSupportChannel) throw new Error('All the channel IDs are required!'); - } - /** * Will set the minimum required information for the bot to work on this guild. * @param {BotGuildInfo} botGuildInfo @@ -107,15 +94,13 @@ class BotGuild { * @returns {Promise} * @async */ - async readyUp(client, botGuildInfo) { - this.validateBotGuildInfo(botGuildInfo); - + async readyUp(guild, botGuildInfo) { this.roleIDs = botGuildInfo.roleIDs; this.channelIDs = botGuildInfo.channelIDs; + this.embedColor = botGuildInfo.embedColor; - let guild = await client.guilds.fetch(this._id); - - let adminRole = await guild.roles.fetch(this.roleIDs.adminRole); + this.verification = botGuildInfo.verification; + let adminRole = await guild.roles.resolve(this.roleIDs.adminRole); // try giving the admins administrator perms try { if (!adminRole.permissions.has('ADMINISTRATOR')) @@ -128,22 +113,24 @@ class BotGuild { } // staff role set up - let staffRole = await guild.roles.fetch(this.roleIDs.staffRole); + let staffRole = await guild.roles.resolve(this.roleIDs.staffRole); staffRole.setMentionable(true); staffRole.setHoist(true); staffRole.setPermissions(staffRole.permissions.add(BotGuild.staffPermissions)); // regular member role setup - let memberRole = await guild.roles.fetch(this.roleIDs.memberRole); - memberRole.setMentionable(false); + let memberRole = await guild.roles.resolve(this.roleIDs.memberRole); + memberRole.setMentionable(true); memberRole.setPermissions(memberRole.permissions.add(BotGuild.memberPermissions)); + // mentor role setup + let mentorRole = await guild.roles.resolve(this.roleIDs.mentorRole); + mentorRole.setMentionable(true); + mentorRole.setPermissions(mentorRole.permissions.add(BotGuild.memberPermissions)); + // change the everyone role permissions, we do this so that we can lock rooms. For users to see the server when // verification is off, they need to get the member role when greeted by the bot! - guild.roles.everyone.setPermissions(0); // no permissions for anything like the guest role - - // create the archive category - this.channelIDs.archiveCategory = (await this.createArchiveCategory(guild)).id; + // guild.roles.everyone.setPermissions(0); // no permissions for anything like the guest role this.isSetUpComplete = true; @@ -152,79 +139,6 @@ class BotGuild { return this; } - /** - * Creates the archive category. - * @returns {Promise} - * @param {Guild} guild - * @private - * @async - */ - async createArchiveCategory(guild) { - let overwrites = [ - { - id: this.roleIDs.everyoneRole, - deny: ['VIEW_CHANNEL'] - }, - { - id: this.roleIDs.memberRole, - allow: ['VIEW_CHANNEL'], - }, - { - id: this.roleIDs.staffRole, - allow: ['VIEW_CHANNEL'], - } - ]; - - // position is used to create archive at the very bottom! - var position = (guild.channels.cache.filter(channel => channel.type === 'category')).size; - return await guild.channels.create('💼archive', { - type: 'category', - position: position + 1, - permissionOverwrites: overwrites, - }); - } - - /** - * Will create the admin channels with the correct roles. - * @param {Guild} guild - * @param {Role} adminRole - * @param {Role} everyoneRole - * @returns {Promise<{TextChannel, TextChannel}>} - {Admin Console, Admin Log Channel} - * @static - * @async - */ - static async createAdminChannels(guild, adminRole, everyoneRole) { - let adminCategory = await guild.channels.create('Admins', { - type: 'category', - permissionOverwrites: [ - { - id: adminRole.id, - allow: 'VIEW_CHANNEL' - }, - { - id: everyoneRole.id, - deny: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'CONNECT'] - } - ] - }); - - let adminConsoleChannel = await guild.channels.create('console', { - type: 'text', - parent: adminCategory, - }); - - let adminLogChannel = await guild.channels.create('logs', { - type: 'text', - parent: adminCategory, - }); - - adminCategory.children.forEach(channel => channel.lockPermissions()); - - winston.loggers.get(guild.id).event(`The botGuild has run the create admin channels function.`, {event: "Bot Guild"}); - - return {adminConsoleChannel: adminConsoleChannel, adminLog: adminLogChannel}; - } - /** * @typedef VerificationChannels * @property {String} welcomeChannelID @@ -246,139 +160,28 @@ class BotGuild { * @return {Promise} * @async */ - async setUpVerification(client, guestRoleId, types, verificationChannels = null) { - /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); - - try { - var guestRole = await guild.roles.fetch(guestRoleId); - } catch (error) { - throw new Error('The given guest role ID is not valid for this guild!'); - } - guestRole.setMentionable(false); - guestRole.setPermissions(0); + async setUpVerification(guild, guestRoleId, types, welcomeSupportChannel) { + // guestRole.setMentionable(false); + // guestRole.setPermissions(0); this.verification.guestRoleID = guestRoleId; - if (verificationChannels) { - this.verification.welcomeChannelID = verificationChannels.welcomeChannelID; - this.verification.welcomeSupportChannelID = verificationChannels.welcomeChannelSupportID; - - /** @type {TextChannel} */ - var welcomeChannel = guild.channels.resolve(this.verification.welcomeChannelID); - await welcomeChannel.bulkDelete(100, true); - } else { - let welcomeCategory = await guild.channels.create('Welcome', { - type: 'category', - permissionOverwrites: [ - { - id: this.roleIDs.everyoneRole, - allow: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'] - }, - { - id: this.roleIDs.memberRole, - deny: ['VIEW_CHANNEL', 'SEND_MESSAGES', 'READ_MESSAGE_HISTORY'] - }, - ], - }); - - var welcomeChannel = await guild.channels.create('welcome', { - type: 'text', - parent: welcomeCategory, - }); - - // update welcome channel to not send messages - welcomeChannel.updateOverwrite(this.roleIDs.everyoneRole, { - SEND_MESSAGES: false, - }); - - let welcomeChannelSupport = await guild.channels.create('welcome-support', { - type: 'text', - parent: welcomeCategory, - }); - - this.verification.welcomeChannelID = welcomeChannel.id; - this.verification.welcomeSupportChannelID = welcomeChannelSupport.id; - } + this.verification.welcomeSupportChannelID = welcomeSupportChannel; // add the types to the type map. - types.forEach((type, index, list) => { - this.verification.verificationRoles.set(type.type.toLowerCase(), type.roleId); + types.forEach(async (type, index, list) => { + try { + await guild.roles.resolve(type.roleId); + this.verification.verificationRoles.set(type.name.toLowerCase(), type.roleId); + } catch (error) { + throw new Error('The given verification role ID is not valid for this guild!'); + } }); - - const embed = new MessageEmbed().setTitle('Welcome to the ' + guild.name + ' Discord server!') - .setDescription('In order to verify that you have registered for ' + guild.name + ', please respond to the bot (me) via DM!') - .addField('Do you need assistance?', 'Head over to the welcome-support channel and ping the admins!') - .setColor(this.colors.embedColor); - welcomeChannel.send(embed).then(msg => msg.pin()); - this.verification.isEnabled = true; - guild.setCommandEnabled('verify', true); - - winston.loggers.get(this._id).event(`The botGuild has set up the verification system. Verification channels ${verificationChannels === null ? 'were created' : 'were given'}. - Guest role id: ${guestRoleId}. The types used are: ${types.join()}`, {event: "Bot Guild"}); + // guild.setCommandEnabled('verify', true); return this; } - /** - * Sets up the attendance functionality. - * @param {SapphireClient} client - * @param {String} attendeeRoleID - * @returns {Promise} - * @async - */ - async setUpAttendance(client, attendeeRoleID) { - this.attendance.attendeeRoleID = attendeeRoleID; - this.attendance.isEnabled = true; - /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); - guild.setCommandEnabled('start-attend', true); - guild.setCommandEnabled('attend', true); - - winston.loggers.get(this._id).event(`The botGuild has set up the attendance functionality. Attendee role id is ${attendeeRoleID}`, {event: "Bot Guild"}); - return this; - } - - /** - * Will set up the firebase announcements. - * @param {SapphireClient} client - * @param {String} announcementChannelID - * @async - */ - async setUpAnnouncements(client, announcementChannelID) { - /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); - - let announcementChannel = guild.channels.resolve(announcementChannelID); - if (!announcementChannel) throw new Error('The announcement channel ID is not valid for this guild!'); - - this.announcement.isEnabled = true; - this.announcement.announcementChannelID = announcementChannelID; - - winston.loggers.get(this._id).event(`The botGuild has set up the announcement functionality with the channel ID ${announcementChannelID}`, {event: "Bot Guild"}); - - // TODO Firebase setup - // start query listener for announcements - // nwFirebase.firestore().collection('Hackathons').doc('nwHacks2021').collection('Announcements').onSnapshot(querySnapshot => { - // // exit if we are at the initial state - // if (isInitState) { - // isInitState = false; - // return; - // } - - // querySnapshot.docChanges().forEach(change => { - // if (change.type === 'added') { - // const embed = new Discord.MessageEmbed() - // .setColor(botGuild.colors.announcementEmbedColor) - // .setTitle('Announcement') - // .setDescription(change.doc.data()['content']); - - // announcementChannel.send('<@&' + discordServices.roleIDs.attendeeRole + '>', { embed: embed }); - // } - // }); - // }); - } - /** * Creates the stamps roles and adds them to this BotGuild. If stamps roles are given * then no roles are created! @@ -389,36 +192,36 @@ class BotGuild { * @returns {Promise} * @async */ - async setUpStamps(client, stampAmount = 0, stampCollectionTime = 60, stampRoleIDs = []) { - let guild = await client.guilds.fetch(this._id); - - if (stampRoleIDs.length > 0) { - stampRoleIDs.forEach((ID, index, array) => { - this.addStamp(ID, index); - }); - winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. The stamp roles were given. Stamp collection time is set at ${stampCollectionTime}.` [{stampIds: stampRoleIDs}]); - } else { - for (let i = 0; i < stampAmount; i++) { - let role = await guild.roles.create({ - data: { - name: 'Stamp Role #' + i, - hoist: true, - color: discordServices.randomColor(), - } - }); - - this.addStamp(role.id, i); - } - winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. Stamps were created by me, I created ${stampAmount} stamps. Stamp collection time is set at ${stampCollectionTime}.`, {event: "Bot Guild"}); - } - - this.stamps.stampCollectionTime = stampCollectionTime; - this.stamps.isEnabled = true; - - // this.setCommandStatus(client); - - return this; - } + // async setUpStamps(client, stampAmount = 0, stampCollectionTime = 60, stampRoleIDs = []) { + // let guild = await client.guilds.resolve(this._id); + + // if (stampRoleIDs.length > 0) { + // stampRoleIDs.forEach((ID, index, array) => { + // this.addStamp(ID, index); + // }); + // winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. The stamp roles were given. Stamp collection time is set at ${stampCollectionTime}.` [{stampIds: stampRoleIDs}]); + // } else { + // for (let i = 0; i < stampAmount; i++) { + // let role = await guild.roles.create({ + // data: { + // name: 'Stamp Role #' + i, + // hoist: true, + // color: discordServices.randomColor(), + // } + // }); + + // this.addStamp(role.id, i); + // } + // winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. Stamps were created by me, I created ${stampAmount} stamps. Stamp collection time is set at ${stampCollectionTime}.`, {event: "Bot Guild"}); + // } + + // this.stamps.stampCollectionTime = stampCollectionTime; + // this.stamps.isEnabled = true; + + // // this.setCommandStatus(client); + + // return this; + // } /** * Adds a stamp to the stamp collection. Does not save the mongoose document! @@ -431,38 +234,6 @@ class BotGuild { winston.loggers.get(this._id).event(`The botGuild has added a stamp with number ${stampNumber} linked to role id ${roleId}`, {event: "Bot Guild"}); } - /** - * Enables the report commands and sends the reports to the given channel. - * @param {SapphireClient} client - * @param {String} incomingReportChannelID - * @returns {Promise} - * @async - */ - async setUpReport(client, incomingReportChannelID) { - /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); - - this.report.isEnabled = true; - this.report.incomingReportChannelID = incomingReportChannelID; - - guild.setCommandEnabled('report', true); - - winston.loggers.get(this._id).event(`The botGuild has set up the report functionality. It will send reports to the channel id ${incomingReportChannelID}`, {event: "Bot Guild"}); - return this; - } - - /** - * Will enable the ask command. - * @param {SapphireClient} client - */ - async setUpAsk(client) { - /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); - this.ask.isEnabled = true; - guild.setCommandEnabled('ask', true); - winston.loggers.get(this._id).event(`The botGuild has enabled the ask command!`, {event: "Bot Guild"}); - } - /** * Will enable and disable the appropriate commands by looking at what is enabled in the botGuild. * @param {SapphireClient} client @@ -470,7 +241,7 @@ class BotGuild { */ async setCommandStatus(client) { /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.fetch(this._id); + let guild = await client.guilds.resolve(this._id); // guild.setGroupEnabled('verification', this.verification.isEnabled); // guild.setGroupEnabled('attendance', this.attendance.isEnabled); diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index af3bd670..608c8c71 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -68,7 +68,7 @@ class StartMentorCave extends Command { // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); const publicRole = interaction.options.getRole('request_ticket_role'); const inactivePeriod = interaction.options.getInteger('inactivity_time'); - const bufferTime = inactivePeriod / 2; + // const bufferTime = inactivePeriod / 2; const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 856484ec..c8022271 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -1,5 +1,6 @@ const { Command } = require('@sapphire/framework'); -const { Interaction, MessageEmbed } = require('discord.js'); +const { Interaction, MessageEmbed, PermissionFlagsBits } = require('discord.js'); +const BotGuild = require('../../db/mongo/BotGuild'); /** * The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: @@ -36,6 +37,13 @@ class Pronouns extends Command { * @param {Interaction} interaction */ async chatInputRun(interaction) { + const guild = interaction.guild; + this.botGuild = await BotGuild.findById(guild.id); + const userId = interaction.user.id; + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } const sheRole = interaction.guild.roles.cache.find(role => role.name === 'she/her'); const heRole = interaction.guild.roles.cache.find(role => role.name === 'he/him'); const theyRole = interaction.guild.roles.cache.find(role => role.name === 'they/them'); diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 75435f05..f4e608a5 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -30,6 +30,16 @@ class StartVerification extends Command { */ async chatInputRun(interaction) { this.botGuild = await BotGuild.findById(interaction.guild.id); + const guild = interaction.guild; + const userId = interaction.user.id; + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + if (!this.botGuild.verification.isEnabled) { + await interaction.reply({ content: 'Verification has not been enabled!', ephemeral: true}); + return; + } const embed = new MessageEmbed() .setTitle(`Please click the button below to check-in to the ${interaction.guild.name} server! Make sure you know which email you used to apply to ${interaction.guild.name}!`); @@ -42,7 +52,7 @@ class StartVerification extends Command { .setStyle('PRIMARY'), ); interaction.reply({ content: 'Verification started!', ephemeral: true }); - const msg = await interaction.channel.send({ content: 'If you have not already, make sure to enable emojis and embeds/link previews in your personal Discord settings! If you have any issues, please find an organizer!', embeds: [embed], components: [row] }); + const msg = await interaction.channel.send({ content: 'If you have not already, make sure to enable DMs, emojis, and embeds/link previews in your personal Discord settings! If you have any issues, please find an organizer!', embeds: [embed], components: [row] }); const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); checkInCollector.on('collect', async i => { @@ -92,7 +102,14 @@ class StartVerification extends Command { types.forEach(type => { if (this.botGuild.verification.verificationRoles.has(type)) { const member = interaction.guild.members.cache.get(submitted.user.id); - let roleId = this.botGuild.verification.verificationRoles.get(type); + let roleId; + if (type === 'staff') { + roleId = this.botGuild.roleIDs.staffRole; + } else if (type === 'mentor') { + roleId = this.botGuild.roleIDs.mentorRole; + } else { + roleId = this.botGuild.verification.verificationRoles.get(type); + } member.roles.add(roleId); if (correctTypes.length === 0) member.roles.remove(this.botGuild.verification.guestRoleID); correctTypes.push(type); diff --git a/db/mongo/BotGuild.d.ts b/db/mongo/BotGuild.d.ts index 828ae393..d5d6cc3a 100644 --- a/db/mongo/BotGuild.d.ts +++ b/db/mongo/BotGuild.d.ts @@ -37,67 +37,19 @@ interface BotGuild extends Document { verificationRoles: Map }, - attendance: { - /** @required */ - isEnabled: Boolean, - attendeeRoleID: String, - }, - - stamps: { - /** @required */ - isEnabled: Boolean, - /** */ - stampRoleIDs: Map, - /** The first stamp role Id given to all users */ - stamp0thRoleId: String, - stampCollectionTime: Number, - }, - - report: { - /** @required */ - isEnabled: Boolean, - incomingReportChannelID: String, - }, - - announcement: { - /** @required */ - isEnabled: Boolean, - announcementChannelID: String, - }, - - ask: { - /** @required */ - isEnabled: Boolean, - }, - - /** - * This is some text - */ - blackList: Map, - - /** - * The caves created in this guild. - */ - caves: Map, - - /** - * An object with some nice colors to use! - */ - colors: { - /** @hexColor */ - embedColor: String, - /** @hexColor */ - questionEmbedColor: String, - /** @hexColor */ - announcementEmbedColor: String, - /** @hexColor */ - tfTeamEmbedColor: String, - /** @hexColor */ - tfHackerEmbedColor: String, - /** @hexColor */ - specialDMEmbedColor: String, - }, - + // stamps: { + // /** @required */ + // isEnabled: Boolean, + // /** */ + // stampRoleIDs: Map, + // /** The first stamp role Id given to all users */ + // stamp0thRoleId: String, + // stampCollectionTime: Number, + // }, + + /** @hexColor */ + embedColor: String, + /** * The botGuild id must equal the guild id * @required @@ -109,11 +61,6 @@ interface BotGuild extends Document { */ isSetUpComplete: Boolean, - /** - * The prefix used by the bot in this guild. - */ - prefix: String, - /** * Will set the minimum required information for the bot to work on this guild. * @param {BotGuildInfo} botGuildInfo diff --git a/db/mongo/BotGuild.js b/db/mongo/BotGuild.js index f38f03ce..f140074b 100644 --- a/db/mongo/BotGuild.js +++ b/db/mongo/BotGuild.js @@ -35,12 +35,6 @@ const BotGuildSchema = new Schema({ botSpamChannel: { type: String, }, - botSupportChannel: { - type: String, - }, - archiveCategory: { - type: String, - }, incomingTicketsChannel: { type: String, }, @@ -60,9 +54,6 @@ const BotGuildSchema = new Schema({ guestRoleID: { type: String, }, - welcomeChannelID: { - type: String, - }, welcomeSupportChannelID: { type: String, }, @@ -72,96 +63,27 @@ const BotGuildSchema = new Schema({ } }, - attendance: { - isEnabled: { - type: Boolean, - default: false, - }, - attendeeRoleID: { - type: String, - }, - }, - - stamps: { - isEnabled: { - type: Boolean, - default: false, - }, - stampRoleIDs: { - type: Map, - default: new Map(), - }, - stamp0thRoleId: { - type: String, - }, - stampCollectionTime: { - type: Number, - default: 60, - }, - }, - - report: { - isEnabled: { - type: Boolean, - default: false, - }, - incomingReportChannelID: { - type: String, - }, - }, - - announcement: { - isEnabled: { - type: Boolean, - default: false, - }, - announcementChannelID: { - type: String, - }, - }, - - ask: { - isEnabled: { - type: Boolean, - default: false, - }, - }, - - blackList: { - type: Map, - default: new Map(), - }, - - caves: { - type: Map, - default: new Map(), - }, - - colors: { - embedColor: { - type: String, - default: '#26fff4', - }, - questionEmbedColor: { - type: String, - default: '#f4ff26', - }, - announcementEmbedColor: { - type: String, - default: '#9352d9', - }, - tfTeamEmbedColor: { - type: String, - default: '#60c2e6', - }, - tfHackerEmbedColor: { - type: String, - default: '#d470cd', - }, - specialDMEmbedColor: { - type: String, - default: '#fc6b03', - }, + // stamps: { + // isEnabled: { + // type: Boolean, + // default: false, + // }, + // stampRoleIDs: { + // type: Map, + // default: new Map(), + // }, + // stamp0thRoleId: { + // type: String, + // }, + // stampCollectionTime: { + // type: Number, + // default: 60, + // }, + // }, + + embedColor: { + type: String, + default: '#26fff4', }, _id: { @@ -173,12 +95,6 @@ const BotGuildSchema = new Schema({ type: Boolean, default: false, }, - - prefix: { - type: String, - default: '!', - required: true, - }, }); BotGuildSchema.loadClass(BotGuildClass); From 4afdad470d711c92f5a65f8f75f67f1a5081a1ab Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Wed, 1 Nov 2023 07:21:03 -0700 Subject: [PATCH 14/67] init bot update to api 9 WIP WHY DID IT SUDDENLY STOP WORKING --- commands/essentials/init-bot.js | 418 +++++++++++--------------------- 1 file changed, 144 insertions(+), 274 deletions(-) diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index bc6a0860..6efe52b6 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -1,9 +1,9 @@ -const { Command, CommandoGuild } = require('discord.js-commando'); -const { Message, TextChannel, Snowflake, Guild, ColorResolvable, Role, Permissions } = require('discord.js'); -const { sendMsgToChannel, addRoleToMember, } = require('../../discord-services'); +const { Command } = require('@sapphire/framework'); +const { TextChannel, Snowflake, Guild, ColorResolvable, Role, Permissions, PermissionsBitField, PermissionFlagsBits } = require('discord.js'); +const { sendMsgToChannel, addRoleToMember, discordLog, } = require('../../discord-services'); const BotGuild = require('../../db/mongo/BotGuild'); const winston = require('winston'); -const Console = require('../../classes/UI/Console/console'); +const fetch = require('node-fetch'); const { MessagePrompt, StringPrompt, NumberPrompt, SpecialPrompt, RolePrompt, ChannelPrompt } = require('advanced-discord.js-prompts'); /** @@ -14,244 +14,177 @@ const { MessagePrompt, StringPrompt, NumberPrompt, SpecialPrompt, RolePrompt, Ch * @extends Command */ class InitBot extends Command { - constructor(client) { - super(client, { - name: 'init-bot', - group: 'essentials', - memberName: 'initialize the bot', - description: 'Will start the bot given some information.', - hidden: true, - args: [] + constructor(context, options) { + super(context, { + ...options, + description: 'Configurations for this guild.' }); } + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName('init-bot') + .setDescription(this.description) + .addChannelOption(option => + option.setName('admin_console') + .setDescription('Mention the admin console channel') + .setRequired(true)) + .addRoleOption(option => + option.setName('admin') + .setDescription('Mention the admin role') + .setRequired(true)) + .addChannelOption(option => + option.setName('admin_log') + .setDescription('Mention the admin log channel') + .setRequired(true)) + .addRoleOption(option => + option.setName('staff') + .setDescription('Mention the staff role') + .setRequired(true)) + .addRoleOption(option => + option.setName('member') + .setDescription('Mention the member (general participant) role') + .setRequired(true)) + .addRoleOption(option => + option.setName('mentor') + .setDescription('Mention the mentor role') + .setRequired(true)) + .addBooleanOption(option => + option.setName('use_verification') + .setDescription('Whether verification will be used') + .setRequired(true)) + .addRoleOption(option => + option.setName('guest') + .setDescription('Mention the guest role **if verification is on**') + .setRequired(false)) + .addChannelOption(option => + option.setName('welcome_support_channel') + .setDescription('Mention the channel for verification issues (must be viewable to guests) **if verification is on**!') + .setRequired(false)) + .addAttachmentOption(option => + option.setName('verification_roles') + .setDescription('File: array of objects! Each role string in the participants\' database and corresponding role ID **if verification is on**.')) + // .addBooleanOption(option => + // option.setName('use_stamps') + // .setDescription('Whether stamps will be used') + // .setRequired(true)) + // .addIntegerOption(option => + // option.setName('number_of_stamps') + // .setDescription('Number of stamps **if stamps is on**') + // .setRequired(false)) + // .addIntegerOption(option => + // option.setName('stamp_time') + // .setDescription('Time, in seconds, each stamp is open for claiming **if stamps is on**') + // .setRequired(false)) + // .addRoleOption(option => + // option.setName('0th_stamp_role') + // .setDescription('Mention the starting stamp role **if stamps is on**') + // .setRequired(false)) + .addStringOption(option => + option.setName('embed_colour') + .setDescription('Hex code of embed colour') + .setRequired(false)) + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) + ), + { + idHints: 1051737348502728764 + }; + } + /** * @param {Message} message */ - async run(message) { - message.delete(); + async chatInputRun(interaction) { // easy constants to use - var channel = message.channel; - const userId = message.author.id; + var channel = interaction.channel; + const userId = interaction.user.id; /** @type {CommandoGuild} */ - const guild = message.guild; - const everyoneRole = message.guild.roles.everyone; + const guild = interaction.guild; + const everyoneRole = interaction.guild.roles.everyone; const botGuild = await BotGuild.findById(guild.id); // make sure the user had manage server permission - if (!message.member.hasPermission('MANAGE_GUILD')) { - message.reply('Only admins can use this command!').then(msg => msg.delete({ timeout: 5000 })); + if (!interaction.member.permissionsIn(interaction.channel).has('ADMINISTRATOR')) { + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; } if (botGuild?.isSetUpComplete) { - sendMsgToChannel(channel, userId, 'This guild is already set up!!', 30); + await interaction.reply({ content: 'This server is already set up!', ephemeral: true }); return; } - let initialMsg = await sendMsgToChannel(channel, userId, `This is the Factotum Bot Initialization procedure. - First, I will ask two short questions. - Then, we will move over to another channel and continue the initialization process. - PLEASE read every question carefully! All the questions will end with instructions on how to respond. - This is only the initial setup. You will be able to change things in the future!`); - - await new Promise((resolve) => setTimeout(resolve, 15000)); - - // grab the admin role - const adminRole = await this.askOrCreate('admin', channel, userId, guild, '#008369'); - addRoleToMember(message.member, adminRole); - - // create the admin channel room - let { adminConsoleChannel, adminLog } = await BotGuild.createAdminChannels(guild, adminRole, everyoneRole); - await sendMsgToChannel(channel, userId, 'The admin channels have been created successfully! <#' + adminConsoleChannel.id + '>. Lets jump over there and continue yes?!', 10); - - initialMsg.delete(); - - // transition to the admin console - channel = adminConsoleChannel; - await sendMsgToChannel(channel, userId, 'I am over here!!! Lets continue!'); - - const adminConsole = new Console({ - title: 'Factotum Admin Console', - description: 'In this console you will be able to edit bot information.', - channel: adminConsoleChannel, - guild: guild, - }); - adminConsole.addField('Role Changes', 'You are free to change role colors and role names! However, stamp role numbers can NOT be changed. You can add permissions but don\'t remove any!'); - adminConsole.sendConsole(); + const adminConsole = interaction.options.getChannel('admin_console'); + const admin = interaction.options.getRole('admin'); + const adminLog = interaction.options.getChannel('admin_log'); + const staff = interaction.options.getRole('staff'); + const member = interaction.options.getRole('member'); + const mentor = interaction.options.getRole('mentor'); + const useVerification = interaction.options.getBoolean('use_verification'); + let guest; + let welcomeSupportChannel; + let verificationRoles; + const verification = { + isEnabled: useVerification + }; + if (useVerification) { + guest = interaction.options.getRole('guest'); + welcomeSupportChannel = interaction.options.getChannel('welcome_support_channel'); + verificationRoles = interaction.options.getAttachment('verification_roles'); + console.log(verificationRoles.url); + try { + const response = await fetch(verificationRoles.url); + console.log('response' + response); + let res = await response.json(); + console.log(res); + // botGuild.setUpVerification(guild, guest, res, welcomeSupportChannel); + } catch (error) { + console.error('error: ' + error); + interaction.reply({ content: error, ephemeral: true}); + } + } + // const useStamps = interaction.options.getBoolean('use_stamps'); + // let numberOfStamps; + // let stampTime; + // let firstStampRole; + // const stamps = { + // isEnabled: useStamps + // }; + // if (useStamps) { + // numberOfStamps = interaction.options.getInteger('number_of_stamps'); + // stampTime = interaction.options.getInteger('stamp_time'); + // firstStampRole = interaction.options.getInteger('0th_stamp_role'); + // botGuild.setUpStamps(this.client, numberOfStamps, stampTime, firstStampRole); + // } + const embedColor = interaction.options.getString('embed_colour'); // ask the user to move our role up the list - await sendMsgToChannel(channel, userId, `Before we move on, could you please move my role up the role list as high as possible, this will give me the ability to assign roles! - I will wait for 15 seconds but feel free to take as long as you need! - You can learn more here: https://support.discord.com/hc/en-us/articles/214836687-Role-Management-101`, 30); - await new Promise((resolve) => setTimeout(resolve, 15000)); - - // grab the staff role - const staffRole = await this.askOrCreate('staff', channel, userId, guild, '#00D166'); - adminConsole.addField('The staff role:', `<@&${staffRole.id}>`); - - // get the regular member, this role will have the general member permissions - const memberRole = await this.askOrCreate('member', channel, userId, guild, '#006798'); - adminConsole.addField('The member role:', `<@&${memberRole.id}>`); - - // bot support channel prompt - let botSupportChannel = await ChannelPrompt.single({ prompt: 'What channel can the bot use to contact users when DMs are not available?', channel, userId, cancelable: false }); - adminConsole.addField('Channel used to contact Users with DM issues', `<#${botSupportChannel.id}>`); + await interaction.reply({content: 'Before we move on, could you please move my role up the role list as high as possible, this will give me the ability to assign roles!', ephemeral: true}); - botGuild.readyUp(this.client, { + await botGuild.readyUp(guild, { + verification, + embedColor, roleIDs: { - adminRole: adminRole.id, - staffRole: staffRole.id, + adminRole: admin.id, + staffRole: staff.id, everyoneRole: everyoneRole.id, - memberRole: memberRole.id, + memberRole: member.id, + mentorRole: mentor.id }, channelIDs: { adminLog: adminLog.id, - adminConsole: adminConsoleChannel.id, - botSupportChannel: botSupportChannel.id, + adminConsole: adminConsole.id } }); - // ask if verification will be used - var isVerification; - try { - isVerification = await SpecialPrompt.boolean({ prompt: 'Will you be using the verification service?', channel, userId, cancelable: true }); - } catch (error) { - winston.loggers.get(guild.id).warning(`Handled an error when setting up verification, and thus was not set up. Error was ${error.name}`, { event: 'InitBot Command', data: error }); - isVerification = false; - } - - if (isVerification) { - // ask for guest role - var guestRole = await this.askOrCreate('guest', channel, userId, guild, '#969C9F'); - - let infoMsg = await sendMsgToChannel(channel, userId, 'I need to know what types to verify when a user tries to verify. Please follow the instructions, it will let you add as many types as you wish.'); - - let types = await this.getVerificationTypes(channel, userId, guestRole, memberRole); - infoMsg.delete(); - await botGuild.setUpVerification(this.client, guestRole.id, types); - - sendMsgToChannel(channel, userId, 'The verification service has been set up correctly!', 8); - let typeListString = types.map(typeInfo => `${typeInfo.type} -> <@&${typeInfo.roleId}>`).join(', '); - adminConsole.addField('Verification Feature', `IS ENABLED!\n Guest Role: <@&${guestRole.id}>\nTypes: ${typeListString}`); - } else { - sendMsgToChannel(channel, userId, 'Verification service was not set due to Prompt cancellation.', 8); - adminConsole.addField('Verification Feature', 'IS NOT ENABLED!'); - } - - // only do attendance if verification is on! - if (isVerification) { - var isAttendance; - try { - isAttendance = await SpecialPrompt.boolean({ prompt: 'Will you be using the attendance service?', channel, userId }); - } catch (error) { - winston.loggers.get(guild.id).warning(`Handled an error when setting up verification, and thus was not set up. Error was ${error.name}`, { event: 'InitBot Command', data: error }); - isAttendance = false; - } - - if (isAttendance) { - const attendeeRole = await this.askOrCreate('attendee', channel, userId, guild, '#0099E1'); - await botGuild.setUpAttendance(this.client, attendeeRole.id); - - sendMsgToChannel(channel, userId, 'The attendance service has been set up correctly!', 8); - adminConsole.addField('Attendance Feature', `IS ENABLED!\n Attendance Role: <@&${attendeeRole.id}>`); - } else { - sendMsgToChannel(channel, userId, 'Attendance was not set up!', 8); - adminConsole.addField('Attendance Feature', 'IS NOT ENABLED!'); - } - } else { - sendMsgToChannel(channel, userId, 'Attendance was not set up!', 8); - adminConsole.addField('Attendance Feature', 'IS NOT ENABLED!'); - } - - // ask if the announcements will be used - try { - if (await SpecialPrompt.boolean({ prompt: 'Have firebase announcements been set up code-side? If not say no, or the bot will fail!', channel, userId })) { - let announcementChannel = await ChannelPrompt.single('What channel should announcements be sent to? If you don\'t have it, create it and come back, do not cancel.'); - await botGuild.setUpAnnouncements(this.client, announcementChannel.id); - - sendMsgToChannel(channel, userId, 'The announcements have been set up correctly!', 8); - adminConsole.addField('Announcement Feature', `IS ENABLED!\n Announcements Channel: <#${announcementChannel.id}>`); - } else { - sendMsgToChannel(channel, userId, 'Announcements functionality was not set up.', 8); - adminConsole.addField('Announcement Feature', 'IS NOT ENABLED!'); - } - } catch (error) { - sendMsgToChannel(channel, userId, 'Announcements functionality was not set up due to a Prompt cancellation.', 8); - adminConsole.addField('Announcement Feature', 'IS NOT ENABLED!'); - } - - - // ask if the stamps will be used - var isStamps; - try { - isStamps = await SpecialPrompt.boolean({ prompt: 'Will you be using the stamp service?', channel, userId }); - } catch { - isStamps = false; - } - if (isStamps) { - var numberOfStamps = 0; - try { - numberOfStamps = await NumberPrompt.single({ prompt: 'How many stamps do you want?', channel, userId, cancelable: true }); - } catch (error) {/** Do nothing */ } - - await botGuild.setUpStamps(this.client, numberOfStamps); - guild.setGroupEnabled('stamps', true); - - sendMsgToChannel(channel, userId, 'The stamp roles have been created, you can change their name and/or color, but their stamp number is final!', 8); - adminConsole.addField('Stamps Feature', 'IS ENABLED!\n You can change the role\'s name and color, but their number is final. For example, stamps 3 is stamp 3 even if its name changes.'); - } else { - sendMsgToChannel(channel, userId, 'The stamp functionality was not set up.', 8); - adminConsole.addField('Stamps Feature', 'IS NOT ENABLED!'); - } - - - // ask if the user will use the report functionality - var isReport; - try { - isReport = await SpecialPrompt.boolean({ prompt: 'Will you be using the report functionality?', channel, userId }); - } catch { - isReport = false; - } - if (isReport) { - var incomingReportChannel; - try { - incomingReportChannel = await ChannelPrompt.single({ prompt: 'What channel should prompts be sent to? We recommend this channel be accessible to your staff.', channel, userId }); - } catch {/** Do nothing */ } - - // Send report to report channel or admin log if none given! - let channelId = incomingReportChannel ? incomingReportChannel.id : adminLog.id; - await botGuild.setUpReport(this.client, channelId); - - sendMsgToChannel(channel, userId, `The report command is available and reports will be sent to: <#${channelId}>`, 8); - adminConsole.addField('Report Feature', `IS ENABLED!\n Reports are sent to <#${channelId}>`); - } else { - sendMsgToChannel(channel, userId, 'Report command is not enabled.', 8); - adminConsole.addField('Report Feature', 'IS NOT ENABLED!'); - } - - - // ask if the user wants to use the experimental !ask command - var isAsk; - try { - isAsk = await SpecialPrompt.boolean({ prompt: 'Do you want to let users use the experimental !ask command?', channel, userId }); - } catch { - isAsk = false; - } - if (isAsk) { - botGuild.setUpAsk(this.client); - sendMsgToChannel(channel, userId, 'The ask command is now available to the server users.', 8); - adminConsole.addField('Experimental Ask Command', 'IS ENABLED!'); - } else { - sendMsgToChannel(channel, userId, 'Ask command is not enabled.', 8); - adminConsole.addField('Experimental Ask Command', 'IS NOT ENABLED!'); - } - await botGuild.save(); - botGuild.setCommandStatus(this.client); + // botGuild.setCommandStatus(this.client); - sendMsgToChannel(channel, userId, 'The bot is set and ready to hack!', 8); + await interaction.followUp('The bot is set and ready to hack!'); + discordLog(guild, '<@' + userId + '> ran init-bot!'); } /** @@ -260,30 +193,6 @@ class InitBot extends Command { * @property {String} roleId */ - /** - * Prompts the user for a verification type and if they want to add more. Will call itself if true - * for a recursive call. - * @param {TextChannel} channel - * @param {String} userId - * @returns {Promise} - * @async - */ - async getVerificationTypes(channel, userId, guestRole, memberRole) { - let {type, role} = await this.getValidVerificationTypes(channel, userId, guestRole, memberRole); - - if (await SpecialPrompt.boolean({ prompt: 'Would you like to add another verification option?', channel, userId })) { - return (await this.getVerificationTypes(channel, userId, guestRole, memberRole)).concat([{ - type: type, - roleId: role.id, - }]); - } else { - return [{ - type: type, - roleId: role.id, - }]; - } - } - async getValidVerificationTypes(channel, userId, guestRole, memberRole) { let typeMsg = await MessagePrompt.prompt({ prompt: `Please tell me the type and mention the role for a verification option. @@ -298,46 +207,7 @@ class InitBot extends Command { 'Please try again.', 30); return await this.getValidVerificationTypes(channel, userId, guestRole, memberRole); } - return {type, role}; - } - - /** - * Will ask the user if a role has been created, if so, then prompt it, else then create it. - * @param {String} roleName - the role name - * @param {TextChannel} channel - the text channel were to prompt - * @param {Snowflake} userId - the user id to prompt to - * @param {Guild} guild - the current guild - * @param {ColorResolvable} - the role color - * @async - * @returns {Promise} - */ - async askOrCreate(roleName, channel, userId, guild, color) { - try { - let hasRole = await SpecialPrompt.boolean({ prompt: 'Have you created the ' + roleName + ' role? You can go ahead and create it if you wish, or let me do the hard work.', channel, userId }); - if (hasRole) { - return await RolePrompt.single({ prompt: 'What is the ' + roleName + ' role?', channel, userId }); - } else { - if (roleName === 'admin') { - return await guild.roles.create({ - data: { - name: await StringPrompt.single({ prompt: 'What name would you like the ' + roleName + ' role to have?', channel, userId }), - color: color, - permissions: Permissions.FLAGS.ADMINISTRATOR, - } - }); - } else { - return await guild.roles.create({ - data: { - name: await StringPrompt.single({ prompt: 'What name would you like the ' + roleName + ' role to have?', channel, userId }), - color: color, - } - }); - } - } - } catch (error) { - sendMsgToChannel(channel, userId, 'You need to complete this prompt please try again!', 5); - return await this.askOrCreate(roleName, channel, userId, guild, color); - } + return { type, role }; } } module.exports = InitBot; \ No newline at end of file From 31db6206d514859a932060a4e976dacfb1783898 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Sun, 5 Nov 2023 05:07:11 -0800 Subject: [PATCH 15/67] finish init-bot and UI improvements + bug fixes for verification and discord contests --- classes/Bot/bot-guild.d.ts | 2 ++ classes/Bot/bot-guild.js | 2 -- commands/a_activity/discord-contests.js | 29 ++++++++++++++------- commands/essentials/init-bot.js | 15 +++++------ commands/verification/start-verification.js | 7 +++-- db/mongo/BotGuild.d.ts | 2 ++ 6 files changed, 34 insertions(+), 23 deletions(-) diff --git a/classes/Bot/bot-guild.d.ts b/classes/Bot/bot-guild.d.ts index faea5de5..c1dddbc5 100644 --- a/classes/Bot/bot-guild.d.ts +++ b/classes/Bot/bot-guild.d.ts @@ -65,6 +65,8 @@ interface BotGuild extends Document { */ async readyUp(client, botGuildInfo); + setUpVerification(guild, guestRoleId, types, welcomeSupportChannel); + /** * Staff role permissions. * @static diff --git a/classes/Bot/bot-guild.js b/classes/Bot/bot-guild.js index 600a2152..42af0b23 100644 --- a/classes/Bot/bot-guild.js +++ b/classes/Bot/bot-guild.js @@ -99,7 +99,6 @@ class BotGuild { this.channelIDs = botGuildInfo.channelIDs; this.embedColor = botGuildInfo.embedColor; - this.verification = botGuildInfo.verification; let adminRole = await guild.roles.resolve(this.roleIDs.adminRole); // try giving the admins administrator perms try { @@ -166,7 +165,6 @@ class BotGuild { this.verification.guestRoleID = guestRoleId; this.verification.welcomeSupportChannelID = welcomeSupportChannel; - // add the types to the type map. types.forEach(async (type, index, list) => { try { diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index 0b5fc170..ca7224e3 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -61,7 +61,9 @@ class DiscordContests extends Command { // this.botGuild = this.botGuild; let guild = interaction.guild; this.botGuild = await BotGuild.findById(guild.id); - let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + let adminLog = guild.channels.resolve(this.botGuild.channelIDs.adminLog); + let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); var interval; @@ -74,6 +76,11 @@ class DiscordContests extends Command { interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } + + if (Object.values(this.botGuild.roleIDs).includes(roleId) || Object.values(this.botGuild.verification.verificationRoles).includes(roleId)) { + interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); + return; + } // try { // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); // timeInterval = 1000 * 60 * num; @@ -134,7 +141,7 @@ class DiscordContests extends Command { // }); const startEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.botGuild.embedColor) .setTitle(string) .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); @@ -158,7 +165,9 @@ class DiscordContests extends Command { } }); - const controlPanel = await botSpamChannel.send({ content: 'Discord contests started by <@' + userId + '>', components: [row] }); + interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); + const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); + adminLog.send('Discord contests started by <@' + userId + '>'); const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); const collector = controlPanel.createMessageComponentCollector({filter}); collector.on('collect', async i => { @@ -168,14 +177,14 @@ class DiscordContests extends Command { } else if (interval != null && !paused && i.customId == 'pause') { clearInterval(interval); paused = true; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest paused by <@' + i.user.id + '>!'); - await i.reply({ content: 'Discord contest has been paused!', ephemeral: true }); + await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); } else if (paused && i.customId == 'play') { await sendQuestion(this.botGuild); interval = setInterval(sendQuestion, timeInterval, this.botGuild); paused = false; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); - await i.reply({ content: 'Discord contest has been un-paused!', ephemeral: true }); + await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); } else { await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); } @@ -242,7 +251,7 @@ class DiscordContests extends Command { channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }).then(async (msg) => { if (answers.length === 0) { //send message to console - const questionMsg = await botSpamChannel.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); + const questionMsg = await adminConsole.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.adminRole)); const collector = await questionMsg.createMessageComponentCollector({ filter }); @@ -251,7 +260,7 @@ class DiscordContests extends Command { const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); const winnerFilter = message => message.author.id === i.user.id; // error? - const winnerCollector = botSpamChannel.createMessageCollector({ filter: winnerFilter, max: 1 }); + const winnerCollector = await adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); winnerCollector.on('collect', async m => { if (m.mentions.members.size > 0) { const member = await m.mentions.members.first(); @@ -322,7 +331,7 @@ class DiscordContests extends Command { async function recordWinner(member) { try { let email = await lookupById(guild.id, member.id); - discordLog(guild, `Discord contest winner: ${member.id} - ${email}`); + discordLog(guild, `Discord contest winner: <@${member.id}> - ${email}`); } catch (error) { console.log(error); } diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 6efe52b6..ace1958b 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -131,19 +131,17 @@ class InitBot extends Command { isEnabled: useVerification }; if (useVerification) { - guest = interaction.options.getRole('guest'); - welcomeSupportChannel = interaction.options.getChannel('welcome_support_channel'); + guest = interaction.options.getRole('guest').id; + welcomeSupportChannel = interaction.options.getChannel('welcome_support_channel').id; verificationRoles = interaction.options.getAttachment('verification_roles'); - console.log(verificationRoles.url); + // if () try { const response = await fetch(verificationRoles.url); - console.log('response' + response); let res = await response.json(); - console.log(res); - // botGuild.setUpVerification(guild, guest, res, welcomeSupportChannel); + await botGuild.setUpVerification(guild, guest, res, welcomeSupportChannel); } catch (error) { console.error('error: ' + error); - interaction.reply({ content: error, ephemeral: true}); + interaction.reply({ content: 'An error occurred with the file upload or verification roles upload!', ephemeral: true}); } } // const useStamps = interaction.options.getBoolean('use_stamps'); @@ -159,7 +157,7 @@ class InitBot extends Command { // firstStampRole = interaction.options.getInteger('0th_stamp_role'); // botGuild.setUpStamps(this.client, numberOfStamps, stampTime, firstStampRole); // } - const embedColor = interaction.options.getString('embed_colour'); + const embedColor = interaction.options.getString('embed_colour') || '#26fff4'; // ask the user to move our role up the list await interaction.reply({content: 'Before we move on, could you please move my role up the role list as high as possible, this will give me the ability to assign roles!', ephemeral: true}); @@ -179,7 +177,6 @@ class InitBot extends Command { adminConsole: adminConsole.id } }); - await botGuild.save(); // botGuild.setCommandStatus(this.client); diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index f4e608a5..32e04787 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -100,7 +100,7 @@ class StartVerification extends Command { var correctTypes = []; types.forEach(type => { - if (this.botGuild.verification.verificationRoles.has(type)) { + if (this.botGuild.verification.verificationRoles.has(type) || type === 'staff' || type === 'mentor') { const member = interaction.guild.members.cache.get(submitted.user.id); let roleId; if (type === 'staff') { @@ -111,7 +111,10 @@ class StartVerification extends Command { roleId = this.botGuild.verification.verificationRoles.get(type); } member.roles.add(roleId); - if (correctTypes.length === 0) member.roles.remove(this.botGuild.verification.guestRoleID); + if (correctTypes.length === 0) { + member.roles.remove(this.botGuild.verification.guestRoleID); + member.roles.add(this.botGuild.roleIDs.memberRole); + } correctTypes.push(type); } else { discordLog(interaction.guild, `VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); diff --git a/db/mongo/BotGuild.d.ts b/db/mongo/BotGuild.d.ts index d5d6cc3a..31056387 100644 --- a/db/mongo/BotGuild.d.ts +++ b/db/mongo/BotGuild.d.ts @@ -70,6 +70,8 @@ interface BotGuild extends Document { */ async readyUp(client, botGuildInfo); + async setUpVerification(guild, guestRoleId, types, welcomeSupportChannel); + /** * Staff role permissions. * @static From a0efda8665b0d6a6ca3f627e154f759aea4f06aa Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 7 Nov 2023 01:48:20 -0800 Subject: [PATCH 16/67] make verification arguments mandatory for init-bot --- commands/essentials/init-bot.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index ace1958b..452fafb7 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -57,14 +57,15 @@ class InitBot extends Command { .addRoleOption(option => option.setName('guest') .setDescription('Mention the guest role **if verification is on**') - .setRequired(false)) + .setRequired(true)) .addChannelOption(option => option.setName('welcome_support_channel') .setDescription('Mention the channel for verification issues (must be viewable to guests) **if verification is on**!') - .setRequired(false)) + .setRequired(true)) .addAttachmentOption(option => option.setName('verification_roles') - .setDescription('File: array of objects! Each role string in the participants\' database and corresponding role ID **if verification is on**.')) + .setDescription('File: array of objects! Each role string in the participants\' database and corresponding role ID **if verification is on**.') + .setRequired(true)) // .addBooleanOption(option => // option.setName('use_stamps') // .setDescription('Whether stamps will be used') From b40c96bd97bef8cc009c3e0ee58fb4aa1e0bc736 Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Tue, 7 Nov 2023 03:04:22 -0800 Subject: [PATCH 17/67] fix bug where mentor cave had no channels --- classes/Bot/bot-guild.d.ts | 4 ++ .../a_start_commands/start-mentor-cave.js | 48 ++++++++++++++----- commands/essentials/init-bot.js | 6 +-- db/mongo/BotGuild.d.ts | 6 ++- db/mongo/BotGuild.js | 8 +++- 5 files changed, 53 insertions(+), 19 deletions(-) diff --git a/classes/Bot/bot-guild.d.ts b/classes/Bot/bot-guild.d.ts index c1dddbc5..24d442a2 100644 --- a/classes/Bot/bot-guild.d.ts +++ b/classes/Bot/bot-guild.d.ts @@ -19,6 +19,10 @@ interface BotGuild extends Document { adminConsole: String, adminLog: String, botSpamChannel: String, + }, + + mentorTickets: { + // ticketNumber: Number, incomingTicketsChannel: String, mentorRoleSelectionChannel: String, requestTicketChannel: String diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 608c8c71..4f35c3a2 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -41,10 +41,18 @@ class StartMentorCave extends Command { option.setName('request_ticket_role') .setDescription('Tag the role that is allowed to request tickets') .setRequired(true)) - // .addRoleOption(option => - // option.setName('additional_mentor_role') - // .setDescription('Tag up to one additional role **aside from mentors and staff** that is allowed to help with tickets') - // .setRequired(false)) + .addChannelOption(option => + option.setName('mentor_role_selection_channel') + .setDescription('Tag the channel where mentors can select their specialties') + .setRequired(false)) + .addChannelOption(option => + option.setName('incoming_tickets_channel') + .setDescription('Tag the channel where mentor tickets will be sent') + .setRequired(false)) + .addChannelOption(option => + option.setName('request_ticket_channel') + .setDescription('Tag the channel where hackers can request tickets') + .setRequired(false)) ), { idHints: 1051737344937566229 @@ -62,7 +70,13 @@ class StartMentorCave extends Command { let userId = interaction.user.id; let guild = interaction.guild; this.botGuild = await BotGuild.findById(guild.id); - let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + + if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + let adminConsole = await guild.channels.resolve(this.botGuild.channelIDs.adminConsole); this.ticketCount = 0; // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); @@ -70,13 +84,25 @@ class StartMentorCave extends Command { const inactivePeriod = interaction.options.getInteger('inactivity_time'); // const bufferTime = inactivePeriod / 2; const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); - - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { - await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + const mentorRoleSelectionChannel = interaction.options.getChannel('mentor_role_selection_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.mentorRoleSelectionChannel); + const incomingTicketsChannel = interaction.options.getChannel('incoming_tickets_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.incomingTicketsChannel); + const requestTicketChannel = interaction.options.getChannel('request_ticket_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.requestTicketChannel); + if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { + await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); return; } - interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); + if (mentorRoleSelectionChannel != this.botGuild.mentorTickets.mentorRoleSelectionChannel || incomingTicketsChannel != this.botGuild.mentorTickets.incomingTicketsChannel || requestTicketChannel != this.botGuild.mentorTickets.requestTicketChannel) { + await interaction.deferReply(); + this.botGuild.mentorTickets.mentorRoleSelectionChannel = mentorRoleSelectionChannel.id; + this.botGuild.mentorTickets.incomingTicketsChannel = incomingTicketsChannel.id; + this.botGuild.mentorTickets.requestTicketChannel = requestTicketChannel.id; + await this.botGuild.save(); + await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); + } else { + await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); + } + discordLog(guild, 'Mentor cave started by <@' + userId + '>'); // these are all old code that create channels rather than using existing channels @@ -131,9 +157,6 @@ class StartMentorCave extends Command { // parent: mentorCategory // } // ); - const mentorRoleSelectionChannel = guild.channels.resolve(this.botGuild.channelIDs.mentorRoleSelectionChannel); - const incomingTicketsChannel = guild.channels.resolve(this.botGuild.channelIDs.incomingTicketsChannel); - const requestTicketChannel = guild.channels.resolve(this.botGuild.channelIDs.requestTicketChannel); //TODO: allow staff to add more roles const htmlCssEmoji = '💻'; @@ -197,7 +220,6 @@ class StartMentorCave extends Command { roleSelectionMsg.react(key); } - const notBotFilter = i => !i.user.bot; const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); collector.on('collect', async (reaction, user) => { if (emojisMap.has(reaction.emoji.name)) { diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 452fafb7..5cc00b6a 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -56,15 +56,15 @@ class InitBot extends Command { .setRequired(true)) .addRoleOption(option => option.setName('guest') - .setDescription('Mention the guest role **if verification is on**') + .setDescription('Mention the guest role.') .setRequired(true)) .addChannelOption(option => option.setName('welcome_support_channel') - .setDescription('Mention the channel for verification issues (must be viewable to guests) **if verification is on**!') + .setDescription('Mention the channel for verification issues (must be viewable to guests)!') .setRequired(true)) .addAttachmentOption(option => option.setName('verification_roles') - .setDescription('File: array of objects! Each role string in the participants\' database and corresponding role ID **if verification is on**.') + .setDescription('File: array of objects! Each role string in the participants\' database and corresponding role ID.') .setRequired(true)) // .addBooleanOption(option => // option.setName('use_stamps') diff --git a/db/mongo/BotGuild.d.ts b/db/mongo/BotGuild.d.ts index 31056387..a4087bd4 100644 --- a/db/mongo/BotGuild.d.ts +++ b/db/mongo/BotGuild.d.ts @@ -19,9 +19,11 @@ interface BotGuild extends Document { channelIDs: { adminConsole: String, adminLog: String, - botSupportChannel: String, - archiveCategory: String, botSpamChannel: String, + }, + + mentorTickets: { + // ticketNumber: Number, incomingTicketsChannel: String, mentorRoleSelectionChannel: String, requestTicketChannel: String diff --git a/db/mongo/BotGuild.js b/db/mongo/BotGuild.js index f140074b..98655042 100644 --- a/db/mongo/BotGuild.js +++ b/db/mongo/BotGuild.js @@ -34,7 +34,13 @@ const BotGuildSchema = new Schema({ }, botSpamChannel: { type: String, - }, + } + }, + + mentorTickets: { + // ticketNumber: { + // type: Number, + // }, incomingTicketsChannel: { type: String, }, From 64ee73097fe20446eb66bae2e825bd2672ce9098 Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Tue, 7 Nov 2023 18:26:22 -0800 Subject: [PATCH 18/67] git ignored registrations.csv --- .gitignore | 3 ++- registrations.csv | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 registrations.csv diff --git a/.gitignore b/.gitignore index fcf311f1..4e4702fc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .env node_modules/ logs/ -.idea/ \ No newline at end of file +.idea/ +.registrations.csv \ No newline at end of file diff --git a/registrations.csv b/registrations.csv new file mode 100644 index 00000000..e69de29b From 6c0a9c7c5ac112febeecbaad8cea382cdd8b9491 Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Tue, 7 Nov 2023 18:26:41 -0800 Subject: [PATCH 19/67] git ignored registrations.csv --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 4e4702fc..45121b40 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ node_modules/ logs/ .idea/ -.registrations.csv \ No newline at end of file +registrations.csv \ No newline at end of file From 4da848da787408ee9ccd1cee0a1177b33f83fc8e Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Tue, 7 Nov 2023 18:27:33 -0800 Subject: [PATCH 20/67] deleted registrations.csv --- registrations.csv | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 registrations.csv diff --git a/registrations.csv b/registrations.csv deleted file mode 100644 index e69de29b..00000000 From f93f47b8dc622a6f3c3732f5029883709ab390ba Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Wed, 8 Nov 2023 04:56:23 -0800 Subject: [PATCH 21/67] add load questions command --- commands/essentials/init-bot.js | 1 - commands/firebase_scripts/load-questions.js | 62 +++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 commands/firebase_scripts/load-questions.js diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 5cc00b6a..093f10ac 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -86,7 +86,6 @@ class InitBot extends Command { option.setName('embed_colour') .setDescription('Hex code of embed colour') .setRequired(false)) - .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) ), { idHints: 1051737348502728764 diff --git a/commands/firebase_scripts/load-questions.js b/commands/firebase_scripts/load-questions.js new file mode 100644 index 00000000..d6141a08 --- /dev/null +++ b/commands/firebase_scripts/load-questions.js @@ -0,0 +1,62 @@ +const { Command } = require('@sapphire/framework'); +require('dotenv').config(); +const FirebaseServices = require('../../db/firebase/firebase-services'); +const fetch = require('node-fetch'); + +/** + * The self care command will send pre made reminders from firebase to the command channel. These reminders are self + * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. + * @category Commands + * @subcategory Admin-Utility + * @extends Command + */ +class LoadQuestions extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Adds Discord Contest questions to database', + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addAttachmentOption(option => + option.setName('questions') + .setDescription('JSON file only! Make sure each question is an object.') + .setRequired(true)) + ), + { + idHints: '1171789829479079976', + }; + } + + async chatInputRun(interaction) { + // const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK); + // let app = FirebaseServices.initializeFirebaseAdmin('factotum', adminSDK, 'https://nwplus-bot.firebaseio.com'); + + const guildId = interaction.guild.id; + const file = interaction.options.getAttachment('questions'); + let res; + await interaction.deferReply(); + try { + const response = await fetch(file.url); + res = await response.json(); + + let db = FirebaseServices.apps.get('nwPlusBotAdmin').firestore(); + var count = 0; + res.forEach(question => { + count++; + + var docRef = db.collection('guilds').doc(guildId).collection('questions').doc(); + docRef.set({ ...question, asked: false }); + }); + await interaction.editReply({ content: count + ' questions added!', ephemeral: true }); + } catch (error) { + await interaction.editReply({ content: 'Something went wrong! Error msg: ' + error, ephemeral: true }); + } + } +} +module.exports = LoadQuestions; From 8359df9933313976af7a38fedefe1e2836c0f99e Mon Sep 17 00:00:00 2001 From: maggiew7 Date: Wed, 8 Nov 2023 05:00:28 -0800 Subject: [PATCH 22/67] stringify idHints --- commands/a_activity/discord-contests.js | 2 +- commands/a_start_commands/start-mentor-cave.js | 2 +- commands/a_utility/pronouns.js | 2 +- commands/a_utility/self-care.js | 2 +- commands/essentials/init-bot.js | 2 +- commands/verification/start-verification.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index ca7224e3..228681f5 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -43,7 +43,7 @@ class DiscordContests extends Command { .setRequired(false)) ), { - idHints: 1051737343729610812 + idHints: '1051737343729610812' }; } diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 4f35c3a2..8bfc9bf9 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -55,7 +55,7 @@ class StartMentorCave extends Command { .setRequired(false)) ), { - idHints: 1051737344937566229 + idHints: '1051737344937566229' }; } diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index c8022271..cc788712 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -28,7 +28,7 @@ class Pronouns extends Command { .setDescription(this.description) ), { - idHints: 1051737347441569813 + idHints: '1051737347441569813' }; } diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 21987417..42f3abe8 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -41,7 +41,7 @@ class SelfCareReminders extends Command { .setRequired(false)) ), { - idHints: 1052699103143927859 + idHints: '1052699103143927859' }; } diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 093f10ac..329f3470 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -88,7 +88,7 @@ class InitBot extends Command { .setRequired(false)) ), { - idHints: 1051737348502728764 + idHints: '1051737348502728764' }; } diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 32e04787..6a4d8a26 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -20,7 +20,7 @@ class StartVerification extends Command { .setDescription(this.description) ), { - idHints: 1060545714133938309 + idHints: '1060545714133938309' }; } From 5eb2bf30bad0adfe33729a19133e8f8f79064f4e Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Fri, 10 Nov 2023 16:17:21 -0800 Subject: [PATCH 23/67] testing check-in --- commands/verification/start-verification.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 32e04787..761e9bc1 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -55,6 +55,11 @@ class StartVerification extends Command { const msg = await interaction.channel.send({ content: 'If you have not already, make sure to enable DMs, emojis, and embeds/link previews in your personal Discord settings! If you have any issues, please find an organizer!', embeds: [embed], components: [row] }); const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); + + // console.log(this.botGuild.verification.guestRoleID) + // console.log(this.botGuild.verification) + + checkInCollector.on('collect', async i => { if (!interaction.guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.verification.guestRoleID)) { await i.reply({ content: 'You are not eligible to be checked in! If you don\'t have correct access to the server, please contact an organizer.', ephemeral: true}); From a79cd8e4103605da1f4b5404e8732bb33de844ef Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Fri, 19 Jan 2024 03:14:20 -0800 Subject: [PATCH 24/67] updated ideation role --- .vscode/settings.json | 4 ++-- commands/a_start_commands/start-mentor-cave.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index a389bf1f..bb329cd3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,7 +1,7 @@ { "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, + "source.fixAll.eslint": "explicit" + }, "eslint.validate": ["javascript"], "cSpell.words": [ diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 8bfc9bf9..694a2cc5 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -190,7 +190,7 @@ class StartMentorCave extends Command { emojisMap.set(unityEmoji, 'Unity'); emojisMap.set(rustEmoji, 'Rust'); emojisMap.set(awsEmoji, 'AWS'); - emojisMap.set(ideationEmoji, 'Ideation'); + emojisMap.set(ideationEmoji, 'Ideation/Pitching'); const mentorRoleColour = guild.roles.cache.find(role => role.id === this.botGuild.roleIDs.mentorRole).hexColor; for (let value of emojisMap.values()) { From ccf5b6c4b689d46749192851152afcd7408ccf8e Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Mon, 4 Mar 2024 01:55:54 -0800 Subject: [PATCH 25/67] added new pronouns --- .../a_start_commands/start-mentor-cave.js | 4 +++- commands/a_utility/pronouns.js | 24 ++++++++++++++++--- docs/Pronouns.html | 9 ++++++- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 694a2cc5..2d523c86 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -174,6 +174,7 @@ class StartMentorCave extends Command { const rustEmoji = '⚙️'; const awsEmoji = '🙂'; const ideationEmoji = '💡'; + const pitchingEmoji = '🎤'; let emojisMap = new Map(); emojisMap.set(htmlCssEmoji, 'HTML/CSS'); @@ -190,7 +191,8 @@ class StartMentorCave extends Command { emojisMap.set(unityEmoji, 'Unity'); emojisMap.set(rustEmoji, 'Rust'); emojisMap.set(awsEmoji, 'AWS'); - emojisMap.set(ideationEmoji, 'Ideation/Pitching'); + emojisMap.set(ideationEmoji, 'Ideation'); + emojisMap.set(pitchingEmoji, 'Pitching'); const mentorRoleColour = guild.roles.cache.find(role => role.id === this.botGuild.roleIDs.mentorRole).hexColor; for (let value of emojisMap.values()) { diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index cc788712..848d1d12 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -7,6 +7,8 @@ const BotGuild = require('../../db/mongo/BotGuild'); * * she/her * * he/him * * they/them + * * she/they + * * he/they * * other pronouns * The roles must be already created on the server for this to work. * @category Commands @@ -47,15 +49,17 @@ class Pronouns extends Command { const sheRole = interaction.guild.roles.cache.find(role => role.name === 'she/her'); const heRole = interaction.guild.roles.cache.find(role => role.name === 'he/him'); const theyRole = interaction.guild.roles.cache.find(role => role.name === 'they/them'); + const sheTheyRole = interaction.guild.roles.cache.find(role => role.name === 'she/they'); + const heTheyRole = interaction.guild.roles.cache.find(role => role.name === 'he/they'); const otherRole = interaction.guild.roles.cache.find(role => role.name === 'other pronouns'); // check to make sure all 4 roles are available - if (!sheRole || !heRole || !theyRole || !otherRole) { + if (!sheRole || !heRole || !theyRole || !sheTheyRole || !heTheyRole || !otherRole) { interaction.reply('Could not find all four roles! Make sure the role names are exactly like stated on the documentation.'); return; } - var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; + var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣']; let embed = new MessageEmbed() .setColor('#0DEFE1') @@ -64,7 +68,9 @@ class Pronouns extends Command { `${emojis[0]} she/her\n` + `${emojis[1]} he/him\n` + `${emojis[2]} they/them\n` - + `${emojis[3]} other pronouns\n`); + + `${emojis[3]} she/they\n` + + `${emojis[4]} he/they\n` + + `${emojis[5]} other pronouns\n`); let messageEmbed = await interaction.channel.send({embeds: [embed]}); emojis.forEach(emoji => messageEmbed.react(emoji)); @@ -89,6 +95,12 @@ class Pronouns extends Command { const member = interaction.guild.members.cache.get(user.id); await member.roles.add(theyRole); } if (reaction.emoji.name === emojis[3]) { + const member = interaction.guild.members.cache.get(user.id); + await member.roles.add(sheTheyRole); + } if (reaction.emoji.name === emojis[4]) { + const member = interaction.guild.members.cache.get(user.id); + await member.roles.add(heTheyRole); + } if (reaction.emoji.name === emojis[5]) { const member = interaction.guild.members.cache.get(user.id); await member.roles.add(otherRole); } @@ -105,6 +117,12 @@ class Pronouns extends Command { const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(theyRole); } if (reaction.emoji.name === emojis[3]) { + const member = interaction.guild.members.cache.get(user.id); + await member.roles.remove(sheTheyRole); + } if (reaction.emoji.name === emojis[4]) { + const member = interaction.guild.members.cache.get(user.id); + await member.roles.remove(heTheyRole); + } if (reaction.emoji.name === emojis[5]) { const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(otherRole); } diff --git a/docs/Pronouns.html b/docs/Pronouns.html index 604d3f28..3fd299d6 100644 --- a/docs/Pronouns.html +++ b/docs/Pronouns.html @@ -104,7 +104,14 @@

Pronouns

Pronouns()

-
The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: * she/her * he/him * they/them * other pronouns The roles must be already created on the server for this to work.
+
The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: +* she/her +* he/him +* they/them +* she/they +* he/they +* other pronouns +The roles must be already created on the server for this to work.
From 737149cd08faee2179f5c3c0ffb0890eb8c2f638 Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Mon, 4 Mar 2024 02:58:01 -0800 Subject: [PATCH 26/67] report command done --- commands/hacker_utility/report.js | 73 ---------- commands/hacker_utility/start-report.js | 178 ++++++++++++++++++++++++ 2 files changed, 178 insertions(+), 73 deletions(-) delete mode 100644 commands/hacker_utility/report.js create mode 100644 commands/hacker_utility/start-report.js diff --git a/commands/hacker_utility/report.js b/commands/hacker_utility/report.js deleted file mode 100644 index a2f98dbf..00000000 --- a/commands/hacker_utility/report.js +++ /dev/null @@ -1,73 +0,0 @@ -const { Command } = require('discord.js-commando'); -const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); -const { MessageEmbed, Message } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild'); - -/** - * The report command allows users to report incidents from the server to the admins. Reports are made - * via the bot's DMs and are 100% anonymous. - * @category Commands - * @subcategory Hacker-Utility - * @extends Command - */ -class Report extends Command { - constructor(client) { - super(client, { - name: 'report', - group: 'hacker_utility', - memberName: 'report to admins', - description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', - // not guild only! - args: [], - }); - } - - /** - * @param {Message} message - */ - async run (message) { - let botGuild = await BotGuild.findById(message.guild.id); - - deleteMessage(message); - - if (!botGuild.report.isEnabled) { - sendMessageToMember(message.author, 'The report functionality is disabled for this guild.'); - return; - } - - const embed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) - .setTitle('Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!') - .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + - 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + - 'Copy paste the format and send it to me in this channel!') - .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + - 'Reason for report (one line):\n' + - 'Detailed Explanation:\n' + - 'Name of channel where the incident occurred (if possible):'); - - // send message to user with report format - var msgEmbed = await message.author.send(embed); - - // await response - msgEmbed.channel.awaitMessages(m => true, {max: 1}).then(async msgs => { - var msg = msgs.first(); - - msgEmbed.delete(); - message.author.send('Thank you for the report! Our admin team will look at it ASAP!'); - - // send the report content to the admin report channel! - var incomingReportChn = await message.guild.channels.resolve(botGuild.report.incomingReportChannelID); - - const adminMsgEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) - .setTitle('There is a new report that needs your attention!') - .setDescription(msg.content); - - // send embed with text message to ping admin - incomingReportChn.send('<@&' + botGuild.roleIDs.adminRole + '> Incoming Report', {embed: adminMsgEmbed}); - }); - - } -} -module.exports = Report; diff --git a/commands/hacker_utility/start-report.js b/commands/hacker_utility/start-report.js new file mode 100644 index 00000000..846285f1 --- /dev/null +++ b/commands/hacker_utility/start-report.js @@ -0,0 +1,178 @@ +// const { Command } = require('discord.js-commando'); +// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); +// const { MessageEmbed, Message } = require('discord.js'); +// const BotGuild = require('../../db/mongo/BotGuild'); + +// /** +// * The report command allows users to report incidents from the server to the admins. Reports are made +// * via the bot's DMs and are 100% anonymous. +// * @category Commands +// * @subcategory Hacker-Utility +// * @extends Command +// */ +// class Report extends Command { +// constructor(client) { +// super(client, { +// name: 'report', +// group: 'hacker_utility', +// memberName: 'report to admins', +// description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', +// // not guild only! +// args: [], +// }); +// } + +// /** +// * @param {Message} message +// */ +// async run (message) { +// let botGuild = await BotGuild.findById(message.guild.id); + +// deleteMessage(message); + +// if (!botGuild.report.isEnabled) { +// sendMessageToMember(message.author, 'The report functionality is disabled for this guild.'); +// return; +// } + +// const embed = new MessageEmbed() +// .setColor(botGuild.colors.embedColor) +// .setTitle('Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!') +// .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + +// 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + +// 'Copy paste the format and send it to me in this channel!') +// .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + +// 'Reason for report (one line):\n' + +// 'Detailed Explanation:\n' + +// 'Name of channel where the incident occurred (if possible):'); + +// // send message to user with report format +// var msgEmbed = await message.author.send(embed); + +// // await response +// msgEmbed.channel.awaitMessages(m => true, {max: 1}).then(async msgs => { +// var msg = msgs.first(); + +// msgEmbed.delete(); +// message.author.send('Thank you for the report! Our admin team will look at it ASAP!'); + +// // send the report content to the admin report channel! +// var incomingReportChn = await message.guild.channels.resolve(botGuild.report.incomingReportChannelID); + +// const adminMsgEmbed = new MessageEmbed() +// .setColor(botGuild.colors.embedColor) +// .setTitle('There is a new report that needs your attention!') +// .setDescription(msg.content); + +// // send embed with text message to ping admin +// incomingReportChn.send('<@&' + botGuild.roleIDs.adminRole + '> Incoming Report', {embed: adminMsgEmbed}); +// }); + +// } +// } +// module.exports = Report; + +const { Command } = require('@sapphire/framework'); +// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); +const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); +const BotGuild = require('../../db/mongo/BotGuild'); +const BotGuildModel = require('../../classes/Bot/bot-guild'); +const { discordLog } = require('../../discord-services'); + +/** + * The report command allows users to report incidents from the server to the admins. Reports are made + * via the bot's DMs and are 100% anonymous. + * @category Commands + * @subcategory Hacker-Utility + * @extends Command + */ +class StartReport extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + ), + { + idHints: '1214159059880517652' + }; + } + + /** + * @param {BotGuildModel} botGuild + * @param {Message} message + */ + async chatInputRun(interaction) { + this.botGuild = await BotGuild.findById(interaction.guild.id); + const guild = interaction.guild; + // const userId = interaction.user.id; + + // const embed = new MessageEmbed() + // .setTitle(`See an issue you'd like to annoymously report at ${interaction.guild.name}? Let our organizers know!`); + + const embed = new MessageEmbed() + .setTitle('Annoymously report users who are not following server or MLH rules. Help makes our community safer!') + .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + + 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + + 'Copy paste the format and send it to me in this channel!') + .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + + 'Reason for report (one line):\n' + + 'Detailed Explanation:\n' + + 'Name of channel where the incident occurred (if possible):'); + // modal timeout warning? + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('report') + .setLabel('Report an issue') + .setStyle('PRIMARY'), + ); + interaction.reply({ content: 'Report started!', ephemeral: true }); + const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); + + const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); + + checkInCollector.on('collect', async i => { + const modal = new Modal() + .setCustomId('reportModal') + .setTitle('Report an issue') + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('issueMessage') + .setLabel('Reason for report:') + .setMinLength(3) + .setMaxLength(320) + .setStyle('SHORT') + .setPlaceholder('Type your issue here...') + .setRequired(true), + ), + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(error => { + }); + + if (submitted) { + const issueMessage = submitted.fields.getTextInputValue('issueMessage'); + + try { + discordLog(interaction.guild, `<@&${interaction.guild.roleIDs.staffRole}> New annoymous report:\n\n ${issueMessage}`); + } catch { + discordLog(interaction.guild, `New annoymous report:\n\n ${issueMessage}`); + } + submitted.reply({ content: 'Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!', ephemeral: true }); + return; + } + }); + } +} +module.exports = StartReport; \ No newline at end of file From a280daf9553d766cfbf4e59a02ebf55986e15f0f Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Tue, 5 Mar 2024 01:39:11 -0800 Subject: [PATCH 27/67] updated pronouns --- commands/a_utility/pronouns.js | 28 +++++----------------------- docs/Pronouns.html | 2 -- 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 848d1d12..7164b495 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -7,8 +7,6 @@ const BotGuild = require('../../db/mongo/BotGuild'); * * she/her * * he/him * * they/them - * * she/they - * * he/they * * other pronouns * The roles must be already created on the server for this to work. * @category Commands @@ -49,28 +47,24 @@ class Pronouns extends Command { const sheRole = interaction.guild.roles.cache.find(role => role.name === 'she/her'); const heRole = interaction.guild.roles.cache.find(role => role.name === 'he/him'); const theyRole = interaction.guild.roles.cache.find(role => role.name === 'they/them'); - const sheTheyRole = interaction.guild.roles.cache.find(role => role.name === 'she/they'); - const heTheyRole = interaction.guild.roles.cache.find(role => role.name === 'he/they'); const otherRole = interaction.guild.roles.cache.find(role => role.name === 'other pronouns'); // check to make sure all 4 roles are available - if (!sheRole || !heRole || !theyRole || !sheTheyRole || !heTheyRole || !otherRole) { + if (!sheRole || !heRole || !theyRole || !otherRole) { interaction.reply('Could not find all four roles! Make sure the role names are exactly like stated on the documentation.'); return; } - var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣']; + var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; let embed = new MessageEmbed() .setColor('#0DEFE1') - .setTitle('Set your pronouns by reacting to one of the emojis!') + .setTitle('Set your pronouns by reacting to one or more of the emojis!') .setDescription( `${emojis[0]} she/her\n` + `${emojis[1]} he/him\n` + `${emojis[2]} they/them\n` - + `${emojis[3]} she/they\n` - + `${emojis[4]} he/they\n` - + `${emojis[5]} other pronouns\n`); + + `${emojis[3]} other pronouns\n`); let messageEmbed = await interaction.channel.send({embeds: [embed]}); emojis.forEach(emoji => messageEmbed.react(emoji)); @@ -95,12 +89,6 @@ class Pronouns extends Command { const member = interaction.guild.members.cache.get(user.id); await member.roles.add(theyRole); } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(sheTheyRole); - } if (reaction.emoji.name === emojis[4]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(heTheyRole); - } if (reaction.emoji.name === emojis[5]) { const member = interaction.guild.members.cache.get(user.id); await member.roles.add(otherRole); } @@ -117,12 +105,6 @@ class Pronouns extends Command { const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(theyRole); } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(sheTheyRole); - } if (reaction.emoji.name === emojis[4]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(heTheyRole); - } if (reaction.emoji.name === emojis[5]) { const member = interaction.guild.members.cache.get(user.id); await member.roles.remove(otherRole); } @@ -130,4 +112,4 @@ class Pronouns extends Command { } } -module.exports = Pronouns; +module.exports = Pronouns; \ No newline at end of file diff --git a/docs/Pronouns.html b/docs/Pronouns.html index 3fd299d6..4df2b964 100644 --- a/docs/Pronouns.html +++ b/docs/Pronouns.html @@ -108,8 +108,6 @@

Pronouns From a0be7198b02cdc9d70c66712c6501d29c3392844 Mon Sep 17 00:00:00 2001 From: DonaldKLee Date: Tue, 5 Mar 2024 01:43:05 -0800 Subject: [PATCH 28/67] made report feature long --- commands/hacker_utility/start-report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/commands/hacker_utility/start-report.js b/commands/hacker_utility/start-report.js index 846285f1..884f34ae 100644 --- a/commands/hacker_utility/start-report.js +++ b/commands/hacker_utility/start-report.js @@ -149,8 +149,8 @@ class StartReport extends Command { .setCustomId('issueMessage') .setLabel('Reason for report:') .setMinLength(3) - .setMaxLength(320) - .setStyle('SHORT') + .setMaxLength(1000) + .setStyle(2) .setPlaceholder('Type your issue here...') .setRequired(true), ), From 95f1441a0585b8c9fefbc5246cd2955d89d9ee21 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 10 Jun 2024 16:56:10 -0700 Subject: [PATCH 29/67] initialized firebase setup --- db/firebase/firebaseUtil.js | 49 +++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 db/firebase/firebaseUtil.js diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js new file mode 100644 index 00000000..d152a659 --- /dev/null +++ b/db/firebase/firebaseUtil.js @@ -0,0 +1,49 @@ +const admin = require('firebase-admin'); + +admin.initializeApp({ + credential: admin.credential.applicationDefault(), + databaseURL: `https:${process.env.FIREBASE_DATABASE_URL}.firebaseio.com`, +}); + +const firestore = admin.firestore(); + +// require('firebase/firestore'); +/** + * The firebase utility module has some useful mongo related helper functions. + * @module FirebaseUtil + */ + +/** @type {Db} */ +var _db; + +module.exports = { + + /** + * Starts a connection to new firestore + */ + async connect() { + console.log('before connecting with firebase'); + _db = firestore; + console.log('Connected to Firebase Firestore'); + }, + + /** + * @returns {Db} + */ + getDb() { + return _db; + }, + + /** + * @returns {Collection} + */ + getBotGuildCol() { + return _db.collection('botGuilds'); + }, + + async mongooseConnect() { + // this is no longer needed but keeping for now + return Promise.resolve(); + } + +}; \ No newline at end of file From 6b3f89565f7533ae665489ae1e050673f21a16d7 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sun, 16 Jun 2024 23:45:33 -0700 Subject: [PATCH 30/67] Update app to use environment values for running modes instead of command line arguments --- app.js | 83 ++++++++++++++++++++++++---------------------------- package.json | 10 +++---- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/app.js b/app.js index b1ebb14e..9d75b060 100644 --- a/app.js +++ b/app.js @@ -12,7 +12,7 @@ const Verification = require('./classes/Bot/Features/Verification/verification') const { StringPrompt } = require('advanced-discord.js-prompts'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); -const { LogLevel, SapphireClient } = require('@sapphire/framework') +const { LogLevel, SapphireClient } = require('@sapphire/framework'); /** * The Main App module houses the bot events, process events, and initializes @@ -26,32 +26,27 @@ const { LogLevel, SapphireClient } = require('@sapphire/framework') * Read command line args to know if prod, dev, or test and what server * First arg is one of prod, dev or test * the second is the test server, but the first one must be test - * @param {string[]} args * @returns {Map} config settings */ -function getConfig(args) { - if (args.length >= 1) { - if (args[0] === 'dev') { - // Default dev - return JSON.parse(process.env.DEV); - } else if (args[0] === 'prod') { - // Production - if (args[1] === 'yes') { - return JSON.parse(process.env.PROD); - } - } else if (args[0] === 'test') { - // Test - const testConfig = JSON.parse(process.env.TEST); - let server = args[1] ?? 0; - if (server === '1') { - return testConfig['ONE']; - } else if (server === '2') { - return testConfig['TWO']; - } else if (server === '3') { - return testConfig['THREE']; - } else if (server === '4') { - return testConfig['FOUR']; - } +function getConfig() { + if (process.env.NODE_ENV === 'DEV') { + // Default dev + return JSON.parse(process.env.DEV); + } else if (process.env.NODE_ENV === 'PROD') { + // Production + return JSON.parse(process.env.PROD); + } else if (process.env.NODE_ENV === 'TEST') { + // Test + const testConfig = JSON.parse(process.env.TEST); + let server = process.env.SERVER; + if (server === '1') { + return testConfig['ONE']; + } else if (server === '2') { + return testConfig['TWO']; + } else if (server === '3') { + return testConfig['THREE']; + } else if (server === '4') { + return testConfig['FOUR']; } } @@ -60,7 +55,7 @@ function getConfig(args) { process.exit(0); } -const config = getConfig(process.argv.slice(2)); +const config = getConfig(); const isLogToConsole = config['consoleLog']; @@ -77,22 +72,22 @@ if (config['sentryLog']) { const bot = new SapphireClient({ defaultPrefix: '!', - caseInsensitiveCommands: true, - logger: { - level: LogLevel.Debug - }, - shards: 'auto', - intents: [ - 'GUILDS', - 'GUILD_MEMBERS', - 'GUILD_BANS', - 'GUILD_EMOJIS_AND_STICKERS', - 'GUILD_VOICE_STATES', - 'GUILD_MESSAGES', - 'GUILD_MESSAGE_REACTIONS', - 'DIRECT_MESSAGES', - 'DIRECT_MESSAGE_REACTIONS' - ], + caseInsensitiveCommands: true, + logger: { + level: LogLevel.Debug + }, + shards: 'auto', + intents: [ + 'GUILDS', + 'GUILD_MEMBERS', + 'GUILD_BANS', + 'GUILD_EMOJIS_AND_STICKERS', + 'GUILD_VOICE_STATES', + 'GUILD_MESSAGES', + 'GUILD_MESSAGE_REACTIONS', + 'DIRECT_MESSAGES', + 'DIRECT_MESSAGE_REACTIONS' + ], }); const customLoggerLevels = { @@ -437,14 +432,14 @@ async function greetNewMember(member, botGuild) { && discordServices.checkForRole(member, botGuild.verification.verificationRoles.get('hacker'))) { try { discordServices.askBoolQuestion(member,botGuild, 'One more thing!', - 'Would you like to receive free [Codex beta](https://openai.com/blog/openai-codex/) access, courtesy of our sponsor OpenAI (first come first served, while supplies last)?\n\n' + + 'Would you like to receive free [Codex beta](https://openai.com/blog/openai-codex/) access, courtesy of our sponsor OpenAI (first come first served, while supplies last)?\n\n' + 'Open AI is giving out prizes to the best 2 projects using Codex or GPT-3:\n' + '- 1st place: $120 worth of credits(2 million words in GPT-3 DaVinci)\n' + '- 2nd place: $60 worth of credits (1 million words in GPT-3 DaVinci)\n\n' + 'If you would like a Codex code, please react with a 👍', - 'Thanks for indicating your interest, you have been added to the list! If you are selected to receive an API key, you will get an email.', email); + 'Thanks for indicating your interest, you have been added to the list! If you are selected to receive an API key, you will get an email.', email); askedAboutCodex = true; } catch (error) { discordServices.sendEmbedToMember(member, { diff --git a/package.json b/package.json index ee1feedc..09214501 100644 --- a/package.json +++ b/package.json @@ -8,11 +8,11 @@ "url": "https://github.com/nwplus/Factotum" }, "scripts": { - "test-1": "node app.js test 1", - "test-2": "node app.js test 2", - "test-3": "node app.js test 3", - "test-4": "node app.js test 4", - "dev": "node app.js dev", + "test-1": "NODE_ENV=TEST SERVER=1 node app.js", + "test-2": "NODE_ENV=TEST SERVER=2 node app.js", + "test-3": "NODE_ENV=TEST SERVER=3 node app.js", + "test-4": "NODE_ENV=TEST SERVER=4 node app.js", + "dev": "NODE_ENV=DEV node app.js", "docs": "jsdoc -c jsdoc-conf.json -r ." }, "author": "JP Garcia, Maggie Wang, Shu Ting Hu", From 427a444d9d32704a550d06e47fabbe5bc2b985f1 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 17 Jun 2024 00:01:35 -0700 Subject: [PATCH 31/67] Update firestore collection references to use new data path --- db/firebase/firebase-services.js | 56 +++++++++++++++++--------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/db/firebase/firebase-services.js b/db/firebase/firebase-services.js index 594fbb1d..d728282c 100644 --- a/db/firebase/firebase-services.js +++ b/db/firebase/firebase-services.js @@ -30,7 +30,9 @@ function initializeFirebaseAdmin(name, adminSDK, databaseURL) { } module.exports.initializeFirebaseAdmin = initializeFirebaseAdmin; - +function getFactotumDoc() { + return apps.get('nwPlusBotAdmin').firestore().collection('ExternalProjects').doc('Factotum'); +} /** * @typedef UserType @@ -54,7 +56,7 @@ module.exports.initializeFirebaseAdmin = initializeFirebaseAdmin; */ async function getQuestion(guildId) { //checks that the question has not been asked - let questionReference = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('questions').where('asked', '==', false).limit(1); + let questionReference = getFactotumDoc().collection('guilds').doc(guildId).collection('questions').where('asked', '==', false).limit(1); let question = (await questionReference.get()).docs[0]; //if there exists an unasked question, change its status to asked if (question != undefined) { @@ -75,7 +77,7 @@ module.exports.getQuestion = getQuestion; */ async function getReminder(guildId) { //checks that the reminder has not been sent - var qref = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('reminders').where('sent', '==', false).limit(1); + var qref = getFactotumDoc().collection('guilds').doc(guildId).collection('reminders').where('sent', '==', false).limit(1); var reminder = (await qref.get()).docs[0]; //if there reminder unsent, change its status to asked if (reminder != undefined) { @@ -106,24 +108,24 @@ module.exports.getReminder = getReminder; */ async function checkEmail(email, guildId) { const cleanEmail = email.trim().toLowerCase(); - const docRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + const docRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); const doc = await docRef.get(); return doc.data(); // var foundEmails = []; // snapshot.forEach(memberDoc => { - // compare each member's email with the given email - // if (memberDoc.get('email') != null) { - // let compare = memberDoc.get('email'); - // if the member's emails is similar to the given email, retrieve and add the email, verification status, and member type of - // the member as an object to the array - // if (compareEmails(email.split('@')[0], compare.split('@')[0])) { - // foundEmails.push({ - // email: compare, - // types: memberDoc.get('types').map(type => type.type), - // }); - // } + // compare each member's email with the given email + // if (memberDoc.get('email') != null) { + // let compare = memberDoc.get('email'); + // if the member's emails is similar to the given email, retrieve and add the email, verification status, and member type of + // the member as an object to the array + // if (compareEmails(email.split('@')[0], compare.split('@')[0])) { + // foundEmails.push({ + // email: compare, + // types: memberDoc.get('types').map(type => type.type), + // }); + // } - // } + // } // }); // return foundEmails; @@ -181,7 +183,7 @@ function compareEmails(searchEmail, dbEmail) { * @private */ async function checkName(firstName, lastName, guildId) { - const snapshot = (await apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').get()).docs; // snapshot of Firestore as array of documents + const snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('members').get()).docs; // snapshot of Firestore as array of documents snapshot.forEach(memberDoc => { if (memberDoc.get('firstName') != null && memberDoc.get('lastName') != null && memberDoc.get('firstName').toLowerCase() === firstName.toLowerCase() && memberDoc.get('lastName').toLowerCase() === lastName.toLowerCase()) { // for each document, check if first and last names match given names @@ -204,7 +206,7 @@ module.exports.checkName = checkName; */ async function addUserData(email, type, guildId, overwrite) { const cleanEmail = email.trim().toLowerCase(); - var documentRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + var documentRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); const doc = await documentRef.get(); if (doc.exists && !overwrite) { @@ -217,7 +219,7 @@ async function addUserData(email, type, guildId, overwrite) { } }); if (!containsType) { - types.push({ type: type, isVerified: false }) + types.push({ type: type, isVerified: false }); } await documentRef.update({ types: types }); } else { @@ -244,7 +246,7 @@ module.exports.addUserData = addUserData; */ async function verify(email, id, guildId) { let emailLowerCase = email.trim().toLowerCase(); - var userRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').where('email', '==', emailLowerCase).limit(1); + var userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('email', '==', emailLowerCase).limit(1); var user = (await userRef.get()).docs[0]; if (user) { let returnTypes = []; @@ -280,7 +282,7 @@ module.exports.verify = verify; * @throws Error if the email provided was not found. */ async function attend(id, guildId) { - var userRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').where('discordId', '==', id).limit(1); + var userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('discordId', '==', id).limit(1); var user = (await userRef.get()).docs[0]; if (user) { @@ -309,7 +311,7 @@ module.exports.attend = attend; */ async function checkCodexActive(guildId) { - var ref = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('codex').doc('active'); + var ref = getFactotumDoc().collection('guilds').doc(guildId).collection('codex').doc('active'); var activeRef = await ref.get(); const data = activeRef.data(); return data.active; @@ -324,7 +326,7 @@ module.exports.checkCodexActive = checkCodexActive; */ async function saveToFirebase(guildId, collection, email) { - var ref = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection(collection).doc(email.toLowerCase()); + var ref = getFactotumDoc().collection('guilds').doc(guildId).collection(collection).doc(email.toLowerCase()); /** @type {FirebaseUser} */ let data = { email: email.toLowerCase() @@ -335,7 +337,7 @@ async function saveToFirebase(guildId, collection, email) { module.exports.saveToFirebase = saveToFirebase; async function lookupById(guildId, memberId) { - var userRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('members').where('discordId', '==', memberId).limit(1); + var userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('discordId', '==', memberId).limit(1); var user = (await userRef.get()).docs[0]; if (user) { /** @type {FirebaseUser} */ @@ -350,7 +352,7 @@ async function lookupById(guildId, memberId) { module.exports.lookupById = lookupById; async function saveToLeaderboard(guildId, memberId) { - var userRef = apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('questionsLeaderboard').doc(memberId); + var userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').doc(memberId); var user = await userRef.get(); if (user.exists) { // var data = user.data(); @@ -368,11 +370,11 @@ async function saveToLeaderboard(guildId, memberId) { module.exports.saveToLeaderboard = saveToLeaderboard; async function retrieveLeaderboard(guildId) { - var snapshot = (await apps.get('nwPlusBotAdmin').firestore().collection('guilds').doc(guildId).collection('questionsLeaderboard').get()).docs; + var snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').get()).docs; let winners = []; snapshot.forEach(doc => { winners.push(doc.data()); - }) + }); winners.sort((a, b) => parseFloat(b.points) - parseFloat(a.points)); return winners; } From 70bd899973439ac64e885c51f93058e37df46598 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 24 Jun 2024 22:37:00 -0700 Subject: [PATCH 32/67] init bot -> uses firebase instead of mongo --- commands/essentials/init-bot.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 329f3470..5493961b 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -1,7 +1,8 @@ const { Command } = require('@sapphire/framework'); const { TextChannel, Snowflake, Guild, ColorResolvable, Role, Permissions, PermissionsBitField, PermissionFlagsBits } = require('discord.js'); const { sendMsgToChannel, addRoleToMember, discordLog, } = require('../../discord-services'); -const BotGuild = require('../../db/mongo/BotGuild'); +// const BotGuild = require('../../db/mongo/BotGuild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil') const winston = require('winston'); const fetch = require('node-fetch'); const { MessagePrompt, StringPrompt, NumberPrompt, SpecialPrompt, RolePrompt, ChannelPrompt } = require('advanced-discord.js-prompts'); @@ -96,6 +97,7 @@ class InitBot extends Command { * @param {Message} message */ async chatInputRun(interaction) { + await firebaseUtil.connect('Factotum'); // easy constants to use var channel = interaction.channel; @@ -104,7 +106,7 @@ class InitBot extends Command { const guild = interaction.guild; const everyoneRole = interaction.guild.roles.everyone; - const botGuild = await BotGuild.findById(guild.id); + const botGuildRef = await firebaseUtil.getFactotumSubCol().doc(guild.id); // make sure the user had manage server permission if (!interaction.member.permissionsIn(interaction.channel).has('ADMINISTRATOR')) { @@ -112,7 +114,8 @@ class InitBot extends Command { return; } - if (botGuild?.isSetUpComplete) { + const botGuildDoc = await botGuildRef.get(); + if (botGuildDoc.exists && botGuildDoc.data().isSetUpComplete) { await interaction.reply({ content: 'This server is already set up!', ephemeral: true }); return; } @@ -134,14 +137,19 @@ class InitBot extends Command { guest = interaction.options.getRole('guest').id; welcomeSupportChannel = interaction.options.getChannel('welcome_support_channel').id; verificationRoles = interaction.options.getAttachment('verification_roles'); - // if () try { const response = await fetch(verificationRoles.url); let res = await response.json(); - await botGuild.setUpVerification(guild, guest, res, welcomeSupportChannel); + + // CHANGE 3 + // await botGuild.setUpVerification(guild, guest, res, welcomeSupportChannel); + verification.roles = res; + verification.guestRoleID = guest; + verification.welcomeSupportChannel = welcomeSupportChannel; } catch (error) { console.error('error: ' + error); interaction.reply({ content: 'An error occurred with the file upload or verification roles upload!', ephemeral: true}); + return; } } // const useStamps = interaction.options.getBoolean('use_stamps'); @@ -162,7 +170,7 @@ class InitBot extends Command { // ask the user to move our role up the list await interaction.reply({content: 'Before we move on, could you please move my role up the role list as high as possible, this will give me the ability to assign roles!', ephemeral: true}); - await botGuild.readyUp(guild, { + await botGuildRef.set({ verification, embedColor, roleIDs: { @@ -175,10 +183,9 @@ class InitBot extends Command { channelIDs: { adminLog: adminLog.id, adminConsole: adminConsole.id - } + }, + isSetUpComplete: true, }); - await botGuild.save(); - // botGuild.setCommandStatus(this.client); await interaction.followUp('The bot is set and ready to hack!'); discordLog(guild, '<@' + userId + '> ran init-bot!'); From 79092768ec02484502bc59294d029803af03f5d6 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 24 Jun 2024 22:37:37 -0700 Subject: [PATCH 33/67] formating document --- db/firebase/firebase-services.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/db/firebase/firebase-services.js b/db/firebase/firebase-services.js index 594fbb1d..eb11acc0 100644 --- a/db/firebase/firebase-services.js +++ b/db/firebase/firebase-services.js @@ -111,19 +111,19 @@ async function checkEmail(email, guildId) { return doc.data(); // var foundEmails = []; // snapshot.forEach(memberDoc => { - // compare each member's email with the given email - // if (memberDoc.get('email') != null) { - // let compare = memberDoc.get('email'); - // if the member's emails is similar to the given email, retrieve and add the email, verification status, and member type of - // the member as an object to the array - // if (compareEmails(email.split('@')[0], compare.split('@')[0])) { - // foundEmails.push({ - // email: compare, - // types: memberDoc.get('types').map(type => type.type), - // }); - // } + // compare each member's email with the given email + // if (memberDoc.get('email') != null) { + // let compare = memberDoc.get('email'); + // if the member's emails is similar to the given email, retrieve and add the email, verification status, and member type of + // the member as an object to the array + // if (compareEmails(email.split('@')[0], compare.split('@')[0])) { + // foundEmails.push({ + // email: compare, + // types: memberDoc.get('types').map(type => type.type), + // }); + // } - // } + // } // }); // return foundEmails; @@ -217,7 +217,7 @@ async function addUserData(email, type, guildId, overwrite) { } }); if (!containsType) { - types.push({ type: type, isVerified: false }) + types.push({ type: type, isVerified: false }); } await documentRef.update({ types: types }); } else { @@ -372,7 +372,7 @@ async function retrieveLeaderboard(guildId) { let winners = []; snapshot.forEach(doc => { winners.push(doc.data()); - }) + }); winners.sort((a, b) => parseFloat(b.points) - parseFloat(a.points)); return winners; } From 3a9009e2e8df633f56c040024b6d7d2d51d5f73f Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 24 Jun 2024 22:39:05 -0700 Subject: [PATCH 34/67] firebase util functions to target new firebase --- db/firebase/firebaseUtil.js | 76 ++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index d152a659..9b8c2960 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -1,11 +1,31 @@ const admin = require('firebase-admin'); -admin.initializeApp({ - credential: admin.credential.applicationDefault(), - databaseURL: `https:${process.env.FIREBASE_DATABASE_URL}.firebaseio.com`, -}); +/** + * The firebase services module has firebase related helper functions. + * @module FirebaseServices + */ + +/** + * All the firebase apps in play stored by their name. + * @type {Map} + */ + const apps = new Map(); + module.exports.apps = apps; + +/** + * Will start an admin connection with the given name + * @param {String} name - name of the connection + * @param {JSON} adminSDK - the JSON file with admin config + * @param {String} databaseURL - the database URL + */ + function initializeFirebaseAdmin(name, adminSDK, databaseURL) { + let app = admin.initializeApp({ + credential: admin.credential.cert(adminSDK), + databaseURL: databaseURL, + }, name); -const firestore = admin.firestore(); + apps.set(name, app); +} // require('firebase/firestore'); /** @@ -14,23 +34,36 @@ const firestore = admin.firestore(); */ /** @type {Db} */ -var _db; +let _db; module.exports = { + apps, /** * Starts a connection to new firestore */ - async connect() { - console.log('before connecting with firebase'); - _db = firestore; + async connect(appName) { + if (appName) { + const app = apps.get(appName); + if (!app) { + throw new Error(`No Firebase app initialized with the name ${appName}`); + } + _db = app.firestore(); + } else { + _db = admin.firestore(); + } console.log('Connected to Firebase Firestore'); }, + initializeFirebaseAdmin, + /** * @returns {Db} */ getDb() { + if (!_db) { + throw new Error('Firestore is not initialized. Call connect() first.'); + } return _db; }, @@ -40,6 +73,31 @@ module.exports = { getBotGuildCol() { return _db.collection('botGuilds'); }, + getExternalProjectsCol() { + if (!_db) { + throw new Error('Firestore is not initialized, call connect() first.') + } + return _db.collection('ExternalProjects'); + }, + getFactotumSubCol() { + const externalProjectsCol = this.getExternalProjectsCol(); + if (!externalProjectsCol) { + throw new Error('ExternalProjects collection is not initialized.') + } + return externalProjectsCol.doc('Factotum').collection('InitBotInfo'); + }, + + /** + * @param {String} appName + * @returns {Firestore} Firestore instance for the given app + */ + getFirestoreInstance(appName) { + const app = apps.get(appName); + if (!app) { + throw new Error(`No Firebase app initialized with the name ${appName}`); + } + return app.firestore(); + }, async mongooseConnect() { // this is no longer needed but keeping for now From 06048469186cdb9a7d2eeca068cea982e44e1529 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sat, 29 Jun 2024 18:18:13 -0700 Subject: [PATCH 35/67] no longer buggy on init for a new server --- app.js | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/app.js b/app.js index b1ebb14e..53b3cb0b 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,6 @@ require('dotenv-flow').config(); const mongoUtil = require('./db/mongo/mongoUtil'); +const firebaseUtil = require('./db/firebase/firebaseUtil'); // const Commando = require('discord.js-commando'); const Discord = require('discord.js'); const firebaseServices = require('./db/firebase/firebase-services'); @@ -12,7 +13,7 @@ const Verification = require('./classes/Bot/Features/Verification/verification') const { StringPrompt } = require('advanced-discord.js-prompts'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); -const { LogLevel, SapphireClient } = require('@sapphire/framework') +const { LogLevel, SapphireClient } = require('@sapphire/framework'); /** * The Main App module houses the bot events, process events, and initializes @@ -77,22 +78,22 @@ if (config['sentryLog']) { const bot = new SapphireClient({ defaultPrefix: '!', - caseInsensitiveCommands: true, - logger: { - level: LogLevel.Debug - }, - shards: 'auto', - intents: [ - 'GUILDS', - 'GUILD_MEMBERS', - 'GUILD_BANS', - 'GUILD_EMOJIS_AND_STICKERS', - 'GUILD_VOICE_STATES', - 'GUILD_MESSAGES', - 'GUILD_MESSAGE_REACTIONS', - 'DIRECT_MESSAGES', - 'DIRECT_MESSAGE_REACTIONS' - ], + caseInsensitiveCommands: true, + logger: { + level: LogLevel.Debug + }, + shards: 'auto', + intents: [ + 'GUILDS', + 'GUILD_MEMBERS', + 'GUILD_BANS', + 'GUILD_EMOJIS_AND_STICKERS', + 'GUILD_VOICE_STATES', + 'GUILD_MESSAGES', + 'GUILD_MESSAGE_REACTIONS', + 'DIRECT_MESSAGES', + 'DIRECT_MESSAGE_REACTIONS' + ], }); const customLoggerLevels = { @@ -155,6 +156,7 @@ bot.once('ready', async () => { // initialize firebase const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK); + const adminSDK2 = JSON.parse(process.env.NWPLUSADMINSDK2); firebaseServices.initializeFirebaseAdmin('nwPlusBotAdmin', adminSDK, 'https://nwplus-bot.firebaseio.com'); mainLogger.warning('Connected to firebase admin sdk successfully!', { event: 'Ready Event' }); @@ -162,6 +164,10 @@ bot.once('ready', async () => { await mongoUtil.mongooseConnect(); mainLogger.warning('Connected to mongoose successfully!', { event: 'Ready Event' }); + firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK2, 'https://nwplus-ubc-dev.firebaseio.com'); + mainLogger.warning('Connected to nwFirebase successfully!', { event: 'Ready Event' }); + firebaseUtil.connect('Factotum'); + // make sure all guilds have a botGuild, this is in case the bot goes offline and its added // to a guild. If botGuild is found, make sure only the correct commands are enabled. bot.guilds.cache.forEach(async (guild, key, guilds) => { @@ -437,14 +443,14 @@ async function greetNewMember(member, botGuild) { && discordServices.checkForRole(member, botGuild.verification.verificationRoles.get('hacker'))) { try { discordServices.askBoolQuestion(member,botGuild, 'One more thing!', - 'Would you like to receive free [Codex beta](https://openai.com/blog/openai-codex/) access, courtesy of our sponsor OpenAI (first come first served, while supplies last)?\n\n' + + 'Would you like to receive free [Codex beta](https://openai.com/blog/openai-codex/) access, courtesy of our sponsor OpenAI (first come first served, while supplies last)?\n\n' + 'Open AI is giving out prizes to the best 2 projects using Codex or GPT-3:\n' + '- 1st place: $120 worth of credits(2 million words in GPT-3 DaVinci)\n' + '- 2nd place: $60 worth of credits (1 million words in GPT-3 DaVinci)\n\n' + 'If you would like a Codex code, please react with a 👍', - 'Thanks for indicating your interest, you have been added to the list! If you are selected to receive an API key, you will get an email.', email); + 'Thanks for indicating your interest, you have been added to the list! If you are selected to receive an API key, you will get an email.', email); askedAboutCodex = true; } catch (error) { discordServices.sendEmbedToMember(member, { From 9ec4e491a1b9be97b4dc08092fe7d615d5604457 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sat, 29 Jun 2024 19:15:26 -0700 Subject: [PATCH 36/67] helper functions for existing mongo guild usage --- app.js | 23 +++++------------ db/firebase/firebaseUtil.js | 49 ++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 18 deletions(-) diff --git a/app.js b/app.js index 53b3cb0b..148e2cab 100644 --- a/app.js +++ b/app.js @@ -1,5 +1,4 @@ require('dotenv-flow').config(); -const mongoUtil = require('./db/mongo/mongoUtil'); const firebaseUtil = require('./db/firebase/firebaseUtil'); // const Commando = require('discord.js-commando'); const Discord = require('discord.js'); @@ -7,7 +6,6 @@ const firebaseServices = require('./db/firebase/firebase-services'); const winston = require('winston'); const fs = require('fs'); const discordServices = require('./discord-services'); -const BotGuild = require('./db/mongo/BotGuild'); const BotGuildModel = require('./classes/Bot/bot-guild'); const Verification = require('./classes/Bot/Features/Verification/verification'); const { StringPrompt } = require('advanced-discord.js-prompts'); @@ -160,10 +158,6 @@ bot.once('ready', async () => { firebaseServices.initializeFirebaseAdmin('nwPlusBotAdmin', adminSDK, 'https://nwplus-bot.firebaseio.com'); mainLogger.warning('Connected to firebase admin sdk successfully!', { event: 'Ready Event' }); - // set mongoose connection - await mongoUtil.mongooseConnect(); - mainLogger.warning('Connected to mongoose successfully!', { event: 'Ready Event' }); - firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK2, 'https://nwplus-ubc-dev.firebaseio.com'); mainLogger.warning('Connected to nwFirebase successfully!', { event: 'Ready Event' }); firebaseUtil.connect('Factotum'); @@ -174,9 +168,9 @@ bot.once('ready', async () => { // create the logger for the guild createALogger(guild.id, guild.name, false, isLogToConsole); - let botGuild = await BotGuild.findById(guild.id); + let botGuild = await firebaseUtil.getBotGuild(guild.id) if (!botGuild) { - newGuild(guild); + await newGuild(guild); mainLogger.verbose(`Created a new botGuild for the guild ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' }); } else { // set all non guarded commands to not enabled for the guild @@ -184,8 +178,6 @@ bot.once('ready', async () => { // if (!group.guarded) guild.setGroupEnabled(group, false); // }); - await botGuild.setCommandStatus(bot); - guild.commandPrefix = botGuild.prefix; mainLogger.verbose(`Found a botGuild for ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' }); @@ -211,15 +203,13 @@ bot.on('guildCreate', /** @param {sapphireClient.Guild} guild */(guild) => { * @param {sapphireClient.Guild} guild * @private */ -function newGuild(guild) { +async function newGuild(guild) { // set all non guarded commands to not enabled for the new guild // bot.registry.groups.forEach((group, key, map) => { // if (!group.guarded) guild.setGroupEnabled(group, false); // }); // create a botGuild object for this new guild. - BotGuild.create({ - _id: guild.id, - }); + await firebaseUtil.createBotGuild(guild.id); } /** @@ -228,8 +218,7 @@ function newGuild(guild) { bot.on('guildDelete', async (guild) => { mainLogger.warning(`The bot was removed from the guild: ${guild.id} - ${guild.name}`); - let botGuild = await BotGuild.findById(guild.id); - botGuild.remove(); + await firebaseUtil.deleteBotGuild(guild.id); mainLogger.verbose(`BotGuild with id: ${guild.id} has been removed!`); }); @@ -270,7 +259,7 @@ bot.on('commandError', (command, error, message) => { * Runs when a new member joins a guild the bot is running in. */ bot.on('guildMemberAdd', async member => { - let botGuild = await BotGuild.findById(member.guild.id); + let botGuild = await firebaseUtil.getBotGuild(member.guild.id); member.roles.add(botGuild.verification.guestRoleID); // if the guild where the user joined is complete then greet and verify. diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 9b8c2960..c6488a45 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -104,4 +104,51 @@ module.exports = { return Promise.resolve(); } -}; \ No newline at end of file +}; + +/** + * Gets the BotGuild document by guild ID + * @param {String} guildId + * @returns {Promise} + */ + async function getBotGuild(guildId) { + const doc = await module.exports.getBotGuildCol().doc(guildId).get(); + return doc.exists ? doc.data() : null; +} + +/** + * Creates a new BotGuild document + * @param {String} guildId + * @returns {Promise} + */ +async function createBotGuild(guildId) { + return await module.exports.getBotGuildCol().doc(guildId).set({ + _id: guildId, + }); +} + +/** + * Deletes a BotGuild document by guild ID + * @param {String} guildId + * @returns {Promise} + */ +async function deleteBotGuild(guildId) { + try { + const docRef = module.exports.getBotGuildCol().doc(guildId); + const doc = await docRef.get(); + if (!doc.exists) { + console.log(`No such document with ID ${guildId}`); + return null; + } + await docRef.delete(); + console.log(`Document with ID ${guildId} successfully deleted`); + return docRef; + } catch (error) { + console.error(`Error deleting document with ID ${guildId}:`, error); + throw error; + } +} + +module.exports.getBotGuild = getBotGuild; +module.exports.createBotGuild = createBotGuild; +module.exports.deleteBotGuild = deleteBotGuild; \ No newline at end of file From eeaad14741d2831f48c36ca24003776f082abcaa Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sat, 29 Jun 2024 19:17:14 -0700 Subject: [PATCH 37/67] setCommandStatus added back again --- app.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app.js b/app.js index 148e2cab..59e5e8c0 100644 --- a/app.js +++ b/app.js @@ -178,6 +178,8 @@ bot.once('ready', async () => { // if (!group.guarded) guild.setGroupEnabled(group, false); // }); + // await botGuild.setCommandStatus(bot); + guild.commandPrefix = botGuild.prefix; mainLogger.verbose(`Found a botGuild for ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' }); From 7d1fe6dc87d67bb8bbb2aa72837a7322b036ecc8 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 30 Jun 2024 21:37:34 -0700 Subject: [PATCH 38/67] removed delete function - we don't want factotum data to be deleted on bot kick --- app.js | 3 --- db/firebase/firebaseUtil.js | 25 +------------------------ 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/app.js b/app.js index b6389e8c..f9a97e5a 100644 --- a/app.js +++ b/app.js @@ -214,9 +214,6 @@ async function newGuild(guild) { */ bot.on('guildDelete', async (guild) => { mainLogger.warning(`The bot was removed from the guild: ${guild.id} - ${guild.name}`); - - await firebaseUtil.deleteBotGuild(guild.id); - mainLogger.verbose(`BotGuild with id: ${guild.id} has been removed!`); }); /** diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index c6488a45..168e4004 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -127,28 +127,5 @@ async function createBotGuild(guildId) { }); } -/** - * Deletes a BotGuild document by guild ID - * @param {String} guildId - * @returns {Promise} - */ -async function deleteBotGuild(guildId) { - try { - const docRef = module.exports.getBotGuildCol().doc(guildId); - const doc = await docRef.get(); - if (!doc.exists) { - console.log(`No such document with ID ${guildId}`); - return null; - } - await docRef.delete(); - console.log(`Document with ID ${guildId} successfully deleted`); - return docRef; - } catch (error) { - console.error(`Error deleting document with ID ${guildId}:`, error); - throw error; - } -} - module.exports.getBotGuild = getBotGuild; -module.exports.createBotGuild = createBotGuild; -module.exports.deleteBotGuild = deleteBotGuild; \ No newline at end of file +module.exports.createBotGuild = createBotGuild; \ No newline at end of file From 337bf16f64b79132eb2193c7b1fbc0fdf4c391dc Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 1 Jul 2024 00:15:59 -0700 Subject: [PATCH 39/67] removed last reference of BotGuild.ts --- classes/Bot/activities/activity.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/classes/Bot/activities/activity.js b/classes/Bot/activities/activity.js index db828392..e7ccdd79 100644 --- a/classes/Bot/activities/activity.js +++ b/classes/Bot/activities/activity.js @@ -1,6 +1,6 @@ const { Guild, Collection, Role, CategoryChannel, TextChannel, MessageEmbed, GuildMember, PermissionOverwriteOption } = require('discord.js'); const winston = require('winston'); -const BotGuild = require('../../../db/mongo/BotGuild'); +const firebaseUtil = require('../../../db/firebase/firebaseUtil'); const BotGuildModel = require('../bot-guild'); const { shuffleArray, sendMsgToChannel } = require('../../../discord-services'); const StampsManager = require('../Features/Stamps/stamps-manager'); @@ -64,7 +64,8 @@ class Activity { // add staff role if (isStaffAuto) { - let staffRoleId = (await BotGuild.findById(channel.guild.id)).roleIDs.staffRole; + let botGuildData = await firebaseUtil.getBotGuild(channel.guild.id); + let staffRoleId = botGuildData.roleIDs.staffRole; allowedRoles.set(staffRoleId, channel.guild.roles.resolve(staffRoleId)); } From 3225ac7c0a6f804d2d0b2925b2bcae9107f277d2 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 1 Jul 2024 22:11:02 -0700 Subject: [PATCH 40/67] firebase-services function migrated to firebaseUtil file --- db/firebase/firebaseUtil.js | 275 +++++++++++++++++++++++++++++++++++- 1 file changed, 274 insertions(+), 1 deletion(-) diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 168e4004..cb775061 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -102,10 +102,283 @@ module.exports = { async mongooseConnect() { // this is no longer needed but keeping for now return Promise.resolve(); - } + }, + + // FIREBASE SERVICE FUNCTIONS IMPORTED BELOW + /** + * Retrieves a question from the db that has not already been asked at the Discord Contests, then marks the question as having been + * asked in the db. + * @param {String} guildId - the id of the guild + * @returns {Object | null} - the data object of a question or null if no more questions + */ + async getQuestion(guildId) { + //checks that the question has not been asked + let questionReference = getFactotumDoc().collection('guilds').doc(guildId).collection('questions').where('asked', '==', false).limit(1); + let question = (await questionReference.get()).docs[0]; + //if there exists an unasked question, change its status to asked + if (question != undefined) { + question.ref.update({ + 'asked': true, + }); + return question.data(); + } + return null; + }, + + /** + * Retrieves self-care reminder from the db that has not already been sent, + * then marks the reminder as having been asked in the db. + * @param {String} guildId - the guild id + * @returns {Object | null} - the data object of a reminder or null if no more reminders + */ + async getReminder(guildId) { + //checks that the reminder has not been sent + let qref = getFactotumDoc().collection('guilds').doc(guildId).collection('reminders').where('sent', '==', false).limit(1); + let reminder = (await qref.get()).docs[0]; + //if there reminder unsent, change its status to asked + if (reminder != undefined) { + reminder.ref.update({ + 'sent': true, + }); + return reminder.data(); + } + return null; + }, + + /** + * Checks to see if the input email matches or is similar to emails in the database + * Returns an array of objects containing emails that match or are similar, along with the verification status of each, + * and returns empty array if none match + * @param {String} email - email to check + * @param {String} guildId - the guild id + * @returns {Promise>} - array of members with similar emails to parameter email + */ + async checkEmail(email, guildId) { + const cleanEmail = email.trim().toLowerCase(); + const docRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + const doc = await docRef.get(); + return doc.data(); + }, + + /** + * Uses Levenshtein Distance to determine whether two emails are within 5 Levenshtein Distance + * @param {String} searchEmail - email to search for similar emails for + * @param {String} dbEmail - email from db to compare to searchEmail + * @returns {Boolean} - Whether the two emails are similar + * @private + */ + compareEmails(searchEmail, dbEmail) { + // matrix to track Levenshtein Distance with + let matrix = new Array(searchEmail.length); + let searchEmailChars = searchEmail.split(''); + let dbEmailChars = dbEmail.split(''); + // initialize second dimension of matrix and set all elements to 0 + for (let i = 0; i < matrix.length; i++) { + matrix[i] = new Array(dbEmail.length); + for (let j = 0; j < matrix[i].length; j++) { + matrix[i][j] = 0; + } + } + // set all elements in the top row and left column to increment by 1 + for (let i = 1; i < searchEmail.length; i++) { + matrix[i][0] = i; + } + for (let j = 1; j < dbEmail.length; j++) { + matrix[0][j] = j; + } + // increment Levenshtein Distance by 1 if there is a letter inserted, deleted, or swapped; store the running tally in the corresponding + // element of the matrix + let substitutionCost; + for (let j = 1; j < dbEmail.length; j++) { + for (let i = 1; i < searchEmail.length; i++) { + if (searchEmailChars[i] === dbEmailChars[j]) { + substitutionCost = 0; + } else { + substitutionCost = 1; + } + matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + substitutionCost); + } + } + return matrix[searchEmail.length - 1][dbEmail.length - 1] <= (Math.min(searchEmail.length, dbEmail.length) / 2); + }, + + /** + * Finds the email of user with given first and last names + * @param {String} firstName - first name of member to match with database + * @param {String} lastName - last name of member to match with database + * @param {String} guildId - the guild id + * @returns {Promise} - email of given member + * @private + */ + async checkName(firstName, lastName, guildId) { + const snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('members').get()).docs; // snapshot of Firestore as array of documents + for (const memberDoc of snapshot) { + if ( + memberDoc.get('firstName') && + memberDoc.get('lastName') && + memberDoc.get('firstName').toLowerCase() === firstName.toLowerCase() && + memberDoc.get('lastName').toLowerCase() === lastName.toLowerCase() + ) { + return memberDoc.get('email'); + } + } + return null; + }, + + /** + * Adds a new guild member to the guild's member collection. Email is used as ID, there can be no duplicates. + * @param {String} email - email of member verified + * @param {String[]} types - types this user might verify for + * @param {String} guildId - the guild id + * @param {GuildMember} [member={}] - member verified + * @param {String} [firstName=''] - users first name + * @param {String} [lastName=''] - users last name + * @async + */ + async addUserData(email, type, guildId, overwrite) { + const cleanEmail = email.trim().toLowerCase(); + const documentRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); + const doc = await documentRef.get(); + + if (doc.exists && !overwrite) { + const types = doc.data().types || []; + const containsType = types.some(existingType => existingType.type === type); + if (!containsType) { + types.push({ type, isVerified: false }); + } + await documentRef.update({ types }); + } else { + const data = { email: cleanEmail, types: [{ isVerified: false, type }] }; + await documentRef.set(data); + } + }, + + /** + * Verifies the any event member via their email. + * @param {String} email - the user email + * @param {String} id - the user's discord snowflake + * @param {String} guildId - the guild id + * @returns {Promise} - the types this user is verified + * @async + * @throws Error if the email provided was not found. + */ + async verify(email, id, guildId) { + let emailLowerCase = email.trim().toLowerCase(); + let userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('email', '==', emailLowerCase).limit(1); + let user = (await userRef.get()).docs[0]; + if (user) { + let returnTypes = []; + + /** @type {FirebaseUser} */ + let data = user.data(); + + data.types.forEach((value, index, array) => { + if (!value.isVerified) { + value.isVerified = true; + value.VerifiedTimestamp = admin.firestore.Timestamp.now(); + returnTypes.push(value.type); + } + }); + + data.discordId = id; + + user.ref.update(data); + + return returnTypes; + } else { + throw new Error('The email provided was not found!'); + } + }, + + /** + * Attends the user via their discord id + * @param {String} id - the user's discord snowflake + * @param {String} guildId - the guild id + * @returns {Promise} - the types this user is verified + * @async + * @throws Error if the email provided was not found. + */ + async attend(id, guildId) { + let userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('discordId', '==', id).limit(1); + let user = (await userRef.get()).docs[0]; + + if (user) { + /** @type {FirebaseUser} */ + let data = user.data(); + + data.types.forEach((value, index, array) => { + if (value.isVerified) { + value.isAttending = true; + value.AttendingTimestamp = admin.firestore.Timestamp.now(); + } + }); + + user.ref.update(data); + } else { + throw new Error('The discord id provided was not found!'); + } + }, + + /** + * check if codex is set to active (very hacky atm, it's just a document in the "codex" collection with a boolean + * field called "active") + * @param {String} guildId + * @returns boolean of whether codex is set to active + */ + async checkCodexActive(guildId) { + let ref = getFactotumDoc().collection('guilds').doc(guildId).collection('codex').doc('active'); + let activeRef = await ref.get(); + const data = activeRef.data(); + return data.active; + }, + + /** + * stores email to firebase collection + * @param {String} guildId + * @param {String} collection - name of collection to store email in + * @param {String} email - user's email + */ + async saveToFirebase(guildId, collection, email) { + let ref = getFactotumDoc().collection('guilds').doc(guildId).collection(collection).doc(email.toLowerCase()); + /** @type {FirebaseUser} */ + let data = { + email: email.toLowerCase() + }; + + await ref.set(data); + }, + + async lookupById(guildId, memberId) { + const userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('discordId', '==', memberId).limit(1); + const user = (await userRef.get()).docs[0]; + return user ? user.data().email : undefined; + }, + + async saveToLeaderboard(guildId, memberId) { + const userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').doc(memberId); + const user = await userRef.get(); + if (user.exists) { + const data = user.data(); + data.points++; + await userRef.update(data); + } else { + const data = { memberId, points: 1 }; + await userRef.set(data); + } + }, + + async retrieveLeaderboard(guildId) { + const snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').get()).docs; + const winners = snapshot.map(doc => doc.data()).sort((a, b) => b.points - a.points); + return winners; + }, }; +function getFactotumDoc() { + return apps.get('nwPlusBotAdmin').firestore().collection('ExternalProjects').doc('Factotum'); +} + /** * Gets the BotGuild document by guild ID * @param {String} guildId From 608a224fa34ef9638a63c1344e79a148d694402e Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 1 Jul 2024 22:11:35 -0700 Subject: [PATCH 41/67] quick env setup switched so only 1 firebase is initialized --- app.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index f9a97e5a..dd0ef374 100644 --- a/app.js +++ b/app.js @@ -2,7 +2,6 @@ require('dotenv-flow').config(); const firebaseUtil = require('./db/firebase/firebaseUtil'); // const Commando = require('discord.js-commando'); const Discord = require('discord.js'); -const firebaseServices = require('./db/firebase/firebase-services'); const winston = require('winston'); const fs = require('fs'); const discordServices = require('./discord-services'); @@ -149,11 +148,8 @@ bot.once('ready', async () => { // initialize firebase const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK); - const adminSDK2 = JSON.parse(process.env.NWPLUSADMINSDK2); - firebaseServices.initializeFirebaseAdmin('nwPlusBotAdmin', adminSDK, 'https://nwplus-bot.firebaseio.com'); - mainLogger.warning('Connected to firebase admin sdk successfully!', { event: 'Ready Event' }); - firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK2, 'https://nwplus-ubc-dev.firebaseio.com'); + firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK, 'https://nwplus-ubc-dev.firebaseio.com'); mainLogger.warning('Connected to nwFirebase successfully!', { event: 'Ready Event' }); firebaseUtil.connect('Factotum'); @@ -422,7 +418,7 @@ async function greetNewMember(member, botGuild) { try { await Verification.verify(member, email, member.guild, botGuild); - if (!askedAboutCodex && await firebaseServices.checkCodexActive(member.guild.id) + if (!askedAboutCodex && await firebaseUtil.checkCodexActive(member.guild.id) && discordServices.checkForRole(member, botGuild.verification.verificationRoles.get('hacker'))) { try { discordServices.askBoolQuestion(member,botGuild, 'One more thing!', From 90109e68e0b50da8d7d3bb40007607e1a58092b2 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Mon, 1 Jul 2024 22:14:16 -0700 Subject: [PATCH 42/67] replaced all instances of firebase-services --- classes/Bot/Features/Verification/verification.js | 6 +++--- commands/a_activity/discord-contests.js | 2 +- commands/a_utility/self-care.js | 2 +- commands/firebase_scripts/load-questions.js | 6 +++--- commands/verification/add-members.js | 2 +- commands/verification/check-email.js | 2 +- commands/verification/get-email.js | 2 +- commands/verification/start-verification.js | 4 ++-- db/firebase/parser.js | 6 +++--- discord-services.js | 9 ++++----- 10 files changed, 20 insertions(+), 21 deletions(-) diff --git a/classes/Bot/Features/Verification/verification.js b/classes/Bot/Features/Verification/verification.js index 2d7f9ae1..58351ac7 100644 --- a/classes/Bot/Features/Verification/verification.js +++ b/classes/Bot/Features/Verification/verification.js @@ -1,6 +1,6 @@ const { GuildMember, Guild } = require('discord.js'); const discordServices = require('../../../../discord-services'); -const firebaseServices = require('../../../../db/firebase/firebase-services'); +const firebaseUtil = require('../../../../db/firebase/firebaseUtil'); const BotGuildModel = require('../../bot-guild'); const winston = require('winston'); @@ -28,7 +28,7 @@ class Verification { // try to get member types, error will mean no email was found try { - var types = await firebaseServices.verify(email, member.id, member.guild.id); + let types = await firebaseUtil.verify(email, member.id, member.guild.id); } catch (error) { logger.warning(`The email provided (${email}) by user ${member.id} was not found, got an error: ${error}`, { event: 'Verification' }); discordServices.sendEmbedToMember(member, { @@ -97,7 +97,7 @@ class Verification { static async attend(member, botGuild) { try { // wait for attend to end, then give role - await firebaseServices.attend(member.id, member.guild.id); + await firebaseUtil.attend(member.id, member.guild.id); discordServices.addRoleToMember(member, botGuild.attendance.attendeeRoleID); discordServices.sendEmbedToMember(member, { title: 'Attendance Success', diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index 228681f5..ca926593 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -1,7 +1,7 @@ const { Command } = require('@sapphire/framework'); const { discordLog, checkForRole } = require('../../discord-services'); const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); -const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebase-services'); +const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 42f3abe8..3ed0e0ca 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -2,7 +2,7 @@ const { Command } = require('@sapphire/framework'); const PermissionCommand = require('../../classes/permission-command'); const { discordLog } = require('../../discord-services'); const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); -const { getReminder } = require('../../db/firebase/firebase-services'); +const { getReminder } = require('../../db/firebase/firebaseUtil'); const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); diff --git a/commands/firebase_scripts/load-questions.js b/commands/firebase_scripts/load-questions.js index d6141a08..88b25fab 100644 --- a/commands/firebase_scripts/load-questions.js +++ b/commands/firebase_scripts/load-questions.js @@ -1,6 +1,6 @@ const { Command } = require('@sapphire/framework'); require('dotenv').config(); -const FirebaseServices = require('../../db/firebase/firebase-services'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); const fetch = require('node-fetch'); /** @@ -45,8 +45,8 @@ class LoadQuestions extends Command { const response = await fetch(file.url); res = await response.json(); - let db = FirebaseServices.apps.get('nwPlusBotAdmin').firestore(); - var count = 0; + let db = firebaseUtil.apps.get('nwPlusBotAdmin').firestore(); + let count = 0; res.forEach(question => { count++; diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index 133cc240..a06f4fea 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -1,7 +1,7 @@ const { Command } = require('@sapphire/framework'); const BotGuild = require('../../db/mongo/BotGuild'); const { Modal, MessageActionRow, TextInputComponent } = require('discord.js'); -const { addUserData } = require('../../db/firebase/firebase-services'); +const { addUserData } = require('../../db/firebase/firebaseUtil'); class AddMembers extends Command { constructor(context, options) { diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index 93a72fac..fed035eb 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -1,4 +1,4 @@ -const { checkEmail } = require("../../db/firebase/firebase-services"); +const { checkEmail } = require("../../db/firebase/firebaseUtil"); const { Command } = require('@sapphire/framework'); const BotGuild = require('../../db/mongo/BotGuild'); diff --git a/commands/verification/get-email.js b/commands/verification/get-email.js index d65045a9..f879b64c 100644 --- a/commands/verification/get-email.js +++ b/commands/verification/get-email.js @@ -1,6 +1,6 @@ const { Command } = require('@sapphire/framework'); const BotGuild = require('../../db/mongo/BotGuild') -const { lookupById } = require('../../db/firebase/firebase-services'); +const { lookupById } = require('../../db/firebase/firebaseUtil'); class GetEmails extends Command { constructor(context, options) { diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index eafb0b48..30762a69 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -2,7 +2,7 @@ const { Command } = require('@sapphire/framework'); const BotGuild = require('../../db/mongo/BotGuild'); const BotGuildModel = require('../../classes/Bot/bot-guild'); const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); -const firebaseServices = require('../../db/firebase/firebase-services'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); const { discordLog } = require('../../discord-services'); class StartVerification extends Command { @@ -90,7 +90,7 @@ class StartVerification extends Command { const email = submitted.fields.getTextInputValue('email'); let types; try { - types = await firebaseServices.verify(email, submitted.user.id, submitted.guild.id); + types = await firebaseUtil.verify(email, submitted.user.id, submitted.guild.id); } catch { submitted.reply({ content: 'Your email could not be found! Please try again or ask an admin for help.', ephemeral: true }); discordLog(interaction.guild, `VERIFY FAILURE : <@${submitted.user.id}> Verified email: ${email} but was a failure, I could not find that email!`); diff --git a/db/firebase/parser.js b/db/firebase/parser.js index 4df5f533..8fa192b4 100644 --- a/db/firebase/parser.js +++ b/db/firebase/parser.js @@ -1,7 +1,7 @@ const csv = require('csv-parser'); const fs = require('fs'); require('dotenv').config(); -const FirebaseServices = require('./firebase-services'); +const firebaseUtil = require('./firebaseUtil'); /** * The firebase parser module has scripts to parse csv data to upload to @@ -11,7 +11,7 @@ const FirebaseServices = require('./firebase-services'); const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK); -let app = FirebaseServices.initializeFirebaseAdmin('factotum', adminSDK, 'https://nwplus-bot.firebaseio.com'); +let app = firebaseUtil.initializeFirebaseAdmin('factotum', adminSDK, 'https://nwplus-bot.firebaseio.com'); // save second argument as type of members to add let type = process.argv[2]; @@ -49,7 +49,7 @@ fs.createReadStream('registrations.csv') // requires a registrations.csv file in all_regs[email] = r; }); - let db = FirebaseServices.apps.get('factotum').firestore(); + let db = firebaseUtil.apps.get('factotum').firestore(); var all = db.collection('guilds').doc(guildId).collection('members').get().then(snapshot => { // get all ids and types of members already in collections and store in idMap diff --git a/discord-services.js b/discord-services.js index f91f191b..1c872a57 100644 --- a/discord-services.js +++ b/discord-services.js @@ -1,7 +1,6 @@ const { GuildMember, TextChannel, Message, User, MessageEmbed, RoleResolvable, Guild } = require('discord.js'); const winston = require('winston'); -const BotGuild = require('./db/mongo/BotGuild'); -const firebaseServices = require('./db/firebase/firebase-services'); +const firebaseUtil = require('./db/firebase/firebaseUtil'); /** * The discord services module has useful discord related functions. @@ -58,7 +57,7 @@ async function sendMessageToMember(member, message, isDelete = false) { if (error.code === 50007) { winston.loggers.get(member?.guild?.id || 'main').warning(`A DM message was sent to user with id ${member.id} but failed, he has been asked to fix this problem!`); let botGuild; - if (member?.guild) botGuild = await BotGuild.findById(member.guild.id); + if (member?.guild) botGuild = await firebaseUtil.getBotGuild(member.guild.id); else { winston.loggers.get(member.guild.id).error('While trying to help a user to get my DMs I could not find a botGuild for which this member is in. I could not help him!'); throw Error(`I could not help ${member.id} due to not finding the guild he is trying to access. I need a member and not a user!`); @@ -173,7 +172,7 @@ module.exports.replaceRoleToMember = replaceRoleToMember; * @async */ async function discordLog(guild, message) { - let botGuild = await BotGuild.findById(guild.id); + let botGuild = await firebaseUtil.getBotGuild(guild.id); if (botGuild?.channelIDs?.adminLog) { guild.channels.resolve(botGuild.channelIDs.adminLog)?.send(message); winston.loggers.get(guild.id).silly(`The following was logged to discord: ${message}`); @@ -296,7 +295,7 @@ async function askBoolQuestion(member, botGuild, title, description, thankYouMes const collector = message.createReactionCollector(filter, { max: 1 }); collector.on('collect', async (reaction, user) => { sendMessageToMember(member, thankYouMessage, false); - await firebaseServices.saveToFirebase(member.guild.id, 'codex', email); + await firebaseUtil.saveToFirebase(member.guild.id, 'codex', email); }); } module.exports.askBoolQuestion = askBoolQuestion; \ No newline at end of file From 0f649196f1c73879cc19ca1708e1b13dccb258d0 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:20:17 -0700 Subject: [PATCH 43/67] Update old get/create BotGuild functions to use new InitBotInfo document --- app.js | 12 ++++---- classes/Bot/activities/activity.js | 4 ++- db/firebase/firebaseUtil.js | 49 ++++++++++++------------------ discord-services.js | 12 ++++---- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/app.js b/app.js index dd0ef374..c9bbff73 100644 --- a/app.js +++ b/app.js @@ -149,7 +149,7 @@ bot.once('ready', async () => { // initialize firebase const adminSDK = JSON.parse(process.env.NWPLUSADMINSDK); - firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK, 'https://nwplus-ubc-dev.firebaseio.com'); + firebaseUtil.initializeFirebaseAdmin('Factotum', adminSDK, process.env.FIREBASE_URL); mainLogger.warning('Connected to nwFirebase successfully!', { event: 'Ready Event' }); firebaseUtil.connect('Factotum'); @@ -159,7 +159,7 @@ bot.once('ready', async () => { // create the logger for the guild createALogger(guild.id, guild.name, false, isLogToConsole); - let botGuild = await firebaseUtil.getBotGuild(guild.id) + let botGuild = await firebaseUtil.getInitBotInfo(guild.id); if (!botGuild) { await newGuild(guild); mainLogger.verbose(`Created a new botGuild for the guild ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' }); @@ -181,7 +181,7 @@ bot.once('ready', async () => { /** * Runs when the bot is added to a guild. */ -bot.on('guildCreate', /** @param {sapphireClient.Guild} guild */(guild) => { +bot.on('guildCreate', /** @param {Discord.Guild} guild */(guild) => { mainLogger.warning(`The bot was added to a new guild: ${guild.id} - ${guild.name}.`, { event: 'Guild Create Event' }); newGuild(guild); @@ -193,7 +193,7 @@ bot.on('guildCreate', /** @param {sapphireClient.Guild} guild */(guild) => { /** * Will set up a new guild. - * @param {sapphireClient.Guild} guild + * @param {Discord.Guild} guild * @private */ async function newGuild(guild) { @@ -202,7 +202,7 @@ async function newGuild(guild) { // if (!group.guarded) guild.setGroupEnabled(group, false); // }); // create a botGuild object for this new guild. - await firebaseUtil.createBotGuild(guild.id); + await firebaseUtil.createInitBotInfoDoc(guild.id); } /** @@ -249,7 +249,7 @@ bot.on('commandError', (command, error, message) => { * Runs when a new member joins a guild the bot is running in. */ bot.on('guildMemberAdd', async member => { - let botGuild = await firebaseUtil.getBotGuild(member.guild.id); + let botGuild = await firebaseUtil.getInitBotInfo(member.guild.id); member.roles.add(botGuild.verification.guestRoleID); // if the guild where the user joined is complete then greet and verify. diff --git a/classes/Bot/activities/activity.js b/classes/Bot/activities/activity.js index e7ccdd79..bfcb70ff 100644 --- a/classes/Bot/activities/activity.js +++ b/classes/Bot/activities/activity.js @@ -64,7 +64,9 @@ class Activity { // add staff role if (isStaffAuto) { - let botGuildData = await firebaseUtil.getBotGuild(channel.guild.id); + let botGuildData = await firebaseUtil.getInitBotInfo( + channel.guild.id + ); let staffRoleId = botGuildData.roleIDs.staffRole; allowedRoles.set(staffRoleId, channel.guild.roles.resolve(staffRoleId)); } diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index cb775061..0aff8a20 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -9,8 +9,8 @@ const admin = require('firebase-admin'); * All the firebase apps in play stored by their name. * @type {Map} */ - const apps = new Map(); - module.exports.apps = apps; +const apps = new Map(); +module.exports.apps = apps; /** * Will start an admin connection with the given name @@ -18,7 +18,7 @@ const admin = require('firebase-admin'); * @param {JSON} adminSDK - the JSON file with admin config * @param {String} databaseURL - the database URL */ - function initializeFirebaseAdmin(name, adminSDK, databaseURL) { +function initializeFirebaseAdmin(name, adminSDK, databaseURL) { let app = admin.initializeApp({ credential: admin.credential.cert(adminSDK), databaseURL: databaseURL, @@ -27,13 +27,12 @@ const admin = require('firebase-admin'); apps.set(name, app); } -// require('firebase/firestore'); /** * The firebase utility module has some useful mongo related helper functions. * @module FirebaseUtil */ -/** @type {Db} */ +/** @type {FirebaseFirestore.Firestore | undefined} */ let _db; module.exports = { @@ -42,7 +41,7 @@ module.exports = { /** * Starts a connection to new firestore */ - async connect(appName) { + async connect(appName) { if (appName) { const app = apps.get(appName); if (!app) { @@ -57,9 +56,6 @@ module.exports = { initializeFirebaseAdmin, - /** - * @returns {Db} - */ getDb() { if (!_db) { throw new Error('Firestore is not initialized. Call connect() first.'); @@ -67,22 +63,19 @@ module.exports = { return _db; }, - /** - * @returns {Collection} - */ getBotGuildCol() { return _db.collection('botGuilds'); }, getExternalProjectsCol() { if (!_db) { - throw new Error('Firestore is not initialized, call connect() first.') + throw new Error('Firestore is not initialized, call connect() first.'); } return _db.collection('ExternalProjects'); }, getFactotumSubCol() { const externalProjectsCol = this.getExternalProjectsCol(); if (!externalProjectsCol) { - throw new Error('ExternalProjects collection is not initialized.') + throw new Error('ExternalProjects collection is not initialized.'); } return externalProjectsCol.doc('Factotum').collection('InitBotInfo'); }, @@ -91,7 +84,7 @@ module.exports = { * @param {String} appName * @returns {Firestore} Firestore instance for the given app */ - getFirestoreInstance(appName) { + getFirestoreInstance(appName) { const app = apps.get(appName); if (!app) { throw new Error(`No Firebase app initialized with the name ${appName}`); @@ -210,7 +203,7 @@ module.exports = { * @returns {Promise} - email of given member * @private */ - async checkName(firstName, lastName, guildId) { + async checkName(firstName, lastName, guildId) { const snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('members').get()).docs; // snapshot of Firestore as array of documents for (const memberDoc of snapshot) { if ( @@ -235,7 +228,7 @@ module.exports = { * @param {String} [lastName=''] - users last name * @async */ - async addUserData(email, type, guildId, overwrite) { + async addUserData(email, type, guildId, overwrite) { const cleanEmail = email.trim().toLowerCase(); const documentRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); const doc = await documentRef.get(); @@ -380,25 +373,23 @@ function getFactotumDoc() { } /** - * Gets the BotGuild document by guild ID - * @param {String} guildId - * @returns {Promise} + * Gets the InitBotInfo document by guild ID + * @param {string} guildId */ - async function getBotGuild(guildId) { - const doc = await module.exports.getBotGuildCol().doc(guildId).get(); +async function getInitBotInfo(guildId) { + const doc = await module.exports.getFactotumSubCol().doc(guildId).get(); return doc.exists ? doc.data() : null; } /** - * Creates a new BotGuild document - * @param {String} guildId - * @returns {Promise} + * Creates a new InitBotInfo document for a new guild + * @param {string} guildId */ -async function createBotGuild(guildId) { - return await module.exports.getBotGuildCol().doc(guildId).set({ +async function createInitBotInfoDoc(guildId) { + return await module.exports.getFactotumSubCol().doc(guildId).set({ _id: guildId, }); } -module.exports.getBotGuild = getBotGuild; -module.exports.createBotGuild = createBotGuild; \ No newline at end of file +module.exports.getInitBotInfo = getInitBotInfo; +module.exports.createInitBotInfoDoc = createInitBotInfoDoc; \ No newline at end of file diff --git a/discord-services.js b/discord-services.js index 1c872a57..406300ce 100644 --- a/discord-services.js +++ b/discord-services.js @@ -56,14 +56,14 @@ async function sendMessageToMember(member, message, isDelete = false) { }).catch(async error => { if (error.code === 50007) { winston.loggers.get(member?.guild?.id || 'main').warning(`A DM message was sent to user with id ${member.id} but failed, he has been asked to fix this problem!`); - let botGuild; - if (member?.guild) botGuild = await firebaseUtil.getBotGuild(member.guild.id); + let initBotInfo; + if (member?.guild) initBotInfo = await firebaseUtil.getInitBotInfo(member.guild.id); else { winston.loggers.get(member.guild.id).error('While trying to help a user to get my DMs I could not find a botGuild for which this member is in. I could not help him!'); throw Error(`I could not help ${member.id} due to not finding the guild he is trying to access. I need a member and not a user!`); } - let botSupportChannel = member.guild.channels.resolve(botGuild.channelIDs.botSupportChannel); + let botSupportChannel = member.guild.channels.resolve(initBotInfo.channelIDs.botSupportChannel); if (botSupportChannel) botSupportChannel.send('<@' + member.id + '> I couldn\'t reach you :(. Please turn on server DMs, explained in this link: https://support.discord.com/hc/en-us/articles/217916488-Blocking-Privacy-Settings-'); } else { throw error; @@ -172,9 +172,9 @@ module.exports.replaceRoleToMember = replaceRoleToMember; * @async */ async function discordLog(guild, message) { - let botGuild = await firebaseUtil.getBotGuild(guild.id); - if (botGuild?.channelIDs?.adminLog) { - guild.channels.resolve(botGuild.channelIDs.adminLog)?.send(message); + let initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + if (initBotInfo?.channelIDs?.adminLog) { + guild.channels.resolve(initBotInfo.channelIDs.adminLog)?.send(message); winston.loggers.get(guild.id).silly(`The following was logged to discord: ${message}`); } else winston.loggers.get(guild.id).error('I was not able to log something to discord!! I could not find the botGuild or the adminLog channel!'); From d6e9075f05458709862d53b395d3f06e3da6d34d Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 8 Jul 2024 00:56:14 -0700 Subject: [PATCH 44/67] Update activity classes and activity+boothing+start commands to use firebase InitBotInfo doc instead of mongodb --- .../Features/Team_Formation/team-formation.js | 815 +++++++++++------- .../Features/Ticket_System/ticket-manager.js | 4 +- classes/Bot/Features/Ticket_System/ticket.js | 4 +- classes/Bot/activities/activity.js | 28 +- classes/Bot/activities/cave.js | 9 +- classes/Bot/activities/workshop.js | 18 +- classes/UI/Room/room.js | 17 +- classes/permission-command.js | 289 ++++--- commands/a_activity/discord-contests.js | 36 +- commands/a_activity/new-workshop.js | 7 +- commands/a_boothing/e-room-directory.js | 7 +- .../start-channel-creation.js | 11 +- .../a_start_commands/start-mentor-cave.js | 39 +- .../a_start_commands/start-team-formation.js | 6 +- .../a_start_commands/start-team-roulette.js | 19 +- 15 files changed, 735 insertions(+), 574 deletions(-) diff --git a/classes/Bot/Features/Team_Formation/team-formation.js b/classes/Bot/Features/Team_Formation/team-formation.js index 75cc30e7..4512f87c 100644 --- a/classes/Bot/Features/Team_Formation/team-formation.js +++ b/classes/Bot/Features/Team_Formation/team-formation.js @@ -1,12 +1,11 @@ const { GuildEmoji, ReactionEmoji, Role, TextChannel, MessageEmbed, Guild, Collection, User, Message, RoleManager } = require('discord.js'); const { sendEmbedToMember, addRoleToMember, deleteMessage, sendMessageToMember, removeRolToMember } = require('../../../../discord-services'); -const BotGuild = require('../../../../db/mongo/BotGuild'); const winston = require('winston'); const Activity = require('../../activities/activity'); -const BotGuildModel = require('../../bot-guild'); const Console = require('../../../UI/Console/console'); const { StringPrompt } = require('advanced-discord.js-prompts'); const Feature = require('../../../UI/Console/feature'); +const firebaseUtil = require('../../../../db/firebase/firebaseUtil') /** * @class TeamFormation @@ -18,342 +17,516 @@ const Feature = require('../../../UI/Console/feature'); * */ class TeamFormation extends Activity { - - static defaultTeamForm = 'Team Member(s): \nTeam Background: \nObjective: \nFun Fact About Team: \nLooking For: '; - static defaultProspectForm = 'Name: \nSchool: \nPlace of Origin: \nSkills: \nFun Fact: \nDeveloper or Designer?:'; - static defaultTeamColor = '#60c2e6'; - static defaultProspectColor = '#d470cd'; + static defaultTeamForm = + "Team Member(s): \nTeam Background: \nObjective: \nFun Fact About Team: \nLooking For: "; + static defaultProspectForm = + "Name: \nSchool: \nPlace of Origin: \nSkills: \nFun Fact: \nDeveloper or Designer?:"; + static defaultTeamColor = "#60c2e6"; + static defaultProspectColor = "#d470cd"; + + /** + * Creates the team role and returns it. + * @param {RoleManager} roleManager + * @returns {Promise} + * @static + * @async + */ + static async createTeamRole(roleManager) { + winston.loggers + .get(roleManager.guild.id) + .verbose(`Team formation team role has been created!`, { + event: "Team Formation", + }); + return await roleManager.create({ + data: { + name: "tf-team-leader", + color: TeamFormation.defaultTeamColor, + }, + }); + } + + /** + * Creates the prospect role and returns it. + * @param {RoleManager} roleManager + * @returns {Promise} + * @static + * @async + */ + static async createProspectRole(roleManager) { + winston.loggers + .get(roleManager.guild.id) + .verbose(`Team formation prospect role has been created!`, { + event: "Team Formation", + }); + return await roleManager.create({ + data: { + name: "tf-prospect", + color: TeamFormation.defaultProspectColor, + }, + }); + } + + /** + * @typedef TeamFormationPartyInfo + * @property {GuildEmoji | ReactionEmoji} emoji - the emoji used to add this party to the team formation + * @property {Role} role - the role given to the users of this party + * @property {String} [form] - the form added to the signup embed for users to respond to. Will not be added if signupEmbed given! + * @property {MessageEmbed} [signupEmbed] - the embed sent to users when they sign up, must include the form! + */ + + /** + * @typedef TeamFormationChannels + * @property {TextChannel} info - the info channel where users read about this activity + * @property {TextChannel} teamCatalogue - the channel where team info is posted + * @property {TextChannel} prospectCatalogue - the channel where prospect info is posted + */ + + /** + * @callback SignupEmbedCreator + * @param {String} teamEmoji - the emoji used by teams to sign up + * @param {String} prospectEmoji - the emoji used by prospects to sign up + * @param {Boolean} isNotificationEnabled - true if parties will be notified when the other party has a new post + * @return {MessageEmbed} + */ + + /** + * @typedef TeamFormationInfo + * @property {TeamFormationPartyInfo} teamInfo + * @property {TeamFormationPartyInfo} prospectInfo + * @property {Guild} guild + * @property {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @property {Collection} activityRoles + * @property {Boolean} [isNotificationsEnabled] + * @property {SignupEmbedCreator} [signupEmbedCreator] + */ + + /** + * Create a new team formation. + * @param {TeamFormationInfo} teamFormationInfo - the team formation information + */ + constructor(teamFormationInfo) { + super({ + activityName: "Team Formation", + guild: teamFormationInfo.guild, + roleParticipants: teamFormationInfo.activityRoles, + initBotInfo: teamFormationInfo.initBotInfo, + }); + + if (!teamFormationInfo?.teamInfo || !teamFormationInfo?.prospectInfo) + throw new Error("Team and prospect info must be given!"); + this.validatePartyInfo(teamFormationInfo.teamInfo); + this.validatePartyInfo(teamFormationInfo.prospectInfo); + if (!teamFormationInfo?.guild) + throw new Error("A guild is required for a team formation!"); /** - * Creates the team role and returns it. - * @param {RoleManager} roleManager - * @returns {Promise} - * @static - * @async + * The team information, those teams willing to join will use this. + * @type {TeamFormationPartyInfo} */ - static async createTeamRole(roleManager) { - winston.loggers.get(roleManager.guild.id).verbose(`Team formation team role has been created!`, { event: "Team Formation" }); - return await roleManager.create({ - data: { - name: 'tf-team-leader', - color: TeamFormation.defaultTeamColor, - } - }); - } - - /** - * Creates the prospect role and returns it. - * @param {RoleManager} roleManager - * @returns {Promise} - * @static - * @async - */ - static async createProspectRole(roleManager) { - winston.loggers.get(roleManager.guild.id).verbose(`Team formation prospect role has been created!`, { event: "Team Formation" }); - return await roleManager.create({ - data: { - name: 'tf-prospect', - color: TeamFormation.defaultProspectColor, - } - }); - } + this.teamInfo = { + emoji: teamFormationInfo.teamInfo.emoji, + role: teamFormationInfo.teamInfo.role, + form: teamFormationInfo.teamInfo?.form || TeamFormation.defaultTeamForm, + signupEmbed: teamFormationInfo.teamInfo?.signupEmbed, + }; /** - * @typedef TeamFormationPartyInfo - * @property {GuildEmoji | ReactionEmoji} emoji - the emoji used to add this party to the team formation - * @property {Role} role - the role given to the users of this party - * @property {String} [form] - the form added to the signup embed for users to respond to. Will not be added if signupEmbed given! - * @property {MessageEmbed} [signupEmbed] - the embed sent to users when they sign up, must include the form! + * The prospect info, those solo users that want to join a team will use this info. + * @type {TeamFormationPartyInfo} */ + this.prospectInfo = { + emoji: teamFormationInfo.prospectInfo.emoji, + role: teamFormationInfo.prospectInfo.role, + form: + teamFormationInfo.prospectInfo?.form || + TeamFormation.defaultProspectForm, + signupEmbed: teamFormationInfo.prospectInfo?.signupEmbed, + }; /** - * @typedef TeamFormationChannels - * @property {TextChannel} info - the info channel where users read about this activity - * @property {TextChannel} teamCatalogue - the channel where team info is posted - * @property {TextChannel} prospectCatalogue - the channel where prospect info is posted + * The channels that a team formation activity needs. + * @type {TeamFormationChannels} */ + this.channels = {}; /** - * @callback SignupEmbedCreator - * @param {String} teamEmoji - the emoji used by teams to sign up - * @param {String} prospectEmoji - the emoji used by prospects to sign up - * @param {Boolean} isNotificationEnabled - true if parties will be notified when the other party has a new post - * @return {MessageEmbed} + * True if the parties will be notified when the opposite party has a new post. + * @type {Boolean} */ + this.isNotificationEnabled = + teamFormationInfo?.isNotificationsEnabled || false; /** - * @typedef TeamFormationInfo - * @property {TeamFormationPartyInfo} teamInfo - * @property {TeamFormationPartyInfo} prospectInfo - * @property {Guild} guild - * @property {BotGuildModel} botGuild - * @property {Collection} activityRoles - * @property {Boolean} [isNotificationsEnabled] - * @property {SignupEmbedCreator} [signupEmbedCreator] - */ - - /** - * Create a new team formation. - * @param {TeamFormationInfo} teamFormationInfo - the team formation information - */ - constructor(teamFormationInfo) { - - super({ - activityName: 'Team Formation', - guild: teamFormationInfo.guild, - roleParticipants: teamFormationInfo.activityRoles, - botGuild: teamFormationInfo.botGuild - }); - - if (!teamFormationInfo?.teamInfo || !teamFormationInfo?.prospectInfo) throw new Error('Team and prospect info must be given!'); - this.validatePartyInfo(teamFormationInfo.teamInfo); - this.validatePartyInfo(teamFormationInfo.prospectInfo); - if (!teamFormationInfo?.guild) throw new Error('A guild is required for a team formation!'); - - /** - * The team information, those teams willing to join will use this. - * @type {TeamFormationPartyInfo} - */ - this.teamInfo = { - emoji : teamFormationInfo.teamInfo.emoji, - role : teamFormationInfo.teamInfo.role, - form: teamFormationInfo.teamInfo?.form || TeamFormation.defaultTeamForm, - signupEmbed : teamFormationInfo.teamInfo?.signupEmbed, - } - - /** - * The prospect info, those solo users that want to join a team will use this info. - * @type {TeamFormationPartyInfo} - */ - this.prospectInfo = { - emoji: teamFormationInfo.prospectInfo.emoji, - role: teamFormationInfo.prospectInfo.role, - form: teamFormationInfo.prospectInfo?.form || TeamFormation.defaultProspectForm, - signupEmbed : teamFormationInfo.prospectInfo?.signupEmbed, - } - - /** - * The channels that a team formation activity needs. - * @type {TeamFormationChannels} - */ - this.channels = {}; - - /** - * True if the parties will be notified when the opposite party has a new post. - * @type {Boolean} - */ - this.isNotificationEnabled = teamFormationInfo?.isNotificationsEnabled || false; - - /** - * A creator of the info embed in case you want it to be different. - * @type {SignupEmbedCreator} - */ - this.signupEmbedCreator = teamFormationInfo?.signupEmbedCreator || null; - - winston.loggers.get(this.guild.id).event(`A Team formation has been created!`, { event: "Team Formation"}); - winston.loggers.get(this.guild.id).verbose(`A Team formation has been created!`, { event: "Team Formation", data: {teamFormationInfo: teamFormationInfo}}); - } - - /** - * Validates a TeamFormationPartyInfo object - * @param {TeamFormationPartyInfo} partyInfo - the party info to validate - * @private + * A creator of the info embed in case you want it to be different. + * @type {SignupEmbedCreator} */ - validatePartyInfo(partyInfo) { - if (!partyInfo?.emoji && typeof partyInfo.emoji != (GuildEmoji || ReactionEmoji)) throw new Error('A Discord emoji is required for a TeamFormationPartyInfo'); - if (!partyInfo?.role && typeof partyInfo.role != Role) throw new Error ('A Discord Role is required in a TeamFormationPartyInfo'); - if (partyInfo.signupEmbed && typeof partyInfo.signupEmbed != MessageEmbed) throw new Error('The message embed must be a Discord Message Embed'); - if (partyInfo.form && typeof partyInfo.form != 'string') throw new Error('The form must be a string!'); - } - - async init() { - await super.init(); - await this.createChannels(); + this.signupEmbedCreator = teamFormationInfo?.signupEmbedCreator || null; + + winston.loggers + .get(this.guild.id) + .event(`A Team formation has been created!`, { event: "Team Formation" }); + winston.loggers + .get(this.guild.id) + .verbose(`A Team formation has been created!`, { + event: "Team Formation", + data: { teamFormationInfo: teamFormationInfo }, + }); + } + + /** + * Validates a TeamFormationPartyInfo object + * @param {TeamFormationPartyInfo} partyInfo - the party info to validate + * @private + */ + validatePartyInfo(partyInfo) { + if ( + !partyInfo?.emoji && + typeof partyInfo.emoji != (GuildEmoji || ReactionEmoji) + ) + throw new Error( + "A Discord emoji is required for a TeamFormationPartyInfo" + ); + if (!partyInfo?.role && typeof partyInfo.role != Role) + throw new Error("A Discord Role is required in a TeamFormationPartyInfo"); + if (partyInfo.signupEmbed && typeof partyInfo.signupEmbed != MessageEmbed) + throw new Error("The message embed must be a Discord Message Embed"); + if (partyInfo.form && typeof partyInfo.form != "string") + throw new Error("The form must be a string!"); + } + + async init() { + await super.init(); + await this.createChannels(); + } + + /** + * Will create the TeamFormationChannels object with new channels to use with a new TeamFormation + * @async + */ + async createChannels() { + this.room.channels.category.setName("🏅Team Formation"); + this.room.channels.generalText.delete(); + this.room.channels.generalVoice.delete(); + + this.channels.info = await this.room.addRoomChannel({ + name: "👀team-formation", + permissions: [ + { + id: this.initBotInfo.roleIDs.everyoneRole, + permissions: { SEND_MESSAGES: false }, + }, + ], + isSafe: true, + }); + + this.channels.prospectCatalogue = await this.room.addRoomChannel({ + name: "🙋🏽prospect-catalogue", + info: { + topic: + "Information about users looking to join teams can be found here. Happy hunting!!!", + }, + permissions: [ + { + id: this.initBotInfo.roleIDs.everyoneRole, + permissions: { SEND_MESSAGES: false }, + }, + ], + isSafe: true, + }); + + this.channels.teamCatalogue = await this.room.addRoomChannel({ + name: "💼team-catalogue", + info: { + topic: + "Channel for teams to post about themselves and who they are looking for! Expect people to send you private messages.", + }, + permissions: [ + { + id: this.initBotInfo.roleIDs.everyoneRole, + permissions: { SEND_MESSAGES: false }, + }, + ], + isSafe: true, + }); + + winston.loggers + .get(this.guild.id) + .verbose(`Team formation channels have been created!`, { + event: "Team Formation", + }); + } + + /** + * Will start the activity! + * @param {SignupEmbedCreator} [signupEmbedCreator] - embed creator for the sign in + * @async + */ + async start(signupEmbedCreator = null) { + let embed; + + if (signupEmbedCreator) { + embed = signupEmbedCreator( + this.teamInfo.emoji, + this.prospectInfo.emoji, + this.isNotificationEnabled + ); + } else { + embed = new MessageEmbed() + .setColor((await firebaseUtil.getInitBotInfo(this.guild.id)).colors.embedColor) + .setTitle("Team Formation Information") + .setDescription( + "Welcome to the team formation section! If you are looking for a team or need a few more members to complete your ultimate group, you are in the right place!" + ) + .addField( + "How does this work?", + "* Once you react to this message, I will send you a template you need to fill out and send back to me via DM. \n* Then I will post your information in the channels below. \n* Then, other members, teams, or yourself can browse these channels and reach out via DM!" + ) + .addField( + "Disclaimer!!", + "By participating in this activity, you consent to other server members sending you a DM." + ) + .addField( + "Teams looking for new members", + "React with " + + this.teamInfo.emoji.toString() + + " and the bot will send you instructions." + ) + .addField( + "Prospects looking for a team", + "React with " + + this.prospectInfo.emoji.toString() + + " and the bot will send you instructions." + ); } - /** - * Will create the TeamFormationChannels object with new channels to use with a new TeamFormation - * @async - */ - async createChannels() { - - this.room.channels.category.setName('🏅Team Formation'); - this.room.channels.generalText.delete(); - this.room.channels.generalVoice.delete(); - - this.channels.info = await this.room.addRoomChannel({ - name: '👀team-formation', - permissions: [{ id: this.botGuild.roleIDs.everyoneRole, permissions: { SEND_MESSAGES: false }}], - isSafe: true, - }); - - this.channels.prospectCatalogue = await this.room.addRoomChannel({ - name: '🙋🏽prospect-catalogue', - info: { - topic: 'Information about users looking to join teams can be found here. Happy hunting!!!', + let signupMsg = await this.channels.info.send(embed); + signupMsg.react(this.teamInfo.emoji); + signupMsg.react(this.prospectInfo.emoji); + + winston.loggers + .get(this.guild.id) + .event( + `The team formation has started. ${ + signupEmbedCreator + ? "A custom embed creator was used" + : "The default embed was used." + }`, + { event: "Team Formation" } + ); + winston.loggers + .get(this.guild.id) + .verbose( + `The team formation has started. ${ + signupEmbedCreator + ? "A custom embed creator was used" + : "The default embed was used." + }`, + { event: "Team Formation", data: { embed: embed } } + ); + + const signupCollector = signupMsg.createReactionCollector( + (reaction, user) => + !user.bot && + (reaction.emoji.name === this.teamInfo.emoji.name || + reaction.emoji.name === this.prospectInfo.emoji.name) + ); + + signupCollector.on("collect", (reaction, user) => { + let isTeam = reaction.emoji.name === this.teamInfo.emoji.name; + winston.loggers + .get(this.guild.id) + .userStats( + `The user ${user.id} is signing up to the team formation as a ${ + isTeam ? "team" : "prospect" + }.`, + { event: "Team Formation" } + ); + this.reachOutToUser(user, isTeam); + }); + } + + /** + * Will reach out to the user to ask for the form response to add to the catalogue. + * @param {User} user - the user joining the team formation activity + * @param {Boolean} isTeam - true if the user represents a team, else false + * @async + */ + async reachOutToUser(user, isTeam) { + let logger = winston.loggers.get(this.guild.id); + + let console = new Console({ + title: `Team Formation - ${isTeam ? "Team Format" : "Prospect Format"}`, + description: + "We are very excited for you to find your perfect " + + (isTeam ? "team members." : "team.") + + "\n* Please **copy and paste** the following format in your next message. " + + "\n* Try to respond to all the sections! \n* Once you are ready to submit, react to this message with 🇩 and then send me your information!\n" + + "* Once you fill your team, please come back and click the ⛔ emoji.", + channel: await user.createDM(), + guild: this.guild, + }); + + if (this.isNotificationEnabled) + console.addField( + "READ THIS!", + "As soon as you submit your form, you will be notified of every new " + + (isTeam ? "available prospect." : "available team.") + + " Once you close your form, you will stop receiving notifications!" + ); + + await console.addField( + "Format:", + isTeam + ? this.teamInfo.form || TeamFormation.defaultTeamForm + : this.prospectInfo.form || TeamFormation.defaultProspectForm + ); + + await console.addFeature( + Feature.create({ + name: "Send completed form", + description: + "React to this emoji, wait for my prompt, and send the finished form.", + emoji: "🇩", + callback: async (user, reaction, stopInteracting, console) => { + // gather and send the form from the user + try { + var catalogueMsg = await this.gatherForm(user, isTeam); + logger.verbose( + `I was able to get the user's team formation response: ${catalogueMsg.cleanContent}`, + { event: "Team Formation" } + ); + } catch (error) { + logger.warning( + `While waiting for a user's team formation response I found an error: ${error}`, + { event: "Team Formation" } + ); + user.dmChannel + .send( + "You have canceled the prompt. You can try again at any time!" + ) + .then((msg) => msg.delete({ timeout: 10000 })); + stopInteracting(); + return; + } + + // confirm the post has been received + sendEmbedToMember( + user, + { + title: "Team Formation", + description: + "Thanks for sending me your information, you should see it pop up in the respective channel under the team formation category." + + `Once you find your ${ + isTeam ? "members" : "ideal team" + } please react to my original message with ⛔ so I can remove your post. Good luck!!!`, }, - permissions: [{ id: this.botGuild.roleIDs.everyoneRole, permissions: { SEND_MESSAGES: false }}], - isSafe: true, - }); - - this.channels.teamCatalogue = await this.room.addRoomChannel({ - name: '💼team-catalogue', - info: { - topic: 'Channel for teams to post about themselves and who they are looking for! Expect people to send you private messages.', - }, - permissions: [{ id: this.botGuild.roleIDs.everyoneRole, permissions: { SEND_MESSAGES: false }}], - isSafe: true, - }); - - winston.loggers.get(this.guild.id).verbose(`Team formation channels have been created!`, { event: "Team Formation" }); - } - - /** - * Will start the activity! - * @param {SignupEmbedCreator} [signupEmbedCreator] - embed creator for the sign in - * @async - */ - async start(signupEmbedCreator = null) { - let embed; - - if (signupEmbedCreator) { - embed = signupEmbedCreator(this.teamInfo.emoji, this.prospectInfo.emoji, this.isNotificationEnabled); - } else { - embed = new MessageEmbed() - .setColor((await (BotGuild.findById(this.guild.id))).colors.embedColor) - .setTitle('Team Formation Information') - .setDescription('Welcome to the team formation section! If you are looking for a team or need a few more members to complete your ultimate group, you are in the right place!') - .addField('How does this work?', '* Once you react to this message, I will send you a template you need to fill out and send back to me via DM. \n* Then I will post your information in the channels below. \n* Then, other members, teams, or yourself can browse these channels and reach out via DM!') - .addField('Disclaimer!!', 'By participating in this activity, you consent to other server members sending you a DM.') - .addField('Teams looking for new members', 'React with ' + this.teamInfo.emoji.toString() + ' and the bot will send you instructions.') - .addField('Prospects looking for a team', 'React with ' + this.prospectInfo.emoji.toString() + ' and the bot will send you instructions.'); - } - - let signupMsg = await this.channels.info.send(embed); - signupMsg.react(this.teamInfo.emoji); - signupMsg.react(this.prospectInfo.emoji); - - winston.loggers.get(this.guild.id).event(`The team formation has started. ${signupEmbedCreator ? 'A custom embed creator was used' : 'The default embed was used.'}`, { event: "Team Formation"}); - winston.loggers.get(this.guild.id).verbose(`The team formation has started. ${signupEmbedCreator ? 'A custom embed creator was used' : 'The default embed was used.'}`, { event: "Team Formation", data: { embed: embed }}); - - const signupCollector = signupMsg.createReactionCollector((reaction, user) => !user.bot && (reaction.emoji.name === this.teamInfo.emoji.name || reaction.emoji.name === this.prospectInfo.emoji.name)); - - signupCollector.on('collect', (reaction, user) => { - let isTeam = reaction.emoji.name === this.teamInfo.emoji.name; - winston.loggers.get(this.guild.id).userStats(`The user ${user.id} is signing up to the team formation as a ${isTeam ? "team" : "prospect"}.`, { event: "Team Formation" }) - this.reachOutToUser(user, isTeam); - }); - } - - /** - * Will reach out to the user to ask for the form response to add to the catalogue. - * @param {User} user - the user joining the team formation activity - * @param {Boolean} isTeam - true if the user represents a team, else false - * @async - */ - async reachOutToUser(user, isTeam) { - let logger = winston.loggers.get(this.guild.id); - - let console = new Console({ - title: `Team Formation - ${isTeam ? 'Team Format' : 'Prospect Format'}`, - description: 'We are very excited for you to find your perfect ' + (isTeam ? 'team members.' : 'team.') + '\n* Please **copy and paste** the following format in your next message. ' + - '\n* Try to respond to all the sections! \n* Once you are ready to submit, react to this message with 🇩 and then send me your information!\n' + - '* Once you fill your team, please come back and click the ⛔ emoji.', - channel: await user.createDM(), - guild: this.guild, - }); - - if (this.isNotificationEnabled) console.addField('READ THIS!', 'As soon as you submit your form, you will be notified of every new ' + (isTeam ? 'available prospect.' : 'available team.') + - ' Once you close your form, you will stop receiving notifications!'); - - await console.addField('Format:', isTeam ? this.teamInfo.form || TeamFormation.defaultTeamForm : this.prospectInfo.form || TeamFormation.defaultProspectForm); - - await console.addFeature(Feature.create({ - name: 'Send completed form', - description: 'React to this emoji, wait for my prompt, and send the finished form.', - emoji: '🇩', - callback: async (user, reaction, stopInteracting, console) => { - // gather and send the form from the user - try { - var catalogueMsg = await this.gatherForm(user, isTeam); - logger.verbose(`I was able to get the user's team formation response: ${catalogueMsg.cleanContent}`, { event: "Team Formation" }); - } catch (error) { - logger.warning(`While waiting for a user's team formation response I found an error: ${error}`, { event: "Team Formation" }); - user.dmChannel.send('You have canceled the prompt. You can try again at any time!').then(msg => msg.delete({timeout: 10000})); - stopInteracting(); - return; - } - - // confirm the post has been received - sendEmbedToMember(user, { - title: 'Team Formation', - description: 'Thanks for sending me your information, you should see it pop up in the respective channel under the team formation category.' + - `Once you find your ${isTeam ? 'members' : 'ideal team'} please react to my original message with ⛔ so I can remove your post. Good luck!!!`, - }, 15); - logger.event(`The user ${user.id} has successfully sent their information to the team formation feature.`, { event: "Team Formation" }); - - // add role to the user - addRoleToMember(this.guild.member(user), isTeam ? this.teamInfo.role : this.prospectInfo.role); - - // add remove post feature - await console.addFeature(Feature.create({ - name: 'Done with team formation!', - description: 'React with this emoji if you are done with team formation.', - emoji: '⛔', - callback: (user, reaction, stopInteracting, console) => { - // remove message sent to channel - deleteMessage(catalogueMsg); - - // confirm deletion - sendMessageToMember(user, 'This is great! You are all set! Have fun with your new team! Your message has been deleted.', true); - - removeRolToMember(this.guild.member(user), isTeam ? this.teamInfo.role : this.prospectInfo.role); - - logger.event(`The user ${user.id} has found a team and has been removed from the team formation feature.`, { event: "Team Formation" }); - - console.delete(); - } - })); - - console.removeFeature('🇩'); - - stopInteracting(); - } - })); - - console.sendConsole(); - } - - /** - * Will gather the form from a user to add to the catalogues and send it to the correct channel. - * @param {User} user - the user being prompted - * @param {Boolean} isTeam - true if the user is a team looking for members - * @returns {Promise} - the catalogue message - * @async - * @throws Error if user cancels or takes too long to respond to prompt - */ - async gatherForm(user, isTeam) { - - var formContent = await StringPrompt.single({ - prompt: 'Please send me your completed form, if you do not follow the form your post will be deleted!', - channel: user.dmChannel, userId: user.id, time: 30, cancelable: true, - }); - - const embed = new MessageEmbed() - .setTitle('Information about them can be found below:') - .setDescription(formContent + '\nDM me to talk -> <@' + user.id + '>') - .setColor(isTeam ? this.teamInfo.role.hexColor : this.prospectInfo.role.hexColor); - - let channel = isTeam ? this.channels.teamCatalogue : this.channels.prospectCatalogue; - - let catalogueMsg = await channel.send( - (this.isNotificationEnabled ? '<@&' + (isTeam ? this.prospectInfo.role.id : this.teamInfo.role.id) + '>, ' : '') + - '<@' + user.id + (isTeam ? '> and their team are looking for more team members!' : '> is looking for a team to join!'), {embed: embed}); - - winston.loggers.get(channel.guild.id).verbose(`A message with the user's information has been sent to the channel ${channel.name} with id ${channel.id}.`, { event: "Team Formation", data: embed}); - - return catalogueMsg; - } - + 15 + ); + logger.event( + `The user ${user.id} has successfully sent their information to the team formation feature.`, + { event: "Team Formation" } + ); + + // add role to the user + addRoleToMember( + this.guild.member(user), + isTeam ? this.teamInfo.role : this.prospectInfo.role + ); + + // add remove post feature + await console.addFeature( + Feature.create({ + name: "Done with team formation!", + description: + "React with this emoji if you are done with team formation.", + emoji: "⛔", + callback: (user, reaction, stopInteracting, console) => { + // remove message sent to channel + deleteMessage(catalogueMsg); + + // confirm deletion + sendMessageToMember( + user, + "This is great! You are all set! Have fun with your new team! Your message has been deleted.", + true + ); + + removeRolToMember( + this.guild.member(user), + isTeam ? this.teamInfo.role : this.prospectInfo.role + ); + + logger.event( + `The user ${user.id} has found a team and has been removed from the team formation feature.`, + { event: "Team Formation" } + ); + + console.delete(); + }, + }) + ); + + console.removeFeature("🇩"); + + stopInteracting(); + }, + }) + ); + + console.sendConsole(); + } + + /** + * Will gather the form from a user to add to the catalogues and send it to the correct channel. + * @param {User} user - the user being prompted + * @param {Boolean} isTeam - true if the user is a team looking for members + * @returns {Promise} - the catalogue message + * @async + * @throws Error if user cancels or takes too long to respond to prompt + */ + async gatherForm(user, isTeam) { + var formContent = await StringPrompt.single({ + prompt: + "Please send me your completed form, if you do not follow the form your post will be deleted!", + channel: user.dmChannel, + userId: user.id, + time: 30, + cancelable: true, + }); + + const embed = new MessageEmbed() + .setTitle("Information about them can be found below:") + .setDescription(formContent + "\nDM me to talk -> <@" + user.id + ">") + .setColor( + isTeam ? this.teamInfo.role.hexColor : this.prospectInfo.role.hexColor + ); + + let channel = isTeam + ? this.channels.teamCatalogue + : this.channels.prospectCatalogue; + + let catalogueMsg = await channel.send( + (this.isNotificationEnabled + ? "<@&" + + (isTeam ? this.prospectInfo.role.id : this.teamInfo.role.id) + + ">, " + : "") + + "<@" + + user.id + + (isTeam + ? "> and their team are looking for more team members!" + : "> is looking for a team to join!"), + { embed: embed } + ); + + winston.loggers + .get(channel.guild.id) + .verbose( + `A message with the user's information has been sent to the channel ${channel.name} with id ${channel.id}.`, + { event: "Team Formation", data: embed } + ); + + return catalogueMsg; + } } module.exports = TeamFormation; \ No newline at end of file diff --git a/classes/Bot/Features/Ticket_System/ticket-manager.js b/classes/Bot/Features/Ticket_System/ticket-manager.js index 1e3eea46..27892645 100644 --- a/classes/Bot/Features/Ticket_System/ticket-manager.js +++ b/classes/Bot/Features/Ticket_System/ticket-manager.js @@ -208,7 +208,7 @@ class TicketManager { // check if role has mentors in it if (role.members.size <= 0) { sendMsgToChannel(channel, user.id, 'There are no mentors available with that role. Please request another role or the general role!', 10); - winston.loggers.get(this.parent.botGuild._id).userStats(`The cave ${this.parent.name} received a ticket from user ${user.id} but was canceled due to no mentor having the role ${role.name}.`, { event: 'Ticket Manager' }); + winston.loggers.get(this.parent.initBotInfo._id).userStats(`The cave ${this.parent.name} received a ticket from user ${user.id} but was canceled due to no mentor having the role ${role.name}.`, { event: 'Ticket Manager' }); return; } @@ -216,7 +216,7 @@ class TicketManager { var promptMsg = await MessagePrompt.prompt({prompt: 'Please send ONE message with: \n* A one liner of your problem ' + '\n* Mention your team members using @friendName (example: @John).', channel, userId: user.id, cancelable: true, time: 45}); } catch (error) { - winston.loggers.get(this.parent.botGuild._id).warning(`New ticket was canceled due to error: ${error}`, { event: 'Ticket Manager' }); + winston.loggers.get(this.parent.initBotInfo._id).warning(`New ticket was canceled due to error: ${error}`, { event: 'Ticket Manager' }); return; } diff --git a/classes/Bot/Features/Ticket_System/ticket.js b/classes/Bot/Features/Ticket_System/ticket.js index 361aad9f..353389ee 100644 --- a/classes/Bot/Features/Ticket_System/ticket.js +++ b/classes/Bot/Features/Ticket_System/ticket.js @@ -43,7 +43,7 @@ class Ticket { * @type {Room} */ this.room = ticketManager.systemWideTicketInfo.isAdvancedMode ? - new Room(ticketManager.parent.guild, ticketManager.parent.botGuild, `Ticket-${ticketNumber}`, new Collection(), hackers.clone()) : + new Room(ticketManager.parent.guild, ticketManager.parent.initBotInfo, `Ticket-${ticketNumber}`, new Collection(), hackers.clone()) : null; /** @@ -269,7 +269,7 @@ class Ticket { title: 'Original Question', description: `<@${this.group.first().id}> has the question: ${this.question}`, channel: this.room.channels.generalText, - color: this.ticketManager.parent.botGuild.colors.embedColor, + color: this.ticketManager.parent.initBotInfo.colors.embedColor, guild: this.ticketManager.parent.guild, }); diff --git a/classes/Bot/activities/activity.js b/classes/Bot/activities/activity.js index bfcb70ff..42681103 100644 --- a/classes/Bot/activities/activity.js +++ b/classes/Bot/activities/activity.js @@ -14,7 +14,7 @@ const Feature = require('../../UI/Console/feature'); * @property {string} activityName - the name of this activity! * @property {Guild} guild - the guild where the new activity lives * @property {Collection} roleParticipants - roles allowed to view activity - * @property {BotGuildModel} botGuild + * @property {FirebaseFirestore.DocumentData | null | undefined} initBotInfo */ /** @@ -79,7 +79,7 @@ class Activity { * @constructor * @param {ActivityInfo} ActivityInfo */ - constructor({ activityName, guild, roleParticipants, botGuild }) { + constructor({ activityName, guild, roleParticipants, initBotInfo }) { /** * The name of this activity. * @type {string} @@ -96,7 +96,7 @@ class Activity { * The room this activity lives in. * @type {Room} */ - this.room = new Room(guild, botGuild, activityName, roleParticipants); + this.room = new Room(guild, initBotInfo, activityName, roleParticipants); /** * The admin console with activity features. @@ -105,15 +105,15 @@ class Activity { this.adminConsole = new Console({ title: `Activity ${activityName} Console`, description: 'This activity\'s information can be found below, you can also find the features available.', - channel: guild.channels.resolve(botGuild.channelIDs.adminConsole), + channel: guild.channels.resolve(initBotInfo.channelIDs.adminConsole), guild: this.guild, }); /** - * The mongoose BotGuildModel Object - * @type {BotGuildModel} + * The Firestore bot info object + * @type {FirebaseFirestore.DocumentData | null | undefined} */ - this.botGuild = botGuild; + this.initBotInfo = initBotInfo; winston.loggers.get(guild.id).event(`An activity named ${this.name} was created.`, { data: { permissions: roleParticipants } }); } @@ -165,7 +165,7 @@ class Activity { description: 'Archive the activity, text channels are saved.', emoji: '💼', callback: (user, reaction, stopInteracting, console) => { - let archiveCategory = this.guild.channels.resolve(this.botGuild.channelIDs.archiveCategory); + let archiveCategory = this.guild.channels.resolve(this.initBotInfo.channelIDs.archiveCategory); this.archive(archiveCategory); } }, @@ -372,7 +372,7 @@ class Activity { */ async distributeStamp(channel, userId) { - if (!this.botGuild.stamps.isEnabled) { + if (!this.initBotInfo.stamps.isEnabled) { sendMsgToChannel(channel, userId, 'The stamp system is not enabled in this server!', 10); return; } @@ -381,8 +381,8 @@ class Activity { let seenUsers = new Collection(); const promptEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) - .setTitle('React within ' + this.botGuild.stamps.stampCollectionTime + ' seconds of the posting of this message to get a stamp for ' + this.name + '!'); + .setColor(this.initBotInfo.colors.embedColor) + .setTitle('React within ' + this.initBotInfo.stamps.stampCollectionTime + ' seconds of the posting of this message to get a stamp for ' + this.name + '!'); // send embed to general text or prompt for channel let promptMsg; @@ -399,14 +399,14 @@ class Activity { promptMsg.react('👍'); // reaction collector, time is needed in milliseconds, we have it in seconds - const collector = promptMsg.createReactionCollector((reaction, user) => !user.bot, { time: (1000 * this.botGuild.stamps.stampCollectionTime) }); + const collector = promptMsg.createReactionCollector((reaction, user) => !user.bot, { time: (1000 * this.initBotInfo.stamps.stampCollectionTime) }); collector.on('collect', async (reaction, user) => { // grab the member object of the reacted user const member = this.guild.member(user); if (!seenUsers.has(user.id)) { - StampsManager.parseRole(member, this.name, this.botGuild); + StampsManager.parseRole(member, this.name, this.initBotInfo); seenUsers.set(user.id, user.username); } }); @@ -433,7 +433,7 @@ class Activity { let joinEmoji = '🚗'; - const embed = new MessageEmbed().setTitle('Activity Rules').setDescription(rules).addField('To join the activity:', `React to this message with ${joinEmoji}`).setColor(this.botGuild.colors.embedColor); + const embed = new MessageEmbed().setTitle('Activity Rules').setDescription(rules).addField('To join the activity:', `React to this message with ${joinEmoji}`).setColor(this.initBotInfo.colors.embedColor); const embedMsg = await rulesChannel.send(embed); diff --git a/classes/Bot/activities/cave.js b/classes/Bot/activities/cave.js index 2c611e09..6196eded 100644 --- a/classes/Bot/activities/cave.js +++ b/classes/Bot/activities/cave.js @@ -1,6 +1,5 @@ const { Guild, Collection, Role, TextChannel, MessageEmbed, GuildEmoji, ReactionEmoji, CategoryChannel } = require('discord.js'); const { sendMsgToChannel, addRoleToMember, removeRolToMember } = require('../../../discord-services'); -const BotGuildModel = require('../bot-guild'); const Room = require('../../UI/Room/room'); const Console = require('../../UI/Console/console'); const Feature = require('../../UI/Console/feature'); @@ -54,15 +53,15 @@ class Cave extends Activity { /** * @constructor * @param {CaveOptions} caveOptions - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Guild} guild */ - constructor(caveOptions, botGuild, guild) { + constructor(caveOptions, initBotInfo, guild) { super({ activityName: caveOptions.name, guild: guild, roleParticipants: new Collection([[caveOptions.role.id, caveOptions.role]]), - botGuild: botGuild, + initBotInfo, }); /** @@ -92,7 +91,7 @@ class Cave extends Activity { * The public room for this cave. * @type {Room} */ - this.publicRoom = new Room(guild, botGuild, `👉🏽👈🏽${caveOptions.name} Help`, caveOptions.publicRoles); + this.publicRoom = new Room(guild, initBotInfo, `👉🏽👈🏽${caveOptions.name} Help`, caveOptions.publicRoles); /** * The console where cave members can get sub roles. diff --git a/classes/Bot/activities/workshop.js b/classes/Bot/activities/workshop.js index bb562acb..14b57863 100644 --- a/classes/Bot/activities/workshop.js +++ b/classes/Bot/activities/workshop.js @@ -34,8 +34,8 @@ class Workshop extends Activity { * @param {Boolean} [isLowTechSolution=true] * @param {Collection} [TARoles] - roles with TA permissions */ - constructor({activityName, guild, roleParticipants, botGuild}, isLowTechSolution = true, TARoles) { - super({activityName, guild, roleParticipants, botGuild}); + constructor({activityName, guild, roleParticipants, initBotInfo}, isLowTechSolution = true, TARoles) { + super({activityName, guild, roleParticipants, initBotInfo}); /** * @type {Collection} - roles with TA permissions @@ -117,8 +117,8 @@ class Workshop extends Activity { isSafe: true, }); - this.botGuild.blackList.set(this.assistanceChannel.id, 3000); - this.botGuild.save(); + this.initBotInfo.blackList.set(this.assistanceChannel.id, 3000); + this.initBotInfo.save(); if (this.isLowTechSolution) { this.ticketManager = new TicketManager(this, { @@ -151,7 +151,7 @@ class Workshop extends Activity { }, isAdvancedMode: false, } - }, this.guild, this.botGuild); + }, this.guild, this.initBotInfo); } winston.loggers.get(this.guild.id).event(`The activity ${this.name} was transformed to a workshop.`, {event: 'Activity'}); @@ -275,7 +275,7 @@ class Workshop extends Activity { // where users can request assistance const outgoingTicketEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle(this.name + ' Help Desk') .setDescription('Welcome to the ' + this.name + ' help desk. There are two ways to get help explained below:') .addField('Simple or Theoretical Questions', 'If you have simple or theory questions, ask them in the main banter channel!') @@ -328,7 +328,7 @@ class Workshop extends Activity { getTAChannelPermissions() { /** The permissions for the TA channels */ let TAChannelPermissions = [ - { id: this.botGuild.roleIDs.everyoneRole, permissions: { VIEW_CHANNEL: false } }, + { id: this.initBotInfo.roleIDs.everyoneRole, permissions: { VIEW_CHANNEL: false } }, ]; // add regular activity members to the TA perms list as non tas, so they cant see that channel @@ -367,7 +367,7 @@ class Workshop extends Activity { } let qEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle(poll.title) .setDescription(description); @@ -483,7 +483,7 @@ class Workshop extends Activity { let oneLiner = await StringPrompt.single({prompt: 'Please send to this channel a one-liner of your problem or question. You have 20 seconds to respond', channel: this.assistanceChannel, userId: user.id }); const hackerEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('Hey there! We got you signed up to talk to a TA!') .setDescription('You are number: ' + position + ' in the wait list.') .addField(!this.isLowTechSolution ? 'JOIN THE VOICE CHANNEL!' : 'KEEP AN EYE ON YOUR DMs', diff --git a/classes/UI/Room/room.js b/classes/UI/Room/room.js index c3830b66..fc56e670 100644 --- a/classes/UI/Room/room.js +++ b/classes/UI/Room/room.js @@ -1,6 +1,5 @@ const winston = require('winston'); const { GuildCreateChannelOptions, User, Guild, Collection, Role, CategoryChannel, VoiceChannel, TextChannel, OverwriteResolvable, PermissionOverwriteOption } = require('discord.js'); -const BotGuildModel = require('../../Bot/bot-guild'); const { deleteChannel } = require('../../../discord-services'); /** @@ -30,12 +29,12 @@ class Room { /** * @param {Guild} guild - the guild in which the room lives - * @param {BotGuildModel} botGuild - the botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {String} name - name of the room * @param {Collection} [rolesAllowed=Collection()] - the participants able to view this room * @param {Collection} [usersAllowed=Collection()] - the individual users allowed to see the room */ - constructor(guild, botGuild, name, rolesAllowed = new Collection(), usersAllowed = new Collection()) { + constructor(guild, initBotInfo, name, rolesAllowed = new Collection(), usersAllowed = new Collection()) { /** * The name of this room. Will remove all leading and trailing whitespace and @@ -78,10 +77,10 @@ class Room { }; /** - * The mongoose BotGuildModel Object - * @type {BotGuildModel} + * The Firestore bot info object + * @type {FirebaseFirestore.DocumentData | null | undefined} */ - this.botGuild = botGuild; + this.initBotInfo = initBotInfo; /** * True if the room is locked, false otherwise. @@ -152,7 +151,7 @@ class Room { /** @type {OverwriteResolvable[]} */ let overwrites = [ { - id: this.botGuild.roleIDs.everyoneRole, + id: this.initBotInfo.roleIDs.everyoneRole, deny: ['VIEW_CHANNEL'], }]; this.rolesAllowed.each(role => overwrites.push({ id: role.id, allow: ['VIEW_CHANNEL'] })); @@ -238,7 +237,7 @@ class Room { // remove all voice channels in the category one at a time to not get a UI glitch this.channels.category.children.forEach(async (channel, key) => { - this.botGuild.blackList.delete(channel.id); + this.initBotInfo.blackList.delete(channel.id); if (channel.type === 'text') { let channelName = channel.name; await channel.setName(`${this.name}-${channelName}`); @@ -248,7 +247,7 @@ class Room { await deleteChannel(this.channels.category); - this.botGuild.save(); + this.initBotInfo.save(); winston.loggers.get(this.guild.id).event(`The activity ${this.name} was archived!`, {event: 'Activity'}); } diff --git a/classes/permission-command.js b/classes/permission-command.js index c2669478..8ec3bd5b 100644 --- a/classes/permission-command.js +++ b/classes/permission-command.js @@ -1,146 +1,145 @@ -const Discord = require('discord.js'); -const { Command, CommandoMessage, CommandoClientOptions, CommandInfo } = require('discord.js-commando'); -const BotGuild = require('../db/mongo/BotGuild'); -const BotGuildModel = require('./Bot/bot-guild'); -const discordServices = require('../discord-services'); -const winston = require('winston'); - -/** - * The PermissionCommand is a custom command that extends the discord js commando Command class. - * This Command subclass adds role and channel permission checks before the command is run. It also - * removes the message used to call the command. - * @extends Command - */ -class PermissionCommand extends Command { - - /** - * Our custom command information for validation - * @typedef {Object} CommandPermissionInfo - * @property {string} role - the role this command can be run by, one of FLAGS - * @property {string} channel - the channel where this command can be run, one of FLAGS - * @property {string} roleMessage - the message to be sent for an incorrect role - * @property {string} channelMessage - the message to be sent for an incorrect channel - * @property {Boolean} dmOnly - true if this command can only be used on a DM - */ - - /** - * Constructor for our custom command, calls the parent constructor. - * @param {CommandoClientOptions} client - the client the command is for - * @param {CommandInfo} info - the information for this commando command - * @param {CommandPermissionInfo} permissionInfo - the custom information for this command - */ - constructor(client, info, permissionInfo) { - super(client, info); - - /** - * The permission info - * @type {CommandPermissionInfo} - * @private - */ - this.permissionInfo = this.validateInfo(permissionInfo); - } - - /** - * Adds default values if not found on the object. - * @param {CommandPermissionInfo} permissionInfo - * @returns {CommandPermissionInfo} - * @private - */ - validateInfo(permissionInfo) { - // Make sure permissionInfo is an object, if not given then create empty object - if (typeof permissionInfo != 'object') permissionInfo = {}; - if (!permissionInfo?.channelMessage) permissionInfo.channelMessage = 'Hi, the command you just used is not available on that channel!'; - if (!permissionInfo?.roleMessage) permissionInfo.roleMessage = 'Hi, the command you just used is not available to your current role!'; - permissionInfo.dmOnly = permissionInfo?.dmOnly ?? false; - return permissionInfo; - } - - - /** - * Run command used by Command class. Has the permission checks and runs the child runCommand method. - * @param {Discord.Message} message - * @param {Object|string|string[]} args - * @param {boolean} fromPattern - * @param {Promise>} result - * @override - * @private - */ - async run(message, args, fromPattern, result){ - - // delete the message - discordServices.deleteMessage(message); - - /** @type {BotGuildModel} */ - let botGuild; - if (message?.guild) botGuild = await BotGuild.findById(message.guild.id); - else botGuild = null; - - // check for DM only, when true, all other checks should not happen! - if (this.permissionInfo.dmOnly) { - if (message.channel.type != 'dm') { - discordServices.sendEmbedToMember(message.member, { - title: 'Error', - description: 'The command you just tried to use is only usable via DM!', - }); - winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); - return; - } - } else { - // Make sure it is only used in the permitted channel - if (this.permissionInfo?.channel) { - let channelID = botGuild.channelIDs[this.permissionInfo.channel]; - - if (channelID && message.channel.id != channelID) { - discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true); - winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); - return; - } - } - // Make sure only the permitted role can call it - else if (this.permissionInfo?.role) { - - let roleID = botGuild.roleIDs[this.permissionInfo.role]; - - // if staff role then check for staff and admin, else check the given role - if (roleID && (roleID === botGuild.roleIDs.staffRole && - (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, botGuild.roleIDs.adminRole))) || - (roleID != botGuild.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) { - discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true); - winston.loggers.get(botGuild?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); - return; - } - } - } - this.runCommand(botGuild, message, args, fromPattern, result); - } - - - /** - * Required class by children, will throw error if not implemented! - * @param {BotGuildModel} botGuild - * @param {CommandoMessage} message - * @param {Object} args - * @param {Boolean} fromPattern - * @param {Promise<*>} result - * @abstract - * @protected - */ - runCommand(botGuild, message, args, fromPattern, result) { - throw new Error('You need to implement the runCommand method!'); - } -} - -/** - * String permission flags used for command permissions. - * * ADMIN_ROLE : only admins can use this command - * * STAFF_ROLE : staff and admin can use this command - * * ADMIN_CONSOLE : can only be used in the admin console - * @enum {String} - */ -PermissionCommand.FLAGS = { - ADMIN_ROLE: 'adminRole', - STAFF_ROLE: 'staffRole', - ADMIN_CONSOLE: 'adminConsole', -}; - +const Discord = require('discord.js'); +const { Command, CommandoMessage, CommandoClientOptions, CommandInfo } = require('discord.js-commando'); +const firebaseUtil = require('../db/firebase/firebaseUtil'); +const discordServices = require('../discord-services'); +const winston = require('winston'); + +/** + * The PermissionCommand is a custom command that extends the discord js commando Command class. + * This Command subclass adds role and channel permission checks before the command is run. It also + * removes the message used to call the command. + * @extends Command + */ +class PermissionCommand extends Command { + + /** + * Our custom command information for validation + * @typedef {Object} CommandPermissionInfo + * @property {string} role - the role this command can be run by, one of FLAGS + * @property {string} channel - the channel where this command can be run, one of FLAGS + * @property {string} roleMessage - the message to be sent for an incorrect role + * @property {string} channelMessage - the message to be sent for an incorrect channel + * @property {Boolean} dmOnly - true if this command can only be used on a DM + */ + + /** + * Constructor for our custom command, calls the parent constructor. + * @param {CommandoClientOptions} client - the client the command is for + * @param {CommandInfo} info - the information for this commando command + * @param {CommandPermissionInfo} permissionInfo - the custom information for this command + */ + constructor(client, info, permissionInfo) { + super(client, info); + + /** + * The permission info + * @type {CommandPermissionInfo} + * @private + */ + this.permissionInfo = this.validateInfo(permissionInfo); + } + + /** + * Adds default values if not found on the object. + * @param {CommandPermissionInfo} permissionInfo + * @returns {CommandPermissionInfo} + * @private + */ + validateInfo(permissionInfo) { + // Make sure permissionInfo is an object, if not given then create empty object + if (typeof permissionInfo != 'object') permissionInfo = {}; + if (!permissionInfo?.channelMessage) permissionInfo.channelMessage = 'Hi, the command you just used is not available on that channel!'; + if (!permissionInfo?.roleMessage) permissionInfo.roleMessage = 'Hi, the command you just used is not available to your current role!'; + permissionInfo.dmOnly = permissionInfo?.dmOnly ?? false; + return permissionInfo; + } + + + /** + * Run command used by Command class. Has the permission checks and runs the child runCommand method. + * @param {Discord.Message} message + * @param {Object|string|string[]} args + * @param {boolean} fromPattern + * @param {Promise>} result + * @override + * @private + */ + async run(message, args, fromPattern, result){ + + // delete the message + discordServices.deleteMessage(message); + + /** @type {BotGuildModel} */ + let initBotInfo; + if (message?.guild) initBotInfo = await firebaseUtil.getInitBotInfo(message.guild.id); + else initBotInfo = null; + + // check for DM only, when true, all other checks should not happen! + if (this.permissionInfo.dmOnly) { + if (message.channel.type != 'dm') { + discordServices.sendEmbedToMember(message.member, { + title: 'Error', + description: 'The command you just tried to use is only usable via DM!', + }); + winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); + return; + } + } else { + // Make sure it is only used in the permitted channel + if (this.permissionInfo?.channel) { + let channelID = initBotInfo.channelIDs[this.permissionInfo.channel]; + + if (channelID && message.channel.id != channelID) { + discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true); + winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); + return; + } + } + // Make sure only the permitted role can call it + else if (this.permissionInfo?.role) { + + let roleID = initBotInfo.roleIDs[this.permissionInfo.role]; + + // if staff role then check for staff and admin, else check the given role + if (roleID && (roleID === initBotInfo.roleIDs.staffRole && + (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, initBotInfo.roleIDs.adminRole))) || + (roleID != initBotInfo.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) { + discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true); + winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); + return; + } + } + } + this.runCommand(initBotInfo, message, args, fromPattern, result); + } + + + /** + * Required class by children, will throw error if not implemented! + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {CommandoMessage} message + * @param {Object} args + * @param {Boolean} fromPattern + * @param {Promise<*>} result + * @abstract + * @protected + */ + runCommand(initBotInfo, message, args, fromPattern, result) { + throw new Error('You need to implement the runCommand method!'); + } +} + +/** + * String permission flags used for command permissions. + * * ADMIN_ROLE : only admins can use this command + * * STAFF_ROLE : staff and admin can use this command + * * ADMIN_CONSOLE : can only be used in the admin console + * @enum {String} + */ +PermissionCommand.FLAGS = { + ADMIN_ROLE: 'adminRole', + STAFF_ROLE: 'staffRole', + ADMIN_CONSOLE: 'adminConsole', +}; + module.exports = PermissionCommand; \ No newline at end of file diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index ca926593..a851cdfa 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -2,8 +2,7 @@ const { Command } = require('@sapphire/framework'); const { discordLog, checkForRole } = require('../../discord-services'); const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); -const BotGuild = require('../../db/mongo/BotGuild'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); /** * The DiscordContests class handles all functions related to Discord contests. It will ask questions in set intervals and pick winners @@ -60,10 +59,10 @@ class DiscordContests extends Command { let userId = interaction.user.id; // this.botGuild = this.botGuild; let guild = interaction.guild; - this.botGuild = await BotGuild.findById(guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); - let adminLog = guild.channels.resolve(this.botGuild.channelIDs.adminLog); - let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + let adminLog = guild.channels.resolve(this.initBotInfo.channelIDs.adminLog); + let adminConsole = guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); var interval; @@ -72,12 +71,12 @@ class DiscordContests extends Command { var startNow = interaction.options.getBoolean('start_question_now'); var roleId = interaction.options.getRole('notify'); - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } - if (Object.values(this.botGuild.roleIDs).includes(roleId) || Object.values(this.botGuild.verification.verificationRoles).includes(roleId)) { + if (Object.values(this.initBotInfo.roleIDs).includes(roleId) || Object.values(this.initBotInfo.verification.verificationRoles).includes(roleId)) { interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); return; } @@ -141,7 +140,7 @@ class DiscordContests extends Command { // }); const startEmbed = new MessageEmbed() - .setColor(this.botGuild.embedColor) + .setColor(this.initBotInfo.embedColor) .setTitle(string) .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); @@ -168,7 +167,7 @@ class DiscordContests extends Command { interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); adminLog.send('Discord contests started by <@' + userId + '>'); - const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); + const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.adminRole)); const collector = controlPanel.createMessageComponentCollector({filter}); collector.on('collect', async i => { if (i.customId == 'refresh') { @@ -180,8 +179,8 @@ class DiscordContests extends Command { await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); } else if (paused && i.customId == 'play') { - await sendQuestion(this.botGuild); - interval = setInterval(sendQuestion, timeInterval, this.botGuild); + await sendQuestion(this.initBotInfo); + interval = setInterval(sendQuestion, timeInterval, this.initBotInfo); paused = false; await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); @@ -192,9 +191,9 @@ class DiscordContests extends Command { //starts the interval, and sends the first question immediately if startNow is true if (startNow) { - await sendQuestion(this.botGuild); + await sendQuestion(this.initBotInfo); } - interval = setInterval(sendQuestion, timeInterval, this.botGuild); + interval = setInterval(sendQuestion, timeInterval, this.initBotInfo); async function updateLeaderboard(memberId) { if (memberId) { @@ -215,17 +214,18 @@ class DiscordContests extends Command { } /** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * sendQuestion is the function that picks and sends the next question, then picks the winner by matching participants' messages * against the answer(s) or receives the winner from Staff. Once it reaches the end it will notify Staff in the Logs channel and * list all the winners in order. */ - async function sendQuestion(botGuild) { + async function sendQuestion(initBotInfo) { //get question's parameters from db var data = await getQuestion(guild.id); //sends results to Staff after all questions have been asked and stops looping if (data === null) { - discordLog(guild, '<@&' + botGuild.roleIDs.staffRole + '> Discord contests have ended!'); + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> Discord contests have ended!'); clearInterval(interval); return; } @@ -251,9 +251,9 @@ class DiscordContests extends Command { channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }).then(async (msg) => { if (answers.length === 0) { //send message to console - const questionMsg = await adminConsole.send({ content: '<@&' + botGuild.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); + const questionMsg = await adminConsole.send({ content: '<@&' + initBotInfo.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); - const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(botGuild.roleIDs.adminRole)); + const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); const collector = await questionMsg.createMessageComponentCollector({ filter }); collector.on('collect', async i => { @@ -287,7 +287,7 @@ class DiscordContests extends Command { }); } else { //automatically mark answers - const filter = m => !m.author.bot && (botGuild.verification.isEnabled ? checkForRole(m.member, botGuild.verification.verificationRoles.get('hacker')) : checkForRole(m.member, botGuild.roleIDs.member)); + const filter = m => !m.author.bot && (initBotInfo.verification.isEnabled ? checkForRole(m.member, initBotInfo.verification.roles.get('hacker')) : checkForRole(m.member, initBotInfo.roleIDs.memberRole)); const collector = channel.createMessageCollector({ filter, time: timeInterval * 0.75 }); collector.on('collect', async m => { diff --git a/commands/a_activity/new-workshop.js b/commands/a_activity/new-workshop.js index 37fd90f9..09801b56 100644 --- a/commands/a_activity/new-workshop.js +++ b/commands/a_activity/new-workshop.js @@ -1,6 +1,5 @@ const { replyAndDelete } = require('../../discord-services'); const { Message, Collection } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const Workshop = require('../../classes/Bot/activities/workshop'); const PermissionCommand = require('../../classes/permission-command'); const Activity = require('../../classes/Bot/activities/activity'); @@ -40,12 +39,12 @@ class NewWorkshop extends PermissionCommand { /** * Required class by children, should contain the command code. - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - the message that has the command * @param {Object} args * @param {String} args.activityName - the activity for this activity command */ - async runCommand(botGuild, message, {activityName}) { + async runCommand(initBotInfo, message, {activityName}) { // prompt user for roles that will be allowed to see this activity. @@ -66,7 +65,7 @@ class NewWorkshop extends PermissionCommand { \n We recommend the low tech solution!`, channel: message.channel, userId: message.author.id}); - let workshop = await new Workshop({activityName, guild: message.guild, roleParticipants, botGuild}, isLowTechSolution,TARoles).init(); + let workshop = await new Workshop({activityName, guild: message.guild, roleParticipants, initBotInfo: initBotInfo}, isLowTechSolution,TARoles).init(); workshop.sendConsoles(); diff --git a/commands/a_boothing/e-room-directory.js b/commands/a_boothing/e-room-directory.js index 75bd1536..27f369cd 100644 --- a/commands/a_boothing/e-room-directory.js +++ b/commands/a_boothing/e-room-directory.js @@ -1,7 +1,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { Message, MessageEmbed, Role, Collection} = require('discord.js'); const { deleteMessage } = require('../../discord-services'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const winston = require('winston'); const { StringPrompt, RolePrompt, SpecialPrompt } = require('advanced-discord.js-prompts'); @@ -36,9 +35,9 @@ class ERoomDirectory extends PermissionCommand { * the user) that it is open. * * @param {Message} message - messaged that called this command - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { // helpful vars let channel = message.channel; @@ -68,7 +67,7 @@ class ERoomDirectory extends PermissionCommand { winston.loggers.get(message.guild.id).warning(`Got an error: ${error} but I let it go since we expected it from the prompt.`, { event: 'E-Room-Directory Command' }); } // add staff role - roomRoles.set(botGuild.roleIDs.staffRole, message.guild.roles.resolve(botGuild.roleIDs.staffRole)); + roomRoles.set(initBotInfo.roleIDs.staffRole, message.guild.roles.resolve(initBotInfo.roleIDs.staffRole)); // prompt user for emoji to use let emoji = await SpecialPrompt.singleEmoji({prompt: 'What emoji do you want to use to open/close the room?', channel, userId}); diff --git a/commands/a_start_commands/start-channel-creation.js b/commands/a_start_commands/start-channel-creation.js index d8d08e71..e12c816c 100644 --- a/commands/a_start_commands/start-channel-creation.js +++ b/commands/a_start_commands/start-channel-creation.js @@ -1,7 +1,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { sendEmbedToMember, replyAndDelete } = require('../../discord-services'); const { Message, MessageEmbed } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { StringPrompt, MessagePrompt } = require('advanced-discord.js-prompts'); const ChannelPrompt = require('advanced-discord.js-prompts/prompts/channel-prompt'); @@ -34,10 +33,10 @@ class StartChannelCreation extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - the message in which the command was run */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { try { // grab current channel @@ -49,18 +48,18 @@ class StartChannelCreation extends PermissionCommand { // grab channel creation category and update permissions var category = channel.parent; - category.updateOverwrite(botGuild.roleIDs.everyoneRole, { + category.updateOverwrite(initBotInfo.roleIDs.everyoneRole, { VIEW_CHANNEL: false, }); - channel.updateOverwrite(botGuild.roleIDs.everyoneRole, { + channel.updateOverwrite(initBotInfo.roleIDs.everyoneRole, { VIEW_CHANNEL: true, }); // create and send embed message to channel with emoji collector const msgEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('Private Channel Creation') .setDescription('Do you need a private channel to work with your friends? Or a voice channel to get to know a mentor, here you can create private text or voice channels.' + ' However do know that server admins will have access to these channels, and the bot will continue to monitor for bad language, so please follow the rules!') diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 2d523c86..6b28540e 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -2,12 +2,11 @@ const { Command } = require('@sapphire/framework'); const { Interaction, MessageEmbed } = require('discord.js'); const { randomColor, discordLog } = require('../../discord-services'); const { Message, Collection } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild'); const winston = require('winston'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); const { MessageActionRow, MessageButton } = require('discord.js'); const { MessageSelectMenu, Modal, TextInputComponent } = require('discord.js'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); /** * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. @@ -59,24 +58,20 @@ class StartMentorCave extends Command { }; } - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - the message in which the command was run - */ async chatInputRun(interaction) { try { // helpful prompt vars let channel = interaction.channel; let userId = interaction.user.id; let guild = interaction.guild; - this.botGuild = await BotGuild.findById(guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } - let adminConsole = await guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + let adminConsole = await guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); this.ticketCount = 0; // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); @@ -84,20 +79,20 @@ class StartMentorCave extends Command { const inactivePeriod = interaction.options.getInteger('inactivity_time'); // const bufferTime = inactivePeriod / 2; const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); - const mentorRoleSelectionChannel = interaction.options.getChannel('mentor_role_selection_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.mentorRoleSelectionChannel); - const incomingTicketsChannel = interaction.options.getChannel('incoming_tickets_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.incomingTicketsChannel); - const requestTicketChannel = interaction.options.getChannel('request_ticket_channel') ?? await guild.channels.resolve(this.botGuild.mentorTickets.requestTicketChannel); + const mentorRoleSelectionChannel = interaction.options.getChannel('mentor_role_selection_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.mentorRoleSelectionChannel); + const incomingTicketsChannel = interaction.options.getChannel('incoming_tickets_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.incomingTicketsChannel); + const requestTicketChannel = interaction.options.getChannel('request_ticket_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.requestTicketChannel); if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); return; } - if (mentorRoleSelectionChannel != this.botGuild.mentorTickets.mentorRoleSelectionChannel || incomingTicketsChannel != this.botGuild.mentorTickets.incomingTicketsChannel || requestTicketChannel != this.botGuild.mentorTickets.requestTicketChannel) { + if (mentorRoleSelectionChannel != this.initBotInfo.mentorTickets.mentorRoleSelectionChannel || incomingTicketsChannel != this.initBotInfo.mentorTickets.incomingTicketsChannel || requestTicketChannel != this.initBotInfo.mentorTickets.requestTicketChannel) { await interaction.deferReply(); - this.botGuild.mentorTickets.mentorRoleSelectionChannel = mentorRoleSelectionChannel.id; - this.botGuild.mentorTickets.incomingTicketsChannel = incomingTicketsChannel.id; - this.botGuild.mentorTickets.requestTicketChannel = requestTicketChannel.id; - await this.botGuild.save(); + this.initBotInfo.mentorTickets.mentorRoleSelectionChannel = mentorRoleSelectionChannel.id; + this.initBotInfo.mentorTickets.incomingTicketsChannel = incomingTicketsChannel.id; + this.initBotInfo.mentorTickets.requestTicketChannel = requestTicketChannel.id; + await this.initBotInfo.save(); await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); } else { await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); @@ -194,7 +189,7 @@ class StartMentorCave extends Command { emojisMap.set(ideationEmoji, 'Ideation'); emojisMap.set(pitchingEmoji, 'Pitching'); - const mentorRoleColour = guild.roles.cache.find(role => role.id === this.botGuild.roleIDs.mentorRole).hexColor; + const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; for (let value of emojisMap.values()) { const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); if (!findRole) { @@ -357,7 +352,7 @@ class StartMentorCave extends Command { }); if (submitted) { - const role = i.values[0] === 'None of the above' ? this.botGuild.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; + const role = i.values[0] === 'None of the above' ? this.initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; const description = submitted.fields.getTextInputValue('ticketDescription'); const location = submitted.fields.getTextInputValue('location'); // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); @@ -545,7 +540,7 @@ class StartMentorCave extends Command { const adminEmbed = new MessageEmbed() .setTitle('Mentor Cave Console') - .setColor(this.botGuild.colors.embedColor); + .setColor(this.initBotInfo.colors.embedColor); const adminRow = new MessageActionRow() .addComponents( @@ -556,7 +551,7 @@ class StartMentorCave extends Command { ); const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); - const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(this.botGuild.roleIDs.adminRole) }); + const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) }); adminCollector.on('collect', async adminInteraction => { if (adminInteraction.customId === 'addRole') { const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); @@ -632,7 +627,7 @@ class StartMentorCave extends Command { activityListener.on('end', async collected => { if (!ticketText.parentId || !ticketVoice.parentId) return; if (collected.size === 0 && ticketVoice.members.size === 0 && ticketMsg.embeds[0].color != '#90EE90') { - const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).map(member => member.id); + const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) && !member.user.bot).map(member => member.id); const msgText = '<@' + remainingMembers.join('><@') + '> Hello! I detected some inactivity in this channel. If you are done and would like to leave this ticket, please go to the pinned message and click the "Leave" button. If you would like to keep this channel a little longer, please click the button below.\n**If no action is taken in the next ' + bufferTime + ' minutes, the channels for this ticket will be deleted automatically.**'; const row = new MessageActionRow() .addComponents( diff --git a/commands/a_start_commands/start-team-formation.js b/commands/a_start_commands/start-team-formation.js index 3ee1f866..e24d7c5c 100644 --- a/commands/a_start_commands/start-team-formation.js +++ b/commands/a_start_commands/start-team-formation.js @@ -36,10 +36,10 @@ class StartTeamFormation extends PermissionCommand { } /** - * + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - the message in which the command was run */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { // helpful prompt vars let channel = message.channel; let userId = message.author.id; @@ -66,7 +66,7 @@ class StartTeamFormation extends PermissionCommand { TeamFormation.defaultProspectForm : await StringPrompt.single({ prompt: 'Please send your form for teams now:', channel, userId }), }, guild: message.guild, - botGuild: botGuild, + initBotInfo, activityRoles, isNotificationsEnabled: await SpecialPrompt.boolean({prompt: 'Do you want to notify users when the opposite party has a new post?', channel, userId}), }); diff --git a/commands/a_start_commands/start-team-roulette.js b/commands/a_start_commands/start-team-roulette.js index 277bda64..03ce8313 100644 --- a/commands/a_start_commands/start-team-roulette.js +++ b/commands/a_start_commands/start-team-roulette.js @@ -2,7 +2,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { sendEmbedToMember } = require('../../discord-services'); const { TextChannel, Snowflake, Message, MessageEmbed, Collection, GuildChannelManager, User } = require('discord.js'); const Team = require('../../classes/Bot/Features/Team_Roulette/team'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { MemberPrompt, SpecialPrompt, ChannelPrompt } = require('advanced-discord.js-prompts'); /** @@ -37,12 +36,12 @@ class StartTeamRoulette extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - the message in which the command was run */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { - this.botGuild = botGuild; + this.initBotInfo = initBotInfo; /** * The solo join emoji. @@ -92,7 +91,7 @@ class StartTeamRoulette extends PermissionCommand { // create and send embed message to channel with emoji collector const msgEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('Team Roulette Information') .setDescription('Welcome to the team roulette section! If you are looking to join a random team, you are in the right place!') .addField('How does this work?', 'Reacting to this message will get you or your team on a list. I will try to assign you a team of 4 as fast as possible. When I do I will notify you on a private text channel with your new team!') @@ -282,7 +281,7 @@ class StartTeamRoulette extends PermissionCommand { let listEmoji = '📰'; const adminEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('Team Roulette Console') .setDescription('Team roulette is ready and operational! <#' + channel.id + '>.') .addField('Check the list!', 'React with ' + listEmoji + ' to get a message with the roulette team lists.'); @@ -296,7 +295,7 @@ class StartTeamRoulette extends PermissionCommand { reaction.users.remove(user.id); let infoEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('Team Roulette Information') .setDescription('These are all the teams that are still waiting.'); @@ -315,8 +314,8 @@ class StartTeamRoulette extends PermissionCommand { }); // add channel to black list - this.botGuild.blackList.set(channel.id, 5000); - this.botGuild.save(); + this.initBotInfo.blackList.set(channel.id, 5000); + this.initBotInfo.save(); return channel; } @@ -370,7 +369,7 @@ class StartTeamRoulette extends PermissionCommand { let leaveEmoji = '👋'; const infoEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('WELCOME TO YOUR NEW TEAM!!!') .setDescription('This is your new team, please get to know each other by creating a voice channel in a new Discord server or via this text channel. Best of luck!') .addField('Leave the Team', 'If you would like to leave this team react to this message with ' + leaveEmoji); From 9ef115bbcaf6ad108bf63ca33a1e076dbd0f0e50 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 8 Jul 2024 01:11:45 -0700 Subject: [PATCH 45/67] Update all commands except verification to use InitBotInfo doc instead of mongo --- classes/Bot/Features/Stamps/stamps-manager.js | 35 +++++++++---------- commands/a_utility/change-pre-fix.js | 9 +++-- commands/a_utility/clear-chat.js | 11 +++--- commands/a_utility/pronouns.js | 6 ++-- commands/a_utility/role-selector.js | 7 ++-- commands/a_utility/self-care.js | 32 ++++++++--------- commands/a_utility/set-bot-activity.js | 3 +- commands/attendance/attend.js | 9 +++-- commands/attendance/start-attend.js | 18 +++++----- commands/essentials/help.js | 8 ++--- commands/essentials/init-bot.js | 3 +- commands/hacker_utility/ask.js | 11 +++--- commands/stamps/change-stamp-time.js | 9 +++-- commands/stamps/password-stamp.js | 9 +++-- commands/stamps/raffle.js | 13 ++++--- 15 files changed, 85 insertions(+), 98 deletions(-) diff --git a/classes/Bot/Features/Stamps/stamps-manager.js b/classes/Bot/Features/Stamps/stamps-manager.js index 01693aef..22fc4257 100644 --- a/classes/Bot/Features/Stamps/stamps-manager.js +++ b/classes/Bot/Features/Stamps/stamps-manager.js @@ -1,7 +1,6 @@ const { Collection, MessageEmbed, GuildMember } = require('discord.js'); const winston = require('winston'); const { addRoleToMember, sendEmbedToMember, replaceRoleToMember, sendMessageToMember } = require('../../../../discord-services'); -const BotGuildModel = require('../../bot-guild'); const Activity = require('../../activities/activity'); /** @@ -13,10 +12,10 @@ class StampsManager { * Will let hackers get a stamp for attending the activity. * @param {Activity} activity - activity to use * @param {Number} [time] - time to wait till collector closes, in seconds - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @async */ - static async distributeStamp(activity, botGuild, time = 60) { + static async distributeStamp(activity, initBotInfo, time = 60) { winston.loggers.get(activity.guild.id).event(`Activity named ${activity.name} is distributing stamps.`, {event: 'Activity Manager'}); @@ -24,7 +23,7 @@ class StampsManager { let seenUsers = new Collection(); const promptEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('React within ' + time + ' seconds of the posting of this message to get a stamp for ' + activity.name + '!'); let promptMsg = await activity.generalText.send(promptEmbed); @@ -38,7 +37,7 @@ class StampsManager { const member = activity.generalText.guild.member(user); if (!seenUsers.has(user.id)) { - this.parseRole(member, activity.name, botGuild); + this.parseRole(member, activity.name, initBotInfo); seenUsers.set(user.id, user.username); } }); @@ -57,41 +56,41 @@ class StampsManager { * Upgrade the stamp role of a member. * @param {GuildMember} member - the member to add the new role to * @param {String} activityName - the name of the activity - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @throws Error if the botGuild has stamps disabled */ - static parseRole(member, activityName, botGuild) { - if (!botGuild.stamps.isEnabled) { - winston.loggers.get(botGuild._id).error(`Stamp system is turned off for guild ${botGuild._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`, { event: 'Activity Manager' }); - throw Error(`Stamp system is turned of for guild ${botGuild._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`); + static parseRole(member, activityName, initBotInfo) { + if (!initBotInfo.stamps.isEnabled) { + winston.loggers.get(initBotInfo._id).error(`Stamp system is turned off for guild ${initBotInfo._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`, { event: 'Activity Manager' }); + throw Error(`Stamp system is turned of for guild ${initBotInfo._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`); } - let role = member.roles.cache.find(role => botGuild.stamps.stampRoleIDs.has(role.id)); + let role = member.roles.cache.find(role => initBotInfo.stamps.stampRoleIDs.has(role.id)); if (role === undefined) { - addRoleToMember(member, botGuild.stamps.stamp0thRoleId); + addRoleToMember(member, initBotInfo.stamps.stamp0thRoleId); sendEmbedToMember(member, 'I did not find an existing stamp role for you so I gave you one for attending ' + activityName + '. Please contact an admin if there was a problem.', true); - winston.loggers.get(botGuild._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he has no stamp, I gave them the first stamp!`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he has no stamp, I gave them the first stamp!`, {event: 'Activity Manager'}); return; } - let stampNumber = botGuild.stamps.stampRoleIDs.get(role.id); - if (stampNumber === botGuild.stamps.stampRoleIDs.size - 1) { + let stampNumber = initBotInfo.stamps.stampRoleIDs.get(role.id); + if (stampNumber === initBotInfo.stamps.stampRoleIDs.size - 1) { sendMessageToMember(member, 'You already have the maximum allowed number of stamps!', true); - winston.loggers.get(botGuild._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he is already in the max stamp ${stampNumber}`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he is already in the max stamp ${stampNumber}`, {event: 'Activity Manager'}); return; } let newRoleID; - botGuild.stamps.stampRoleIDs.forEach((num, key, map) => { + initBotInfo.stamps.stampRoleIDs.forEach((num, key, map) => { if (num === stampNumber + 1) newRoleID = key; }); if (newRoleID != undefined) { replaceRoleToMember(member, role.id, newRoleID); sendMessageToMember(member, 'You have received a higher stamp for attending ' + activityName + '!', true); - winston.loggers.get(botGuild._id).userStats(`Activity named ${activityName} gave a stamp to the user with id ${member.id} going from stamp number ${stampNumber} to ${stampNumber + 1}`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} gave a stamp to the user with id ${member.id} going from stamp number ${stampNumber} to ${stampNumber + 1}`, {event: 'Activity Manager'}); } } } diff --git a/commands/a_utility/change-pre-fix.js b/commands/a_utility/change-pre-fix.js index 323f266a..79ea150a 100644 --- a/commands/a_utility/change-pre-fix.js +++ b/commands/a_utility/change-pre-fix.js @@ -1,6 +1,5 @@ const { Message } = require('discord.js'); const PermissionCommand = require('../../classes/permission-command'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { StringPrompt } = require('advanced-discord.js-prompts'); /** @@ -24,16 +23,16 @@ class ChangePreFix extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { let options = ['!', '#', '$', '%', '&', '?', '|', '°']; let prefix = StringPrompt.restricted({ prompt: 'What would you like to use as the prefix?', channel: message.channel, userId: message.author.id }, options); - botGuild.prefix = prefix; - botGuild.save(); + initBotInfo.prefix = prefix; + initBotInfo.save(); message.guild.commandPrefix = prefix; diff --git a/commands/a_utility/clear-chat.js b/commands/a_utility/clear-chat.js index b1e77876..c43c65b5 100644 --- a/commands/a_utility/clear-chat.js +++ b/commands/a_utility/clear-chat.js @@ -2,20 +2,19 @@ const { Command } = require('@sapphire/framework'); const { Interaction } = require('discord.js'); const { discordLog } = require('../../discord-services'); - class ClearChat extends Command { constructor(context, options) { super(context, { - ...options, - description: 'Clear most recent 100 messages younger than 2 weeks.' + ...options, + description: 'Clear most recent 100 messages younger than 2 weeks.' }); } registerApplicationCommands(registry) { registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) + builder + .setName(this.name) + .setDescription(this.description) ) } diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 7164b495..41691a97 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -1,6 +1,6 @@ const { Command } = require('@sapphire/framework'); const { Interaction, MessageEmbed, PermissionFlagsBits } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); /** * The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: @@ -38,9 +38,9 @@ class Pronouns extends Command { */ async chatInputRun(interaction) { const guild = interaction.guild; - this.botGuild = await BotGuild.findById(guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); const userId = interaction.user.id; - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } diff --git a/commands/a_utility/role-selector.js b/commands/a_utility/role-selector.js index 97fff8f0..b1951374 100644 --- a/commands/a_utility/role-selector.js +++ b/commands/a_utility/role-selector.js @@ -2,7 +2,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { checkForRole, addRoleToMember, removeRolToMember } = require('../../discord-services'); const { Message, Role, Collection } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { SpecialPrompt, RolePrompt, StringPrompt } = require('advanced-discord.js-prompts'); const Console = require('../../classes/UI/Console/console'); const Feature = require('../../classes/UI/Console/feature'); @@ -31,10 +30,10 @@ class RoleSelector extends PermissionCommand { /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - the command message */ - async runCommand (botGuild, message) { + async runCommand(initBotInfo, message) { // the emoji for staff to add new transfers let newTransferEmoji = '🆕'; @@ -59,7 +58,7 @@ class RoleSelector extends PermissionCommand { callback: async (user, reaction, stopInteracting, console) => { let channel = console.channel; // staff add new transfer - if (checkForRole(message.guild.member(user), botGuild.roleIDs.staffRole)) { + if (checkForRole(message.guild.member(user), initBotInfo.roleIDs.staffRole)) { try { var role = await RolePrompt.single({ diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 3ed0e0ca..22c5ac6d 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -3,9 +3,8 @@ const PermissionCommand = require('../../classes/permission-command'); const { discordLog } = require('../../discord-services'); const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); const { getReminder } = require('../../db/firebase/firebaseUtil'); -const BotGuild = require('../../db/mongo/BotGuild'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); +const firebaseUtil = require('../../db/firebase/firebaseUtil') /** * The self care command will send pre made reminders from firebase to the command channel. These reminders are self @@ -50,16 +49,15 @@ class SelfCareReminders extends Command { let channel = interaction.channel; let userId = interaction.user.id; - // this.botGuild = this.botGuild; let guild = interaction.guild; - this.botGuild = await BotGuild.findById(guild.id); - let adminConsole = guild.channels.resolve(this.botGuild.channelIDs.adminConsole); + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + let adminConsole = guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); var timeInterval = interaction.options.getInteger('interval') * 60000; var startNow = interaction.options.getBoolean('start_reminder_now'); var roleId = interaction.options.getRole('notify'); - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); return; } @@ -82,7 +80,7 @@ class SelfCareReminders extends Command { ); const startEmbed = new MessageEmbed() - .setColor(this.botGuild.colors.embedColor) + .setColor(this.initBotInfo.colors.embedColor) .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); @@ -90,19 +88,19 @@ class SelfCareReminders extends Command { roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); - const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole)); + const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.adminRole)); const collector = controlPanel.createMessageComponentCollector(filter); collector.on('collect', async i => { if (interval != null && !paused && i.customId == 'pause') { clearInterval(interval); paused = true; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Self care reminders paused by <@' + i.user.id + '>!'); + await guild.channels.resolve(this.initBotInfo.channelIDs.adminLog).send('Self care reminders paused by <@' + i.user.id + '>!'); await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); } else if (paused && i.customId == 'play') { - await sendReminder(this.botGuild); - interval = setInterval(sendReminder, timeInterval, this.botGuild); + await sendReminder(this.initBotInfo); + interval = setInterval(sendReminder, timeInterval, this.initBotInfo); paused = false; - await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Self care reminders restarted by <@' + i.user.id + '>!'); + await guild.channels.resolve(this.initBotInfo.channelIDs.adminLog).send('Self care reminders restarted by <@' + i.user.id + '>!'); await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); } else { await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); @@ -112,19 +110,19 @@ class SelfCareReminders extends Command { //starts the interval, and sends the first reminder immediately if startNow is true if (startNow) { - sendReminder(this.botGuild); + sendReminder(this.initBotInfo); } - interval = setInterval(sendReminder, timeInterval, this.botGuild); + interval = setInterval(sendReminder, timeInterval, this.initBotInfo); // sendReminder is the function that picks and sends the next reminder - async function sendReminder(botGuild) { + async function sendReminder(initBotInfo) { //get reminders parameters from db var data = await getReminder(guild.id); //report in admin logs that there are no more messages //TODO: consider having it just loop through the db again? if (data === null) { - discordLog(guild, '<@&' + botGuild.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); clearInterval(interval); return; } @@ -132,7 +130,7 @@ class SelfCareReminders extends Command { let reminder = data.reminder; const qEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle(reminder); roleId ? channel.send({ content: 'Hey <@&' + roleId + '> remember:', embeds: [qEmbed] }) : channel.send({ embeds: [qEmbed] }); diff --git a/commands/a_utility/set-bot-activity.js b/commands/a_utility/set-bot-activity.js index 1618fc66..c54791fc 100644 --- a/commands/a_utility/set-bot-activity.js +++ b/commands/a_utility/set-bot-activity.js @@ -1,7 +1,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { discordLog } = require('../../discord-services'); const { Message } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * The !set-bot-activity will set the bot's activity. @@ -36,7 +35,7 @@ class SetBotActivity extends PermissionCommand { * @param {String} args.status */ - async runCommand(botGuild, message, {status}) { + async runCommand(initBotInfo, message, {status}) { this.client.user.setActivity(status, {type: 'PLAYING'}); } } diff --git a/commands/attendance/attend.js b/commands/attendance/attend.js index 7a06d738..551b7a4f 100644 --- a/commands/attendance/attend.js +++ b/commands/attendance/attend.js @@ -2,7 +2,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { Message } = require('discord.js'); const { checkForRole, sendEmbedToMember } = require('../../discord-services'); const Verification = require('../../classes/Bot/Features/Verification/verification'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * Attends the user who runs this command. The user must have the guild ID. Can only be run @@ -33,14 +32,14 @@ class Attend extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message * @param {Object} args * @param {String} args.guildId */ - async runCommand(botGuild, message, { guildId }) { + async runCommand(initBotInfo, message, { guildId }) { // check if the user needs to attend, else warn and return - if (checkForRole(message.author, botGuild.attendance.attendeeRoleID)) { + if (checkForRole(message.author, initBotInfo.attendance.attendeeRoleID)) { sendEmbedToMember(message.author, { title: 'Attend Error', description: 'You do not need to attend! Happy hacking!!!' @@ -59,7 +58,7 @@ class Attend extends PermissionCommand { let member = guild.member(message.author.id); // call the firebase services attendHacker function - Verification.attend(member, botGuild); + Verification.attend(member, initBotInfo); } } module.exports = Attend; \ No newline at end of file diff --git a/commands/attendance/start-attend.js b/commands/attendance/start-attend.js index 196ba789..8a99c4a9 100644 --- a/commands/attendance/start-attend.js +++ b/commands/attendance/start-attend.js @@ -34,10 +34,10 @@ class StartAttend extends PermissionCommand { * If existsChannel is true, asks user to indicate the channel to use. Else asks user to indicate the category under which the * channel should be created, and then creates it. In both cases it will send an embed containing the instructions for hackers to * check in. - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - message containing command */ - async runCommand(botGuild, message) { + async runCommand(initBotInfo, message) { var channel; // register the attend command just in case its needed @@ -71,21 +71,21 @@ class StartAttend extends PermissionCommand { return; } - channel.updateOverwrite(botGuild.roleIDs.everyoneRole, { SEND_MESSAGES: false }); + channel.updateOverwrite(initBotInfo.roleIDs.everyoneRole, { SEND_MESSAGES: false }); //send embed with information and tagging hackers let attendEmoji = '🔋'; const embed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('Hey there!') .setDescription('In order to indicate that you are participating, please react to this message with ' + attendEmoji) .addField('Do you need assistance?', 'Head over to the support channel and ping the admins!'); - let embedMsg = await channel.send('<@&' + botGuild.roleIDs.memberRole + '>', {embed: embed}); + let embedMsg = await channel.send('<@&' + initBotInfo.roleIDs.memberRole + '>', {embed: embed}); embedMsg.pin(); embedMsg.react(attendEmoji); - botGuild.blackList.set(channel.id, 1000); - botGuild.save(); + initBotInfo.blackList.set(channel.id, 1000); + initBotInfo.save(); // reaction collector to attend hackers let embedMsgCollector = embedMsg.createReactionCollector((reaction, user) => !user.bot && reaction.emoji.name === attendEmoji); @@ -94,8 +94,8 @@ class StartAttend extends PermissionCommand { let member = message.guild.member(user.id); // check if user needs to attend - if (!checkForRole(member, botGuild.attendance.attendeeRoleID)) { - Verification.attend(member, botGuild); + if (!checkForRole(member, initBotInfo.attendance.attendeeRoleID)) { + Verification.attend(member, initBotInfo); } else { sendEmbedToMember(member, { title: 'Attend Error', diff --git a/commands/essentials/help.js b/commands/essentials/help.js index 5cea0c9f..b29d717b 100644 --- a/commands/essentials/help.js +++ b/commands/essentials/help.js @@ -2,7 +2,7 @@ const { Command, CommandoGuild } = require('discord.js-commando'); const { deleteMessage, checkForRole } = require('../../discord-services'); const { MessageEmbed } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); /** * The help command shows all the available commands for the user via DM message. @@ -26,7 +26,7 @@ class Help extends Command { */ async run(message) { - let botGuild = await BotGuild.findById(message.guild.id); + let initBotInfo = await firebaseUtil.getInitBotInfo(message.guild.id); /** @type {CommandoGuild} */ let guild = message.guild; @@ -42,7 +42,7 @@ class Help extends Command { } else { deleteMessage(message); - if ((checkForRole(message.member, botGuild.roleIDs.staffRole))) { + if ((checkForRole(message.member, initBotInfo.roleIDs.staffRole))) { commandGroups = this.client.registry.groups; } else { commandGroups = this.client.registry.findGroups('utility', true); @@ -61,7 +61,7 @@ class Help extends Command { var length = commands.length; const textEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('Commands Available for you') .setDescription('All other interactions with me will be via emoji reactions!') .setTimestamp(); diff --git a/commands/essentials/init-bot.js b/commands/essentials/init-bot.js index 5493961b..f2eef3db 100644 --- a/commands/essentials/init-bot.js +++ b/commands/essentials/init-bot.js @@ -1,8 +1,7 @@ const { Command } = require('@sapphire/framework'); const { TextChannel, Snowflake, Guild, ColorResolvable, Role, Permissions, PermissionsBitField, PermissionFlagsBits } = require('discord.js'); const { sendMsgToChannel, addRoleToMember, discordLog, } = require('../../discord-services'); -// const BotGuild = require('../../db/mongo/BotGuild'); -const firebaseUtil = require('../../db/firebase/firebaseUtil') +const firebaseUtil = require('../../db/firebase/firebaseUtil'); const winston = require('winston'); const fetch = require('node-fetch'); const { MessagePrompt, StringPrompt, NumberPrompt, SpecialPrompt, RolePrompt, ChannelPrompt } = require('advanced-discord.js-prompts'); diff --git a/commands/hacker_utility/ask.js b/commands/hacker_utility/ask.js index 925c225e..a5cd7753 100644 --- a/commands/hacker_utility/ask.js +++ b/commands/hacker_utility/ask.js @@ -2,7 +2,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { checkForRole, sendMessageToMember, } = require('../../discord-services'); const { MessageEmbed, Collection, Message, } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * The ask command tries to imitate a thread like functionality from slack. Users can ask questions, and then other @@ -32,12 +31,12 @@ class AskQuestion extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message * @param {Object} args * @param {String} args.question */ - async runCommand(botGuild, message, {question}) { + async runCommand(initBotInfo, message, {question}) { // if question is blank let user know via DM and exit if (question === '') { @@ -51,7 +50,7 @@ class AskQuestion extends PermissionCommand { // message embed to be used for question const qEmbed = new MessageEmbed() - .setColor(botGuild.colors.questionEmbedColor) + .setColor(initBotInfo.colors.questionEmbedColor) .setTitle('Question from ' + message.author.username) .setDescription(question); @@ -93,7 +92,7 @@ class AskQuestion extends PermissionCommand { // if cancel then do nothing if (response.content.toLowerCase() != 'cancel') { // if user has a mentor role, they get a special title - if (checkForRole(response.member, botGuild.roleIDs.staffRole)) { + if (checkForRole(response.member, initBotInfo.roleIDs.staffRole)) { msg.edit(msg.embeds[0].addField('🤓 ' + user.username + ' Responded:', response.content)); } else { // add a field to the message embed with the response @@ -126,7 +125,7 @@ class AskQuestion extends PermissionCommand { // remove emoji will remove the message else if (reaction.emoji.name === '⛔') { // check that user is staff - if (checkForRole(msg.guild.member(user), botGuild.roleIDs.staffRole)) { + if (checkForRole(msg.guild.member(user), initBotInfo.roleIDs.staffRole)) { msg.delete(); } else { sendMessageToMember(user, 'Deleting a question is only available to staff!', true); diff --git a/commands/stamps/change-stamp-time.js b/commands/stamps/change-stamp-time.js index e759678c..90e20928 100644 --- a/commands/stamps/change-stamp-time.js +++ b/commands/stamps/change-stamp-time.js @@ -1,7 +1,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { replyAndDelete } = require('../../discord-services'); const { Message } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * Change the time users get to react to get a stamp from activity stamp distributions. It defaults to 60 seconds. @@ -32,15 +31,15 @@ class ChangeStampTime extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message * @param {Object} args * @param {Number} args.newTime */ - async runCommand(botGuild, message, {newTime}) { + async runCommand(initBotInfo, message, {newTime}) { - botGuild.stamps.stampCollectionTime = newTime; - botGuild.save(); + initBotInfo.stamps.stampCollectionTime = newTime; + initBotInfo.save(); replyAndDelete(message, 'Stamp collection will now give hackers ' + newTime + ' seconds to collect stamp.'); } diff --git a/commands/stamps/password-stamp.js b/commands/stamps/password-stamp.js index 80001581..bd2d2cd9 100644 --- a/commands/stamps/password-stamp.js +++ b/commands/stamps/password-stamp.js @@ -1,7 +1,6 @@ const { sendEmbedToMember, sendMessageToMember, deleteMessage } = require('../../discord-services'); const { MessageEmbed, Message, Snowflake, Collection } = require('discord.js'); const PermissionCommand = require('../../classes/permission-command'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); const StampsManager = require('../../classes/Bot/Features/Stamps/stamps-manager'); const { StringPrompt, ChannelPrompt } = require('advanced-discord.js-prompts'); @@ -49,14 +48,14 @@ class PasswordStamp extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message * @param {Object} args * @param {String} args.activityName * @param {String} args.password * @param {Number} args.stopTime */ - async runCommand(botGuild, message, {activityName, password, stopTime}) { + async runCommand(initBotInfo, message, {activityName, password, stopTime}) { // helpful vars let channel = message.channel; let userId = message.author.id; @@ -77,7 +76,7 @@ class PasswordStamp extends PermissionCommand { } const qEmbed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('React with anything to claim a stamp for attending ' + activityName) .setDescription('Once you react to this message, check for a DM from this bot. **You can only emoji this message once!**') .addField('A Password Is Required!', 'Through the Bot\'s DM, you will have 3 attempts in the first 60 seconds to enter the correct password.'); @@ -122,7 +121,7 @@ class PasswordStamp extends PermissionCommand { //update role and stop collecting if password matches if (m.content.toLowerCase() === password.toLowerCase()) { - StampsManager.parseRole(member, activityName, botGuild); + StampsManager.parseRole(member, activityName, initBotInfo); correctPassword = true; pwdCollector.stop(); diff --git a/commands/stamps/raffle.js b/commands/stamps/raffle.js index f71adc6a..1356f4b5 100644 --- a/commands/stamps/raffle.js +++ b/commands/stamps/raffle.js @@ -1,6 +1,5 @@ const PermissionCommand = require('../../classes/permission-command'); const { Message, MessageEmbed } = require('discord.js'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * Picks x amount of winners from the stamp contest. The more stamps a user has, the more chances they have of winning. @@ -37,15 +36,15 @@ class Raffle extends PermissionCommand { * into an array. Then it chooses random numbers and picks the id corresponding to that index until it has numberOfWinners unique * winners. * - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message - message used to call the command * @param {Object} args * @param {integer} args.numberOfWinners - number of winners to be drawn */ - async runCommand(botGuild, message, {numberOfWinners}) { + async runCommand(initBotInfo, message, {numberOfWinners}) { //check that numberOfWinners is less than the number of people with stamp roles or it will infinite loop - let validMembers = message.guild.members.cache.filter(member => member.roles.cache.has(botGuild.roleIDs.memberRole)); + let validMembers = message.guild.members.cache.filter(member => member.roles.cache.has(initBotInfo.roleIDs.memberRole)); var memberCount = validMembers.size; if (memberCount <= numberOfWinners) { message.channel.send('Whoa there, you want more winners than hackers!').then((msg) => { @@ -58,9 +57,9 @@ class Raffle extends PermissionCommand { var entries = new Array(); validMembers.forEach(member => { - let roleId = member.roles.cache.find(role => botGuild.stamps.stampRoleIDs.has(role.id)); + let roleId = member.roles.cache.find(role => initBotInfo.stamps.stampRoleIDs.has(role.id)); if (!roleId) return; - let stampNumber = botGuild.stamps.stampRoleIDs.get(roleId); + let stampNumber = initBotInfo.stamps.stampRoleIDs.get(roleId); for (let i = 0; i < stampNumber; i++) { entries.push(member.user.id); @@ -80,7 +79,7 @@ class Raffle extends PermissionCommand { } let winnersList = Array.from(winners); const embed = new MessageEmbed() - .setColor(botGuild.colors.embedColor) + .setColor(initBotInfo.colors.embedColor) .setTitle('The winners of the raffle draw are:') .setDescription( winnersList.map(id => `<@${id}>`).join(', ')); await message.channel.send(embed); From 0ec0625c6c36287f745de0735f0d09512a8b522f Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 8 Jul 2024 01:20:57 -0700 Subject: [PATCH 46/67] Finish updating all commands to use InitBotInfo doc instead of mongo --- classes/Bot/Features/Stamps/stamps-manager.js | 10 +++---- .../Features/Ticket_System/ticket-manager.js | 4 +-- .../Bot/Features/Verification/verification.js | 27 +++++++++--------- classes/Bot/bot-guild.js | 12 ++++---- classes/permission-command.js | 6 ++-- commands/verification/add-members.js | 12 +++----- commands/verification/check-email.js | 10 +++---- commands/verification/get-email.js | 22 +++++++-------- commands/verification/start-verification.js | 28 ++++++++----------- commands/verification/verify.js | 9 +++--- db/firebase/firebaseUtil.js | 4 +-- 11 files changed, 65 insertions(+), 79 deletions(-) diff --git a/classes/Bot/Features/Stamps/stamps-manager.js b/classes/Bot/Features/Stamps/stamps-manager.js index 22fc4257..8d0a38fe 100644 --- a/classes/Bot/Features/Stamps/stamps-manager.js +++ b/classes/Bot/Features/Stamps/stamps-manager.js @@ -61,8 +61,8 @@ class StampsManager { */ static parseRole(member, activityName, initBotInfo) { if (!initBotInfo.stamps.isEnabled) { - winston.loggers.get(initBotInfo._id).error(`Stamp system is turned off for guild ${initBotInfo._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`, { event: 'Activity Manager' }); - throw Error(`Stamp system is turned of for guild ${initBotInfo._id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`); + winston.loggers.get(initBotInfo.id).error(`Stamp system is turned off for guild ${initBotInfo.id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`, { event: 'Activity Manager' }); + throw Error(`Stamp system is turned of for guild ${initBotInfo.id} but I was asked to parse a role for member ${member.id} for activity ${activityName}.`); } let role = member.roles.cache.find(role => initBotInfo.stamps.stampRoleIDs.has(role.id)); @@ -71,14 +71,14 @@ class StampsManager { addRoleToMember(member, initBotInfo.stamps.stamp0thRoleId); sendEmbedToMember(member, 'I did not find an existing stamp role for you so I gave you one for attending ' + activityName + '. Please contact an admin if there was a problem.', true); - winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he has no stamp, I gave them the first stamp!`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo.id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he has no stamp, I gave them the first stamp!`, {event: 'Activity Manager'}); return; } let stampNumber = initBotInfo.stamps.stampRoleIDs.get(role.id); if (stampNumber === initBotInfo.stamps.stampRoleIDs.size - 1) { sendMessageToMember(member, 'You already have the maximum allowed number of stamps!', true); - winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he is already in the max stamp ${stampNumber}`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo.id).userStats(`Activity named ${activityName} tried to give a stamp to the user with id ${member.id} but he is already in the max stamp ${stampNumber}`, {event: 'Activity Manager'}); return; } let newRoleID; @@ -90,7 +90,7 @@ class StampsManager { if (newRoleID != undefined) { replaceRoleToMember(member, role.id, newRoleID); sendMessageToMember(member, 'You have received a higher stamp for attending ' + activityName + '!', true); - winston.loggers.get(initBotInfo._id).userStats(`Activity named ${activityName} gave a stamp to the user with id ${member.id} going from stamp number ${stampNumber} to ${stampNumber + 1}`, {event: 'Activity Manager'}); + winston.loggers.get(initBotInfo.id).userStats(`Activity named ${activityName} gave a stamp to the user with id ${member.id} going from stamp number ${stampNumber} to ${stampNumber + 1}`, {event: 'Activity Manager'}); } } } diff --git a/classes/Bot/Features/Ticket_System/ticket-manager.js b/classes/Bot/Features/Ticket_System/ticket-manager.js index 27892645..5c3e2839 100644 --- a/classes/Bot/Features/Ticket_System/ticket-manager.js +++ b/classes/Bot/Features/Ticket_System/ticket-manager.js @@ -208,7 +208,7 @@ class TicketManager { // check if role has mentors in it if (role.members.size <= 0) { sendMsgToChannel(channel, user.id, 'There are no mentors available with that role. Please request another role or the general role!', 10); - winston.loggers.get(this.parent.initBotInfo._id).userStats(`The cave ${this.parent.name} received a ticket from user ${user.id} but was canceled due to no mentor having the role ${role.name}.`, { event: 'Ticket Manager' }); + winston.loggers.get(this.parent.initBotInfo.id).userStats(`The cave ${this.parent.name} received a ticket from user ${user.id} but was canceled due to no mentor having the role ${role.name}.`, { event: 'Ticket Manager' }); return; } @@ -216,7 +216,7 @@ class TicketManager { var promptMsg = await MessagePrompt.prompt({prompt: 'Please send ONE message with: \n* A one liner of your problem ' + '\n* Mention your team members using @friendName (example: @John).', channel, userId: user.id, cancelable: true, time: 45}); } catch (error) { - winston.loggers.get(this.parent.initBotInfo._id).warning(`New ticket was canceled due to error: ${error}`, { event: 'Ticket Manager' }); + winston.loggers.get(this.parent.initBotInfo.id).warning(`New ticket was canceled due to error: ${error}`, { event: 'Ticket Manager' }); return; } diff --git a/classes/Bot/Features/Verification/verification.js b/classes/Bot/Features/Verification/verification.js index 58351ac7..8460929a 100644 --- a/classes/Bot/Features/Verification/verification.js +++ b/classes/Bot/Features/Verification/verification.js @@ -1,7 +1,6 @@ const { GuildMember, Guild } = require('discord.js'); const discordServices = require('../../../../discord-services'); const firebaseUtil = require('../../../../db/firebase/firebaseUtil'); -const BotGuildModel = require('../../bot-guild'); const winston = require('winston'); /** @@ -14,12 +13,12 @@ class Verification { * @param {GuildMember} member - member to verify * @param {String} email - email to verify with * @param {Guild} guild - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @async * @static * @throws Error if email is not valid! */ - static async verify(member, email, guild, botGuild) { + static async verify(member, email, guild, initBotInfo) { if (!discordServices.validateEmail(email)) { throw new Error('Email is not valid!!'); } @@ -55,8 +54,8 @@ class Verification { // check for correct types with botGuild verification info and give the roles types.forEach((type, index, array) => { - if (botGuild.verification.verificationRoles.has(type)) { - let roleId = botGuild.verification.verificationRoles.get(type); + if (initBotInfo.verification.verificationRoles.has(type)) { + let roleId = initBotInfo.verification.verificationRoles.get(type); logger.verbose(`User ${member.id} has type ${type} in list index ${index} and it was found, he got the role ${roleId}`, { event: 'Verification' }); discordServices.addRoleToMember(member, roleId); correctTypes.push(type); @@ -68,12 +67,12 @@ class Verification { // extra check to see if types were found, give stamp role if available and let user know of success if (correctTypes.length > 0) { - discordServices.replaceRoleToMember(member, botGuild.verification.guestRoleID, botGuild.roleIDs.memberRole); - if (botGuild.stamps.isEnabled) discordServices.addRoleToMember(member, botGuild.stamps.stamp0thRoleId); + discordServices.replaceRoleToMember(member, initBotInfo.verification.guestRoleID, initBotInfo.roleIDs.memberRole); + if (initBotInfo.stamps.isEnabled) discordServices.addRoleToMember(member, initBotInfo.stamps.stamp0thRoleId); discordServices.sendEmbedToMember(member, { title: `${guild.name} Verification Success`, description: `You have been verified as a ${correctTypes.join()}, good luck and have fun!`, - color: botGuild.colors.specialDMEmbedColor, + color: initBotInfo.colors.specialDMEmbedColor, }); discordServices.discordLog(guild, `VERIFY SUCCESS : <@${member.id}> Verified email: ${email} successfully as ${correctTypes.join()}.`); logger.event(`User ${member.id} was verified with email ${email} successfully as ${correctTypes.join()}.`, { event: 'Verification' }); @@ -92,24 +91,24 @@ class Verification { /** * Will attend the user and give it the attendee role. * @param {GuildMember} member - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo */ - static async attend(member, botGuild) { + static async attend(member, initBotInfo) { try { // wait for attend to end, then give role await firebaseUtil.attend(member.id, member.guild.id); - discordServices.addRoleToMember(member, botGuild.attendance.attendeeRoleID); + discordServices.addRoleToMember(member, initBotInfo.attendance.attendeeRoleID); discordServices.sendEmbedToMember(member, { title: 'Attendance Success', description: 'You have been marked as attending, thank you and good luck!', - color: botGuild.colors.specialDMEmbedColor, + color: initBotInfo.colors.specialDMEmbedColor, }); discordServices.discordLog(member.guild, `ATTEND SUCCESS : <@${member.id}> has been marked as attending!`); - winston.loggers.get(botGuild._id).event(`User ${member.id} was marked as attending!`, { event: 'Verification' }); + winston.loggers.get(initBotInfo.id).event(`User ${member.id} was marked as attending!`, { event: 'Verification' }); } catch (error) { // email was not found, let admins know! discordServices.discordLog(member.guild, `ATTEND WARNING : <@${member.id}> tried to attend but I could not find his discord ID! He might be an impostor!!!`); - winston.loggers.get(botGuild._id).warning(`User ${member.id} could not be marked as attending, I could not find his discord ID, he could be an impostor! Got the error: ${error}`, { event: 'Verification' }); + winston.loggers.get(initBotInfo.id).warning(`User ${member.id} could not be marked as attending, I could not find his discord ID, he could be an impostor! Got the error: ${error}`, { event: 'Verification' }); } } diff --git a/classes/Bot/bot-guild.js b/classes/Bot/bot-guild.js index 42af0b23..7784d61e 100644 --- a/classes/Bot/bot-guild.js +++ b/classes/Bot/bot-guild.js @@ -133,7 +133,7 @@ class BotGuild { this.isSetUpComplete = true; - winston.loggers.get(this._id).event(`The botGuild has run the ready up function.`, {event: "Bot Guild"}); + winston.loggers.get(this.id).event(`The botGuild has run the ready up function.`, {event: "Bot Guild"}); return this; } @@ -191,13 +191,13 @@ class BotGuild { * @async */ // async setUpStamps(client, stampAmount = 0, stampCollectionTime = 60, stampRoleIDs = []) { - // let guild = await client.guilds.resolve(this._id); + // let guild = await client.guilds.resolve(this.id); // if (stampRoleIDs.length > 0) { // stampRoleIDs.forEach((ID, index, array) => { // this.addStamp(ID, index); // }); - // winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. The stamp roles were given. Stamp collection time is set at ${stampCollectionTime}.` [{stampIds: stampRoleIDs}]); + // winston.loggers.get(this.id).event(`The botGuild has set up the stamp functionality. The stamp roles were given. Stamp collection time is set at ${stampCollectionTime}.` [{stampIds: stampRoleIDs}]); // } else { // for (let i = 0; i < stampAmount; i++) { // let role = await guild.roles.create({ @@ -210,7 +210,7 @@ class BotGuild { // this.addStamp(role.id, i); // } - // winston.loggers.get(this._id).event(`The botGuild has set up the stamp functionality. Stamps were created by me, I created ${stampAmount} stamps. Stamp collection time is set at ${stampCollectionTime}.`, {event: "Bot Guild"}); + // winston.loggers.get(this.id).event(`The botGuild has set up the stamp functionality. Stamps were created by me, I created ${stampAmount} stamps. Stamp collection time is set at ${stampCollectionTime}.`, {event: "Bot Guild"}); // } // this.stamps.stampCollectionTime = stampCollectionTime; @@ -229,7 +229,7 @@ class BotGuild { addStamp(roleId, stampNumber) { if (stampNumber === 0) this.stamps.stamp0thRoleId = roleId; this.stamps.stampRoleIDs.set(roleId, stampNumber); - winston.loggers.get(this._id).event(`The botGuild has added a stamp with number ${stampNumber} linked to role id ${roleId}`, {event: "Bot Guild"}); + winston.loggers.get(this.id).event(`The botGuild has added a stamp with number ${stampNumber} linked to role id ${roleId}`, {event: "Bot Guild"}); } /** @@ -239,7 +239,7 @@ class BotGuild { */ async setCommandStatus(client) { /** @type {SapphireClient.Guild} */ - let guild = await client.guilds.resolve(this._id); + let guild = await client.guilds.resolve(this.id); // guild.setGroupEnabled('verification', this.verification.isEnabled); // guild.setGroupEnabled('attendance', this.attendance.isEnabled); diff --git a/classes/permission-command.js b/classes/permission-command.js index 8ec3bd5b..f10893c1 100644 --- a/classes/permission-command.js +++ b/classes/permission-command.js @@ -81,7 +81,7 @@ class PermissionCommand extends Command { title: 'Error', description: 'The command you just tried to use is only usable via DM!', }); - winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); + winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); return; } } else { @@ -91,7 +91,7 @@ class PermissionCommand extends Command { if (channelID && message.channel.id != channelID) { discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true); - winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); + winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); return; } } @@ -105,7 +105,7 @@ class PermissionCommand extends Command { (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, initBotInfo.roleIDs.adminRole))) || (roleID != initBotInfo.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) { discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true); - winston.loggers.get(initBotInfo?._id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); + winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); return; } } diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index a06f4fea..7a82a973 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -1,5 +1,5 @@ const { Command } = require('@sapphire/framework'); -const BotGuild = require('../../db/mongo/BotGuild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); const { Modal, MessageActionRow, TextInputComponent } = require('discord.js'); const { addUserData } = require('../../db/firebase/firebaseUtil'); @@ -27,22 +27,18 @@ class AddMembers extends Command { ) } - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - */ async chatInputRun(interaction) { - this.botGuild = await BotGuild.findById(interaction.guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const userId = interaction.user.id; const guild = interaction.guild; const participantsType = interaction.options.getString('participantstype'); const overwrite = interaction.options.getBoolean('overwrite') ?? false; - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } - if (!this.botGuild.verification.verificationRoles.has(participantsType)) { + if (!this.initBotInfo.verification.verificationRoles.has(participantsType)) { await interaction.reply({ content: 'The role you entered does not exist!', ephemeral: true }); return; } diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index fed035eb..77444d4d 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -1,6 +1,6 @@ const { checkEmail } = require("../../db/firebase/firebaseUtil"); const { Command } = require('@sapphire/framework'); -const BotGuild = require('../../db/mongo/BotGuild'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); class CheckEmail extends Command { constructor(context, options) { @@ -23,17 +23,17 @@ class CheckEmail extends Command { } async chatInputRun(interaction) { - this.botGuild = await BotGuild.findById(interaction.guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const guild = interaction.guild; const email = interaction.options.getString('email'); - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } - let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + let botSpamChannel = guild.channels.resolve(this.initBotInfo.channelIDs.botSpamChannel); const userData = await checkEmail(email, guild.id); - interaction.reply({content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); + interaction.reply({content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); if (userData) { if (userData.discordId && userData.types) { diff --git a/commands/verification/get-email.js b/commands/verification/get-email.js index f879b64c..b0e9ef57 100644 --- a/commands/verification/get-email.js +++ b/commands/verification/get-email.js @@ -1,5 +1,5 @@ const { Command } = require('@sapphire/framework'); -const BotGuild = require('../../db/mongo/BotGuild') +const firebaseUtil = require('../../db/firebase/firebaseUtil'); const { lookupById } = require('../../db/firebase/firebaseUtil'); class GetEmails extends Command { @@ -11,30 +11,30 @@ class GetEmails extends Command { } registerApplicationCommands(registry) { - registry.registerContextMenuCommand((builder) => - builder - .setName(this.name) - .setType(2) - ); + registry.registerContextMenuCommand((builder) => + builder + .setName(this.name) + .setType(2) + ); } async contextMenuRun(interaction) { const guild = interaction.guild; const userId = interaction.user.id; - this.botGuild = this.botGuild = await BotGuild.findById(guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } - let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + let botSpamChannel = guild.channels.resolve(this.initBotInfo.channelIDs.botSpamChannel); const email = await lookupById(guild.id, interaction.targetUser.id) if (email) { - interaction.reply({ content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); + interaction.reply({ content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is: ' + email); return; } else { - interaction.reply({ content: 'Visit <#' + this.botGuild.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); + interaction.reply({ content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true }); botSpamChannel.send('<@' + interaction.targetUser.id + '>\'s email is not in our database!'); return; } diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 30762a69..1be87d47 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -1,8 +1,6 @@ const { Command } = require('@sapphire/framework'); -const BotGuild = require('../../db/mongo/BotGuild'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); -const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); const firebaseUtil = require('../../db/firebase/firebaseUtil'); +const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); const { discordLog } = require('../../discord-services'); class StartVerification extends Command { @@ -24,19 +22,15 @@ class StartVerification extends Command { }; } - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - */ async chatInputRun(interaction) { - this.botGuild = await BotGuild.findById(interaction.guild.id); + this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const guild = interaction.guild; const userId = interaction.user.id; - if (!guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)) { + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } - if (!this.botGuild.verification.isEnabled) { + if (!this.initBotInfo.verification.isEnabled) { await interaction.reply({ content: 'Verification has not been enabled!', ephemeral: true}); return; } @@ -61,7 +55,7 @@ class StartVerification extends Command { checkInCollector.on('collect', async i => { - if (!interaction.guild.members.cache.get(i.user.id).roles.cache.has(this.botGuild.verification.guestRoleID)) { + if (!interaction.guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.verification.guestRoleID)) { await i.reply({ content: 'You are not eligible to be checked in! If you don\'t have correct access to the server, please contact an organizer.', ephemeral: true}); return; } @@ -105,20 +99,20 @@ class StartVerification extends Command { var correctTypes = []; types.forEach(type => { - if (this.botGuild.verification.verificationRoles.has(type) || type === 'staff' || type === 'mentor') { + if (this.initBotInfo.verification.verificationRoles.has(type) || type === 'staff' || type === 'mentor') { const member = interaction.guild.members.cache.get(submitted.user.id); let roleId; if (type === 'staff') { - roleId = this.botGuild.roleIDs.staffRole; + roleId = this.initBotInfo.roleIDs.staffRole; } else if (type === 'mentor') { - roleId = this.botGuild.roleIDs.mentorRole; + roleId = this.initBotInfo.roleIDs.mentorRole; } else { - roleId = this.botGuild.verification.verificationRoles.get(type); + roleId = this.initBotInfo.verification.verificationRoles.get(type); } member.roles.add(roleId); if (correctTypes.length === 0) { - member.roles.remove(this.botGuild.verification.guestRoleID); - member.roles.add(this.botGuild.roleIDs.memberRole); + member.roles.remove(this.initBotInfo.verification.guestRoleID); + member.roles.add(this.initBotInfo.roleIDs.memberRole); } correctTypes.push(type); } else { diff --git a/commands/verification/verify.js b/commands/verification/verify.js index fa19cd77..49276c2a 100644 --- a/commands/verification/verify.js +++ b/commands/verification/verify.js @@ -2,7 +2,6 @@ const PermissionCommand = require('../../classes/permission-command'); const { sendEmbedToMember, sendMessageToMember, checkForRole, validateEmail } = require('../../discord-services'); const { Message } = require('discord.js'); const Verification = require('../../classes/Bot/Features/Verification/verification'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); /** * Will verify the user running the command, needs the user's email and guild ID. Can only @@ -39,16 +38,16 @@ class Verify extends PermissionCommand { } /** - * @param {BotGuildModel} botGuild + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo * @param {Message} message * @param {Object} args * @param {String} args.email * @param {String} args.guildId */ - async runCommand(botGuild, message, { email, guildId }) { + async runCommand(initBotInfo, message, { email, guildId }) { // check if the user needs to verify, else warn and return - if (!checkForRole(member, botGuild.roleIDs.guestRole)) { + if (!checkForRole(member, initBotInfo.roleIDs.guestRole)) { sendEmbedToMember(member, { title: 'Verify Error', description: 'You do not need to verify, you are already more than a guest!' @@ -74,7 +73,7 @@ class Verify extends PermissionCommand { // Call the verify function try { - Verification.verify(member, email, guild, botGuild); + Verification.verify(member, email, guild, initBotInfo); } catch (error) { sendEmbedToMember(member, { title: 'Verification Error', diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 0aff8a20..8bd6cbc6 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -386,9 +386,7 @@ async function getInitBotInfo(guildId) { * @param {string} guildId */ async function createInitBotInfoDoc(guildId) { - return await module.exports.getFactotumSubCol().doc(guildId).set({ - _id: guildId, - }); + return await module.exports.getFactotumSubCol().doc(guildId).set({}); } module.exports.getInitBotInfo = getInitBotInfo; From 2c5857f65d531e12cf2d7cd1c7b68209fe8e8fa6 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Fri, 12 Jul 2024 14:07:26 -0700 Subject: [PATCH 47/67] addressed Daniels comment - using _db --- db/firebase/firebaseUtil.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 8bd6cbc6..a7b7bc4a 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -369,7 +369,7 @@ module.exports = { }; function getFactotumDoc() { - return apps.get('nwPlusBotAdmin').firestore().collection('ExternalProjects').doc('Factotum'); + return _db.collection('ExternalProjects').doc('Factotum'); } /** From 1f67fa813daba429b442033d537d20faa16bdd5c Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:36:56 -0700 Subject: [PATCH 48/67] Save pronoun message/channel ids + app now loads each guild sequentially respecting awaits --- app.js | 22 ++++- commands/a_utility/pronouns.js | 149 +++++++++++++++++++++++---------- db/firebase/firebaseUtil.js | 7 ++ 3 files changed, 130 insertions(+), 48 deletions(-) diff --git a/app.js b/app.js index c9bbff73..79dd41cd 100644 --- a/app.js +++ b/app.js @@ -11,6 +11,7 @@ const { StringPrompt } = require('advanced-discord.js-prompts'); const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); const { LogLevel, SapphireClient } = require('@sapphire/framework'); +const Pronouns = require('./commands/a_utility/pronouns'); /** * The Main App module houses the bot events, process events, and initializes @@ -155,7 +156,8 @@ bot.once('ready', async () => { // make sure all guilds have a botGuild, this is in case the bot goes offline and its added // to a guild. If botGuild is found, make sure only the correct commands are enabled. - bot.guilds.cache.forEach(async (guild, key, guilds) => { + const guildsArr = Array.from(bot.guilds.cache); + for (const [_, guild] of guildsArr) { // create the logger for the guild createALogger(guild.id, guild.name, false, isLogToConsole); @@ -171,11 +173,23 @@ bot.once('ready', async () => { // await botGuild.setCommandStatus(bot); - guild.commandPrefix = botGuild.prefix; - mainLogger.verbose(`Found a botGuild for ${guild.id} - ${guild.name} on bot ready.`, { event: 'Ready Event' }); + + if (botGuild.isSetUpComplete) { + mainLogger.verbose('Trying to restore existing pronoun command message'); + /** @type {Pronouns} */ + const pronounsCommand = bot.stores.get('commands').get('pronouns'); + const error = await pronounsCommand.tryRestoreReactionListeners(guild); + if (error) { + mainLogger.warning(error); + } else { + mainLogger.verbose('Restored pronoun command message'); + } + } + + guild.commandPrefix = botGuild.prefix; } - }); + } }); /** diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 41691a97..bac6d121 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -1,7 +1,9 @@ const { Command } = require('@sapphire/framework'); -const { Interaction, MessageEmbed, PermissionFlagsBits } = require('discord.js'); +const { Interaction, MessageEmbed, PermissionFlagsBits, Guild, Message, MessageManager } = require('discord.js'); const firebaseUtil = require('../../db/firebase/firebaseUtil'); +var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; + /** * The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: * * she/her @@ -44,10 +46,8 @@ class Pronouns extends Command { interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); return; } - const sheRole = interaction.guild.roles.cache.find(role => role.name === 'she/her'); - const heRole = interaction.guild.roles.cache.find(role => role.name === 'he/him'); - const theyRole = interaction.guild.roles.cache.find(role => role.name === 'they/them'); - const otherRole = interaction.guild.roles.cache.find(role => role.name === 'other pronouns'); + + const { sheRole, heRole, theyRole, otherRole } = getPronounRoles(guild); // check to make sure all 4 roles are available if (!sheRole || !heRole || !theyRole || !otherRole) { @@ -55,8 +55,6 @@ class Pronouns extends Command { return; } - var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; - let embed = new MessageEmbed() .setColor('#0DEFE1') .setTitle('Set your pronouns by reacting to one or more of the emojis!') @@ -69,47 +67,110 @@ class Pronouns extends Command { let messageEmbed = await interaction.channel.send({embeds: [embed]}); emojis.forEach(emoji => messageEmbed.react(emoji)); interaction.reply({content: 'Pronouns selector started!', ephemeral: true}); + + listenToReactions(guild, messageEmbed); - let filter = (reaction, user) => { - return user.bot != true && emojis.includes(reaction.emoji.name); - }; - - // create collector - const reactionCollector = messageEmbed.createReactionCollector({filter, dispose: true}); - - // on emoji reaction - reactionCollector.on('collect', async (reaction, user) => { - if (reaction.emoji.name === emojis[0]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(sheRole); - } if (reaction.emoji.name === emojis[1]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(heRole); - } if (reaction.emoji.name === emojis[2]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(theyRole); - } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.add(otherRole); - } + const savedMessagesCol = firebaseUtil.getFactotumSubCol().doc(guild.id).collection('SavedMessages'); + await savedMessagesCol.doc('pronouns').set({ + messageId: messageEmbed.id, + channelId: messageEmbed.channel.id, }); + } - reactionCollector.on('remove', async (reaction, user) => { - if (reaction.emoji.name === emojis[0]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(sheRole); - } if (reaction.emoji.name === emojis[1]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(heRole); - } if (reaction.emoji.name === emojis[2]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(theyRole); - } if (reaction.emoji.name === emojis[3]) { - const member = interaction.guild.members.cache.get(user.id); - await member.roles.remove(otherRole); + /** + * Checks Firebase for an existing stored reaction listener - + * restores the listeners for the reaction if it exists, otherwise does nothing + * @param {Guild} guild + */ + async tryRestoreReactionListeners(guild) { + const savedMessagesSubCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const pronounDoc = await savedMessagesSubCol.doc('pronouns').get(); + if (pronounDoc.exists) { + const { messageId, channelId } = pronounDoc.data(); + const channel = await this.container.client.channels.fetch(channelId); + if (channel) { + try { + /** @type {Message} */ + const message = await channel.messages.fetch(messageId); + listenToReactions(guild, message); + } catch (e) { + // message doesn't exist anymore + return e; + } + } else { + return 'Saved message channel does not exist'; } - }); - + } else { + return 'No existing saved message for pronouns command'; + } } } + +/** + * + * @param {Guild} guild + */ +function getPronounRoles(guild) { + const sheRole = guild.roles.cache.find(role => role.name === 'she/her'); + const heRole = guild.roles.cache.find(role => role.name === 'he/him'); + const theyRole = guild.roles.cache.find(role => role.name === 'they/them'); + const otherRole = guild.roles.cache.find(role => role.name === 'other pronouns'); + return { sheRole, heRole, theyRole, otherRole }; +} + +/** + * Adds reaction listeners to a message to add/remove pronoun rules from users upon reacting + * @param {Guild} guild + * @param {Message} message + */ +function listenToReactions(guild, message) { + const { sheRole, heRole, theyRole, otherRole } = getPronounRoles(guild); + + let filter = (reaction, user) => { + return user.bot != true && emojis.includes(reaction.emoji.name); + }; + + // create collector + const reactionCollector = message.createReactionCollector({filter, dispose: true}); + + // on emoji reaction + reactionCollector.on('collect', async (reaction, user) => { + if (reaction.emoji.name === emojis[0]) { + const member = guild.members.cache.get(user.id); + await member.roles.add(sheRole); + } + if (reaction.emoji.name === emojis[1]) { + const member = guild.members.cache.get(user.id); + await member.roles.add(heRole); + } + if (reaction.emoji.name === emojis[2]) { + const member = guild.members.cache.get(user.id); + await member.roles.add(theyRole); + } + if (reaction.emoji.name === emojis[3]) { + const member = guild.members.cache.get(user.id); + await member.roles.add(otherRole); + } + }); + + reactionCollector.on('remove', async (reaction, user) => { + if (reaction.emoji.name === emojis[0]) { + const member = guild.members.cache.get(user.id); + await member.roles.remove(sheRole); + } + if (reaction.emoji.name === emojis[1]) { + const member = guild.members.cache.get(user.id); + await member.roles.remove(heRole); + } + if (reaction.emoji.name === emojis[2]) { + const member = guild.members.cache.get(user.id); + await member.roles.remove(theyRole); + } + if (reaction.emoji.name === emojis[3]) { + const member = guild.members.cache.get(user.id); + await member.roles.remove(otherRole); + } + }); +} + module.exports = Pronouns; \ No newline at end of file diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index a7b7bc4a..948977aa 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -79,6 +79,13 @@ module.exports = { } return externalProjectsCol.doc('Factotum').collection('InitBotInfo'); }, + /** + * @param {string} guildId + */ + getSavedMessagesSubCol(guildId) { + const factotumSubCol = this.getFactotumSubCol(); + return factotumSubCol.doc(guildId).collection('SavedMessages'); + }, /** * @param {String} appName From c4e56cf8a7fa575f5646936020fae8f7d8efe58b Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sat, 3 Aug 2024 00:05:41 -0700 Subject: [PATCH 49/67] Change var declarations to const in pronouns.js --- commands/a_utility/pronouns.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index bac6d121..7a1d1f49 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -2,7 +2,7 @@ const { Command } = require('@sapphire/framework'); const { Interaction, MessageEmbed, PermissionFlagsBits, Guild, Message, MessageManager } = require('discord.js'); const firebaseUtil = require('../../db/firebase/firebaseUtil'); -var emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; +const emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; /** * The pronouns command sends a role reaction console for users to select a pronoun role out of 4 options: From 24b5cb9085c97835e7104014db56b9220cd778aa Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sat, 3 Aug 2024 16:02:24 -0700 Subject: [PATCH 50/67] initial work for email checking: --- commands/verification/check-email.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index 77444d4d..0f2f5f2e 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -22,16 +22,28 @@ class CheckEmail extends Command { ) } + // IS EDITED async chatInputRun(interaction) { this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const guild = interaction.guild; + console.log(guild, ' is the guild'); const email = interaction.options.getString('email'); + const userId = interaction.user.id; + console.log(userId, ' is the userId'); + console.log(email, ' is the email'); if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } let botSpamChannel = guild.channels.resolve(this.initBotInfo.channelIDs.botSpamChannel); + if (!botSpamChannel) { + return interaction.reply({ + content: `The channel with ID ${this.initBotInfo.channelIDs.botSpamChannel} could not be found. Please check the configuration.`, + ephemeral: true + }); + } + const userData = await checkEmail(email, guild.id); interaction.reply({content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); From b212ca25bea9b01aa9d30293e3a0407963753d3a Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sat, 3 Aug 2024 16:29:35 -0700 Subject: [PATCH 51/67] creates the pronoun roles if they do not exist --- commands/a_utility/pronouns.js | 37 +++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 7a1d1f49..6a679969 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -10,7 +10,6 @@ const emojis = ['1️⃣', '2️⃣', '3️⃣', '4️⃣']; * * he/him * * they/them * * other pronouns - * The roles must be already created on the server for this to work. * @category Commands * @subcategory Admin-Utility * @extends Command @@ -47,7 +46,7 @@ class Pronouns extends Command { return; } - const { sheRole, heRole, theyRole, otherRole } = getPronounRoles(guild); + const { sheRole, heRole, theyRole, otherRole } = await getPronounRoles(guild); // check to make sure all 4 roles are available if (!sheRole || !heRole || !theyRole || !otherRole) { @@ -110,21 +109,41 @@ class Pronouns extends Command { * * @param {Guild} guild */ -function getPronounRoles(guild) { - const sheRole = guild.roles.cache.find(role => role.name === 'she/her'); - const heRole = guild.roles.cache.find(role => role.name === 'he/him'); - const theyRole = guild.roles.cache.find(role => role.name === 'they/them'); - const otherRole = guild.roles.cache.find(role => role.name === 'other pronouns'); +async function getPronounRoles(guild) { + const sheRole = guild.roles.cache.find(role => role.name === 'she/her') || await createRole(guild, 'she/her', '#99AAB5'); + const heRole = guild.roles.cache.find(role => role.name === 'he/him') || await createRole(guild, 'he/him', '#99AAB5'); + const theyRole = guild.roles.cache.find(role => role.name === 'they/them') || await createRole(guild, 'they/them', '#99AAB5'); + const otherRole = guild.roles.cache.find(role => role.name === 'other pronouns') || await createRole(guild, 'other pronouns', '#99AAB5'); return { sheRole, heRole, theyRole, otherRole }; } +/** + * Creates a role in the guild with the specified name and color. + * @param {Guild} guild + * @param {string} name + * @param {string} color + */ + async function createRole(guild, name, color) { + try { + const role = await guild.roles.create({ + name: name, + color: color, + reason: `Creating ${name} role for pronoun selector` + }); + return role; + } catch (error) { + console.error(`Failed to create role ${name}:`, error); + throw new Error(`Could not create role ${name}`); + } +} + /** * Adds reaction listeners to a message to add/remove pronoun rules from users upon reacting * @param {Guild} guild * @param {Message} message */ -function listenToReactions(guild, message) { - const { sheRole, heRole, theyRole, otherRole } = getPronounRoles(guild); +async function listenToReactions(guild, message) { + const { sheRole, heRole, theyRole, otherRole } = await getPronounRoles(guild); let filter = (reaction, user) => { return user.bot != true && emojis.includes(reaction.emoji.name); From 8bf6c7bbfd34578bae86532023bf3b77179f6084 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sat, 27 Jul 2024 18:57:03 -0700 Subject: [PATCH 52/67] Fixed keep_pinned option for clear-chat command --- commands/a_utility/clear-chat.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/commands/a_utility/clear-chat.js b/commands/a_utility/clear-chat.js index c43c65b5..4b0f25ed 100644 --- a/commands/a_utility/clear-chat.js +++ b/commands/a_utility/clear-chat.js @@ -1,5 +1,4 @@ const { Command } = require('@sapphire/framework'); -const { Interaction } = require('discord.js'); const { discordLog } = require('../../discord-services'); class ClearChat extends Command { @@ -10,20 +9,27 @@ class ClearChat extends Command { }); } + /** + * + * @param {Command.Registry} registry + */ registerApplicationCommands(registry) { registry.registerChatInputCommand((builder) => builder .setName(this.name) .setDescription(this.description) - ) + .addBooleanOption((option) => + option // + .setName('keep_pinned') + .setDescription('If true any pinned messages will not be removed')) + ); } /** - * @param {Interaction} interaction - * @param {Object} args - the command arguments - * @param {Boolean} args.keepPinned - if true any pinned messages will not be removed + * @param {Command.ChatInputInteraction} interaction */ - async chatInputRun (interaction, {keepPinned}) { + async chatInputRun (interaction) { + const keepPinned = interaction.options.getBoolean('keep_pinned', false); if (keepPinned) { // other option is to get all channel messages, filter of the pined channels and pass those to bulkDelete, might be to costly? @@ -34,6 +40,7 @@ class ClearChat extends Command { await interaction.channel.bulkDelete(100, true).catch(console.error); } discordLog(interaction.guild, 'CHANNEL CLEAR ' + interaction.channel.name); + return interaction.reply({ content: 'Messages successfully deleted' }); } } module.exports = ClearChat; From e0e84142dc409b742513667e87d3ff035156775c Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sat, 27 Jul 2024 21:03:30 -0700 Subject: [PATCH 53/67] Fix self care command --- commands/a_utility/self-care.js | 289 ++++++++++---------- commands/firebase_scripts/load-questions.js | 3 +- db/firebase/firebaseUtil.js | 3 +- package-lock.json | 106 +++---- package.json | 2 +- 5 files changed, 208 insertions(+), 195 deletions(-) diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 22c5ac6d..4c30a4c1 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -1,140 +1,149 @@ -const { Command } = require('@sapphire/framework'); -const PermissionCommand = require('../../classes/permission-command'); -const { discordLog } = require('../../discord-services'); -const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); -const { getReminder } = require('../../db/firebase/firebaseUtil'); -const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); -const firebaseUtil = require('../../db/firebase/firebaseUtil') - -/** - * The self care command will send pre made reminders from firebase to the command channel. These reminders are self - * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. - * @category Commands - * @subcategory Admin-Utility - * @extends Command - */ -class SelfCareReminders extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Sends self-care reminders at designated times.', - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - .addIntegerOption(option => - option.setName('interval') - .setDescription('Time (minutes) between reminders') - .setRequired(true)) - .addRoleOption(option => - option.setName('notify') - .setDescription('Role to notify when a reminder drops') - .setRequired(false)) - .addBooleanOption(option => - option.setName('start_reminder_now') - .setDescription('True to start first reminder now, false to start it after one interval') - .setRequired(false)) - ), - { - idHints: '1052699103143927859' - }; - } - - async chatInputRun(interaction) { - var interval; - - let channel = interaction.channel; - let userId = interaction.user.id; - let guild = interaction.guild; - this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - let adminConsole = guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); - - var timeInterval = interaction.options.getInteger('interval') * 60000; - var startNow = interaction.options.getBoolean('start_reminder_now'); - var roleId = interaction.options.getRole('notify'); - - if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { - interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - // keeps track of whether it has been paused - var paused = false; - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('play') - .setLabel('Play') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('pause') - .setLabel('Pause') - .setStyle('PRIMARY'), - ); - - const startEmbed = new MessageEmbed() - .setColor(this.initBotInfo.colors.embedColor) - .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); - - interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); - - roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); - - const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); - const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.adminRole)); - const collector = controlPanel.createMessageComponentCollector(filter); - collector.on('collect', async i => { - if (interval != null && !paused && i.customId == 'pause') { - clearInterval(interval); - paused = true; - await guild.channels.resolve(this.initBotInfo.channelIDs.adminLog).send('Self care reminders paused by <@' + i.user.id + '>!'); - await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); - } else if (paused && i.customId == 'play') { - await sendReminder(this.initBotInfo); - interval = setInterval(sendReminder, timeInterval, this.initBotInfo); - paused = false; - await guild.channels.resolve(this.initBotInfo.channelIDs.adminLog).send('Self care reminders restarted by <@' + i.user.id + '>!'); - await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); - } else { - await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); - } - }); - - - //starts the interval, and sends the first reminder immediately if startNow is true - if (startNow) { - sendReminder(this.initBotInfo); - } - interval = setInterval(sendReminder, timeInterval, this.initBotInfo); - - // sendReminder is the function that picks and sends the next reminder - async function sendReminder(initBotInfo) { - //get reminders parameters from db - var data = await getReminder(guild.id); - - //report in admin logs that there are no more messages - //TODO: consider having it just loop through the db again? - if (data === null) { - discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); - clearInterval(interval); - return; - } - - let reminder = data.reminder; - - const qEmbed = new MessageEmbed() - .setColor(initBotInfo.colors.embedColor) - .setTitle(reminder); - - roleId ? channel.send({ content: 'Hey <@&' + roleId + '> remember:', embeds: [qEmbed] }) : channel.send({ embeds: [qEmbed] }); - } - } -} -module.exports = SelfCareReminders; +const { Command } = require('@sapphire/framework'); +const { discordLog } = require('../../discord-services'); +const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); +const { getReminder } = require('../../db/firebase/firebaseUtil'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); +const { Message } = require('discord.js'); + +/** + * The self care command will send pre made reminders from firebase to the command channel. These reminders are self + * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. + * @category Commands + * @subcategory Admin-Utility + * @extends Command + */ +class SelfCareReminders extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Sends self-care reminders at designated times.', + }); + } + + /** + * + * @param {Command.Registry} registry + */ + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addIntegerOption(option => + option.setName('interval') + .setDescription('Time (minutes) between reminders') + .setRequired(true)) + .addRoleOption(option => + option.setName('notify') + .setDescription('Role to notify when a reminder drops') + .setRequired(false)) + .addBooleanOption(option => + option.setName('start_reminder_now') + .setDescription('True to start first reminder now, false to start it after one interval') + .setRequired(false)) + ), + { + idHints: '1052699103143927859' + }; + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + * @returns + */ + async chatInputRun(interaction) { + let interval; + + let channel = interaction.channel; + let userId = interaction.user.id; + let guild = interaction.guild; + let initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + + let timeInterval = interaction.options.getInteger('interval') * 60000; + let startNow = interaction.options.getBoolean('start_reminder_now'); + let roleId = interaction.options.getRole('notify'); + + if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { + interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + // keeps track of whether it has been paused + let paused = false; + + const startEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); + + interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); + + roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('play') + .setLabel('Play') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('pause') + .setLabel('Pause') + .setStyle('PRIMARY'), + ); + + /** @type {Message} */ + const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); + const isAdminOrStaffFilter = (i) => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = controlPanel.createMessageComponentCollector({ filter: isAdminOrStaffFilter }); + collector.on('collect', async (i) => { + if (interval != null && !paused && i.customId == 'pause') { + clearInterval(interval); + paused = true; + discordLog(guild, 'Self care reminders paused by <@' + i.user.id + '>!'); + await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); + } else if (paused && i.customId == 'play') { + await sendReminder(initBotInfo); + interval = setInterval(sendReminder, timeInterval, initBotInfo); + paused = false; + discordLog(guild, 'Self care reminders restarted by <@' + i.user.id + '>!'); + await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); + } else { + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); + } + }); + + + //starts the interval, and sends the first reminder immediately if startNow is true + if (startNow) { + sendReminder(initBotInfo); + } + interval = setInterval(sendReminder, timeInterval, initBotInfo); + + // sendReminder is the function that picks and sends the next reminder + async function sendReminder(initBotInfo) { + //get reminders parameters from db + const data = await getReminder(guild.id); + + //report in admin logs that there are no more messages + //TODO: consider having it just loop through the db again? + if (data === null) { + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); + clearInterval(interval); + return; + } + + let reminder = data.reminder; + + const qEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle(reminder); + + roleId ? channel.send({ content: 'Hey <@&' + roleId + '> remember:', embeds: [qEmbed] }) : channel.send({ embeds: [qEmbed] }); + } + } +} +module.exports = SelfCareReminders; diff --git a/commands/firebase_scripts/load-questions.js b/commands/firebase_scripts/load-questions.js index 88b25fab..04bde3bd 100644 --- a/commands/firebase_scripts/load-questions.js +++ b/commands/firebase_scripts/load-questions.js @@ -4,8 +4,7 @@ const firebaseUtil = require('../../db/firebase/firebaseUtil'); const fetch = require('node-fetch'); /** - * The self care command will send pre made reminders from firebase to the command channel. These reminders are self - * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. + * Loads discord contest questions from input JSON file * @category Commands * @subcategory Admin-Utility * @extends Command diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 948977aa..a82293b7 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -133,7 +133,8 @@ module.exports = { */ async getReminder(guildId) { //checks that the reminder has not been sent - let qref = getFactotumDoc().collection('guilds').doc(guildId).collection('reminders').where('sent', '==', false).limit(1); + let qref = module.exports.getFactotumSubCol().doc(guildId) + .collection('Reminders').where('sent', '==', false).limit(1); let reminder = (await qref.get()).docs[0]; //if there reminder unsent, change its status to asked if (reminder != undefined) { diff --git a/package-lock.json b/package-lock.json index 0f904417..efc3f55d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@discordjs/voice": "^0.11.0", "@google-cloud/firestore": "^4.15.1", - "@sapphire/framework": "^3.1.3", + "@sapphire/framework": "^3.2.0", "@sentry/node": "^6.14.3", "@sentry/tracing": "^6.14.3", "advanced-discord.js-prompts": "^1.7.0", @@ -455,23 +455,26 @@ } }, "node_modules/@sapphire/discord-utilities": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-2.11.6.tgz", - "integrity": "sha512-oFZYl7Prtqy8nD3ymmMpSMRy9Sdhp8E5PSI8n3OoBkHr4eSBvoOcBTxKapvRkHWes+amWeH/uB4UH31x7mG0KQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-2.12.0.tgz", + "integrity": "sha512-E/Qqb8PwEoX/WLVfcGvTglTgEGGcc/2rGtKBqhMcHcEEtNIY8dhQVYbW/KMNJpR/J81OqUJquVzpkzRe6fQWiw==", + "dependencies": { + "discord-api-types": "^0.36.3" + }, "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/discord.js-utilities": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sapphire/discord.js-utilities/-/discord.js-utilities-5.0.1.tgz", - "integrity": "sha512-J3PmnHW0c3Z8VylopSmQzQBunAiBmlMaPYxbeIRZB47xYiMTz/7bcVT9/8z6efprLyM2MdOdJrapoeArodU9CQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@sapphire/discord.js-utilities/-/discord.js-utilities-5.1.2.tgz", + "integrity": "sha512-zKXUkVzueT3Zag9D/ubpey0g/vLXLCVVFlmYoZqpkx1HsTLSTKz4hxbD7IQ/8q7rvI5Pm/Ex1jajPHMLXKmlpw==", "dependencies": { - "@sapphire/discord-utilities": "^2.11.6", + "@sapphire/discord-utilities": "^2.12.0", "@sapphire/duration": "^1.0.0", - "@sapphire/utilities": "^3.9.3", - "tslib": "^2.4.0" + "@sapphire/utilities": "^3.11.0", + "tslib": "^2.4.1" }, "engines": { "node": ">=16.6.0", @@ -479,29 +482,28 @@ } }, "node_modules/@sapphire/duration": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/duration/-/duration-1.0.0.tgz", - "integrity": "sha512-B+6nKYnBmIlqqbamcR4iBvbQHz6/Kq2JUVM0rA3lQ+aYUYDdcA1Spt66CKtPWwdTYEtSv0VY6Jv27WCtFNYTUg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@sapphire/duration/-/duration-1.1.2.tgz", + "integrity": "sha512-m+DpXedUHdnH3rM6P9Hiyb9dpdXKb8WeTAVIug0QuN8tarQedbymbOor+UFmBfCbKOkoW9HvGK10xDwDvSfKrw==", "engines": { "node": ">=v14.0.0", "npm": ">=7.0.0" } }, "node_modules/@sapphire/framework": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-3.1.3.tgz", - "integrity": "sha512-YHB6oeY095vrRv8ksW0zm/GSWuw+vDLvU0BzJBTGI0KYQfNq2d6OVCjY0ADxFgQSgC9AxOHj2h2GvGcLET+S4g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-3.2.0.tgz", + "integrity": "sha512-GrxOlyxrydhwXH68zVwhaEys0XLqEJg6o0TCZLtEYhhDY6oRoOl1MwEiomS1cbDYNRpPkdY7ljHcxD44U6cZ9g==", "dependencies": { "@discordjs/builders": "^0.16.0", - "@sapphire/discord-utilities": "^2.11.6", - "@sapphire/discord.js-utilities": "^5.0.1", - "@sapphire/lexure": "^1.1.1", - "@sapphire/pieces": "^3.5.2", + "@sapphire/discord-utilities": "^2.12.0", + "@sapphire/discord.js-utilities": "^5.1.2", + "@sapphire/lexure": "^1.1.2", + "@sapphire/pieces": "^3.6.0", "@sapphire/ratelimits": "^2.4.5", - "@sapphire/result": "^2.5.0", + "@sapphire/result": "^2.6.0", "@sapphire/stopwatch": "^1.5.0", - "@sapphire/utilities": "^3.10.0", - "tslib": "^2.4.0" + "@sapphire/utilities": "^3.11.0" }, "engines": { "node": ">=16.6.0", @@ -3706,9 +3708,9 @@ "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" }, "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tweetnacl": { "version": "1.0.3", @@ -4418,41 +4420,43 @@ "integrity": "sha512-JkLdIsP8fPAdh9ZZjrbHWR/+mZj0wvKS5ICibcLrRI1j84UmLMshx5n9QmL8b95d4onJ2xxiyugTgSAX7AalmA==" }, "@sapphire/discord-utilities": { - "version": "2.11.6", - "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-2.11.6.tgz", - "integrity": "sha512-oFZYl7Prtqy8nD3ymmMpSMRy9Sdhp8E5PSI8n3OoBkHr4eSBvoOcBTxKapvRkHWes+amWeH/uB4UH31x7mG0KQ==" + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@sapphire/discord-utilities/-/discord-utilities-2.12.0.tgz", + "integrity": "sha512-E/Qqb8PwEoX/WLVfcGvTglTgEGGcc/2rGtKBqhMcHcEEtNIY8dhQVYbW/KMNJpR/J81OqUJquVzpkzRe6fQWiw==", + "requires": { + "discord-api-types": "^0.36.3" + } }, "@sapphire/discord.js-utilities": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@sapphire/discord.js-utilities/-/discord.js-utilities-5.0.1.tgz", - "integrity": "sha512-J3PmnHW0c3Z8VylopSmQzQBunAiBmlMaPYxbeIRZB47xYiMTz/7bcVT9/8z6efprLyM2MdOdJrapoeArodU9CQ==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@sapphire/discord.js-utilities/-/discord.js-utilities-5.1.2.tgz", + "integrity": "sha512-zKXUkVzueT3Zag9D/ubpey0g/vLXLCVVFlmYoZqpkx1HsTLSTKz4hxbD7IQ/8q7rvI5Pm/Ex1jajPHMLXKmlpw==", "requires": { - "@sapphire/discord-utilities": "^2.11.6", + "@sapphire/discord-utilities": "^2.12.0", "@sapphire/duration": "^1.0.0", - "@sapphire/utilities": "^3.9.3", - "tslib": "^2.4.0" + "@sapphire/utilities": "^3.11.0", + "tslib": "^2.4.1" } }, "@sapphire/duration": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sapphire/duration/-/duration-1.0.0.tgz", - "integrity": "sha512-B+6nKYnBmIlqqbamcR4iBvbQHz6/Kq2JUVM0rA3lQ+aYUYDdcA1Spt66CKtPWwdTYEtSv0VY6Jv27WCtFNYTUg==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@sapphire/duration/-/duration-1.1.2.tgz", + "integrity": "sha512-m+DpXedUHdnH3rM6P9Hiyb9dpdXKb8WeTAVIug0QuN8tarQedbymbOor+UFmBfCbKOkoW9HvGK10xDwDvSfKrw==" }, "@sapphire/framework": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-3.1.3.tgz", - "integrity": "sha512-YHB6oeY095vrRv8ksW0zm/GSWuw+vDLvU0BzJBTGI0KYQfNq2d6OVCjY0ADxFgQSgC9AxOHj2h2GvGcLET+S4g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@sapphire/framework/-/framework-3.2.0.tgz", + "integrity": "sha512-GrxOlyxrydhwXH68zVwhaEys0XLqEJg6o0TCZLtEYhhDY6oRoOl1MwEiomS1cbDYNRpPkdY7ljHcxD44U6cZ9g==", "requires": { "@discordjs/builders": "^0.16.0", - "@sapphire/discord-utilities": "^2.11.6", - "@sapphire/discord.js-utilities": "^5.0.1", - "@sapphire/lexure": "^1.1.1", - "@sapphire/pieces": "^3.5.2", + "@sapphire/discord-utilities": "^2.12.0", + "@sapphire/discord.js-utilities": "^5.1.2", + "@sapphire/lexure": "^1.1.2", + "@sapphire/pieces": "^3.6.0", "@sapphire/ratelimits": "^2.4.5", - "@sapphire/result": "^2.5.0", + "@sapphire/result": "^2.6.0", "@sapphire/stopwatch": "^1.5.0", - "@sapphire/utilities": "^3.10.0", - "tslib": "^2.4.0" + "@sapphire/utilities": "^3.11.0" } }, "@sapphire/lexure": { @@ -6975,9 +6979,9 @@ "integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg==" }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "tweetnacl": { "version": "1.0.3", diff --git a/package.json b/package.json index 09214501..3a5a7898 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "dependencies": { "@discordjs/voice": "^0.11.0", "@google-cloud/firestore": "^4.15.1", - "@sapphire/framework": "^3.1.3", + "@sapphire/framework": "^3.2.0", "@sentry/node": "^6.14.3", "@sentry/tracing": "^6.14.3", "advanced-discord.js-prompts": "^1.7.0", From 7e2e0a7ad476d6fdc4fa378c292a46f7da59b643 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sat, 27 Jul 2024 21:28:19 -0700 Subject: [PATCH 54/67] Fix start-report command --- commands/a_utility/self-care.js | 297 ++++++++++---------- commands/hacker_utility/start-report.js | 353 ++++++++++++------------ 2 files changed, 324 insertions(+), 326 deletions(-) diff --git a/commands/a_utility/self-care.js b/commands/a_utility/self-care.js index 4c30a4c1..5e7f04a8 100644 --- a/commands/a_utility/self-care.js +++ b/commands/a_utility/self-care.js @@ -1,149 +1,148 @@ -const { Command } = require('@sapphire/framework'); -const { discordLog } = require('../../discord-services'); -const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); -const { getReminder } = require('../../db/firebase/firebaseUtil'); -const firebaseUtil = require('../../db/firebase/firebaseUtil'); -const { Message } = require('discord.js'); - -/** - * The self care command will send pre made reminders from firebase to the command channel. These reminders are self - * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. - * @category Commands - * @subcategory Admin-Utility - * @extends Command - */ -class SelfCareReminders extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Sends self-care reminders at designated times.', - }); - } - - /** - * - * @param {Command.Registry} registry - */ - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - .addIntegerOption(option => - option.setName('interval') - .setDescription('Time (minutes) between reminders') - .setRequired(true)) - .addRoleOption(option => - option.setName('notify') - .setDescription('Role to notify when a reminder drops') - .setRequired(false)) - .addBooleanOption(option => - option.setName('start_reminder_now') - .setDescription('True to start first reminder now, false to start it after one interval') - .setRequired(false)) - ), - { - idHints: '1052699103143927859' - }; - } - - /** - * - * @param {Command.ChatInputInteraction} interaction - * @returns - */ - async chatInputRun(interaction) { - let interval; - - let channel = interaction.channel; - let userId = interaction.user.id; - let guild = interaction.guild; - let initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); - - let timeInterval = interaction.options.getInteger('interval') * 60000; - let startNow = interaction.options.getBoolean('start_reminder_now'); - let roleId = interaction.options.getRole('notify'); - - if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { - interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - // keeps track of whether it has been paused - let paused = false; - - const startEmbed = new MessageEmbed() - .setColor(initBotInfo.embedColor) - .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); - - interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); - - roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('play') - .setLabel('Play') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('pause') - .setLabel('Pause') - .setStyle('PRIMARY'), - ); - - /** @type {Message} */ - const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); - const isAdminOrStaffFilter = (i) => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); - const collector = controlPanel.createMessageComponentCollector({ filter: isAdminOrStaffFilter }); - collector.on('collect', async (i) => { - if (interval != null && !paused && i.customId == 'pause') { - clearInterval(interval); - paused = true; - discordLog(guild, 'Self care reminders paused by <@' + i.user.id + '>!'); - await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); - } else if (paused && i.customId == 'play') { - await sendReminder(initBotInfo); - interval = setInterval(sendReminder, timeInterval, initBotInfo); - paused = false; - discordLog(guild, 'Self care reminders restarted by <@' + i.user.id + '>!'); - await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); - } else { - await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); - } - }); - - - //starts the interval, and sends the first reminder immediately if startNow is true - if (startNow) { - sendReminder(initBotInfo); - } - interval = setInterval(sendReminder, timeInterval, initBotInfo); - - // sendReminder is the function that picks and sends the next reminder - async function sendReminder(initBotInfo) { - //get reminders parameters from db - const data = await getReminder(guild.id); - - //report in admin logs that there are no more messages - //TODO: consider having it just loop through the db again? - if (data === null) { - discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); - clearInterval(interval); - return; - } - - let reminder = data.reminder; - - const qEmbed = new MessageEmbed() - .setColor(initBotInfo.embedColor) - .setTitle(reminder); - - roleId ? channel.send({ content: 'Hey <@&' + roleId + '> remember:', embeds: [qEmbed] }) : channel.send({ embeds: [qEmbed] }); - } - } -} -module.exports = SelfCareReminders; +const { Command } = require('@sapphire/framework'); +const { discordLog } = require('../../discord-services'); +const { MessageEmbed, MessageActionRow, MessageButton } = require('discord.js'); +const { getReminder } = require('../../db/firebase/firebaseUtil'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); +const { Message } = require('discord.js'); + +/** + * The self care command will send pre made reminders from firebase to the command channel. These reminders are self + * care reminders. Will prompt a role to mention with each reminder. We recommend that be an opt-in role. + * @category Commands + * @subcategory Admin-Utility + * @extends Command + */ +class SelfCareReminders extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Sends self-care reminders at designated times.', + }); + } + + /** + * + * @param {Command.Registry} registry + */ + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addIntegerOption(option => + option.setName('interval') + .setDescription('Time (minutes) between reminders') + .setRequired(true)) + .addRoleOption(option => + option.setName('notify') + .setDescription('Role to notify when a reminder drops') + .setRequired(false)) + .addBooleanOption(option => + option.setName('start_reminder_now') + .setDescription('True to start first reminder now, false to start it after one interval') + .setRequired(false)) + ), + { + idHints: '1052699103143927859' + }; + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + */ + async chatInputRun(interaction) { + let interval; + + let channel = interaction.channel; + let userId = interaction.user.id; + let guild = interaction.guild; + let initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + + let timeInterval = interaction.options.getInteger('interval') * 60000; + let startNow = interaction.options.getBoolean('start_reminder_now'); + let roleId = interaction.options.getRole('notify'); + + if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { + interaction.reply({ message: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + // keeps track of whether it has been paused + let paused = false; + + const startEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle('To encourage healthy hackathon habits, we will be sending hourly self-care reminders!'); + + interaction.reply({ content: 'Self-care reminders started!', ephemeral: true }); + + roleId ? interaction.channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed] }) : interaction.channel.send({ embeds: [startEmbed] }); + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('play') + .setLabel('Play') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('pause') + .setLabel('Pause') + .setStyle('PRIMARY'), + ); + + /** @type {Message} */ + const controlPanel = await adminConsole.send({ content: 'Self care reminders started by <@' + userId + '>', components: [row] }); + const isAdminOrStaffFilter = (i) => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = controlPanel.createMessageComponentCollector({ filter: isAdminOrStaffFilter }); + collector.on('collect', async (i) => { + if (interval != null && !paused && i.customId == 'pause') { + clearInterval(interval); + paused = true; + discordLog(guild, 'Self care reminders paused by <@' + i.user.id + '>!'); + await i.reply({ content: 'Self care reminders has been paused!', ephemeral: true }); + } else if (paused && i.customId == 'play') { + await sendReminder(initBotInfo); + interval = setInterval(sendReminder, timeInterval, initBotInfo); + paused = false; + discordLog(guild, 'Self care reminders restarted by <@' + i.user.id + '>!'); + await i.reply({ content: 'Self care reminders has been un-paused!', ephemeral: true }); + } else { + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); + } + }); + + + //starts the interval, and sends the first reminder immediately if startNow is true + if (startNow) { + sendReminder(initBotInfo); + } + interval = setInterval(sendReminder, timeInterval, initBotInfo); + + // sendReminder is the function that picks and sends the next reminder + async function sendReminder(initBotInfo) { + //get reminders parameters from db + const data = await getReminder(guild.id); + + //report in admin logs that there are no more messages + //TODO: consider having it just loop through the db again? + if (data === null) { + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> HI, PLEASE FEED ME more self-care messages!!'); + clearInterval(interval); + return; + } + + let reminder = data.reminder; + + const qEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle(reminder); + + roleId ? channel.send({ content: 'Hey <@&' + roleId + '> remember:', embeds: [qEmbed] }) : channel.send({ embeds: [qEmbed] }); + } + } +} +module.exports = SelfCareReminders; diff --git a/commands/hacker_utility/start-report.js b/commands/hacker_utility/start-report.js index 884f34ae..0a618e51 100644 --- a/commands/hacker_utility/start-report.js +++ b/commands/hacker_utility/start-report.js @@ -1,178 +1,177 @@ -// const { Command } = require('discord.js-commando'); -// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); -// const { MessageEmbed, Message } = require('discord.js'); -// const BotGuild = require('../../db/mongo/BotGuild'); - -// /** -// * The report command allows users to report incidents from the server to the admins. Reports are made -// * via the bot's DMs and are 100% anonymous. -// * @category Commands -// * @subcategory Hacker-Utility -// * @extends Command -// */ -// class Report extends Command { -// constructor(client) { -// super(client, { -// name: 'report', -// group: 'hacker_utility', -// memberName: 'report to admins', -// description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', -// // not guild only! -// args: [], -// }); -// } - -// /** -// * @param {Message} message -// */ -// async run (message) { -// let botGuild = await BotGuild.findById(message.guild.id); - -// deleteMessage(message); - -// if (!botGuild.report.isEnabled) { -// sendMessageToMember(message.author, 'The report functionality is disabled for this guild.'); -// return; -// } - -// const embed = new MessageEmbed() -// .setColor(botGuild.colors.embedColor) -// .setTitle('Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!') -// .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + -// 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + -// 'Copy paste the format and send it to me in this channel!') -// .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + -// 'Reason for report (one line):\n' + -// 'Detailed Explanation:\n' + -// 'Name of channel where the incident occurred (if possible):'); - -// // send message to user with report format -// var msgEmbed = await message.author.send(embed); - -// // await response -// msgEmbed.channel.awaitMessages(m => true, {max: 1}).then(async msgs => { -// var msg = msgs.first(); - -// msgEmbed.delete(); -// message.author.send('Thank you for the report! Our admin team will look at it ASAP!'); - -// // send the report content to the admin report channel! -// var incomingReportChn = await message.guild.channels.resolve(botGuild.report.incomingReportChannelID); - -// const adminMsgEmbed = new MessageEmbed() -// .setColor(botGuild.colors.embedColor) -// .setTitle('There is a new report that needs your attention!') -// .setDescription(msg.content); - -// // send embed with text message to ping admin -// incomingReportChn.send('<@&' + botGuild.roleIDs.adminRole + '> Incoming Report', {embed: adminMsgEmbed}); -// }); - -// } -// } -// module.exports = Report; - -const { Command } = require('@sapphire/framework'); -// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); -const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); -const BotGuild = require('../../db/mongo/BotGuild'); -const BotGuildModel = require('../../classes/Bot/bot-guild'); -const { discordLog } = require('../../discord-services'); - -/** - * The report command allows users to report incidents from the server to the admins. Reports are made - * via the bot's DMs and are 100% anonymous. - * @category Commands - * @subcategory Hacker-Utility - * @extends Command - */ -class StartReport extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - ), - { - idHints: '1214159059880517652' - }; - } - - /** - * @param {BotGuildModel} botGuild - * @param {Message} message - */ - async chatInputRun(interaction) { - this.botGuild = await BotGuild.findById(interaction.guild.id); - const guild = interaction.guild; - // const userId = interaction.user.id; - - // const embed = new MessageEmbed() - // .setTitle(`See an issue you'd like to annoymously report at ${interaction.guild.name}? Let our organizers know!`); - - const embed = new MessageEmbed() - .setTitle('Annoymously report users who are not following server or MLH rules. Help makes our community safer!') - .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + - 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + - 'Copy paste the format and send it to me in this channel!') - .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + - 'Reason for report (one line):\n' + - 'Detailed Explanation:\n' + - 'Name of channel where the incident occurred (if possible):'); - // modal timeout warning? - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('report') - .setLabel('Report an issue') - .setStyle('PRIMARY'), - ); - interaction.reply({ content: 'Report started!', ephemeral: true }); - const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); - - const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); - - checkInCollector.on('collect', async i => { - const modal = new Modal() - .setCustomId('reportModal') - .setTitle('Report an issue') - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('issueMessage') - .setLabel('Reason for report:') - .setMinLength(3) - .setMaxLength(1000) - .setStyle(2) - .setPlaceholder('Type your issue here...') - .setRequired(true), - ), - ]); - await i.showModal(modal); - - const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) - .catch(error => { - }); - - if (submitted) { - const issueMessage = submitted.fields.getTextInputValue('issueMessage'); - - try { - discordLog(interaction.guild, `<@&${interaction.guild.roleIDs.staffRole}> New annoymous report:\n\n ${issueMessage}`); - } catch { - discordLog(interaction.guild, `New annoymous report:\n\n ${issueMessage}`); - } - submitted.reply({ content: 'Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!', ephemeral: true }); - return; - } - }); - } -} +// const { Command } = require('discord.js-commando'); +// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); +// const { MessageEmbed, Message } = require('discord.js'); +// const BotGuild = require('../../db/mongo/BotGuild'); + +// /** +// * The report command allows users to report incidents from the server to the admins. Reports are made +// * via the bot's DMs and are 100% anonymous. +// * @category Commands +// * @subcategory Hacker-Utility +// * @extends Command +// */ +// class Report extends Command { +// constructor(client) { +// super(client, { +// name: 'report', +// group: 'hacker_utility', +// memberName: 'report to admins', +// description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', +// // not guild only! +// args: [], +// }); +// } + +// /** +// * @param {Message} message +// */ +// async run (message) { +// let botGuild = await BotGuild.findById(message.guild.id); + +// deleteMessage(message); + +// if (!botGuild.report.isEnabled) { +// sendMessageToMember(message.author, 'The report functionality is disabled for this guild.'); +// return; +// } + +// const embed = new MessageEmbed() +// .setColor(botGuild.colors.embedColor) +// .setTitle('Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!') +// .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + +// 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + +// 'Copy paste the format and send it to me in this channel!') +// .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + +// 'Reason for report (one line):\n' + +// 'Detailed Explanation:\n' + +// 'Name of channel where the incident occurred (if possible):'); + +// // send message to user with report format +// var msgEmbed = await message.author.send(embed); + +// // await response +// msgEmbed.channel.awaitMessages(m => true, {max: 1}).then(async msgs => { +// var msg = msgs.first(); + +// msgEmbed.delete(); +// message.author.send('Thank you for the report! Our admin team will look at it ASAP!'); + +// // send the report content to the admin report channel! +// var incomingReportChn = await message.guild.channels.resolve(botGuild.report.incomingReportChannelID); + +// const adminMsgEmbed = new MessageEmbed() +// .setColor(botGuild.colors.embedColor) +// .setTitle('There is a new report that needs your attention!') +// .setDescription(msg.content); + +// // send embed with text message to ping admin +// incomingReportChn.send('<@&' + botGuild.roleIDs.adminRole + '> Incoming Report', {embed: adminMsgEmbed}); +// }); + +// } +// } +// module.exports = Report; + +const { Command } = require('@sapphire/framework'); +// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); +const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); +const { discordLog } = require('../../discord-services'); + +/** + * The report command allows users to report incidents from the server to the admins. Reports are made + * via the bot's DMs and are 100% anonymous. + * @category Commands + * @subcategory Hacker-Utility + * @extends Command + */ +class StartReport extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + ), + { + idHints: '1214159059880517652' + }; + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + */ + async chatInputRun(interaction) { + // const userId = interaction.user.id; + + // const embed = new MessageEmbed() + // .setTitle(`See an issue you'd like to annoymously report at ${interaction.guild.name}? Let our organizers know!`); + + const embed = new MessageEmbed() + .setTitle('Anonymously report users who are not following server or MLH rules. Help makes our community safer!') + .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + + 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + + 'Copy paste the format and send it to me in this channel!') + .addFields({ + name: 'Format:', + value: 'User(s) discord username(s) (including discord id number(s)):\n' + + 'Reason for report (one line):\n' + + 'Detailed Explanation:\n' + + 'Name of channel where the incident occurred (if possible):' + }); + // modal timeout warning? + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('report') + .setLabel('Report an issue') + .setStyle('PRIMARY'), + ); + interaction.reply({ content: 'Report started!', ephemeral: true }); + const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); + + const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); + + checkInCollector.on('collect', async i => { + const modal = new Modal() + .setCustomId('reportModal') + .setTitle('Report an issue') + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('issueMessage') + .setLabel('Reason for report:') + .setMinLength(3) + .setMaxLength(1000) + .setStyle(2) + .setPlaceholder('Type your issue here...') + .setRequired(true), + ), + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(error => { + }); + + if (submitted) { + const issueMessage = submitted.fields.getTextInputValue('issueMessage'); + + try { + discordLog(interaction.guild, `<@&${interaction.guild.roleIDs.staffRole}> New anonymous report:\n\n ${issueMessage}`); + } catch { + discordLog(interaction.guild, `New anonymous report:\n\n ${issueMessage}`); + } + submitted.reply({ content: 'Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!', ephemeral: true }); + return; + } + }); + } +} module.exports = StartReport; \ No newline at end of file From 107c36baaccfd93e93d501010da49f9a4a57058c Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sun, 28 Jul 2024 03:44:59 -0700 Subject: [PATCH 55/67] Fix discord-contests and load-questions command --- commands/a_activity/discord-contests.js | 681 ++++++++++---------- commands/firebase_scripts/load-questions.js | 12 +- db/firebase/firebaseUtil.js | 8 +- 3 files changed, 349 insertions(+), 352 deletions(-) diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index a851cdfa..f4996dce 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -1,341 +1,340 @@ -const { Command } = require('@sapphire/framework'); -const { discordLog, checkForRole } = require('../../discord-services'); -const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); -const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); -const firebaseUtil = require('../../db/firebase/firebaseUtil'); - -/** - * The DiscordContests class handles all functions related to Discord contests. It will ask questions in set intervals and pick winners - * based on keywords for those questions that have correct answers. For other questions it will tag staff and staff will be able to tell - * it the winner. It can also be paused and un-paused, and questions can be removed. - * - * Note: all answers are case-insensitive but any extra or missing characters will be considered incorrect. - * @category Commands - * @subcategory Activity - * @extends Command - * @guildonly - */ -class DiscordContests extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Start discord contests.' - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - .addIntegerOption(option => - option.setName('interval') - .setDescription('Time (minutes) between questions') - .setRequired(true)) - .addRoleOption(option => - option.setName('notify') - .setDescription('Role to notify when a question drops') - .setRequired(true)) - .addBooleanOption(option => - option.setName('start_question_now') - .setDescription('True to start first question now, false to start it after one interval') - .setRequired(false)) - ), - { - idHints: '1051737343729610812' - }; - } - - /** - * Stores a map which keeps the questions (strings) as keys and an array of possible answers (strings) as values. It iterates through - * each key in order and asks them in the Discord channel in which it was called at the given intervals. It also listens for emojis - * that tell it to pause, resume, or remove a specified question. - * @param {BotGuildModel} this.botGuild - * @param {Message} message - the message in which this command was called - */ - async chatInputRun(interaction) { - // helpful prompt vars - let channel = interaction.channel; - let userId = interaction.user.id; - // this.botGuild = this.botGuild; - let guild = interaction.guild; - this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); - let adminLog = guild.channels.resolve(this.initBotInfo.channelIDs.adminLog); - let adminConsole = guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); - - var interval; - - //ask user for time interval between questions - var timeInterval = interaction.options.getInteger('interval') * 60000; - var startNow = interaction.options.getBoolean('start_question_now'); - var roleId = interaction.options.getRole('notify'); - - if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { - interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - if (Object.values(this.initBotInfo.roleIDs).includes(roleId) || Object.values(this.initBotInfo.verification.verificationRoles).includes(roleId)) { - interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); - return; - } - // try { - // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); - // timeInterval = 1000 * 60 * num; - - // // ask user whether to start asking questions now(true) or after 1 interval (false) - // var startNow = await SpecialPrompt.boolean({prompt: 'Type "yes" to start first question now, "no" to start one time interval from now. ', channel, userId, cancelable: true}); - - // // id of role to mention when new questions come out - // var roleId = (await RolePrompt.single({prompt: 'What role should I notify with a new Discord contest is available?', channel, userId})).id; - // } catch (error) { - // channel.send('<@' + userId + '> Command was canceled due to prompt being canceled.').then(msg => msg.delete({timeout: 5000})); - // return; - // } - - //paused keeps track of whether it has been paused - var paused = false; - - /** - * array of winners' ids - * @type {Array} - */ - const winners = []; - - var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!'; - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('play') - .setLabel('Play') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('pause') - .setLabel('Pause') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('refresh') - .setLabel('Refresh leaderboard') - .setStyle('PRIMARY'), - ); - - - // const playFilter = i => i.customId == 'play' && !i.user.bot && (guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)); - // const playCollector = adminConsole.createMessageComponentCollector(playFilter); - // playCollector.on('collect', async i => { - // console.log('play collector') - // if (paused) { - // sendQuestion(this.botGuild); - // interval = setInterval(sendQuestion, timeInterval, this.botGuild); - // paused = false; - // await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); - // await i.reply('<@&' + i.user.id + '> Discord contest has been un-paused!'); - // } - // }); - - const startEmbed = new MessageEmbed() - .setColor(this.initBotInfo.embedColor) - .setTitle(string) - .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') - .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); - - const leaderboard = new MessageEmbed() - .setTitle('Leaderboard'); - - let pinnedMessage = await channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed, leaderboard] }); - pinnedMessage.pin(); - pinnedMessage.react('🍀'); - - const roleSelectionCollector = pinnedMessage.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true}); - roleSelectionCollector.on('collect', (reaction, user) => { - if (reaction.emoji.name === '🍀') { - guild.members.cache.get(user.id).roles.add(roleId); - } - }); - roleSelectionCollector.on('remove', (reaction, user) => { - if (reaction.emoji.name === '🍀') { - guild.members.cache.get(user.id).roles.remove(roleId); - } - }); - - interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); - const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); - adminLog.send('Discord contests started by <@' + userId + '>'); - const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(this.initBotInfo.roleIDs.adminRole)); - const collector = controlPanel.createMessageComponentCollector({filter}); - collector.on('collect', async i => { - if (i.customId == 'refresh') { - await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }); - await updateLeaderboard(null); - } else if (interval != null && !paused && i.customId == 'pause') { - clearInterval(interval); - paused = true; - await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); - await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); - } else if (paused && i.customId == 'play') { - await sendQuestion(this.initBotInfo); - interval = setInterval(sendQuestion, timeInterval, this.initBotInfo); - paused = false; - await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); - await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); - } else { - await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); - } - }); - - //starts the interval, and sends the first question immediately if startNow is true - if (startNow) { - await sendQuestion(this.initBotInfo); - } - interval = setInterval(sendQuestion, timeInterval, this.initBotInfo); - - async function updateLeaderboard(memberId) { - if (memberId) { - await saveToLeaderboard(guild.id, memberId); - } - const winnersList = await retrieveLeaderboard(guild.id); - var leaderboardString = ''; - winnersList.forEach(winner => { - leaderboardString += '<@' + winner.memberId + '>: '; - if (winner.points > 1) { - leaderboardString += winner.points + ' points\n'; - } else if (winner.points == 1) { - leaderboardString += '1 point\n'; - } - }); - const newLeaderboard = new MessageEmbed(leaderboard).setDescription(leaderboardString); - pinnedMessage.edit({ embeds: [startEmbed, newLeaderboard] }); - } - - /** - * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * sendQuestion is the function that picks and sends the next question, then picks the winner by matching participants' messages - * against the answer(s) or receives the winner from Staff. Once it reaches the end it will notify Staff in the Logs channel and - * list all the winners in order. - */ - async function sendQuestion(initBotInfo) { - //get question's parameters from db - var data = await getQuestion(guild.id); - - //sends results to Staff after all questions have been asked and stops looping - if (data === null) { - discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> Discord contests have ended!'); - clearInterval(interval); - return; - } - - let question = data.question; - let answers = data.answers; - let needAllAnswers = data.needAllAnswers; - - const qEmbed = new MessageEmbed() - .setTitle('A new Discord Contest Question:') - .setDescription(question); - - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('winner') - .setLabel('Select winner') - .setStyle('PRIMARY'), - ); - - - channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }).then(async (msg) => { - if (answers.length === 0) { - //send message to console - const questionMsg = await adminConsole.send({ content: '<@&' + initBotInfo.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); - - const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); - const collector = await questionMsg.createMessageComponentCollector({ filter }); - - collector.on('collect', async i => { - const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); - - const winnerFilter = message => message.author.id === i.user.id; // error? - const winnerCollector = await adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); - winnerCollector.on('collect', async m => { - if (m.mentions.members.size > 0) { - const member = await m.mentions.members.first(); - const memberId = await member.user.id; - await m.delete(); - await questionMsg.delete(); - await i.editReply('<@' + memberId + '> has been recorded!'); - row.components[0].setDisabled(true); - // row.components[0].setDisabled(); - await channel.send('Congrats <@' + memberId + '> for the best answer to the last question!'); - // winners.push(memberId); - await updateLeaderboard(memberId); - collector.stop(); - // await recordWinner(memberId); - } else { - await m.delete(); - // await winnerRequest.deleteReply(); - let errorMsg = await i.editReply({ content: 'Message does not include a user mention!' }); - setTimeout(function () { - errorMsg.delete(); - }, 5000); - } - }); - }); - } else { - //automatically mark answers - const filter = m => !m.author.bot && (initBotInfo.verification.isEnabled ? checkForRole(m.member, initBotInfo.verification.roles.get('hacker')) : checkForRole(m.member, initBotInfo.roleIDs.memberRole)); - const collector = channel.createMessageCollector({ filter, time: timeInterval * 0.75 }); - - collector.on('collect', async m => { - if (!needAllAnswers) { - // for questions that have numbers as answers, the answer has to match at least one of the correct answers exactly - if (!isNaN(answers[0])) { - if (answers.some(correctAnswer => m.content === correctAnswer)) { - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } else if (answers.some(correctAnswer => m.content.toLowerCase().includes(correctAnswer.toLowerCase()))) { - //for most questions, an answer that contains at least once item of the answer array is correct - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } else { - //check if all answers in answer array are in the message - if (answers.every((answer) => m.content.toLowerCase().includes(answer.toLowerCase()))) { - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(', ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } - }); - - collector.on('end', async () => { - await channel.send('Answers are no longer being accepted. Stay tuned for the next question!'); - }); - } - }); - } - - async function recordWinner(member) { - try { - let email = await lookupById(guild.id, member.id); - discordLog(guild, `Discord contest winner: <@${member.id}> - ${email}`); - } catch (error) { - console.log(error); - } - } - } -} -module.exports = DiscordContests; +const { Command } = require('@sapphire/framework'); +const { discordLog, checkForRole } = require('../../discord-services'); +const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); +const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); + +/** + * The DiscordContests class handles all functions related to Discord contests. It will ask questions in set intervals and pick winners + * based on keywords for those questions that have correct answers. For other questions it will tag staff and staff will be able to tell + * it the winner. It can also be paused and un-paused, and questions can be removed. + * + * Note: all answers are case-insensitive but any extra or missing characters will be considered incorrect. + * @category Commands + * @subcategory Activity + * @extends Command + * @guildonly + */ +class DiscordContests extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Start discord contests.' + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addIntegerOption(option => + option.setName('interval') + .setDescription('Time (minutes) between questions') + .setRequired(true)) + .addRoleOption(option => + option.setName('notify') + .setDescription('Role to notify when a question drops') + .setRequired(true)) + .addBooleanOption(option => + option.setName('start_question_now') + .setDescription('True to start first question now, false to start it after one interval') + .setRequired(false)) + ), + { + idHints: '1051737343729610812' + }; + } + + /** + * Stores a map which keeps the questions (strings) as keys and an array of possible answers (strings) as values. It iterates through + * each key in order and asks them in the Discord channel in which it was called at the given intervals. It also listens for emojis + * that tell it to pause, resume, or remove a specified question. + * @param {Command.ChatInputInteraction} interaction + */ + async chatInputRun(interaction) { + // helpful prompt vars + let channel = interaction.channel; + let userId = interaction.user.id; + let guild = interaction.guild; + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + let adminLog = await guild.channels.fetch(initBotInfo.channelIDs.adminLog); + let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + + let interval; + + //ask user for time interval between questions + let timeInterval = interaction.options.getInteger('interval') * 60000; + let startNow = interaction.options.getBoolean('start_question_now'); + let roleId = interaction.options.getRole('notify'); + + if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { + interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + if (Object.values(initBotInfo.roleIDs).includes(roleId.id) || Array(initBotInfo.verification.roles).find((r) => r.roleId === roleId.id)) { + interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); + return; + } + // try { + // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); + // timeInterval = 1000 * 60 * num; + + // // ask user whether to start asking questions now(true) or after 1 interval (false) + // var startNow = await SpecialPrompt.boolean({prompt: 'Type "yes" to start first question now, "no" to start one time interval from now. ', channel, userId, cancelable: true}); + + // // id of role to mention when new questions come out + // var roleId = (await RolePrompt.single({prompt: 'What role should I notify with a new Discord contest is available?', channel, userId})).id; + // } catch (error) { + // channel.send('<@' + userId + '> Command was canceled due to prompt being canceled.').then(msg => msg.delete({timeout: 5000})); + // return; + // } + + //paused keeps track of whether it has been paused + var paused = false; + + /** + * array of winners' ids + * @type {Array} + */ + const winners = []; + + var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!'; + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('play') + .setLabel('Play') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('pause') + .setLabel('Pause') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('refresh') + .setLabel('Refresh leaderboard') + .setStyle('PRIMARY'), + ); + + + // const playFilter = i => i.customId == 'play' && !i.user.bot && (guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)); + // const playCollector = adminConsole.createMessageComponentCollector(playFilter); + // playCollector.on('collect', async i => { + // console.log('play collector') + // if (paused) { + // sendQuestion(this.botGuild); + // interval = setInterval(sendQuestion, timeInterval, this.botGuild); + // paused = false; + // await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); + // await i.reply('<@&' + i.user.id + '> Discord contest has been un-paused!'); + // } + // }); + + const startEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle(string) + .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') + .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); + + const leaderboard = new MessageEmbed() + .setTitle('Leaderboard'); + + let pinnedMessage = await channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed, leaderboard] }); + pinnedMessage.pin(); + pinnedMessage.react('🍀'); + + const roleSelectionCollector = pinnedMessage.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true}); + roleSelectionCollector.on('collect', (reaction, user) => { + if (reaction.emoji.name === '🍀') { + guild.members.cache.get(user.id).roles.add(roleId); + } + }); + roleSelectionCollector.on('remove', (reaction, user) => { + if (reaction.emoji.name === '🍀') { + guild.members.cache.get(user.id).roles.remove(roleId); + } + }); + + interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); + const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); + adminLog.send('Discord contests started by <@' + userId + '>'); + const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = controlPanel.createMessageComponentCollector({filter}); + collector.on('collect', async i => { + if (i.customId == 'refresh') { + await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }); + await updateLeaderboard(null); + } else if (interval != null && !paused && i.customId == 'pause') { + clearInterval(interval); + paused = true; + await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); + } else if (paused && i.customId == 'play') { + await sendQuestion(initBotInfo); + interval = setInterval(sendQuestion, timeInterval, initBotInfo); + paused = false; + await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); + } else { + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); + } + }); + + //starts the interval, and sends the first question immediately if startNow is true + if (startNow) { + await sendQuestion(initBotInfo); + } + interval = setInterval(sendQuestion, timeInterval, initBotInfo); + + async function updateLeaderboard(memberId) { + if (memberId) { + await saveToLeaderboard(guild.id, memberId); + } + const winnersList = await retrieveLeaderboard(guild.id); + var leaderboardString = ''; + winnersList.forEach(winner => { + leaderboardString += '<@' + winner.memberId + '>: '; + if (winner.points > 1) { + leaderboardString += winner.points + ' points\n'; + } else if (winner.points == 1) { + leaderboardString += '1 point\n'; + } + }); + const newLeaderboard = new MessageEmbed(leaderboard).setDescription(leaderboardString); + pinnedMessage.edit({ embeds: [startEmbed, newLeaderboard] }); + } + + /** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * sendQuestion is the function that picks and sends the next question, then picks the winner by matching participants' messages + * against the answer(s) or receives the winner from Staff. Once it reaches the end it will notify Staff in the Logs channel and + * list all the winners in order. + */ + async function sendQuestion(initBotInfo) { + //get question's parameters from db + let data = await getQuestion(guild.id); + + //sends results to Staff after all questions have been asked and stops looping + if (data === null) { + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> Discord contests have ended!'); + clearInterval(interval); + return; + } + + /** @type {string} */ + let question = data.question; + /** @type {string[]} */ + let answers = data.answers; + let needAllAnswers = data.needAllAnswers; + + const qEmbed = new MessageEmbed() + .setTitle('A new Discord Contest Question:') + .setDescription(question); + + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('winner') + .setLabel('Select winner') + .setStyle('PRIMARY'), + ); + + + await channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }); + if (answers.length === 0) { + //send message to console + const questionMsg = await adminConsole.send({ content: '<@&' + initBotInfo.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); + + const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = await questionMsg.createMessageComponentCollector({ filter }); + + collector.on('collect', async i => { + const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); + + const winnerFilter = message => message.author.id === i.user.id; // error? + const winnerCollector = await adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); + winnerCollector.on('collect', async m => { + if (m.mentions.members.size > 0) { + const member = await m.mentions.members.first(); + const memberId = await member.user.id; + await m.delete(); + await questionMsg.delete(); + await i.editReply('<@' + memberId + '> has been recorded!'); + row.components[0].setDisabled(true); + // row.components[0].setDisabled(); + await channel.send('Congrats <@' + memberId + '> for the best answer to the last question!'); + // winners.push(memberId); + await updateLeaderboard(memberId); + collector.stop(); + // await recordWinner(memberId); + } else { + await m.delete(); + // await winnerRequest.deleteReply(); + let errorMsg = await i.editReply({ content: 'Message does not include a user mention!' }); + setTimeout(function () { + errorMsg.delete(); + }, 5000); + } + }); + }); + } else { + //automatically mark answers + const filter = m => !m.author.bot && (initBotInfo.verification.isEnabled ? checkForRole(m.member, initBotInfo.verification.roles.find((r) => r.name === 'hacker')?.roleId) : checkForRole(m.member, initBotInfo.roleIDs.memberRole)); + const collector = channel.createMessageCollector({ filter, time: timeInterval * 0.75 }); + + collector.on('collect', async m => { + if (!needAllAnswers) { + // for questions that have numbers as answers, the answer has to match at least one of the correct answers exactly + if (!isNaN(answers[0])) { + if (answers.some(correctAnswer => m.content === correctAnswer)) { + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } else if (answers.some(correctAnswer => m.content.toLowerCase().includes(correctAnswer.toLowerCase()))) { + //for most questions, an answer that contains at least once item of the answer array is correct + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } else { + //check if all answers in answer array are in the message + if (answers.every((answer) => m.content.toLowerCase().includes(answer.toLowerCase()))) { + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(', ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } + }); + + collector.on('end', async () => { + await channel.send('Answers are no longer being accepted. Stay tuned for the next question!'); + }); + } + } + + async function recordWinner(member) { + try { + let email = await lookupById(guild.id, member.id); + discordLog(guild, `Discord contest winner: <@${member.id}> - ${email}`); + } catch (error) { + console.log(error); + } + } + } +} +module.exports = DiscordContests; diff --git a/commands/firebase_scripts/load-questions.js b/commands/firebase_scripts/load-questions.js index 04bde3bd..05325915 100644 --- a/commands/firebase_scripts/load-questions.js +++ b/commands/firebase_scripts/load-questions.js @@ -38,21 +38,17 @@ class LoadQuestions extends Command { const guildId = interaction.guild.id; const file = interaction.options.getAttachment('questions'); - let res; await interaction.deferReply(); try { const response = await fetch(file.url); - res = await response.json(); + const res = await response.json(); - let db = firebaseUtil.apps.get('nwPlusBotAdmin').firestore(); - let count = 0; res.forEach(question => { - count++; - - var docRef = db.collection('guilds').doc(guildId).collection('questions').doc(); + const docRef = firebaseUtil.getFactotumSubCol().doc(guildId) + .collection('Questions').doc(); docRef.set({ ...question, asked: false }); }); - await interaction.editReply({ content: count + ' questions added!', ephemeral: true }); + await interaction.editReply({ content: res + ' questions added!', ephemeral: true }); } catch (error) { await interaction.editReply({ content: 'Something went wrong! Error msg: ' + error, ephemeral: true }); } diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index a82293b7..522d6c8a 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -113,7 +113,8 @@ module.exports = { */ async getQuestion(guildId) { //checks that the question has not been asked - let questionReference = getFactotumDoc().collection('guilds').doc(guildId).collection('questions').where('asked', '==', false).limit(1); + let questionReference = module.exports.getFactotumSubCol().doc(guildId) + .collection('Questions').where('asked', '==', false).limit(1); let question = (await questionReference.get()).docs[0]; //if there exists an unasked question, change its status to asked if (question != undefined) { @@ -356,7 +357,8 @@ module.exports = { }, async saveToLeaderboard(guildId, memberId) { - const userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').doc(memberId); + const userRef = module.exports.getFactotumSubCol().doc(guildId) + .collection('QuestionsLeaderboard').doc(memberId); const user = await userRef.get(); if (user.exists) { const data = user.data(); @@ -369,7 +371,7 @@ module.exports = { }, async retrieveLeaderboard(guildId) { - const snapshot = (await getFactotumDoc().collection('guilds').doc(guildId).collection('questionsLeaderboard').get()).docs; + const snapshot = (await module.exports.getFactotumSubCol().doc(guildId).collection('QuestionsLeaderboard').get()).docs; const winners = snapshot.map(doc => doc.data()).sort((a, b) => b.points - a.points); return winners; }, From 2fcea1780b6769b7c8655605fb48f55aac369fce Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sun, 4 Aug 2024 21:17:38 -0700 Subject: [PATCH 56/67] Fix start-mentor-cave command and prepare for saving message id functionality --- .../a_start_commands/start-mentor-cave.js | 825 ++++++++++-------- commands/verification/add-members.js | 48 +- 2 files changed, 495 insertions(+), 378 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 6b28540e..b6e4c5b6 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -1,13 +1,54 @@ const { Command } = require('@sapphire/framework'); -const { Interaction, MessageEmbed } = require('discord.js'); -const { randomColor, discordLog } = require('../../discord-services'); -const { Message, Collection } = require('discord.js'); -const winston = require('winston'); -const { NumberPrompt, SpecialPrompt, RolePrompt } = require('advanced-discord.js-prompts'); -const { MessageActionRow, MessageButton } = require('discord.js'); -const { MessageSelectMenu, Modal, TextInputComponent } = require('discord.js'); +const { MessageEmbed, Guild, Role } = require('discord.js'); +const { discordLog } = require('../../discord-services'); +const { + MessageSelectMenu, + Modal, + TextInputComponent, + Message, + MessageActionRow, + MessageButton, + GuildBasedChannel +} = require('discord.js'); const firebaseUtil = require('../../db/firebase/firebaseUtil'); +//TODO: allow staff to add more roles +const htmlCssEmoji = '💻'; +const jsTsEmoji = '🕸️'; +const pythonEmoji = '🐍'; +const sqlEmoji = '🐬'; +const reactEmoji = '⚛️'; +const noSqlEmoji = '🔥'; +const javaEmoji = '☕'; +const cEmoji = '🎮'; +const cSharpEmoji = '💼'; +const reduxEmoji = '☁️'; +const figmaEmoji = '🎨'; +const unityEmoji = '🧊'; +const rustEmoji = '⚙️'; +const awsEmoji = '🙂'; +const ideationEmoji = '💡'; +const pitchingEmoji = '🎤'; + +let emojisMap = new Map([ + [htmlCssEmoji, 'HTML/CSS'], + [jsTsEmoji, 'JavaScript/TypeScript'], + [pythonEmoji, 'Python'], + [sqlEmoji, 'SQL'], + [reactEmoji, 'React'], + [noSqlEmoji, 'NoSQL'], + [javaEmoji, 'Java'], + [cEmoji, 'C/C++'], + [cSharpEmoji, 'C#'], + [reduxEmoji, 'Redux'], + [figmaEmoji, 'Figma'], + [unityEmoji, 'Unity'], + [rustEmoji, 'Rust'], + [awsEmoji, 'AWS'], + [ideationEmoji, 'Ideation'], + [pitchingEmoji, 'Pitching'] +]); + /** * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. * @category Commands @@ -58,10 +99,14 @@ class StartMentorCave extends Command { }; } + /** + * + * @param {Command.ChatInputInteraction} interaction + * @returns + */ async chatInputRun(interaction) { try { // helpful prompt vars - let channel = interaction.channel; let userId = interaction.user.id; let guild = interaction.guild; this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); @@ -71,28 +116,61 @@ class StartMentorCave extends Command { return; } - let adminConsole = await guild.channels.resolve(this.initBotInfo.channelIDs.adminConsole); + let adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); this.ticketCount = 0; // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); const publicRole = interaction.options.getRole('request_ticket_role'); - const inactivePeriod = interaction.options.getInteger('inactivity_time'); // const bufferTime = inactivePeriod / 2; const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); - const mentorRoleSelectionChannel = interaction.options.getChannel('mentor_role_selection_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.mentorRoleSelectionChannel); - const incomingTicketsChannel = interaction.options.getChannel('incoming_tickets_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.incomingTicketsChannel); - const requestTicketChannel = interaction.options.getChannel('request_ticket_channel') ?? await guild.channels.resolve(this.initBotInfo.mentorTickets.requestTicketChannel); + + const mentorRoleSelectionChannelId = + this.initBotInfo.mentorTickets?.mentorRoleSelectionChannel; + const incomingTicketsChannelId = + this.initBotInfo.mentorTickets?.incomingTicketsChannel; + const requestTicketChannelId = + this.initBotInfo.mentorTickets?.requestTicketChannel; + + const mentorRoleSelectionChannel = interaction.options.getChannel( + 'mentor_role_selection_channel' + ) ?? (mentorRoleSelectionChannelId + ? await guild.channels.fetch(mentorRoleSelectionChannelId) + : null); + + const incomingTicketsChannel = interaction.options.getChannel( + 'incoming_tickets_channel' + ) ?? (incomingTicketsChannelId + ? await guild.channels.fetch(incomingTicketsChannelId) + : null); + + const requestTicketChannel = interaction.options.getChannel( + 'request_ticket_channel' + ) ?? (requestTicketChannelId + ? await guild.channels.fetch(requestTicketChannelId) + : null); + if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); return; } - if (mentorRoleSelectionChannel != this.initBotInfo.mentorTickets.mentorRoleSelectionChannel || incomingTicketsChannel != this.initBotInfo.mentorTickets.incomingTicketsChannel || requestTicketChannel != this.initBotInfo.mentorTickets.requestTicketChannel) { + if ( + mentorRoleSelectionChannel.id != mentorRoleSelectionChannelId || + incomingTicketsChannel.id != incomingTicketsChannelId || + requestTicketChannel.id != requestTicketChannelId + ) { await interaction.deferReply(); - this.initBotInfo.mentorTickets.mentorRoleSelectionChannel = mentorRoleSelectionChannel.id; - this.initBotInfo.mentorTickets.incomingTicketsChannel = incomingTicketsChannel.id; - this.initBotInfo.mentorTickets.requestTicketChannel = requestTicketChannel.id; - await this.initBotInfo.save(); + await firebaseUtil + .getFactotumSubCol() + .doc(guild.id) + .set({ + mentorTickets: { + mentorRoleSelectionChannel: mentorRoleSelectionChannel.id, + incomingTicketsChannel: incomingTicketsChannel.id, + requestTicketChannel: requestTicketChannel.id, + }, + }, + { merge: true }); await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); } else { await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); @@ -153,42 +231,6 @@ class StartMentorCave extends Command { // } // ); - //TODO: allow staff to add more roles - const htmlCssEmoji = '💻'; - const jsTsEmoji = '🕸️'; - const pythonEmoji = '🐍'; - const sqlEmoji = '🐬'; - const reactEmoji = '⚛️'; - const noSqlEmoji = '🔥'; - const javaEmoji = '☕'; - const cEmoji = '🎮'; - const cSharpEmoji = '💼'; - const reduxEmoji = '☁️'; - const figmaEmoji = '🎨'; - const unityEmoji = '🧊'; - const rustEmoji = '⚙️'; - const awsEmoji = '🙂'; - const ideationEmoji = '💡'; - const pitchingEmoji = '🎤'; - - let emojisMap = new Map(); - emojisMap.set(htmlCssEmoji, 'HTML/CSS'); - emojisMap.set(jsTsEmoji, 'JavaScript/TypeScript'); - emojisMap.set(pythonEmoji, 'Python'); - emojisMap.set(sqlEmoji, 'SQL'); - emojisMap.set(reactEmoji, 'React'); - emojisMap.set(noSqlEmoji, 'NoSQL'); - emojisMap.set(javaEmoji, 'Java'); - emojisMap.set(cEmoji, 'C/C++'); - emojisMap.set(cSharpEmoji, 'C#'); - emojisMap.set(reduxEmoji, 'Redux'); - emojisMap.set(figmaEmoji, 'Figma'); - emojisMap.set(unityEmoji, 'Unity'); - emojisMap.set(rustEmoji, 'Rust'); - emojisMap.set(awsEmoji, 'AWS'); - emojisMap.set(ideationEmoji, 'Ideation'); - emojisMap.set(pitchingEmoji, 'Pitching'); - const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; for (let value of emojisMap.values()) { const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); @@ -217,23 +259,7 @@ class StartMentorCave extends Command { roleSelectionMsg.react(key); } - const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); - collector.on('collect', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const value = emojisMap.get(reaction.emoji.name); - const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - await guild.members.cache.get(user.id).roles.add(findRole); - } - }); - - collector.on('remove', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const member = guild.members.cache.get(user.id); - const value = emojisMap.get(reaction.emoji.name); - const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); - } - }); + listenToRoleReactions(guild, roleSelectionMsg); // channel.guild.channels.create('mentors-general', // { @@ -312,235 +338,20 @@ class StartMentorCave extends Command { ); const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - - const selectMenuFilter = i => !i.user.bot; - const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); - selectMenuCollector.on('collect', async i => { - if (i.customId === 'ticketType') { - requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { - await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); - return; - } - const modal = new Modal() - .setCustomId('ticketSubmitModal') - .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('ticketDescription') - .setLabel('Brief description of your problem') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setPlaceholder('Describe your problem here') - .setRequired(true), - ), - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('location') - .setLabel('Where would you like to meet your mentor?') - .setPlaceholder('Help your mentor find you!') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setRequired(true), - ) - ]); - await i.showModal(modal); - - const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) - .catch(error => { - }); - - if (submitted) { - const role = i.values[0] === 'None of the above' ? this.initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; - const description = submitted.fields.getTextInputValue('ticketDescription'); - const location = submitted.fields.getTextInputValue('location'); - // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); - const ticketNumber = this.ticketCount; - this.ticketCount++; - const newTicketEmbed = new MessageEmbed() - .setTitle('Ticket #' + ticketNumber) - .setColor('#d3d3d3') - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - }, - // { - // name: 'OK with being helped online?', - // value: helpFormat - // } - ]); - const ticketAcceptanceRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('acceptIrl') - .setLabel('Accept ticket (in-person)') - .setStyle('PRIMARY'), - ); - // .addComponents( - // new MessageButton() - // .setCustomId('acceptOnline') - // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') - // .setStyle('PRIMARY'), - // ); - - const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); - submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); - const ticketReminder = setTimeout(() => { - ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); - }, reminderTime * 60000); - - const confirmationEmbed = new MessageEmbed() - .setTitle('Your ticket is number ' + ticketNumber) - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - } - ]); - const deleteTicketRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('deleteTicket') - .setLabel('Delete ticket') - .setStyle('DANGER'), - ); - const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); - const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); - deleteTicketCollector.on('collect', async deleteInteraction => { - await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); - clearTimeout(ticketReminder); - deleteInteraction.reply('Ticket deleted!'); - ticketReceipt.edit({ components: [] }); - }); - - const ticketAcceptFilter = i => !i.user.bot && i.isButton(); - const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); - ticketAcceptanceCollector.on('collect', async acceptInteraction => { - const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); - if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { - await ticketReceipt.edit({ components: [] }); - clearTimeout(ticketReminder); - ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); - } - if (acceptInteraction.customId === 'acceptIrl') { - // TODO: mark as complete? - submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); - } - // if (acceptInteraction.customId === 'acceptOnline') { - // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); - // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); - // let ticketChannelOverwrites = - // [{ - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'], - // }, - // { - // id: acceptInteraction.user.id, - // allow: ['VIEW_CHANNEL'], - // }, - // { - // id: submitted.user.id, - // allow: ['VIEW_CHANNEL'], - // }]; - - // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: ticketChannelOverwrites - // } - // ); - - // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, - // { - // type: 'GUILD_TEXT', - // parent: ticketCategory - // } - // ); - - // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', - // { - // type: 'GUILD_VOICE', - // parent: ticketCategory - // } - // ); - - // const ticketChannelEmbed = new MessageEmbed() - // .setColor(this.botGuild.colors.embedColor) - // .setTitle('Ticket description') - // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); - - // const ticketChannelButtons = new MessageActionRow() - // .addComponents( - // new MessageButton() - // .setCustomId('addMembers') - // .setLabel('Add Members to Channels') - // .setStyle('PRIMARY'), - // ) - // .addComponents( - // new MessageButton() - // .setCustomId('leaveTicket') - // .setLabel('Leave') - // .setStyle('DANGER'), - // ); - // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); - // ticketChannelInfoMsg.pin(); - - // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); - // ticketChannelCollector.on('collect', async ticketInteraction => { - // if (ticketInteraction.customId === 'addMembers') { - // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) - // .then(() => { - // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; - // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) - // .then(async collected => { - // if (collected.first().mentions.members.size === 0) { - // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); - // } else { - // var newMembersArray = []; - // collected.first().mentions.members.forEach(member => { - // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); - // newMembersArray.push(member.id); - // }); - // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); - // } - // }) - // .catch(collected => { - // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); - // }); - // }); - // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { - // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); - // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); - // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { - // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); - // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); - // } - // } else { - // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); - // } - // }); - // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - // } - }); - } - - } - }); + + listenToRequestConsole( + guild, + requestTicketConsole, + // requestTicketEmbed, + // selectMenuRow, + publicRole, + reminderTime, + incomingTicketsChannel + ); const adminEmbed = new MessageEmbed() .setTitle('Mentor Cave Console') - .setColor(this.initBotInfo.colors.embedColor); + .setColor(this.initBotInfo.embedColor); const adminRow = new MessageActionRow() .addComponents( @@ -549,65 +360,20 @@ class StartMentorCave extends Command { .setLabel('Add Mentor Role') .setStyle('PRIMARY'), ); - + const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); - const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) }); - adminCollector.on('collect', async adminInteraction => { - if (adminInteraction.customId === 'addRole') { - const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); - const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); - let roleName; - roleNameCollector.on('collect', async collected => { - if (collected.content.toLowerCase() != 'cancel') { - roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); - const roleExists = guild.roles.cache.filter(role => { - role.name === `M-${roleName}`; - }).size != 0; - if (!roleExists) { - await guild.roles.create( - { - name: `M-${roleName}`, - color: mentorRoleColour, - } - ); - } - const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); - const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); - emojiCollector.on('collect', collected => { - if (emojisMap.has(collected.emoji.name)) { - adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - } else { - emojiCollector.stop(); - emojisMap.set(collected.emoji.name, roleName); - adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); - roleSelectionMsg.react(collected.emoji.name); - - const oldOptions = selectMenuRow.components[0].options; - const newOptions = oldOptions; - newOptions.splice(-1, 0, { label: roleName, value: roleName }); - var newSelectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(newOptions) - ); - requestTicketConsole.edit({ components: [newSelectMenuRow] }); - askForEmoji.delete(); - } - }); - } - askForRoleName.delete(); - collected.delete(); + listenToAdminControls( + this.initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + roleSelection, + selectMenuRow, + requestTicketConsole + ); - }); - } - }); } catch (error) { // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); } @@ -652,7 +418,7 @@ class StartMentorCave extends Command { warning.edit({ components: [disabledButtonRow] }); this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); }) - .catch(error => { + .catch(() => { if (!ticketText.parentId || !ticketVoice.parentId || ticketMsg.embeds[0].color == '#90EE90') return; if (ticketVoice.members.size === 0) { this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Deleted due to inactivity' }])); @@ -667,4 +433,355 @@ class StartMentorCave extends Command { } } } + +/** + * + * @param {Guild} guild + * @param {Message} roleSelectionMsg + */ +async function listenToRoleReactions(guild, roleSelectionMsg) { + const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); + collector.on('collect', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const value = emojisMap.get(reaction.emoji.name); + const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + await guild.members.cache.get(user.id).roles.add(findRole); + } + }); + + collector.on('remove', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const member = guild.members.cache.get(user.id); + const value = emojisMap.get(reaction.emoji.name); + const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); + } + }); +} + +/** + * + * @param {Guild} guild + * @param {Message} requestTicketConsole + * @param {MessageEmbed} requestTicketEmbed + * @param {MessageActionRow} selectMenuRow + * @param {Role} publicRole + * @param {number} reminderTime + * @param {GuildBasedChannel} incomingTicketsChannel + */ +function listenToRequestConsole( + guild, + requestTicketConsole, + // requestTicketEmbed, + // selectMenuRow, + publicRole, + reminderTime, + incomingTicketsChannel +) { + const selectMenuFilter = i => !i.user.bot; + const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); + selectMenuCollector.on('collect', async (i) => { + if (i.customId === 'ticketType') { + // requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { + await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); + return; + } + const modal = new Modal() + .setCustomId('ticketSubmitModal') + .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('ticketDescription') + .setLabel('Brief description of your problem') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setPlaceholder('Describe your problem here') + .setRequired(true), + ), + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('location') + .setLabel('Where would you like to meet your mentor?') + .setPlaceholder('Help your mentor find you!') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setRequired(true), + ) + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(() => { + }); + + if (submitted) { + const role = i.values[0] === 'None of the above' ? this.initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; + const description = submitted.fields.getTextInputValue('ticketDescription'); + const location = submitted.fields.getTextInputValue('location'); + // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); + const ticketNumber = this.ticketCount; + this.ticketCount++; + const newTicketEmbed = new MessageEmbed() + .setTitle('Ticket #' + ticketNumber) + .setColor('#d3d3d3') + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + }, + // { + // name: 'OK with being helped online?', + // value: helpFormat + // } + ]); + const ticketAcceptanceRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('acceptIrl') + .setLabel('Accept ticket (in-person)') + .setStyle('PRIMARY'), + ); + // .addComponents( + // new MessageButton() + // .setCustomId('acceptOnline') + // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') + // .setStyle('PRIMARY'), + // ); + + const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); + submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); + const ticketReminder = setTimeout(() => { + ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); + }, reminderTime * 60000); + + const confirmationEmbed = new MessageEmbed() + .setTitle('Your ticket is number ' + ticketNumber) + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + } + ]); + const deleteTicketRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('deleteTicket') + .setLabel('Delete ticket') + .setStyle('DANGER'), + ); + const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); + const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); + deleteTicketCollector.on('collect', async deleteInteraction => { + await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); + clearTimeout(ticketReminder); + deleteInteraction.reply('Ticket deleted!'); + ticketReceipt.edit({ components: [] }); + }); + + const ticketAcceptFilter = i => !i.user.bot && i.isButton(); + const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); + ticketAcceptanceCollector.on('collect', async acceptInteraction => { + const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); + if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { + await ticketReceipt.edit({ components: [] }); + clearTimeout(ticketReminder); + ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); + } + if (acceptInteraction.customId === 'acceptIrl') { + // TODO: mark as complete? + submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); + acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); + } + // if (acceptInteraction.customId === 'acceptOnline') { + // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); + // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); + // let ticketChannelOverwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: acceptInteraction.user.id, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: submitted.user.id, + // allow: ['VIEW_CHANNEL'], + // }]; + + // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: ticketChannelOverwrites + // } + // ); + + // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, + // { + // type: 'GUILD_TEXT', + // parent: ticketCategory + // } + // ); + + // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', + // { + // type: 'GUILD_VOICE', + // parent: ticketCategory + // } + // ); + + // const ticketChannelEmbed = new MessageEmbed() + // .setColor(this.botGuild.colors.embedColor) + // .setTitle('Ticket description') + // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); + + // const ticketChannelButtons = new MessageActionRow() + // .addComponents( + // new MessageButton() + // .setCustomId('addMembers') + // .setLabel('Add Members to Channels') + // .setStyle('PRIMARY'), + // ) + // .addComponents( + // new MessageButton() + // .setCustomId('leaveTicket') + // .setLabel('Leave') + // .setStyle('DANGER'), + // ); + // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); + // ticketChannelInfoMsg.pin(); + + // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); + // ticketChannelCollector.on('collect', async ticketInteraction => { + // if (ticketInteraction.customId === 'addMembers') { + // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) + // .then(() => { + // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; + // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) + // .then(async collected => { + // if (collected.first().mentions.members.size === 0) { + // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); + // } else { + // var newMembersArray = []; + // collected.first().mentions.members.forEach(member => { + // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); + // newMembersArray.push(member.id); + // }); + // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); + // } + // }) + // .catch(collected => { + // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); + // }); + // }); + // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { + // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); + // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); + // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { + // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); + // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); + // } + // } else { + // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); + // } + // }); + // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + // } + }); + } + + } + }); +} + +/** + * + * @param {Guild} guild + * @param {Message} adminControls + * @param {GuildBasedChannel} adminConsole + * @param {Message} roleSelectionMsg + * @param {MessageEmbed} roleSelection + * @param {MessageActionRow} selectMenuRow + * @param {Message} requestTicketConsole + */ +function listenToAdminControls( + initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + roleSelection, + selectMenuRow, + requestTicketConsole +) { + const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; + const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(initBotInfo.roleIDs.adminRole) }); + adminCollector.on('collect', async (adminInteraction) => { + if (adminInteraction.customId === 'addRole') { + const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); + const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); + let roleName; + roleNameCollector.on('collect', async collected => { + if (collected.content.toLowerCase() != 'cancel') { + roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); + const roleExists = guild.roles.cache.filter(role => { + role.name === `M-${roleName}`; + }).size != 0; + if (!roleExists) { + await guild.roles.create( + { + name: `M-${roleName}`, + color: mentorRoleColour, + } + ); + } + + const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); + const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); + emojiCollector.on('collect', collected => { + if (emojisMap.has(collected.emoji.name)) { + adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + } else { + emojiCollector.stop(); + emojisMap.set(collected.emoji.name, roleName); + adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); + roleSelectionMsg.react(collected.emoji.name); + + const oldOptions = selectMenuRow.components[0].options; + const newOptions = oldOptions; + newOptions.splice(-1, 0, { label: roleName, value: roleName }); + var newSelectMenuRow = new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(newOptions) + ); + requestTicketConsole.edit({ components: [newSelectMenuRow] }); + askForEmoji.delete(); + } + }); + } + askForRoleName.delete(); + collected.delete(); + + }); + } + }); +} + module.exports = StartMentorCave; \ No newline at end of file diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index 7a82a973..ed8d0234 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -38,37 +38,37 @@ class AddMembers extends Command { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } - if (!this.initBotInfo.verification.verificationRoles.has(participantsType)) { + if (!this.initBotInfo.verification.roles.find((r) => r.name === participantsType)) { await interaction.reply({ content: 'The role you entered does not exist!', ephemeral: true }); return; } const modal = new Modal() - .setCustomId('emailsModal') - .setTitle('Enter all emails to be added as ' + participantsType) - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('emails') - .setLabel('Newline-separated Emails') - .setStyle('PARAGRAPH') - .setRequired(true), - ), - ]); - await interaction.showModal(modal); + .setCustomId('emailsModal') + .setTitle('Enter all emails to be added as ' + participantsType) + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('emails') + .setLabel('Newline-separated Emails') + .setStyle('PARAGRAPH') + .setRequired(true), + ), + ]); + await interaction.showModal(modal); - const submitted = await interaction.awaitModalSubmit({ time: 300000, filter: j => j.user.id === interaction.user.id }) - .catch(error => { - }); + const submitted = await interaction.awaitModalSubmit({ time: 300000, filter: j => j.user.id === interaction.user.id }) + .catch(error => { + }); - if (submitted) { - const emailsRaw = submitted.fields.getTextInputValue('emails'); - const emails = emailsRaw.split(/\r?\n|\r|\n/g); - emails.forEach(email => { - addUserData(email, participantsType, interaction.guild.id, overwrite); - }); - submitted.reply({ content: emails.length + ' emails have been added as ' + participantsType, ephemeral: true }) - } + if (submitted) { + const emailsRaw = submitted.fields.getTextInputValue('emails'); + const emails = emailsRaw.split(/\r?\n|\r|\n/g); + emails.forEach(email => { + addUserData(email, participantsType, interaction.guild.id, overwrite); + }); + submitted.reply({ content: emails.length + ' emails have been added as ' + participantsType, ephemeral: true }) + } } } module.exports = AddMembers; \ No newline at end of file From f0fe1c2fcd2515ce118b3db2da996817859b5198 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sun, 4 Aug 2024 21:34:14 -0700 Subject: [PATCH 57/67] Fix ticket counting for start-mentor-cave command --- .../a_start_commands/start-mentor-cave.js | 1583 +++++++++-------- commands/a_utility/pronouns.js | 2 +- 2 files changed, 798 insertions(+), 787 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index b6e4c5b6..7322cb83 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -1,787 +1,798 @@ -const { Command } = require('@sapphire/framework'); -const { MessageEmbed, Guild, Role } = require('discord.js'); -const { discordLog } = require('../../discord-services'); -const { - MessageSelectMenu, - Modal, - TextInputComponent, - Message, - MessageActionRow, - MessageButton, - GuildBasedChannel -} = require('discord.js'); -const firebaseUtil = require('../../db/firebase/firebaseUtil'); - -//TODO: allow staff to add more roles -const htmlCssEmoji = '💻'; -const jsTsEmoji = '🕸️'; -const pythonEmoji = '🐍'; -const sqlEmoji = '🐬'; -const reactEmoji = '⚛️'; -const noSqlEmoji = '🔥'; -const javaEmoji = '☕'; -const cEmoji = '🎮'; -const cSharpEmoji = '💼'; -const reduxEmoji = '☁️'; -const figmaEmoji = '🎨'; -const unityEmoji = '🧊'; -const rustEmoji = '⚙️'; -const awsEmoji = '🙂'; -const ideationEmoji = '💡'; -const pitchingEmoji = '🎤'; - -let emojisMap = new Map([ - [htmlCssEmoji, 'HTML/CSS'], - [jsTsEmoji, 'JavaScript/TypeScript'], - [pythonEmoji, 'Python'], - [sqlEmoji, 'SQL'], - [reactEmoji, 'React'], - [noSqlEmoji, 'NoSQL'], - [javaEmoji, 'Java'], - [cEmoji, 'C/C++'], - [cSharpEmoji, 'C#'], - [reduxEmoji, 'Redux'], - [figmaEmoji, 'Figma'], - [unityEmoji, 'Unity'], - [rustEmoji, 'Rust'], - [awsEmoji, 'AWS'], - [ideationEmoji, 'Ideation'], - [pitchingEmoji, 'Pitching'] -]); - -/** - * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. - * @category Commands - * @subcategory Start-Commands - * @extends PermissionCommand - * @guildonly - */ -class StartMentorCave extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Start mentor cave' - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName('start-mentor-cave') - .setDescription(this.description) - .addIntegerOption(option => - option.setName('inactivity_time') - .setDescription('How long (minutes) before bot asks users to delete ticket channels') - .setRequired(true)) - .addIntegerOption(option => - option.setName('unanswered_ticket_time') - .setDescription('How long (minutes) shall a ticket go unaccepted before the bot sends a reminder to all mentors?') - .setRequired(true)) - .addRoleOption(option => - option.setName('request_ticket_role') - .setDescription('Tag the role that is allowed to request tickets') - .setRequired(true)) - .addChannelOption(option => - option.setName('mentor_role_selection_channel') - .setDescription('Tag the channel where mentors can select their specialties') - .setRequired(false)) - .addChannelOption(option => - option.setName('incoming_tickets_channel') - .setDescription('Tag the channel where mentor tickets will be sent') - .setRequired(false)) - .addChannelOption(option => - option.setName('request_ticket_channel') - .setDescription('Tag the channel where hackers can request tickets') - .setRequired(false)) - ), - { - idHints: '1051737344937566229' - }; - } - - /** - * - * @param {Command.ChatInputInteraction} interaction - * @returns - */ - async chatInputRun(interaction) { - try { - // helpful prompt vars - let userId = interaction.user.id; - let guild = interaction.guild; - this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - - if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { - await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - let adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); - this.ticketCount = 0; - - // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); - const publicRole = interaction.options.getRole('request_ticket_role'); - // const bufferTime = inactivePeriod / 2; - const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); - - const mentorRoleSelectionChannelId = - this.initBotInfo.mentorTickets?.mentorRoleSelectionChannel; - const incomingTicketsChannelId = - this.initBotInfo.mentorTickets?.incomingTicketsChannel; - const requestTicketChannelId = - this.initBotInfo.mentorTickets?.requestTicketChannel; - - const mentorRoleSelectionChannel = interaction.options.getChannel( - 'mentor_role_selection_channel' - ) ?? (mentorRoleSelectionChannelId - ? await guild.channels.fetch(mentorRoleSelectionChannelId) - : null); - - const incomingTicketsChannel = interaction.options.getChannel( - 'incoming_tickets_channel' - ) ?? (incomingTicketsChannelId - ? await guild.channels.fetch(incomingTicketsChannelId) - : null); - - const requestTicketChannel = interaction.options.getChannel( - 'request_ticket_channel' - ) ?? (requestTicketChannelId - ? await guild.channels.fetch(requestTicketChannelId) - : null); - - if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { - await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); - return; - } - - if ( - mentorRoleSelectionChannel.id != mentorRoleSelectionChannelId || - incomingTicketsChannel.id != incomingTicketsChannelId || - requestTicketChannel.id != requestTicketChannelId - ) { - await interaction.deferReply(); - await firebaseUtil - .getFactotumSubCol() - .doc(guild.id) - .set({ - mentorTickets: { - mentorRoleSelectionChannel: mentorRoleSelectionChannel.id, - incomingTicketsChannel: incomingTicketsChannel.id, - requestTicketChannel: requestTicketChannel.id, - }, - }, - { merge: true }); - await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); - } else { - await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); - } - - discordLog(guild, 'Mentor cave started by <@' + userId + '>'); - - // these are all old code that create channels rather than using existing channels - // let overwrites = - // [{ - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'], - // }, - // { - // id: this.botGuild.roleIDs.mentorRole, - // allow: ['VIEW_CHANNEL'], - // }, - // { - // id: this.botGuild.roleIDs.staffRole, - // allow: ['VIEW_CHANNEL'], - // }]; - - // if (additionalMentorRole) { - // overwrites.push({ - // id: additionalMentorRole, - // allow: ['VIEW_CHANNEL'] - // }); - // } - - // let mentorCategory = await channel.guild.channels.create('Mentors', - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: overwrites - // } - // ); - - // let announcementsOverwrites = overwrites; - // announcementsOverwrites.push( - // { - // id: this.botGuild.roleIDs.mentorRole, - // deny: ['SEND_MESSAGES'], - // allow: ['VIEW_CHANNEL'] - // }); - - // await channel.guild.channels.create('mentors-announcements', - // { - // type: 'GUILD_TEXT', - // parent: mentorCategory, - // permissionOverwrites: announcementsOverwrites - // } - // ); - - // const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', - // { - // type: 'GUILD_TEXT', - // topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', - // parent: mentorCategory - // } - // ); - - const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; - for (let value of emojisMap.values()) { - const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - if (!findRole) { - await guild.roles.create( - { - name: `M-${value}`, - color: mentorRoleColour, - } - ); - } - } - - var fields = []; - for (let [key, value] of emojisMap) { - fields.push({ name: key + ' --> ' + value, value: '\u200b' }); - } - - const roleSelection = new MessageEmbed() - .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') - .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') - .addFields(fields); - - const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); - for (let key of emojisMap.keys()) { - roleSelectionMsg.react(key); - } - - listenToRoleReactions(guild, roleSelectionMsg); - - // channel.guild.channels.create('mentors-general', - // { - // type: 'GUILD_TEXT', - // topic: 'Private chat between all mentors and organizers', - // parent: mentorCategory - // } - // ); - - // const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', - // { - // type: 'GUILD_TEXT', - // topic: 'Tickets from hackers will come in here!', - // parent: mentorCategory - // } - // ); - - // const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: [ - // { - // id: this.botGuild.verification.guestRoleID, - // deny: ['VIEW_CHANNEL'], - // }, - // ] - // } - // ); - - // channel.guild.channels.create('quick-questions', - // { - // type: 'GUILD_TEXT', - // topic: 'ask questions for mentors here!', - // parent: mentorHelpCategory - // } - // ); - - // const requestTicketChannel = await channel.guild.channels.create('request-ticket', - // { - // type: 'GUILD_TEXT', - // topic: 'request 1-on-1 help from mentors here!', - // parent: mentorHelpCategory, - // permissionOverwrites: [ - // { - // id: publicRole, - // allow: ['VIEW_CHANNEL'], - // deny: ['SEND_MESSAGES'] - // }, - // { - // id: this.botGuild.roleIDs.staffRole, - // allow: ['VIEW_CHANNEL'] - // }, - // { - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'] - // } - // ] - // } - // ); - - const requestTicketEmbed = new MessageEmbed() - .setTitle('Need 1:1 mentor help?') - .setDescription('Select a technology you need help with and follow the instructions!'); - - var options = []; - for (let value of emojisMap.values()) { - options.push({ label: value, value: value }); - } - options.push({ label: 'None of the above', value: 'None of the above' }); - - const selectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(options) - ); - - const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - - listenToRequestConsole( - guild, - requestTicketConsole, - // requestTicketEmbed, - // selectMenuRow, - publicRole, - reminderTime, - incomingTicketsChannel - ); - - const adminEmbed = new MessageEmbed() - .setTitle('Mentor Cave Console') - .setColor(this.initBotInfo.embedColor); - - const adminRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('addRole') - .setLabel('Add Mentor Role') - .setStyle('PRIMARY'), - ); - - const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); - - listenToAdminControls( - this.initBotInfo, - guild, - adminControls, - adminConsole, - roleSelectionMsg, - roleSelection, - selectMenuRow, - requestTicketConsole - ); - - } catch (error) { - // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); - } - } - - async deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, embed) { - await ticketMsg.edit({ embeds: [embed] }); - ticketText.delete(); - ticketVoice.delete(); - ticketCategory.delete(); - } - - async startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime) { - // message collector that stops when there are no messages for inactivePeriod minutes - if (ticketText.parentId && ticketVoice.parentId) { - const activityListener = ticketText.createMessageCollector({ filter: m => !m.author.bot, idle: inactivePeriod * 60 * 1000 }); - activityListener.on('end', async collected => { - if (!ticketText.parentId || !ticketVoice.parentId) return; - if (collected.size === 0 && ticketVoice.members.size === 0 && ticketMsg.embeds[0].color != '#90EE90') { - const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) && !member.user.bot).map(member => member.id); - const msgText = '<@' + remainingMembers.join('><@') + '> Hello! I detected some inactivity in this channel. If you are done and would like to leave this ticket, please go to the pinned message and click the "Leave" button. If you would like to keep this channel a little longer, please click the button below.\n**If no action is taken in the next ' + bufferTime + ' minutes, the channels for this ticket will be deleted automatically.**'; - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('keepChannels') - .setLabel('Keep Channels') - .setStyle('PRIMARY'), - ); - const warning = await ticketText.send({ content: msgText, components: [row] }); - - warning.awaitMessageComponent({ filter: i => !i.user.bot, time: bufferTime * 60 * 1000 }) - .then(interaction => { - interaction.reply('You have indicated that you need more time. I\'ll check in with you later!'); - const disabledButtonRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('keepChannels') - .setLabel('Keep Channels') - .setDisabled(true) - .setStyle('PRIMARY'), - ); - warning.edit({ components: [disabledButtonRow] }); - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - }) - .catch(() => { - if (!ticketText.parentId || !ticketVoice.parentId || ticketMsg.embeds[0].color == '#90EE90') return; - if (ticketVoice.members.size === 0) { - this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Deleted due to inactivity' }])); - } else { - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - } - }); - } else { - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - } - }); - } - } -} - -/** - * - * @param {Guild} guild - * @param {Message} roleSelectionMsg - */ -async function listenToRoleReactions(guild, roleSelectionMsg) { - const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); - collector.on('collect', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const value = emojisMap.get(reaction.emoji.name); - const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - await guild.members.cache.get(user.id).roles.add(findRole); - } - }); - - collector.on('remove', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const member = guild.members.cache.get(user.id); - const value = emojisMap.get(reaction.emoji.name); - const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); - } - }); -} - -/** - * - * @param {Guild} guild - * @param {Message} requestTicketConsole - * @param {MessageEmbed} requestTicketEmbed - * @param {MessageActionRow} selectMenuRow - * @param {Role} publicRole - * @param {number} reminderTime - * @param {GuildBasedChannel} incomingTicketsChannel - */ -function listenToRequestConsole( - guild, - requestTicketConsole, - // requestTicketEmbed, - // selectMenuRow, - publicRole, - reminderTime, - incomingTicketsChannel -) { - const selectMenuFilter = i => !i.user.bot; - const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); - selectMenuCollector.on('collect', async (i) => { - if (i.customId === 'ticketType') { - // requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { - await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); - return; - } - const modal = new Modal() - .setCustomId('ticketSubmitModal') - .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('ticketDescription') - .setLabel('Brief description of your problem') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setPlaceholder('Describe your problem here') - .setRequired(true), - ), - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('location') - .setLabel('Where would you like to meet your mentor?') - .setPlaceholder('Help your mentor find you!') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setRequired(true), - ) - ]); - await i.showModal(modal); - - const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) - .catch(() => { - }); - - if (submitted) { - const role = i.values[0] === 'None of the above' ? this.initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; - const description = submitted.fields.getTextInputValue('ticketDescription'); - const location = submitted.fields.getTextInputValue('location'); - // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); - const ticketNumber = this.ticketCount; - this.ticketCount++; - const newTicketEmbed = new MessageEmbed() - .setTitle('Ticket #' + ticketNumber) - .setColor('#d3d3d3') - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - }, - // { - // name: 'OK with being helped online?', - // value: helpFormat - // } - ]); - const ticketAcceptanceRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('acceptIrl') - .setLabel('Accept ticket (in-person)') - .setStyle('PRIMARY'), - ); - // .addComponents( - // new MessageButton() - // .setCustomId('acceptOnline') - // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') - // .setStyle('PRIMARY'), - // ); - - const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); - submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); - const ticketReminder = setTimeout(() => { - ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); - }, reminderTime * 60000); - - const confirmationEmbed = new MessageEmbed() - .setTitle('Your ticket is number ' + ticketNumber) - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - } - ]); - const deleteTicketRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('deleteTicket') - .setLabel('Delete ticket') - .setStyle('DANGER'), - ); - const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); - const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); - deleteTicketCollector.on('collect', async deleteInteraction => { - await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); - clearTimeout(ticketReminder); - deleteInteraction.reply('Ticket deleted!'); - ticketReceipt.edit({ components: [] }); - }); - - const ticketAcceptFilter = i => !i.user.bot && i.isButton(); - const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); - ticketAcceptanceCollector.on('collect', async acceptInteraction => { - const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); - if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { - await ticketReceipt.edit({ components: [] }); - clearTimeout(ticketReminder); - ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); - } - if (acceptInteraction.customId === 'acceptIrl') { - // TODO: mark as complete? - submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); - } - // if (acceptInteraction.customId === 'acceptOnline') { - // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); - // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); - // let ticketChannelOverwrites = - // [{ - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'], - // }, - // { - // id: acceptInteraction.user.id, - // allow: ['VIEW_CHANNEL'], - // }, - // { - // id: submitted.user.id, - // allow: ['VIEW_CHANNEL'], - // }]; - - // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: ticketChannelOverwrites - // } - // ); - - // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, - // { - // type: 'GUILD_TEXT', - // parent: ticketCategory - // } - // ); - - // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', - // { - // type: 'GUILD_VOICE', - // parent: ticketCategory - // } - // ); - - // const ticketChannelEmbed = new MessageEmbed() - // .setColor(this.botGuild.colors.embedColor) - // .setTitle('Ticket description') - // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); - - // const ticketChannelButtons = new MessageActionRow() - // .addComponents( - // new MessageButton() - // .setCustomId('addMembers') - // .setLabel('Add Members to Channels') - // .setStyle('PRIMARY'), - // ) - // .addComponents( - // new MessageButton() - // .setCustomId('leaveTicket') - // .setLabel('Leave') - // .setStyle('DANGER'), - // ); - // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); - // ticketChannelInfoMsg.pin(); - - // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); - // ticketChannelCollector.on('collect', async ticketInteraction => { - // if (ticketInteraction.customId === 'addMembers') { - // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) - // .then(() => { - // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; - // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) - // .then(async collected => { - // if (collected.first().mentions.members.size === 0) { - // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); - // } else { - // var newMembersArray = []; - // collected.first().mentions.members.forEach(member => { - // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); - // newMembersArray.push(member.id); - // }); - // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); - // } - // }) - // .catch(collected => { - // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); - // }); - // }); - // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { - // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); - // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); - // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { - // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); - // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); - // } - // } else { - // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); - // } - // }); - // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - // } - }); - } - - } - }); -} - -/** - * - * @param {Guild} guild - * @param {Message} adminControls - * @param {GuildBasedChannel} adminConsole - * @param {Message} roleSelectionMsg - * @param {MessageEmbed} roleSelection - * @param {MessageActionRow} selectMenuRow - * @param {Message} requestTicketConsole - */ -function listenToAdminControls( - initBotInfo, - guild, - adminControls, - adminConsole, - roleSelectionMsg, - roleSelection, - selectMenuRow, - requestTicketConsole -) { - const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; - const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(initBotInfo.roleIDs.adminRole) }); - adminCollector.on('collect', async (adminInteraction) => { - if (adminInteraction.customId === 'addRole') { - const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); - const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); - let roleName; - roleNameCollector.on('collect', async collected => { - if (collected.content.toLowerCase() != 'cancel') { - roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); - const roleExists = guild.roles.cache.filter(role => { - role.name === `M-${roleName}`; - }).size != 0; - if (!roleExists) { - await guild.roles.create( - { - name: `M-${roleName}`, - color: mentorRoleColour, - } - ); - } - - const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); - const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); - emojiCollector.on('collect', collected => { - if (emojisMap.has(collected.emoji.name)) { - adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - } else { - emojiCollector.stop(); - emojisMap.set(collected.emoji.name, roleName); - adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); - roleSelectionMsg.react(collected.emoji.name); - - const oldOptions = selectMenuRow.components[0].options; - const newOptions = oldOptions; - newOptions.splice(-1, 0, { label: roleName, value: roleName }); - var newSelectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(newOptions) - ); - requestTicketConsole.edit({ components: [newSelectMenuRow] }); - askForEmoji.delete(); - } - }); - } - askForRoleName.delete(); - collected.delete(); - - }); - } - }); -} - +const { Command } = require('@sapphire/framework'); +const { MessageEmbed, Guild, Role } = require('discord.js'); +const { discordLog } = require('../../discord-services'); +const { + MessageSelectMenu, + Modal, + TextInputComponent, + Message, + MessageActionRow, + MessageButton, + GuildBasedChannel +} = require('discord.js'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); + +//TODO: allow staff to add more roles +const htmlCssEmoji = '💻'; +const jsTsEmoji = '🕸️'; +const pythonEmoji = '🐍'; +const sqlEmoji = '🐬'; +const reactEmoji = '⚛️'; +const noSqlEmoji = '🔥'; +const javaEmoji = '☕'; +const cEmoji = '🎮'; +const cSharpEmoji = '💼'; +const reduxEmoji = '☁️'; +const figmaEmoji = '🎨'; +const unityEmoji = '🧊'; +const rustEmoji = '⚙️'; +const awsEmoji = '🙂'; +const ideationEmoji = '💡'; +const pitchingEmoji = '🎤'; + +let emojisMap = new Map([ + [htmlCssEmoji, 'HTML/CSS'], + [jsTsEmoji, 'JavaScript/TypeScript'], + [pythonEmoji, 'Python'], + [sqlEmoji, 'SQL'], + [reactEmoji, 'React'], + [noSqlEmoji, 'NoSQL'], + [javaEmoji, 'Java'], + [cEmoji, 'C/C++'], + [cSharpEmoji, 'C#'], + [reduxEmoji, 'Redux'], + [figmaEmoji, 'Figma'], + [unityEmoji, 'Unity'], + [rustEmoji, 'Rust'], + [awsEmoji, 'AWS'], + [ideationEmoji, 'Ideation'], + [pitchingEmoji, 'Pitching'] +]); + +/** + * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. + * @category Commands + * @subcategory Start-Commands + * @extends PermissionCommand + * @guildonly + */ +class StartMentorCave extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Start mentor cave' + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName('start-mentor-cave') + .setDescription(this.description) + .addIntegerOption(option => + option.setName('inactivity_time') + .setDescription('How long (minutes) before bot asks users to delete ticket channels') + .setRequired(true)) + .addIntegerOption(option => + option.setName('unanswered_ticket_time') + .setDescription('How long (minutes) shall a ticket go unaccepted before the bot sends a reminder to all mentors?') + .setRequired(true)) + .addRoleOption(option => + option.setName('request_ticket_role') + .setDescription('Tag the role that is allowed to request tickets') + .setRequired(true)) + .addChannelOption(option => + option.setName('mentor_role_selection_channel') + .setDescription('Tag the channel where mentors can select their specialties') + .setRequired(false)) + .addChannelOption(option => + option.setName('incoming_tickets_channel') + .setDescription('Tag the channel where mentor tickets will be sent') + .setRequired(false)) + .addChannelOption(option => + option.setName('request_ticket_channel') + .setDescription('Tag the channel where hackers can request tickets') + .setRequired(false)) + ), + { + idHints: '1051737344937566229' + }; + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + * @returns + */ + async chatInputRun(interaction) { + try { + // helpful prompt vars + let userId = interaction.user.id; + let guild = interaction.guild; + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + let adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); + + // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); + const publicRole = interaction.options.getRole('request_ticket_role'); + // const bufferTime = inactivePeriod / 2; + const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); + + const mentorRoleSelectionChannelId = + this.initBotInfo.mentorTickets?.mentorRoleSelectionChannel; + const incomingTicketsChannelId = + this.initBotInfo.mentorTickets?.incomingTicketsChannel; + const requestTicketChannelId = + this.initBotInfo.mentorTickets?.requestTicketChannel; + + const mentorRoleSelectionChannel = interaction.options.getChannel( + 'mentor_role_selection_channel' + ) ?? (mentorRoleSelectionChannelId + ? await guild.channels.fetch(mentorRoleSelectionChannelId) + : null); + + const incomingTicketsChannel = interaction.options.getChannel( + 'incoming_tickets_channel' + ) ?? (incomingTicketsChannelId + ? await guild.channels.fetch(incomingTicketsChannelId) + : null); + + const requestTicketChannel = interaction.options.getChannel( + 'request_ticket_channel' + ) ?? (requestTicketChannelId + ? await guild.channels.fetch(requestTicketChannelId) + : null); + + if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { + await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); + return; + } + + if ( + mentorRoleSelectionChannel.id != mentorRoleSelectionChannelId || + incomingTicketsChannel.id != incomingTicketsChannelId || + requestTicketChannel.id != requestTicketChannelId + ) { + await interaction.deferReply(); + await firebaseUtil + .getFactotumSubCol() + .doc(guild.id) + .set({ + mentorTickets: { + mentorRoleSelectionChannel: mentorRoleSelectionChannel.id, + incomingTicketsChannel: incomingTicketsChannel.id, + requestTicketChannel: requestTicketChannel.id, + }, + }, + { merge: true }); + await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); + } else { + await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); + } + + discordLog(guild, 'Mentor cave started by <@' + userId + '>'); + + // these are all old code that create channels rather than using existing channels + // let overwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.mentorRole, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'], + // }]; + + // if (additionalMentorRole) { + // overwrites.push({ + // id: additionalMentorRole, + // allow: ['VIEW_CHANNEL'] + // }); + // } + + // let mentorCategory = await channel.guild.channels.create('Mentors', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: overwrites + // } + // ); + + // let announcementsOverwrites = overwrites; + // announcementsOverwrites.push( + // { + // id: this.botGuild.roleIDs.mentorRole, + // deny: ['SEND_MESSAGES'], + // allow: ['VIEW_CHANNEL'] + // }); + + // await channel.guild.channels.create('mentors-announcements', + // { + // type: 'GUILD_TEXT', + // parent: mentorCategory, + // permissionOverwrites: announcementsOverwrites + // } + // ); + + // const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', + // { + // type: 'GUILD_TEXT', + // topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', + // parent: mentorCategory + // } + // ); + + const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; + for (let value of emojisMap.values()) { + const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + if (!findRole) { + await guild.roles.create( + { + name: `M-${value}`, + color: mentorRoleColour, + } + ); + } + } + + var fields = []; + for (let [key, value] of emojisMap) { + fields.push({ name: key + ' --> ' + value, value: '\u200b' }); + } + + const roleSelection = new MessageEmbed() + .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') + .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') + .addFields(fields); + + const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); + for (let key of emojisMap.keys()) { + roleSelectionMsg.react(key); + } + + listenToRoleReactions(guild, roleSelectionMsg); + + // channel.guild.channels.create('mentors-general', + // { + // type: 'GUILD_TEXT', + // topic: 'Private chat between all mentors and organizers', + // parent: mentorCategory + // } + // ); + + // const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', + // { + // type: 'GUILD_TEXT', + // topic: 'Tickets from hackers will come in here!', + // parent: mentorCategory + // } + // ); + + // const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: [ + // { + // id: this.botGuild.verification.guestRoleID, + // deny: ['VIEW_CHANNEL'], + // }, + // ] + // } + // ); + + // channel.guild.channels.create('quick-questions', + // { + // type: 'GUILD_TEXT', + // topic: 'ask questions for mentors here!', + // parent: mentorHelpCategory + // } + // ); + + // const requestTicketChannel = await channel.guild.channels.create('request-ticket', + // { + // type: 'GUILD_TEXT', + // topic: 'request 1-on-1 help from mentors here!', + // parent: mentorHelpCategory, + // permissionOverwrites: [ + // { + // id: publicRole, + // allow: ['VIEW_CHANNEL'], + // deny: ['SEND_MESSAGES'] + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'] + // }, + // { + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'] + // } + // ] + // } + // ); + + const requestTicketEmbed = new MessageEmbed() + .setTitle('Need 1:1 mentor help?') + .setDescription('Select a technology you need help with and follow the instructions!'); + + var options = []; + for (let value of emojisMap.values()) { + options.push({ label: value, value: value }); + } + options.push({ label: 'None of the above', value: 'None of the above' }); + + const selectMenuRow = new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(options) + ); + + const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + + listenToRequestConsole( + this.initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel + ); + + const adminEmbed = new MessageEmbed() + .setTitle('Mentor Cave Console') + .setColor(this.initBotInfo.embedColor); + + const adminRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('addRole') + .setLabel('Add Mentor Role') + .setStyle('PRIMARY'), + ); + + const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); + + listenToAdminControls( + this.initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + roleSelection, + selectMenuRow, + requestTicketConsole + ); + + } catch (error) { + // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); + } + } + + async deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, embed) { + await ticketMsg.edit({ embeds: [embed] }); + ticketText.delete(); + ticketVoice.delete(); + ticketCategory.delete(); + } + + async startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime) { + // message collector that stops when there are no messages for inactivePeriod minutes + if (ticketText.parentId && ticketVoice.parentId) { + const activityListener = ticketText.createMessageCollector({ filter: m => !m.author.bot, idle: inactivePeriod * 60 * 1000 }); + activityListener.on('end', async collected => { + if (!ticketText.parentId || !ticketVoice.parentId) return; + if (collected.size === 0 && ticketVoice.members.size === 0 && ticketMsg.embeds[0].color != '#90EE90') { + const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) && !member.user.bot).map(member => member.id); + const msgText = '<@' + remainingMembers.join('><@') + '> Hello! I detected some inactivity in this channel. If you are done and would like to leave this ticket, please go to the pinned message and click the "Leave" button. If you would like to keep this channel a little longer, please click the button below.\n**If no action is taken in the next ' + bufferTime + ' minutes, the channels for this ticket will be deleted automatically.**'; + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('keepChannels') + .setLabel('Keep Channels') + .setStyle('PRIMARY'), + ); + const warning = await ticketText.send({ content: msgText, components: [row] }); + + warning.awaitMessageComponent({ filter: i => !i.user.bot, time: bufferTime * 60 * 1000 }) + .then(interaction => { + interaction.reply('You have indicated that you need more time. I\'ll check in with you later!'); + const disabledButtonRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('keepChannels') + .setLabel('Keep Channels') + .setDisabled(true) + .setStyle('PRIMARY'), + ); + warning.edit({ components: [disabledButtonRow] }); + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + }) + .catch(() => { + if (!ticketText.parentId || !ticketVoice.parentId || ticketMsg.embeds[0].color == '#90EE90') return; + if (ticketVoice.members.size === 0) { + this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Deleted due to inactivity' }])); + } else { + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + } + }); + } else { + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + } + }); + } + } + + /** + * Checks Firebase for existing stored listeners - + * restores the listeners if they exist, otherwise does nothing + * @param {Guild} guild + */ + async tryRestoreReactionListeners(guild) { + const savedMessagesSubCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const mentorCaveDoc = await savedMessagesSubCol.doc('mentor-cave').get(); + if (mentorCaveDoc.exists) { + // + } + } +} + +/** + * + * @param {Guild} guild + * @param {Message} roleSelectionMsg + */ +async function listenToRoleReactions(guild, roleSelectionMsg) { + const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); + collector.on('collect', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const value = emojisMap.get(reaction.emoji.name); + const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + await guild.members.cache.get(user.id).roles.add(findRole); + } + }); + + collector.on('remove', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const member = guild.members.cache.get(user.id); + const value = emojisMap.get(reaction.emoji.name); + const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); + } + }); +} + +/** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {Guild} guild + * @param {Message} requestTicketConsole + * @param {MessageEmbed} requestTicketEmbed + * @param {MessageActionRow} selectMenuRow + * @param {Role} publicRole + * @param {number} reminderTime + * @param {GuildBasedChannel} incomingTicketsChannel + */ +function listenToRequestConsole( + initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel +) { + const selectMenuFilter = i => !i.user.bot; + const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); + selectMenuCollector.on('collect', async (i) => { + if (i.customId === 'ticketType') { + // requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { + await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); + return; + } + const modal = new Modal() + .setCustomId('ticketSubmitModal') + .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('ticketDescription') + .setLabel('Brief description of your problem') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setPlaceholder('Describe your problem here') + .setRequired(true), + ), + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('location') + .setLabel('Where would you like to meet your mentor?') + .setPlaceholder('Help your mentor find you!') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setRequired(true), + ) + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(() => { + }); + + if (submitted) { + const role = i.values[0] === 'None of the above' ? initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; + const description = submitted.fields.getTextInputValue('ticketDescription'); + const location = submitted.fields.getTextInputValue('location'); + // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); + const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); + const ticketNumber = (await mentorCaveDoc.get()).data()?.ticketCount ?? 1; + await mentorCaveDoc.set({ ticketCount: ticketNumber + 1 }, { merge: true }); + const newTicketEmbed = new MessageEmbed() + .setTitle('Ticket #' + ticketNumber) + .setColor('#d3d3d3') + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + }, + // { + // name: 'OK with being helped online?', + // value: helpFormat + // } + ]); + const ticketAcceptanceRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('acceptIrl') + .setLabel('Accept ticket (in-person)') + .setStyle('PRIMARY'), + ); + // .addComponents( + // new MessageButton() + // .setCustomId('acceptOnline') + // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') + // .setStyle('PRIMARY'), + // ); + + const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); + submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); + const ticketReminder = setTimeout(() => { + ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); + }, reminderTime * 60000); + + const confirmationEmbed = new MessageEmbed() + .setTitle('Your ticket is number ' + ticketNumber) + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + } + ]); + const deleteTicketRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('deleteTicket') + .setLabel('Delete ticket') + .setStyle('DANGER'), + ); + const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); + const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); + deleteTicketCollector.on('collect', async deleteInteraction => { + await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); + clearTimeout(ticketReminder); + deleteInteraction.reply('Ticket deleted!'); + ticketReceipt.edit({ components: [] }); + }); + + const ticketAcceptFilter = i => !i.user.bot && i.isButton(); + const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); + ticketAcceptanceCollector.on('collect', async acceptInteraction => { + const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); + if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { + await ticketReceipt.edit({ components: [] }); + clearTimeout(ticketReminder); + ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); + } + if (acceptInteraction.customId === 'acceptIrl') { + // TODO: mark as complete? + submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); + acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); + } + // if (acceptInteraction.customId === 'acceptOnline') { + // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); + // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); + // let ticketChannelOverwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: acceptInteraction.user.id, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: submitted.user.id, + // allow: ['VIEW_CHANNEL'], + // }]; + + // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: ticketChannelOverwrites + // } + // ); + + // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, + // { + // type: 'GUILD_TEXT', + // parent: ticketCategory + // } + // ); + + // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', + // { + // type: 'GUILD_VOICE', + // parent: ticketCategory + // } + // ); + + // const ticketChannelEmbed = new MessageEmbed() + // .setColor(this.botGuild.colors.embedColor) + // .setTitle('Ticket description') + // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); + + // const ticketChannelButtons = new MessageActionRow() + // .addComponents( + // new MessageButton() + // .setCustomId('addMembers') + // .setLabel('Add Members to Channels') + // .setStyle('PRIMARY'), + // ) + // .addComponents( + // new MessageButton() + // .setCustomId('leaveTicket') + // .setLabel('Leave') + // .setStyle('DANGER'), + // ); + // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); + // ticketChannelInfoMsg.pin(); + + // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); + // ticketChannelCollector.on('collect', async ticketInteraction => { + // if (ticketInteraction.customId === 'addMembers') { + // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) + // .then(() => { + // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; + // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) + // .then(async collected => { + // if (collected.first().mentions.members.size === 0) { + // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); + // } else { + // var newMembersArray = []; + // collected.first().mentions.members.forEach(member => { + // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); + // newMembersArray.push(member.id); + // }); + // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); + // } + // }) + // .catch(collected => { + // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); + // }); + // }); + // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { + // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); + // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); + // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { + // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); + // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); + // } + // } else { + // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); + // } + // }); + // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + // } + }); + } + + } + }); +} + +/** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {Guild} guild + * @param {Message} adminControls + * @param {GuildBasedChannel} adminConsole + * @param {Message} roleSelectionMsg + * @param {MessageEmbed} roleSelection + * @param {MessageActionRow} selectMenuRow + * @param {Message} requestTicketConsole + */ +function listenToAdminControls( + initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + roleSelection, + selectMenuRow, + requestTicketConsole +) { + const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; + const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(initBotInfo.roleIDs.adminRole) }); + adminCollector.on('collect', async (adminInteraction) => { + if (adminInteraction.customId === 'addRole') { + const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); + const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); + let roleName; + roleNameCollector.on('collect', async collected => { + if (collected.content.toLowerCase() != 'cancel') { + roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); + const roleExists = guild.roles.cache.filter(role => { + role.name === `M-${roleName}`; + }).size != 0; + if (!roleExists) { + await guild.roles.create( + { + name: `M-${roleName}`, + color: mentorRoleColour, + } + ); + } + + const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); + const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); + emojiCollector.on('collect', collected => { + if (emojisMap.has(collected.emoji.name)) { + adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + } else { + emojiCollector.stop(); + emojisMap.set(collected.emoji.name, roleName); + adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); + roleSelectionMsg.react(collected.emoji.name); + + const oldOptions = selectMenuRow.components[0].options; + const newOptions = oldOptions; + newOptions.splice(-1, 0, { label: roleName, value: roleName }); + var newSelectMenuRow = new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(newOptions) + ); + requestTicketConsole.edit({ components: [newSelectMenuRow] }); + askForEmoji.delete(); + } + }); + } + askForRoleName.delete(); + collected.delete(); + + }); + } + }); +} + module.exports = StartMentorCave; \ No newline at end of file diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index 6a679969..ade7c27a 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -69,7 +69,7 @@ class Pronouns extends Command { listenToReactions(guild, messageEmbed); - const savedMessagesCol = firebaseUtil.getFactotumSubCol().doc(guild.id).collection('SavedMessages'); + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(); await savedMessagesCol.doc('pronouns').set({ messageId: messageEmbed.id, channelId: messageEmbed.channel.id, From c2b6359ea2441fd145c807ccaa2c328fe19099c6 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 4 Aug 2024 22:38:56 -0700 Subject: [PATCH 58/67] fixed role collection syntax --- commands/verification/start-verification.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index 1be87d47..cde2540b 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -99,7 +99,9 @@ class StartVerification extends Command { var correctTypes = []; types.forEach(type => { - if (this.initBotInfo.verification.verificationRoles.has(type) || type === 'staff' || type === 'mentor') { + console.log(type, ' is the TYPE'); + const roleObj = this.initBotInfo.verification.roles.find(role => role.name === type); + if (roleObj || type === 'staff' || type === 'mentor') { const member = interaction.guild.members.cache.get(submitted.user.id); let roleId; if (type === 'staff') { @@ -107,13 +109,20 @@ class StartVerification extends Command { } else if (type === 'mentor') { roleId = this.initBotInfo.roleIDs.mentorRole; } else { - roleId = this.initBotInfo.verification.verificationRoles.get(type); + roleId = roleObj.roleId; } - member.roles.add(roleId); - if (correctTypes.length === 0) { - member.roles.remove(this.initBotInfo.verification.guestRoleID); - member.roles.add(this.initBotInfo.roleIDs.memberRole); + if (member && roleId) { + member.roles.add(roleId); + + if (correctTypes.length === 0) { + member.roles.remove(this.initBotInfo.verification.guestRoleID); + member.roles.add(this.initBotInfo.roleIDs.memberRole); + } + correctTypes.push(type); + } else { + console.warn(`Could not add role: ${roleId} for type: ${type}`); } + correctTypes.push(type); } else { discordLog(interaction.guild, `VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); From d3c3fb694f1fedab661722cb3b8f1ecaa99e6ee3 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 4 Aug 2024 23:06:02 -0700 Subject: [PATCH 59/67] updated typing + error w/ dual messaging --- commands/verification/start-verification.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/commands/verification/start-verification.js b/commands/verification/start-verification.js index cde2540b..9e85f7e9 100644 --- a/commands/verification/start-verification.js +++ b/commands/verification/start-verification.js @@ -97,7 +97,7 @@ class StartVerification extends Command { return; } - var correctTypes = []; + let correctTypes = []; types.forEach(type => { console.log(type, ' is the TYPE'); const roleObj = this.initBotInfo.verification.roles.find(role => role.name === type); @@ -122,8 +122,6 @@ class StartVerification extends Command { } else { console.warn(`Could not add role: ${roleId} for type: ${type}`); } - - correctTypes.push(type); } else { discordLog(interaction.guild, `VERIFY WARNING: <@${submitted.user.id}> was of type ${type} but I could not find that type!`); } From 3d1ebb363dbe4d01aff83ba312a91b73703f623a Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 4 Aug 2024 23:06:42 -0700 Subject: [PATCH 60/67] logic of verifying a user if theyre a hacker FIRST then seeing if theyre another role --- db/firebase/firebaseUtil.js | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index a7b7bc4a..9e0fe519 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -257,27 +257,38 @@ module.exports = { */ async verify(email, id, guildId) { let emailLowerCase = email.trim().toLowerCase(); - let userRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').where('email', '==', emailLowerCase).limit(1); + let userRef = getFactotumDoc().collection('InitBotInfo').doc(guildId).collection('applicants').where('email', '==', emailLowerCase).limit(1); let user = (await userRef.get()).docs[0]; + let otherRolesRef = getFactotumDoc().collection('InitBotInfo').doc(guildId).collection('otherRoles').where('email', '==', emailLowerCase).limit(1); + let otherRolesUser = (await otherRolesRef.get()).docs[0]; if (user) { let returnTypes = []; /** @type {FirebaseUser} */ let data = user.data(); + if (!data?.isVerified) { + data.isVerified = true; + data.VerifiedTimestamp = admin.firestore.Timestamp.now(); + data.discordId = id; + await user.ref.update(data); + returnTypes.push("hacker") + } - data.types.forEach((value, index, array) => { - if (!value.isVerified) { - value.isVerified = true; - value.VerifiedTimestamp = admin.firestore.Timestamp.now(); - returnTypes.push(value.type); - } - }); - - data.discordId = id; + return returnTypes; + } else if (otherRolesUser) { + let returnTypes = []; - user.ref.update(data); + /** @type {FirebaseUser} */ + let data = otherRolesUser.data(); + if (!data?.isVerified) { + data.isVerified = true; + data.VerifiedTimestamp = admin.firestore.Timestamp.now(); + data.discordId = id; + await otherRolesUser.ref.update(data); + returnTypes.push(data?.role) + } - return returnTypes; + return returnTypes; } else { throw new Error('The email provided was not found!'); } From d41f1c838e69402f277dfbe6ee7593988a3535ff Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 4 Aug 2024 23:24:52 -0700 Subject: [PATCH 61/67] revert unnecessary changes --- commands/verification/check-email.js | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index 0f2f5f2e..d0113f76 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -22,27 +22,17 @@ class CheckEmail extends Command { ) } - // IS EDITED async chatInputRun(interaction) { this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const guild = interaction.guild; - console.log(guild, ' is the guild'); const email = interaction.options.getString('email'); const userId = interaction.user.id; - console.log(userId, ' is the userId'); - console.log(email, ' is the email'); if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } let botSpamChannel = guild.channels.resolve(this.initBotInfo.channelIDs.botSpamChannel); - if (!botSpamChannel) { - return interaction.reply({ - content: `The channel with ID ${this.initBotInfo.channelIDs.botSpamChannel} could not be found. Please check the configuration.`, - ephemeral: true - }); - } const userData = await checkEmail(email, guild.id); interaction.reply({content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); From b21c3e78008ddae8b1ffab2891905f5cfcaa8a77 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Sun, 4 Aug 2024 23:25:30 -0700 Subject: [PATCH 62/67] more reverting --- commands/verification/check-email.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/commands/verification/check-email.js b/commands/verification/check-email.js index d0113f76..77444d4d 100644 --- a/commands/verification/check-email.js +++ b/commands/verification/check-email.js @@ -26,14 +26,12 @@ class CheckEmail extends Command { this.initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guild.id); const guild = interaction.guild; const email = interaction.options.getString('email'); - const userId = interaction.user.id; if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) } let botSpamChannel = guild.channels.resolve(this.initBotInfo.channelIDs.botSpamChannel); - const userData = await checkEmail(email, guild.id); interaction.reply({content: 'Visit <#' + this.initBotInfo.channelIDs.botSpamChannel + '> for the results', ephemeral: true}); From 115b281017fc37c0f2754a3f4cd1dcbcfc0e7a80 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sat, 10 Aug 2024 18:29:38 -0700 Subject: [PATCH 63/67] Add message saving functionality to mentor cave command --- app.js | 15 +- commands/a_activity/discord-contests.js | 680 +++++++++--------- .../a_start_commands/start-mentor-cave.js | 200 ++++-- 3 files changed, 507 insertions(+), 388 deletions(-) diff --git a/app.js b/app.js index 79dd41cd..6dbbf46f 100644 --- a/app.js +++ b/app.js @@ -179,12 +179,21 @@ bot.once('ready', async () => { mainLogger.verbose('Trying to restore existing pronoun command message'); /** @type {Pronouns} */ const pronounsCommand = bot.stores.get('commands').get('pronouns'); - const error = await pronounsCommand.tryRestoreReactionListeners(guild); - if (error) { - mainLogger.warning(error); + const pronounsError = await pronounsCommand.tryRestoreReactionListeners(guild); + if (pronounsError) { + mainLogger.warning(pronounsError); } else { mainLogger.verbose('Restored pronoun command message'); } + + /** @type {StartMentorCave} */ + const mentorCaveCommand = bot.stores.get('commands').get('start-mentor-cave'); + const mentorCaveError = await mentorCaveCommand.tryRestoreReactionListeners(guild); + if (mentorCaveError) { + mainLogger.warning(mentorCaveError); + } else { + mainLogger.verbose('Restored mentor cave command message'); + } } guild.commandPrefix = botGuild.prefix; diff --git a/commands/a_activity/discord-contests.js b/commands/a_activity/discord-contests.js index f4996dce..a4c8f716 100644 --- a/commands/a_activity/discord-contests.js +++ b/commands/a_activity/discord-contests.js @@ -1,340 +1,340 @@ -const { Command } = require('@sapphire/framework'); -const { discordLog, checkForRole } = require('../../discord-services'); -const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); -const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); -const firebaseUtil = require('../../db/firebase/firebaseUtil'); - -/** - * The DiscordContests class handles all functions related to Discord contests. It will ask questions in set intervals and pick winners - * based on keywords for those questions that have correct answers. For other questions it will tag staff and staff will be able to tell - * it the winner. It can also be paused and un-paused, and questions can be removed. - * - * Note: all answers are case-insensitive but any extra or missing characters will be considered incorrect. - * @category Commands - * @subcategory Activity - * @extends Command - * @guildonly - */ -class DiscordContests extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Start discord contests.' - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - .addIntegerOption(option => - option.setName('interval') - .setDescription('Time (minutes) between questions') - .setRequired(true)) - .addRoleOption(option => - option.setName('notify') - .setDescription('Role to notify when a question drops') - .setRequired(true)) - .addBooleanOption(option => - option.setName('start_question_now') - .setDescription('True to start first question now, false to start it after one interval') - .setRequired(false)) - ), - { - idHints: '1051737343729610812' - }; - } - - /** - * Stores a map which keeps the questions (strings) as keys and an array of possible answers (strings) as values. It iterates through - * each key in order and asks them in the Discord channel in which it was called at the given intervals. It also listens for emojis - * that tell it to pause, resume, or remove a specified question. - * @param {Command.ChatInputInteraction} interaction - */ - async chatInputRun(interaction) { - // helpful prompt vars - let channel = interaction.channel; - let userId = interaction.user.id; - let guild = interaction.guild; - const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); - let adminLog = await guild.channels.fetch(initBotInfo.channelIDs.adminLog); - let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); - - let interval; - - //ask user for time interval between questions - let timeInterval = interaction.options.getInteger('interval') * 60000; - let startNow = interaction.options.getBoolean('start_question_now'); - let roleId = interaction.options.getRole('notify'); - - if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { - interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - if (Object.values(initBotInfo.roleIDs).includes(roleId.id) || Array(initBotInfo.verification.roles).find((r) => r.roleId === roleId.id)) { - interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); - return; - } - // try { - // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); - // timeInterval = 1000 * 60 * num; - - // // ask user whether to start asking questions now(true) or after 1 interval (false) - // var startNow = await SpecialPrompt.boolean({prompt: 'Type "yes" to start first question now, "no" to start one time interval from now. ', channel, userId, cancelable: true}); - - // // id of role to mention when new questions come out - // var roleId = (await RolePrompt.single({prompt: 'What role should I notify with a new Discord contest is available?', channel, userId})).id; - // } catch (error) { - // channel.send('<@' + userId + '> Command was canceled due to prompt being canceled.').then(msg => msg.delete({timeout: 5000})); - // return; - // } - - //paused keeps track of whether it has been paused - var paused = false; - - /** - * array of winners' ids - * @type {Array} - */ - const winners = []; - - var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!'; - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('play') - .setLabel('Play') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('pause') - .setLabel('Pause') - .setStyle('PRIMARY'), - ) - .addComponents( - new MessageButton() - .setCustomId('refresh') - .setLabel('Refresh leaderboard') - .setStyle('PRIMARY'), - ); - - - // const playFilter = i => i.customId == 'play' && !i.user.bot && (guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)); - // const playCollector = adminConsole.createMessageComponentCollector(playFilter); - // playCollector.on('collect', async i => { - // console.log('play collector') - // if (paused) { - // sendQuestion(this.botGuild); - // interval = setInterval(sendQuestion, timeInterval, this.botGuild); - // paused = false; - // await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); - // await i.reply('<@&' + i.user.id + '> Discord contest has been un-paused!'); - // } - // }); - - const startEmbed = new MessageEmbed() - .setColor(initBotInfo.embedColor) - .setTitle(string) - .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') - .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); - - const leaderboard = new MessageEmbed() - .setTitle('Leaderboard'); - - let pinnedMessage = await channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed, leaderboard] }); - pinnedMessage.pin(); - pinnedMessage.react('🍀'); - - const roleSelectionCollector = pinnedMessage.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true}); - roleSelectionCollector.on('collect', (reaction, user) => { - if (reaction.emoji.name === '🍀') { - guild.members.cache.get(user.id).roles.add(roleId); - } - }); - roleSelectionCollector.on('remove', (reaction, user) => { - if (reaction.emoji.name === '🍀') { - guild.members.cache.get(user.id).roles.remove(roleId); - } - }); - - interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); - const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); - adminLog.send('Discord contests started by <@' + userId + '>'); - const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); - const collector = controlPanel.createMessageComponentCollector({filter}); - collector.on('collect', async i => { - if (i.customId == 'refresh') { - await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }); - await updateLeaderboard(null); - } else if (interval != null && !paused && i.customId == 'pause') { - clearInterval(interval); - paused = true; - await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); - await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); - } else if (paused && i.customId == 'play') { - await sendQuestion(initBotInfo); - interval = setInterval(sendQuestion, timeInterval, initBotInfo); - paused = false; - await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); - await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); - } else { - await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); - } - }); - - //starts the interval, and sends the first question immediately if startNow is true - if (startNow) { - await sendQuestion(initBotInfo); - } - interval = setInterval(sendQuestion, timeInterval, initBotInfo); - - async function updateLeaderboard(memberId) { - if (memberId) { - await saveToLeaderboard(guild.id, memberId); - } - const winnersList = await retrieveLeaderboard(guild.id); - var leaderboardString = ''; - winnersList.forEach(winner => { - leaderboardString += '<@' + winner.memberId + '>: '; - if (winner.points > 1) { - leaderboardString += winner.points + ' points\n'; - } else if (winner.points == 1) { - leaderboardString += '1 point\n'; - } - }); - const newLeaderboard = new MessageEmbed(leaderboard).setDescription(leaderboardString); - pinnedMessage.edit({ embeds: [startEmbed, newLeaderboard] }); - } - - /** - * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * sendQuestion is the function that picks and sends the next question, then picks the winner by matching participants' messages - * against the answer(s) or receives the winner from Staff. Once it reaches the end it will notify Staff in the Logs channel and - * list all the winners in order. - */ - async function sendQuestion(initBotInfo) { - //get question's parameters from db - let data = await getQuestion(guild.id); - - //sends results to Staff after all questions have been asked and stops looping - if (data === null) { - discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> Discord contests have ended!'); - clearInterval(interval); - return; - } - - /** @type {string} */ - let question = data.question; - /** @type {string[]} */ - let answers = data.answers; - let needAllAnswers = data.needAllAnswers; - - const qEmbed = new MessageEmbed() - .setTitle('A new Discord Contest Question:') - .setDescription(question); - - - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('winner') - .setLabel('Select winner') - .setStyle('PRIMARY'), - ); - - - await channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }); - if (answers.length === 0) { - //send message to console - const questionMsg = await adminConsole.send({ content: '<@&' + initBotInfo.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); - - const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); - const collector = await questionMsg.createMessageComponentCollector({ filter }); - - collector.on('collect', async i => { - const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); - - const winnerFilter = message => message.author.id === i.user.id; // error? - const winnerCollector = await adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); - winnerCollector.on('collect', async m => { - if (m.mentions.members.size > 0) { - const member = await m.mentions.members.first(); - const memberId = await member.user.id; - await m.delete(); - await questionMsg.delete(); - await i.editReply('<@' + memberId + '> has been recorded!'); - row.components[0].setDisabled(true); - // row.components[0].setDisabled(); - await channel.send('Congrats <@' + memberId + '> for the best answer to the last question!'); - // winners.push(memberId); - await updateLeaderboard(memberId); - collector.stop(); - // await recordWinner(memberId); - } else { - await m.delete(); - // await winnerRequest.deleteReply(); - let errorMsg = await i.editReply({ content: 'Message does not include a user mention!' }); - setTimeout(function () { - errorMsg.delete(); - }, 5000); - } - }); - }); - } else { - //automatically mark answers - const filter = m => !m.author.bot && (initBotInfo.verification.isEnabled ? checkForRole(m.member, initBotInfo.verification.roles.find((r) => r.name === 'hacker')?.roleId) : checkForRole(m.member, initBotInfo.roleIDs.memberRole)); - const collector = channel.createMessageCollector({ filter, time: timeInterval * 0.75 }); - - collector.on('collect', async m => { - if (!needAllAnswers) { - // for questions that have numbers as answers, the answer has to match at least one of the correct answers exactly - if (!isNaN(answers[0])) { - if (answers.some(correctAnswer => m.content === correctAnswer)) { - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } else if (answers.some(correctAnswer => m.content.toLowerCase().includes(correctAnswer.toLowerCase()))) { - //for most questions, an answer that contains at least once item of the answer array is correct - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } else { - //check if all answers in answer array are in the message - if (answers.every((answer) => m.content.toLowerCase().includes(answer.toLowerCase()))) { - await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(', ') + '.'); - // winners.push(m.author.id); - await updateLeaderboard(m.author.id); - collector.stop(); - recordWinner(m.member); - } - } - }); - - collector.on('end', async () => { - await channel.send('Answers are no longer being accepted. Stay tuned for the next question!'); - }); - } - } - - async function recordWinner(member) { - try { - let email = await lookupById(guild.id, member.id); - discordLog(guild, `Discord contest winner: <@${member.id}> - ${email}`); - } catch (error) { - console.log(error); - } - } - } -} -module.exports = DiscordContests; +const { Command } = require('@sapphire/framework'); +const { discordLog, checkForRole } = require('../../discord-services'); +const { Message, MessageEmbed, Snowflake, MessageActionRow, MessageButton } = require('discord.js'); +const { getQuestion, lookupById, saveToLeaderboard, retrieveLeaderboard } = require('../../db/firebase/firebaseUtil'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); + +/** + * The DiscordContests class handles all functions related to Discord contests. It will ask questions in set intervals and pick winners + * based on keywords for those questions that have correct answers. For other questions it will tag staff and staff will be able to tell + * it the winner. It can also be paused and un-paused, and questions can be removed. + * + * Note: all answers are case-insensitive but any extra or missing characters will be considered incorrect. + * @category Commands + * @subcategory Activity + * @extends Command + * @guildonly + */ +class DiscordContests extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Start discord contests.' + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + .addIntegerOption(option => + option.setName('interval') + .setDescription('Time (minutes) between questions') + .setRequired(true)) + .addRoleOption(option => + option.setName('notify') + .setDescription('Role to notify when a question drops') + .setRequired(true)) + .addBooleanOption(option => + option.setName('start_question_now') + .setDescription('True to start first question now, false to start it after one interval') + .setRequired(false)) + ), + { + idHints: '1051737343729610812' + }; + } + + /** + * Stores a map which keeps the questions (strings) as keys and an array of possible answers (strings) as values. It iterates through + * each key in order and asks them in the Discord channel in which it was called at the given intervals. It also listens for emojis + * that tell it to pause, resume, or remove a specified question. + * @param {Command.ChatInputInteraction} interaction + */ + async chatInputRun(interaction) { + // helpful prompt vars + let channel = interaction.channel; + let userId = interaction.user.id; + let guild = interaction.guild; + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + // let botSpamChannel = guild.channels.resolve(this.botGuild.channelIDs.botSpamChannel); + let adminLog = await guild.channels.fetch(initBotInfo.channelIDs.adminLog); + let adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + + let interval; + + //ask user for time interval between questions + let timeInterval = interaction.options.getInteger('interval') * 60000; + let startNow = interaction.options.getBoolean('start_question_now'); + let roleId = interaction.options.getRole('notify'); + + if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { + interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + if (Object.values(initBotInfo.roleIDs).includes(roleId.id) || Array(initBotInfo.verification.roles).find((r) => r.roleId === roleId.id)) { + interaction.reply({ content: 'This role cannot be used! Please pick a role that is specifically for Discord Contest notifications!', ephemeral: true }); + return; + } + // try { + // let num = await NumberPrompt.single({prompt: 'What is the time interval between questions in minutes (integer only)? ', channel, userId, cancelable: true}); + // timeInterval = 1000 * 60 * num; + + // // ask user whether to start asking questions now(true) or after 1 interval (false) + // var startNow = await SpecialPrompt.boolean({prompt: 'Type "yes" to start first question now, "no" to start one time interval from now. ', channel, userId, cancelable: true}); + + // // id of role to mention when new questions come out + // var roleId = (await RolePrompt.single({prompt: 'What role should I notify with a new Discord contest is available?', channel, userId})).id; + // } catch (error) { + // channel.send('<@' + userId + '> Command was canceled due to prompt being canceled.').then(msg => msg.delete({timeout: 5000})); + // return; + // } + + //paused keeps track of whether it has been paused + var paused = false; + + /** + * array of winners' ids + * @type {Array} + */ + const winners = []; + + var string = 'Discord contests starting soon! Answer questions for a chance to win prizes!'; + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('play') + .setLabel('Play') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('pause') + .setLabel('Pause') + .setStyle('PRIMARY'), + ) + .addComponents( + new MessageButton() + .setCustomId('refresh') + .setLabel('Refresh leaderboard') + .setStyle('PRIMARY'), + ); + + + // const playFilter = i => i.customId == 'play' && !i.user.bot && (guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.staffRole) || guild.members.cache.get(userId).roles.cache.has(this.botGuild.roleIDs.adminRole)); + // const playCollector = adminConsole.createMessageComponentCollector(playFilter); + // playCollector.on('collect', async i => { + // console.log('play collector') + // if (paused) { + // sendQuestion(this.botGuild); + // interval = setInterval(sendQuestion, timeInterval, this.botGuild); + // paused = false; + // await guild.channels.resolve(this.botGuild.channelIDs.adminLog).send('Discord contest restarted by <@' + i.user.id + '>!'); + // await i.reply('<@&' + i.user.id + '> Discord contest has been un-paused!'); + // } + // }); + + const startEmbed = new MessageEmbed() + .setColor(initBotInfo.embedColor) + .setTitle(string) + .setDescription('Note: Short-answer questions are non-case sensitive but any extra or missing symbols will be considered incorrect.') + .addFields([{name: 'Click the 🍀 emoji below to be notified when a new question drops!', value: 'You can un-react to stop.'}]); + + const leaderboard = new MessageEmbed() + .setTitle('Leaderboard'); + + let pinnedMessage = await channel.send({ content: '<@&' + roleId + '>', embeds: [startEmbed, leaderboard] }); + pinnedMessage.pin(); + pinnedMessage.react('🍀'); + + const roleSelectionCollector = pinnedMessage.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true}); + roleSelectionCollector.on('collect', (reaction, user) => { + if (reaction.emoji.name === '🍀') { + guild.members.cache.get(user.id).roles.add(roleId); + } + }); + roleSelectionCollector.on('remove', (reaction, user) => { + if (reaction.emoji.name === '🍀') { + guild.members.cache.get(user.id).roles.remove(roleId); + } + }); + + interaction.reply({ content: 'Discord contest has been started!', ephemeral: true }); + const controlPanel = await adminConsole.send({ content: 'Discord contests control panel. Status: Active', components: [row] }); + adminLog.send('Discord contests started by <@' + userId + '>'); + const filter = i => !i.user.bot && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = controlPanel.createMessageComponentCollector({filter}); + collector.on('collect', async i => { + if (i.customId == 'refresh') { + await i.reply({ content: 'Leaderboard refreshed!', ephemeral: true }); + await updateLeaderboard(null); + } else if (interval != null && !paused && i.customId == 'pause') { + clearInterval(interval); + paused = true; + await i.reply({ content: 'Discord contests has been paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Paused'}); + } else if (paused && i.customId == 'play') { + await sendQuestion(initBotInfo); + interval = setInterval(sendQuestion, timeInterval, initBotInfo); + paused = false; + await i.reply({ content: 'Discord contests has been un-paused!', ephemeral: true }); + await controlPanel.edit({ content: 'Discord contests control panel. Status: Active'}); + } else { + await i.reply({ content: 'Wrong button or wrong permissions!', ephemeral: true }); + } + }); + + //starts the interval, and sends the first question immediately if startNow is true + if (startNow) { + await sendQuestion(initBotInfo); + } + interval = setInterval(sendQuestion, timeInterval, initBotInfo); + + async function updateLeaderboard(memberId) { + if (memberId) { + await saveToLeaderboard(guild.id, memberId); + } + const winnersList = await retrieveLeaderboard(guild.id); + var leaderboardString = ''; + winnersList.forEach(winner => { + leaderboardString += '<@' + winner.memberId + '>: '; + if (winner.points > 1) { + leaderboardString += winner.points + ' points\n'; + } else if (winner.points == 1) { + leaderboardString += '1 point\n'; + } + }); + const newLeaderboard = new MessageEmbed(leaderboard).setDescription(leaderboardString); + pinnedMessage.edit({ embeds: [startEmbed, newLeaderboard] }); + } + + /** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * sendQuestion is the function that picks and sends the next question, then picks the winner by matching participants' messages + * against the answer(s) or receives the winner from Staff. Once it reaches the end it will notify Staff in the Logs channel and + * list all the winners in order. + */ + async function sendQuestion(initBotInfo) { + //get question's parameters from db + let data = await getQuestion(guild.id); + + //sends results to Staff after all questions have been asked and stops looping + if (data === null) { + discordLog(guild, '<@&' + initBotInfo.roleIDs.staffRole + '> Discord contests have ended!'); + clearInterval(interval); + return; + } + + /** @type {string} */ + let question = data.question; + /** @type {string[]} */ + let answers = data.answers; + let needAllAnswers = data.needAllAnswers; + + const qEmbed = new MessageEmbed() + .setTitle('A new Discord Contest Question:') + .setDescription(question); + + + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('winner') + .setLabel('Select winner') + .setStyle('PRIMARY'), + ); + + + await channel.send({ content: '<@&' + roleId + '>', embeds: [qEmbed] }); + if (answers.length === 0) { + //send message to console + const questionMsg = await adminConsole.send({ content: '<@&' + initBotInfo.roleIDs.staffRole + '>' + 'need manual review!', embeds: [qEmbed], components: [row] }); + + const filter = i => !i.user.bot && i.customId === 'winner' && (guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.staffRole) || guild.members.cache.get(i.user.id).roles.cache.has(initBotInfo.roleIDs.adminRole)); + const collector = await questionMsg.createMessageComponentCollector({ filter }); + + collector.on('collect', async i => { + const winnerRequest = await i.reply({ content: '<@' + i.user.id + '> Mention the winner in your next message!', fetchReply: true }); + + const winnerFilter = message => message.author.id === i.user.id; // error? + const winnerCollector = await adminConsole.createMessageCollector({ filter: winnerFilter, max: 1 }); + winnerCollector.on('collect', async m => { + if (m.mentions.members.size > 0) { + const member = await m.mentions.members.first(); + const memberId = await member.user.id; + await m.delete(); + await questionMsg.delete(); + await i.editReply('<@' + memberId + '> has been recorded!'); + row.components[0].setDisabled(true); + // row.components[0].setDisabled(); + await channel.send('Congrats <@' + memberId + '> for the best answer to the last question!'); + // winners.push(memberId); + await updateLeaderboard(memberId); + collector.stop(); + // await recordWinner(memberId); + } else { + await m.delete(); + // await winnerRequest.deleteReply(); + let errorMsg = await i.editReply({ content: 'Message does not include a user mention!' }); + setTimeout(function () { + errorMsg.delete(); + }, 5000); + } + }); + }); + } else { + //automatically mark answers + const filter = m => !m.author.bot && (initBotInfo.verification.isEnabled ? checkForRole(m.member, initBotInfo.verification.roles.find((r) => r.name === 'hacker')?.roleId) : checkForRole(m.member, initBotInfo.roleIDs.memberRole)); + const collector = channel.createMessageCollector({ filter, time: timeInterval * 0.75 }); + + collector.on('collect', async m => { + if (!needAllAnswers) { + // for questions that have numbers as answers, the answer has to match at least one of the correct answers exactly + if (!isNaN(answers[0])) { + if (answers.some(correctAnswer => m.content === correctAnswer)) { + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } else if (answers.some(correctAnswer => m.content.toLowerCase().includes(correctAnswer.toLowerCase()))) { + //for most questions, an answer that contains at least once item of the answer array is correct + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(' or ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } else { + //check if all answers in answer array are in the message + if (answers.every((answer) => m.content.toLowerCase().includes(answer.toLowerCase()))) { + await channel.send('Congrats <@' + m.author.id + '> for getting the correct answer! The answer key is ' + answers.join(', ') + '.'); + // winners.push(m.author.id); + await updateLeaderboard(m.author.id); + collector.stop(); + recordWinner(m.member); + } + } + }); + + collector.on('end', async () => { + await channel.send('Answers are no longer being accepted. Stay tuned for the next question!'); + }); + } + } + + async function recordWinner(member) { + try { + let email = await lookupById(guild.id, member.id); + discordLog(guild, `Discord contest winner: <@${member.id}> - ${email}`); + } catch (error) { + console.log(error); + } + } + } +} +module.exports = DiscordContests; diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index 7322cb83..e94c05ce 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -11,6 +11,7 @@ const { GuildBasedChannel } = require('discord.js'); const firebaseUtil = require('../../db/firebase/firebaseUtil'); +const { Client } = require('discord.js'); //TODO: allow staff to add more roles const htmlCssEmoji = '💻'; @@ -116,8 +117,6 @@ class StartMentorCave extends Command { return; } - let adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); - // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); const publicRole = interaction.options.getRole('request_ticket_role'); // const bufferTime = inactivePeriod / 2; @@ -243,23 +242,25 @@ class StartMentorCave extends Command { } } - var fields = []; - for (let [key, value] of emojisMap) { - fields.push({ name: key + ' --> ' + value, value: '\u200b' }); - } - - const roleSelection = new MessageEmbed() - .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') - .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') - .addFields(fields); + const roleSelection = makeRoleSelectionEmbed(); + /** @type {Message} */ const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); - for (let key of emojisMap.keys()) { + for (const key of emojisMap.keys()) { roleSelectionMsg.react(key); } listenToRoleReactions(guild, roleSelectionMsg); + const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); + + mentorCaveDoc.set({ + roleReactions: { + channelId: roleSelectionMsg.channelId, + messageId: roleSelectionMsg.id + } + }, {merge: true}); + // channel.guild.channels.create('mentors-general', // { // type: 'GUILD_TEXT', @@ -323,19 +324,9 @@ class StartMentorCave extends Command { .setTitle('Need 1:1 mentor help?') .setDescription('Select a technology you need help with and follow the instructions!'); - var options = []; - for (let value of emojisMap.values()) { - options.push({ label: value, value: value }); - } - options.push({ label: 'None of the above', value: 'None of the above' }); - - const selectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(options) - ); - + const selectMenuRow = makeSelectMenuRow(); + + /** @type {Message} */ const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); listenToRequestConsole( @@ -347,6 +338,15 @@ class StartMentorCave extends Command { incomingTicketsChannel ); + mentorCaveDoc.set({ + requestTicketConsole: { + channelId: requestTicketConsole.channelId, + messageId: requestTicketConsole.id, + publicRoleId: publicRole.id, + reminderTime + } + }, {merge: true}); + const adminEmbed = new MessageEmbed() .setTitle('Mentor Cave Console') .setColor(this.initBotInfo.embedColor); @@ -359,6 +359,10 @@ class StartMentorCave extends Command { .setStyle('PRIMARY'), ); + + const adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); + + /** @type {Message} */ const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); listenToAdminControls( @@ -367,11 +371,16 @@ class StartMentorCave extends Command { adminControls, adminConsole, roleSelectionMsg, - roleSelection, - selectMenuRow, requestTicketConsole ); + mentorCaveDoc.set({ + adminControls: { + channelId: adminControls.channelId, + messageId: adminControls.id, + } + }, {merge: true}); + } catch (error) { // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); } @@ -439,10 +448,112 @@ class StartMentorCave extends Command { async tryRestoreReactionListeners(guild) { const savedMessagesSubCol = firebaseUtil.getSavedMessagesSubCol(guild.id); const mentorCaveDoc = await savedMessagesSubCol.doc('mentor-cave').get(); - if (mentorCaveDoc.exists) { - // + if (!mentorCaveDoc.exists) return 'Saved messages doc for mentor cave does not exist'; + + const mentorCaveData = mentorCaveDoc.data(); + if (mentorCaveData.extraEmojis) { + for (const [emoji, name] of Object.entries(mentorCaveData.extraEmojis)) { + emojisMap.set(emoji, name); + } + } + + // Get role reaction listener saved details + const { + messageId: reactionMessageId, + channelId: reactionChannelId + } = mentorCaveData.roleReactions; + const reactionChannel = await guild.channels.fetch(reactionChannelId); + if (!reactionChannel) return 'Saved role reactions message info not found'; + + // Get request ticket console saved details + const { + channelId: consoleChannelId, + messageId: consoleMessageId, + publicRoleId, + reminderTime + } = mentorCaveData.requestTicketConsole; + const consoleChannel = await guild.channels.fetch(consoleChannelId); + if (!consoleChannel) return 'Saved request ticket console info not found'; + + // Get admin controls saved details + const { + channelId: controlsChannelId, + messageId: controlsMessageId + } = mentorCaveData.adminControls; + const adminControlsChannel = await guild.channels.fetch(controlsChannelId); + if (!adminControlsChannel) return 'Saved admin controls info not found'; + + + try { + // Restore role reactions listener + /** @type {Message} */ + const roleSelectionMsg = await reactionChannel.messages.fetch(reactionMessageId); + await listenToRoleReactions(guild, roleSelectionMsg); + + // Restore request console listener + /** @type {Message} */ + const requestTicketConsole = await consoleChannel.messages.fetch(consoleMessageId); + const publicRole = await guild.roles.fetch(publicRoleId); + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + const incomingTicketsChannelId = initBotInfo.mentorTickets?.incomingTicketsChannel; + const incomingTicketsChannel = await guild.channels.fetch(incomingTicketsChannelId); + if (incomingTicketsChannelId && incomingTicketsChannel) { + listenToRequestConsole( + initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel + ); + } + + // Restore admin controls listener + const adminControls = await adminControlsChannel.messages.fetch(controlsMessageId); + const adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + listenToAdminControls( + initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + requestTicketConsole + ); + } catch (e) { + // message doesn't exist anymore + return 'Error: ' + e; } + } +} + +function makeRoleSelectionEmbed() { + const fields = []; + for (const [key, value] of emojisMap) { + fields.push({ name: key + ' --> ' + value, value: '\u200b' }); } + + const roleSelection = new MessageEmbed() + .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') + .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') + .addFields(fields); + + return roleSelection; +} + +function makeSelectMenuRow() { + const options = []; + for (const value of emojisMap.values()) { + options.push({ label: value, value: value }); + } + options.push({ label: 'None of the above', value: 'None of the above' }); + + const selectMenuRow = new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(options) + ); + return selectMenuRow; } /** @@ -720,9 +831,7 @@ function listenToRequestConsole( * @param {Guild} guild * @param {Message} adminControls * @param {GuildBasedChannel} adminConsole - * @param {Message} roleSelectionMsg - * @param {MessageEmbed} roleSelection - * @param {MessageActionRow} selectMenuRow + * @param {Message} roleSelectionMsg * @param {Message} requestTicketConsole */ function listenToAdminControls( @@ -731,8 +840,6 @@ function listenToAdminControls( adminControls, adminConsole, roleSelectionMsg, - roleSelection, - selectMenuRow, requestTicketConsole ) { const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; @@ -759,29 +866,32 @@ function listenToAdminControls( const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); - emojiCollector.on('collect', collected => { + emojiCollector.on('collect', (collected) => { if (emojisMap.has(collected.emoji.name)) { adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { setTimeout(() => msg.delete(), 5000); }); } else { emojiCollector.stop(); + firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave').set({ + extraEmojis: { + [collected.emoji.name]: roleName + } + }, { merge: true }); emojisMap.set(collected.emoji.name, roleName); adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { setTimeout(() => msg.delete(), 5000); }); - roleSelectionMsg.edit({ embeds: [new MessageEmbed(roleSelection).addFields([{ name: collected.emoji.name + ' --> ' + roleName, value: '\u200b' }])] }); + roleSelectionMsg.edit({ embeds: [new MessageEmbed(makeRoleSelectionEmbed())] }); roleSelectionMsg.react(collected.emoji.name); - const oldOptions = selectMenuRow.components[0].options; - const newOptions = oldOptions; - newOptions.splice(-1, 0, { label: roleName, value: roleName }); - var newSelectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(newOptions) - ); + const selectMenuOptions = makeSelectMenuRow().components[0].options; + const newSelectMenuRow = + new MessageActionRow().addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(selectMenuOptions) + ); requestTicketConsole.edit({ components: [newSelectMenuRow] }); askForEmoji.delete(); } From f0098645f28b1c09feb30dbbe94d8596b4f081f7 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Mon, 12 Aug 2024 00:16:34 -0700 Subject: [PATCH 64/67] Convert role selector command to slash + add message id saving --- app.js | 10 + classes/permission-command.js | 149 +- .../a_start_commands/start-mentor-cave.js | 1811 ++++++++--------- commands/a_utility/change-pre-fix.js | 2 + commands/a_utility/pronouns.js | 13 +- commands/a_utility/role-selector.js | 362 +++- 6 files changed, 1300 insertions(+), 1047 deletions(-) diff --git a/app.js b/app.js index 6dbbf46f..d90c7727 100644 --- a/app.js +++ b/app.js @@ -12,6 +12,7 @@ const Sentry = require('@sentry/node'); const Tracing = require('@sentry/tracing'); const { LogLevel, SapphireClient } = require('@sapphire/framework'); const Pronouns = require('./commands/a_utility/pronouns'); +const RoleSelector = require('./commands/a_utility/role-selector'); /** * The Main App module houses the bot events, process events, and initializes @@ -194,6 +195,15 @@ bot.once('ready', async () => { } else { mainLogger.verbose('Restored mentor cave command message'); } + + /** @type {RoleSelector} */ + const roleSelectorCommand = bot.stores.get('commands').get('role-selector'); + const roleSelectorError = await roleSelectorCommand.tryRestoreReactionListeners(guild); + if (mentorCaveError) { + mainLogger.warning(roleSelectorError); + } else { + mainLogger.verbose('Restored role selector command message'); + } } guild.commandPrefix = botGuild.prefix; diff --git a/classes/permission-command.js b/classes/permission-command.js index f10893c1..f3269389 100644 --- a/classes/permission-command.js +++ b/classes/permission-command.js @@ -1,13 +1,13 @@ const Discord = require('discord.js'); -const { Command, CommandoMessage, CommandoClientOptions, CommandInfo } = require('discord.js-commando'); +const { Command } = require('@sapphire/framework'); +// const { Command, CommandoMessage, CommandoClientOptions, CommandInfo } = require('discord.js-commando'); const firebaseUtil = require('../db/firebase/firebaseUtil'); const discordServices = require('../discord-services'); const winston = require('winston'); /** - * The PermissionCommand is a custom command that extends the discord js commando Command class. - * This Command subclass adds role and channel permission checks before the command is run. It also - * removes the message used to call the command. + * The PermissionCommand is a custom command that extends the Sapphire Command class. + * This Command subclass adds role and channel permission checks before the command is run. * @extends Command */ class PermissionCommand extends Command { @@ -24,12 +24,14 @@ class PermissionCommand extends Command { /** * Constructor for our custom command, calls the parent constructor. - * @param {CommandoClientOptions} client - the client the command is for - * @param {CommandInfo} info - the information for this commando command - * @param {CommandPermissionInfo} permissionInfo - the custom information for this command + * @param {Command.Context} context - the context of the command + * @param {Command.Options} options - additional command options + * @param {CommandPermissionInfo} permissionInfo - the custom information for this command */ - constructor(client, info, permissionInfo) { - super(client, info); + constructor(context, options, permissionInfo) { + super(context, { + ...options + }); /** * The permission info @@ -39,6 +41,14 @@ class PermissionCommand extends Command { this.permissionInfo = this.validateInfo(permissionInfo); } + /** + * + * @param {Command.Registry} registry + */ + registerApplicationCommands(registry) { + throw new Error('You need to implement the registerApplicationCommands method!'); + } + /** * Adds default values if not found on the object. * @param {CommandPermissionInfo} permissionInfo @@ -54,45 +64,40 @@ class PermissionCommand extends Command { return permissionInfo; } - /** - * Run command used by Command class. Has the permission checks and runs the child runCommand method. - * @param {Discord.Message} message - * @param {Object|string|string[]} args - * @param {boolean} fromPattern - * @param {Promise>} result - * @override - * @private + * + * @param {Command.ChatInputInteraction} interaction */ - async run(message, args, fromPattern, result){ - - // delete the message - discordServices.deleteMessage(message); - - /** @type {BotGuildModel} */ + async chatInputRun(interaction) { + /** @type {FirebaseFirestore.DocumentData | null | undefined} */ let initBotInfo; - if (message?.guild) initBotInfo = await firebaseUtil.getInitBotInfo(message.guild.id); + if (interaction.guild) { + initBotInfo = await firebaseUtil.getInitBotInfo(interaction.guildId); + } else initBotInfo = null; // check for DM only, when true, all other checks should not happen! if (this.permissionInfo.dmOnly) { - if (message.channel.type != 'dm') { - discordServices.sendEmbedToMember(message.member, { - title: 'Error', - description: 'The command you just tried to use is only usable via DM!', + if (interaction.channel.type != 'DM') { + winston.loggers. + get(initBotInfo?.id || 'main'). + warning(`User ${interaction.user.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${interaction.channel.name}.`); + return interaction.reply({ + content: 'The command you just tried to use is only usable via DM!', + ephemeral: true }); - winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); - return; } } else { // Make sure it is only used in the permitted channel if (this.permissionInfo?.channel) { let channelID = initBotInfo.channelIDs[this.permissionInfo.channel]; - if (channelID && message.channel.id != channelID) { - discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true); - winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); - return; + if (channelID && interaction.channelId != channelID) { + winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${interaction.user.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${interaction.channel.name}.`); + return interaction.reply({ + content: this.permissionInfo.channelMessage, + ephemeral: true + }); } } // Make sure only the permitted role can call it @@ -102,29 +107,89 @@ class PermissionCommand extends Command { // if staff role then check for staff and admin, else check the given role if (roleID && (roleID === initBotInfo.roleIDs.staffRole && - (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, initBotInfo.roleIDs.adminRole))) || - (roleID != initBotInfo.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) { - discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true); - winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); - return; + (!discordServices.checkForRole(interaction.member, roleID) && !discordServices.checkForRole(interaction.member, initBotInfo.roleIDs.adminRole))) || + (roleID != initBotInfo.roleIDs.staffRole && !discordServices.checkForRole(interaction.member, roleID))) { + winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${interaction.user.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${interaction.member.roles.cache.array().map((role) => role.name)}`); + return interaction.reply({ + content: this.permissionInfo.roleMessage, + ephemeral: true + }); } } } - this.runCommand(initBotInfo, message, args, fromPattern, result); + this.runCommand(initBotInfo, interaction, args, fromPattern, result); } + // /** + // * Run command used by Command class. Has the permission checks and runs the child runCommand method. + // * @param {Discord.Message} message + // * @param {Object|string|string[]} args + // * @param {boolean} fromPattern + // * @param {Promise>} result + // * @override + // * @private + // */ + // async run(message, args, fromPattern, result){ + + // // delete the message + // discordServices.deleteMessage(message); + + // /** @type {FirebaseFirestore.DocumentData | null | undefined} */ + // let initBotInfo; + // if (message?.guild) initBotInfo = await firebaseUtil.getInitBotInfo(message.guild.id); + // else initBotInfo = null; + + // // check for DM only, when true, all other checks should not happen! + // if (this.permissionInfo.dmOnly) { + // if (message.channel.type != 'dm') { + // discordServices.sendEmbedToMember(message.member, { + // title: 'Error', + // description: 'The command you just tried to use is only usable via DM!', + // }); + // winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in DMs in the channel ${message.channel.name}.`); + // return; + // } + // } else { + // // Make sure it is only used in the permitted channel + // if (this.permissionInfo?.channel) { + // let channelID = initBotInfo.channelIDs[this.permissionInfo.channel]; + + // if (channelID && message.channel.id != channelID) { + // discordServices.sendMessageToMember(message.member, this.permissionInfo.channelMessage, true); + // winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available in the channel ${this.permissionInfo.channel}, in the channel ${message.channel.name}.`); + // return; + // } + // } + // // Make sure only the permitted role can call it + // else if (this.permissionInfo?.role) { + + // let roleID = initBotInfo.roleIDs[this.permissionInfo.role]; + + // // if staff role then check for staff and admin, else check the given role + // if (roleID && (roleID === initBotInfo.roleIDs.staffRole && + // (!discordServices.checkForRole(message.member, roleID) && !discordServices.checkForRole(message.member, initBotInfo.roleIDs.adminRole))) || + // (roleID != initBotInfo.roleIDs.staffRole && !discordServices.checkForRole(message.member, roleID))) { + // discordServices.sendMessageToMember(message.member, this.permissionInfo.roleMessage, true); + // winston.loggers.get(initBotInfo?.id || 'main').warning(`User ${message.author.id} tried to run a permission command ${this.name} that is only available for members with role ${this.permissionInfo.role}, but he has roles: ${message.member.roles.cache.array().map((role) => role.name)}`); + // return; + // } + // } + // } + // this.runCommand(initBotInfo, message, args, fromPattern, result); + // } + /** * Required class by children, will throw error if not implemented! * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * @param {CommandoMessage} message + * @param {Command.ChatInputInteraction} interaction * @param {Object} args * @param {Boolean} fromPattern * @param {Promise<*>} result * @abstract * @protected */ - runCommand(initBotInfo, message, args, fromPattern, result) { + runCommand(initBotInfo, interaction, args, fromPattern, result) { throw new Error('You need to implement the runCommand method!'); } } diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index e94c05ce..e76ba5fc 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -1,908 +1,905 @@ -const { Command } = require('@sapphire/framework'); -const { MessageEmbed, Guild, Role } = require('discord.js'); -const { discordLog } = require('../../discord-services'); -const { - MessageSelectMenu, - Modal, - TextInputComponent, - Message, - MessageActionRow, - MessageButton, - GuildBasedChannel -} = require('discord.js'); -const firebaseUtil = require('../../db/firebase/firebaseUtil'); -const { Client } = require('discord.js'); - -//TODO: allow staff to add more roles -const htmlCssEmoji = '💻'; -const jsTsEmoji = '🕸️'; -const pythonEmoji = '🐍'; -const sqlEmoji = '🐬'; -const reactEmoji = '⚛️'; -const noSqlEmoji = '🔥'; -const javaEmoji = '☕'; -const cEmoji = '🎮'; -const cSharpEmoji = '💼'; -const reduxEmoji = '☁️'; -const figmaEmoji = '🎨'; -const unityEmoji = '🧊'; -const rustEmoji = '⚙️'; -const awsEmoji = '🙂'; -const ideationEmoji = '💡'; -const pitchingEmoji = '🎤'; - -let emojisMap = new Map([ - [htmlCssEmoji, 'HTML/CSS'], - [jsTsEmoji, 'JavaScript/TypeScript'], - [pythonEmoji, 'Python'], - [sqlEmoji, 'SQL'], - [reactEmoji, 'React'], - [noSqlEmoji, 'NoSQL'], - [javaEmoji, 'Java'], - [cEmoji, 'C/C++'], - [cSharpEmoji, 'C#'], - [reduxEmoji, 'Redux'], - [figmaEmoji, 'Figma'], - [unityEmoji, 'Unity'], - [rustEmoji, 'Rust'], - [awsEmoji, 'AWS'], - [ideationEmoji, 'Ideation'], - [pitchingEmoji, 'Pitching'] -]); - -/** - * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. - * @category Commands - * @subcategory Start-Commands - * @extends PermissionCommand - * @guildonly - */ -class StartMentorCave extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Start mentor cave' - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName('start-mentor-cave') - .setDescription(this.description) - .addIntegerOption(option => - option.setName('inactivity_time') - .setDescription('How long (minutes) before bot asks users to delete ticket channels') - .setRequired(true)) - .addIntegerOption(option => - option.setName('unanswered_ticket_time') - .setDescription('How long (minutes) shall a ticket go unaccepted before the bot sends a reminder to all mentors?') - .setRequired(true)) - .addRoleOption(option => - option.setName('request_ticket_role') - .setDescription('Tag the role that is allowed to request tickets') - .setRequired(true)) - .addChannelOption(option => - option.setName('mentor_role_selection_channel') - .setDescription('Tag the channel where mentors can select their specialties') - .setRequired(false)) - .addChannelOption(option => - option.setName('incoming_tickets_channel') - .setDescription('Tag the channel where mentor tickets will be sent') - .setRequired(false)) - .addChannelOption(option => - option.setName('request_ticket_channel') - .setDescription('Tag the channel where hackers can request tickets') - .setRequired(false)) - ), - { - idHints: '1051737344937566229' - }; - } - - /** - * - * @param {Command.ChatInputInteraction} interaction - * @returns - */ - async chatInputRun(interaction) { - try { - // helpful prompt vars - let userId = interaction.user.id; - let guild = interaction.guild; - this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - - if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { - await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); - return; - } - - // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); - const publicRole = interaction.options.getRole('request_ticket_role'); - // const bufferTime = inactivePeriod / 2; - const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); - - const mentorRoleSelectionChannelId = - this.initBotInfo.mentorTickets?.mentorRoleSelectionChannel; - const incomingTicketsChannelId = - this.initBotInfo.mentorTickets?.incomingTicketsChannel; - const requestTicketChannelId = - this.initBotInfo.mentorTickets?.requestTicketChannel; - - const mentorRoleSelectionChannel = interaction.options.getChannel( - 'mentor_role_selection_channel' - ) ?? (mentorRoleSelectionChannelId - ? await guild.channels.fetch(mentorRoleSelectionChannelId) - : null); - - const incomingTicketsChannel = interaction.options.getChannel( - 'incoming_tickets_channel' - ) ?? (incomingTicketsChannelId - ? await guild.channels.fetch(incomingTicketsChannelId) - : null); - - const requestTicketChannel = interaction.options.getChannel( - 'request_ticket_channel' - ) ?? (requestTicketChannelId - ? await guild.channels.fetch(requestTicketChannelId) - : null); - - if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { - await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); - return; - } - - if ( - mentorRoleSelectionChannel.id != mentorRoleSelectionChannelId || - incomingTicketsChannel.id != incomingTicketsChannelId || - requestTicketChannel.id != requestTicketChannelId - ) { - await interaction.deferReply(); - await firebaseUtil - .getFactotumSubCol() - .doc(guild.id) - .set({ - mentorTickets: { - mentorRoleSelectionChannel: mentorRoleSelectionChannel.id, - incomingTicketsChannel: incomingTicketsChannel.id, - requestTicketChannel: requestTicketChannel.id, - }, - }, - { merge: true }); - await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); - } else { - await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); - } - - discordLog(guild, 'Mentor cave started by <@' + userId + '>'); - - // these are all old code that create channels rather than using existing channels - // let overwrites = - // [{ - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'], - // }, - // { - // id: this.botGuild.roleIDs.mentorRole, - // allow: ['VIEW_CHANNEL'], - // }, - // { - // id: this.botGuild.roleIDs.staffRole, - // allow: ['VIEW_CHANNEL'], - // }]; - - // if (additionalMentorRole) { - // overwrites.push({ - // id: additionalMentorRole, - // allow: ['VIEW_CHANNEL'] - // }); - // } - - // let mentorCategory = await channel.guild.channels.create('Mentors', - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: overwrites - // } - // ); - - // let announcementsOverwrites = overwrites; - // announcementsOverwrites.push( - // { - // id: this.botGuild.roleIDs.mentorRole, - // deny: ['SEND_MESSAGES'], - // allow: ['VIEW_CHANNEL'] - // }); - - // await channel.guild.channels.create('mentors-announcements', - // { - // type: 'GUILD_TEXT', - // parent: mentorCategory, - // permissionOverwrites: announcementsOverwrites - // } - // ); - - // const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', - // { - // type: 'GUILD_TEXT', - // topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', - // parent: mentorCategory - // } - // ); - - const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; - for (let value of emojisMap.values()) { - const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - if (!findRole) { - await guild.roles.create( - { - name: `M-${value}`, - color: mentorRoleColour, - } - ); - } - } - - const roleSelection = makeRoleSelectionEmbed(); - - /** @type {Message} */ - const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); - for (const key of emojisMap.keys()) { - roleSelectionMsg.react(key); - } - - listenToRoleReactions(guild, roleSelectionMsg); - - const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); - - mentorCaveDoc.set({ - roleReactions: { - channelId: roleSelectionMsg.channelId, - messageId: roleSelectionMsg.id - } - }, {merge: true}); - - // channel.guild.channels.create('mentors-general', - // { - // type: 'GUILD_TEXT', - // topic: 'Private chat between all mentors and organizers', - // parent: mentorCategory - // } - // ); - - // const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', - // { - // type: 'GUILD_TEXT', - // topic: 'Tickets from hackers will come in here!', - // parent: mentorCategory - // } - // ); - - // const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: [ - // { - // id: this.botGuild.verification.guestRoleID, - // deny: ['VIEW_CHANNEL'], - // }, - // ] - // } - // ); - - // channel.guild.channels.create('quick-questions', - // { - // type: 'GUILD_TEXT', - // topic: 'ask questions for mentors here!', - // parent: mentorHelpCategory - // } - // ); - - // const requestTicketChannel = await channel.guild.channels.create('request-ticket', - // { - // type: 'GUILD_TEXT', - // topic: 'request 1-on-1 help from mentors here!', - // parent: mentorHelpCategory, - // permissionOverwrites: [ - // { - // id: publicRole, - // allow: ['VIEW_CHANNEL'], - // deny: ['SEND_MESSAGES'] - // }, - // { - // id: this.botGuild.roleIDs.staffRole, - // allow: ['VIEW_CHANNEL'] - // }, - // { - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'] - // } - // ] - // } - // ); - - const requestTicketEmbed = new MessageEmbed() - .setTitle('Need 1:1 mentor help?') - .setDescription('Select a technology you need help with and follow the instructions!'); - - const selectMenuRow = makeSelectMenuRow(); - - /** @type {Message} */ - const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - - listenToRequestConsole( - this.initBotInfo, - guild, - requestTicketConsole, - publicRole, - reminderTime, - incomingTicketsChannel - ); - - mentorCaveDoc.set({ - requestTicketConsole: { - channelId: requestTicketConsole.channelId, - messageId: requestTicketConsole.id, - publicRoleId: publicRole.id, - reminderTime - } - }, {merge: true}); - - const adminEmbed = new MessageEmbed() - .setTitle('Mentor Cave Console') - .setColor(this.initBotInfo.embedColor); - - const adminRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('addRole') - .setLabel('Add Mentor Role') - .setStyle('PRIMARY'), - ); - - - const adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); - - /** @type {Message} */ - const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); - - listenToAdminControls( - this.initBotInfo, - guild, - adminControls, - adminConsole, - roleSelectionMsg, - requestTicketConsole - ); - - mentorCaveDoc.set({ - adminControls: { - channelId: adminControls.channelId, - messageId: adminControls.id, - } - }, {merge: true}); - - } catch (error) { - // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); - } - } - - async deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, embed) { - await ticketMsg.edit({ embeds: [embed] }); - ticketText.delete(); - ticketVoice.delete(); - ticketCategory.delete(); - } - - async startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime) { - // message collector that stops when there are no messages for inactivePeriod minutes - if (ticketText.parentId && ticketVoice.parentId) { - const activityListener = ticketText.createMessageCollector({ filter: m => !m.author.bot, idle: inactivePeriod * 60 * 1000 }); - activityListener.on('end', async collected => { - if (!ticketText.parentId || !ticketVoice.parentId) return; - if (collected.size === 0 && ticketVoice.members.size === 0 && ticketMsg.embeds[0].color != '#90EE90') { - const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) && !member.user.bot).map(member => member.id); - const msgText = '<@' + remainingMembers.join('><@') + '> Hello! I detected some inactivity in this channel. If you are done and would like to leave this ticket, please go to the pinned message and click the "Leave" button. If you would like to keep this channel a little longer, please click the button below.\n**If no action is taken in the next ' + bufferTime + ' minutes, the channels for this ticket will be deleted automatically.**'; - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('keepChannels') - .setLabel('Keep Channels') - .setStyle('PRIMARY'), - ); - const warning = await ticketText.send({ content: msgText, components: [row] }); - - warning.awaitMessageComponent({ filter: i => !i.user.bot, time: bufferTime * 60 * 1000 }) - .then(interaction => { - interaction.reply('You have indicated that you need more time. I\'ll check in with you later!'); - const disabledButtonRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('keepChannels') - .setLabel('Keep Channels') - .setDisabled(true) - .setStyle('PRIMARY'), - ); - warning.edit({ components: [disabledButtonRow] }); - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - }) - .catch(() => { - if (!ticketText.parentId || !ticketVoice.parentId || ticketMsg.embeds[0].color == '#90EE90') return; - if (ticketVoice.members.size === 0) { - this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Deleted due to inactivity' }])); - } else { - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - } - }); - } else { - this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - } - }); - } - } - - /** - * Checks Firebase for existing stored listeners - - * restores the listeners if they exist, otherwise does nothing - * @param {Guild} guild - */ - async tryRestoreReactionListeners(guild) { - const savedMessagesSubCol = firebaseUtil.getSavedMessagesSubCol(guild.id); - const mentorCaveDoc = await savedMessagesSubCol.doc('mentor-cave').get(); - if (!mentorCaveDoc.exists) return 'Saved messages doc for mentor cave does not exist'; - - const mentorCaveData = mentorCaveDoc.data(); - if (mentorCaveData.extraEmojis) { - for (const [emoji, name] of Object.entries(mentorCaveData.extraEmojis)) { - emojisMap.set(emoji, name); - } - } - - // Get role reaction listener saved details - const { - messageId: reactionMessageId, - channelId: reactionChannelId - } = mentorCaveData.roleReactions; - const reactionChannel = await guild.channels.fetch(reactionChannelId); - if (!reactionChannel) return 'Saved role reactions message info not found'; - - // Get request ticket console saved details - const { - channelId: consoleChannelId, - messageId: consoleMessageId, - publicRoleId, - reminderTime - } = mentorCaveData.requestTicketConsole; - const consoleChannel = await guild.channels.fetch(consoleChannelId); - if (!consoleChannel) return 'Saved request ticket console info not found'; - - // Get admin controls saved details - const { - channelId: controlsChannelId, - messageId: controlsMessageId - } = mentorCaveData.adminControls; - const adminControlsChannel = await guild.channels.fetch(controlsChannelId); - if (!adminControlsChannel) return 'Saved admin controls info not found'; - - - try { - // Restore role reactions listener - /** @type {Message} */ - const roleSelectionMsg = await reactionChannel.messages.fetch(reactionMessageId); - await listenToRoleReactions(guild, roleSelectionMsg); - - // Restore request console listener - /** @type {Message} */ - const requestTicketConsole = await consoleChannel.messages.fetch(consoleMessageId); - const publicRole = await guild.roles.fetch(publicRoleId); - const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - const incomingTicketsChannelId = initBotInfo.mentorTickets?.incomingTicketsChannel; - const incomingTicketsChannel = await guild.channels.fetch(incomingTicketsChannelId); - if (incomingTicketsChannelId && incomingTicketsChannel) { - listenToRequestConsole( - initBotInfo, - guild, - requestTicketConsole, - publicRole, - reminderTime, - incomingTicketsChannel - ); - } - - // Restore admin controls listener - const adminControls = await adminControlsChannel.messages.fetch(controlsMessageId); - const adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); - listenToAdminControls( - initBotInfo, - guild, - adminControls, - adminConsole, - roleSelectionMsg, - requestTicketConsole - ); - } catch (e) { - // message doesn't exist anymore - return 'Error: ' + e; - } - } -} - -function makeRoleSelectionEmbed() { - const fields = []; - for (const [key, value] of emojisMap) { - fields.push({ name: key + ' --> ' + value, value: '\u200b' }); - } - - const roleSelection = new MessageEmbed() - .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') - .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') - .addFields(fields); - - return roleSelection; -} - -function makeSelectMenuRow() { - const options = []; - for (const value of emojisMap.values()) { - options.push({ label: value, value: value }); - } - options.push({ label: 'None of the above', value: 'None of the above' }); - - const selectMenuRow = new MessageActionRow() - .addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(options) - ); - return selectMenuRow; -} - -/** - * - * @param {Guild} guild - * @param {Message} roleSelectionMsg - */ -async function listenToRoleReactions(guild, roleSelectionMsg) { - const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); - collector.on('collect', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const value = emojisMap.get(reaction.emoji.name); - const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - await guild.members.cache.get(user.id).roles.add(findRole); - } - }); - - collector.on('remove', async (reaction, user) => { - if (emojisMap.has(reaction.emoji.name)) { - const member = guild.members.cache.get(user.id); - const value = emojisMap.get(reaction.emoji.name); - const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); - if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); - } - }); -} - -/** - * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * @param {Guild} guild - * @param {Message} requestTicketConsole - * @param {MessageEmbed} requestTicketEmbed - * @param {MessageActionRow} selectMenuRow - * @param {Role} publicRole - * @param {number} reminderTime - * @param {GuildBasedChannel} incomingTicketsChannel - */ -function listenToRequestConsole( - initBotInfo, - guild, - requestTicketConsole, - publicRole, - reminderTime, - incomingTicketsChannel -) { - const selectMenuFilter = i => !i.user.bot; - const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); - selectMenuCollector.on('collect', async (i) => { - if (i.customId === 'ticketType') { - // requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); - if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { - await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); - return; - } - const modal = new Modal() - .setCustomId('ticketSubmitModal') - .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('ticketDescription') - .setLabel('Brief description of your problem') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setPlaceholder('Describe your problem here') - .setRequired(true), - ), - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('location') - .setLabel('Where would you like to meet your mentor?') - .setPlaceholder('Help your mentor find you!') - .setMaxLength(300) - .setStyle('PARAGRAPH') - .setRequired(true), - ) - ]); - await i.showModal(modal); - - const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) - .catch(() => { - }); - - if (submitted) { - const role = i.values[0] === 'None of the above' ? initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; - const description = submitted.fields.getTextInputValue('ticketDescription'); - const location = submitted.fields.getTextInputValue('location'); - // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); - const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); - const ticketNumber = (await mentorCaveDoc.get()).data()?.ticketCount ?? 1; - await mentorCaveDoc.set({ ticketCount: ticketNumber + 1 }, { merge: true }); - const newTicketEmbed = new MessageEmbed() - .setTitle('Ticket #' + ticketNumber) - .setColor('#d3d3d3') - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - }, - // { - // name: 'OK with being helped online?', - // value: helpFormat - // } - ]); - const ticketAcceptanceRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('acceptIrl') - .setLabel('Accept ticket (in-person)') - .setStyle('PRIMARY'), - ); - // .addComponents( - // new MessageButton() - // .setCustomId('acceptOnline') - // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') - // .setStyle('PRIMARY'), - // ); - - const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); - submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); - const ticketReminder = setTimeout(() => { - ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); - }, reminderTime * 60000); - - const confirmationEmbed = new MessageEmbed() - .setTitle('Your ticket is number ' + ticketNumber) - .addFields([ - { - name: 'Problem description', - value: description - }, - { - name: 'Where to meet', - value: location - } - ]); - const deleteTicketRow = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('deleteTicket') - .setLabel('Delete ticket') - .setStyle('DANGER'), - ); - const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); - const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); - deleteTicketCollector.on('collect', async deleteInteraction => { - await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); - clearTimeout(ticketReminder); - deleteInteraction.reply('Ticket deleted!'); - ticketReceipt.edit({ components: [] }); - }); - - const ticketAcceptFilter = i => !i.user.bot && i.isButton(); - const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); - ticketAcceptanceCollector.on('collect', async acceptInteraction => { - const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); - if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { - await ticketReceipt.edit({ components: [] }); - clearTimeout(ticketReminder); - ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); - } - if (acceptInteraction.customId === 'acceptIrl') { - // TODO: mark as complete? - submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); - acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); - } - // if (acceptInteraction.customId === 'acceptOnline') { - // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); - // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); - // let ticketChannelOverwrites = - // [{ - // id: this.botGuild.roleIDs.everyoneRole, - // deny: ['VIEW_CHANNEL'], - // }, - // { - // id: acceptInteraction.user.id, - // allow: ['VIEW_CHANNEL'], - // }, - // { - // id: submitted.user.id, - // allow: ['VIEW_CHANNEL'], - // }]; - - // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, - // { - // type: 'GUILD_CATEGORY', - // permissionOverwrites: ticketChannelOverwrites - // } - // ); - - // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, - // { - // type: 'GUILD_TEXT', - // parent: ticketCategory - // } - // ); - - // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', - // { - // type: 'GUILD_VOICE', - // parent: ticketCategory - // } - // ); - - // const ticketChannelEmbed = new MessageEmbed() - // .setColor(this.botGuild.colors.embedColor) - // .setTitle('Ticket description') - // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); - - // const ticketChannelButtons = new MessageActionRow() - // .addComponents( - // new MessageButton() - // .setCustomId('addMembers') - // .setLabel('Add Members to Channels') - // .setStyle('PRIMARY'), - // ) - // .addComponents( - // new MessageButton() - // .setCustomId('leaveTicket') - // .setLabel('Leave') - // .setStyle('DANGER'), - // ); - // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); - // ticketChannelInfoMsg.pin(); - - // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); - // ticketChannelCollector.on('collect', async ticketInteraction => { - // if (ticketInteraction.customId === 'addMembers') { - // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) - // .then(() => { - // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; - // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) - // .then(async collected => { - // if (collected.first().mentions.members.size === 0) { - // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); - // } else { - // var newMembersArray = []; - // collected.first().mentions.members.forEach(member => { - // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); - // newMembersArray.push(member.id); - // }); - // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); - // } - // }) - // .catch(collected => { - // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); - // }); - // }); - // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { - // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); - // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); - // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { - // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); - // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); - // } - // } else { - // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); - // } - // }); - // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); - // } - }); - } - - } - }); -} - -/** - * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * @param {Guild} guild - * @param {Message} adminControls - * @param {GuildBasedChannel} adminConsole - * @param {Message} roleSelectionMsg - * @param {Message} requestTicketConsole - */ -function listenToAdminControls( - initBotInfo, - guild, - adminControls, - adminConsole, - roleSelectionMsg, - requestTicketConsole -) { - const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; - const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(initBotInfo.roleIDs.adminRole) }); - adminCollector.on('collect', async (adminInteraction) => { - if (adminInteraction.customId === 'addRole') { - const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); - const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); - let roleName; - roleNameCollector.on('collect', async collected => { - if (collected.content.toLowerCase() != 'cancel') { - roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); - const roleExists = guild.roles.cache.filter(role => { - role.name === `M-${roleName}`; - }).size != 0; - if (!roleExists) { - await guild.roles.create( - { - name: `M-${roleName}`, - color: mentorRoleColour, - } - ); - } - - const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); - const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); - emojiCollector.on('collect', (collected) => { - if (emojisMap.has(collected.emoji.name)) { - adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - } else { - emojiCollector.stop(); - firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave').set({ - extraEmojis: { - [collected.emoji.name]: roleName - } - }, { merge: true }); - emojisMap.set(collected.emoji.name, roleName); - adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { - setTimeout(() => msg.delete(), 5000); - }); - roleSelectionMsg.edit({ embeds: [new MessageEmbed(makeRoleSelectionEmbed())] }); - roleSelectionMsg.react(collected.emoji.name); - - const selectMenuOptions = makeSelectMenuRow().components[0].options; - const newSelectMenuRow = - new MessageActionRow().addComponents( - new MessageSelectMenu() - .setCustomId('ticketType') - .addOptions(selectMenuOptions) - ); - requestTicketConsole.edit({ components: [newSelectMenuRow] }); - askForEmoji.delete(); - } - }); - } - askForRoleName.delete(); - collected.delete(); - - }); - } - }); -} - +const { Command } = require('@sapphire/framework'); +const { MessageEmbed, Guild, Role } = require('discord.js'); +const { discordLog } = require('../../discord-services'); +const { + MessageSelectMenu, + Modal, + TextInputComponent, + Message, + MessageActionRow, + MessageButton, + GuildBasedChannel +} = require('discord.js'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); +const { Client } = require('discord.js'); + +//TODO: allow staff to add more roles +const htmlCssEmoji = '💻'; +const jsTsEmoji = '🕸️'; +const pythonEmoji = '🐍'; +const sqlEmoji = '🐬'; +const reactEmoji = '⚛️'; +const noSqlEmoji = '🔥'; +const javaEmoji = '☕'; +const cEmoji = '🎮'; +const cSharpEmoji = '💼'; +const reduxEmoji = '☁️'; +const figmaEmoji = '🎨'; +const unityEmoji = '🧊'; +const rustEmoji = '⚙️'; +const awsEmoji = '🙂'; +const ideationEmoji = '💡'; +const pitchingEmoji = '🎤'; + +let emojisMap = new Map([ + [htmlCssEmoji, 'HTML/CSS'], + [jsTsEmoji, 'JavaScript/TypeScript'], + [pythonEmoji, 'Python'], + [sqlEmoji, 'SQL'], + [reactEmoji, 'React'], + [noSqlEmoji, 'NoSQL'], + [javaEmoji, 'Java'], + [cEmoji, 'C/C++'], + [cSharpEmoji, 'C#'], + [reduxEmoji, 'Redux'], + [figmaEmoji, 'Figma'], + [unityEmoji, 'Unity'], + [rustEmoji, 'Rust'], + [awsEmoji, 'AWS'], + [ideationEmoji, 'Ideation'], + [pitchingEmoji, 'Pitching'] +]); + +/** + * The start mentor cave command creates a cave for mentors. To know what a cave is look at [cave]{@link Cave} class. + * @category Commands + * @subcategory Start-Commands + * @extends PermissionCommand + * @guildonly + */ +class StartMentorCave extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Start mentor cave' + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName('start-mentor-cave') + .setDescription(this.description) + .addIntegerOption(option => + option.setName('inactivity_time') + .setDescription('How long (minutes) before bot asks users to delete ticket channels') + .setRequired(true)) + .addIntegerOption(option => + option.setName('unanswered_ticket_time') + .setDescription('How long (minutes) shall a ticket go unaccepted before the bot sends a reminder to all mentors?') + .setRequired(true)) + .addRoleOption(option => + option.setName('request_ticket_role') + .setDescription('Tag the role that is allowed to request tickets') + .setRequired(true)) + .addChannelOption(option => + option.setName('mentor_role_selection_channel') + .setDescription('Tag the channel where mentors can select their specialties') + .setRequired(false)) + .addChannelOption(option => + option.setName('incoming_tickets_channel') + .setDescription('Tag the channel where mentor tickets will be sent') + .setRequired(false)) + .addChannelOption(option => + option.setName('request_ticket_channel') + .setDescription('Tag the channel where hackers can request tickets') + .setRequired(false)), + { idHints: '1051737344937566229' }); + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + * @returns + */ + async chatInputRun(interaction) { + try { + // helpful prompt vars + let userId = interaction.user.id; + let guild = interaction.guild; + this.initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + + if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } + + // const additionalMentorRole = interaction.options.getRole('additional_mentor_role'); + const publicRole = interaction.options.getRole('request_ticket_role'); + // const bufferTime = inactivePeriod / 2; + const reminderTime = interaction.options.getInteger('unanswered_ticket_time'); + + const mentorRoleSelectionChannelId = + this.initBotInfo.mentorTickets?.mentorRoleSelectionChannel; + const incomingTicketsChannelId = + this.initBotInfo.mentorTickets?.incomingTicketsChannel; + const requestTicketChannelId = + this.initBotInfo.mentorTickets?.requestTicketChannel; + + const mentorRoleSelectionChannel = interaction.options.getChannel( + 'mentor_role_selection_channel' + ) ?? (mentorRoleSelectionChannelId + ? await guild.channels.fetch(mentorRoleSelectionChannelId) + : null); + + const incomingTicketsChannel = interaction.options.getChannel( + 'incoming_tickets_channel' + ) ?? (incomingTicketsChannelId + ? await guild.channels.fetch(incomingTicketsChannelId) + : null); + + const requestTicketChannel = interaction.options.getChannel( + 'request_ticket_channel' + ) ?? (requestTicketChannelId + ? await guild.channels.fetch(requestTicketChannelId) + : null); + + if (!mentorRoleSelectionChannel || !incomingTicketsChannel || !requestTicketChannel) { + await interaction.reply({ content: 'Please enter all 3 channels!', ephemeral: true }); + return; + } + + if ( + mentorRoleSelectionChannel.id != mentorRoleSelectionChannelId || + incomingTicketsChannel.id != incomingTicketsChannelId || + requestTicketChannel.id != requestTicketChannelId + ) { + await interaction.deferReply(); + await firebaseUtil + .getFactotumSubCol() + .doc(guild.id) + .set({ + mentorTickets: { + mentorRoleSelectionChannel: mentorRoleSelectionChannel.id, + incomingTicketsChannel: incomingTicketsChannel.id, + requestTicketChannel: requestTicketChannel.id, + }, + }, + { merge: true }); + await interaction.editReply({ content: 'Mentor cave activated!', ephemeral: true }); + } else { + await interaction.reply({ content: 'Mentor cave activated!', ephemeral: true }); + } + + discordLog(guild, 'Mentor cave started by <@' + userId + '>'); + + // these are all old code that create channels rather than using existing channels + // let overwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.mentorRole, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'], + // }]; + + // if (additionalMentorRole) { + // overwrites.push({ + // id: additionalMentorRole, + // allow: ['VIEW_CHANNEL'] + // }); + // } + + // let mentorCategory = await channel.guild.channels.create('Mentors', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: overwrites + // } + // ); + + // let announcementsOverwrites = overwrites; + // announcementsOverwrites.push( + // { + // id: this.botGuild.roleIDs.mentorRole, + // deny: ['SEND_MESSAGES'], + // allow: ['VIEW_CHANNEL'] + // }); + + // await channel.guild.channels.create('mentors-announcements', + // { + // type: 'GUILD_TEXT', + // parent: mentorCategory, + // permissionOverwrites: announcementsOverwrites + // } + // ); + + // const mentorRoleSelectionChannel = await channel.guild.channels.create('mentors-role-selection', + // { + // type: 'GUILD_TEXT', + // topic: 'Sign yourself up for specific roles! New roles will be added as requested, only add yourself to one if you feel comfortable responding to questions about the topic.', + // parent: mentorCategory + // } + // ); + + const mentorRoleColour = guild.roles.cache.find(role => role.id === this.initBotInfo.roleIDs.mentorRole).hexColor; + for (let value of emojisMap.values()) { + const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + if (!findRole) { + await guild.roles.create( + { + name: `M-${value}`, + color: mentorRoleColour, + } + ); + } + } + + const roleSelection = makeRoleSelectionEmbed(); + + /** @type {Message} */ + const roleSelectionMsg = await mentorRoleSelectionChannel.send({ embeds: [roleSelection] }); + for (const key of emojisMap.keys()) { + roleSelectionMsg.react(key); + } + + listenToRoleReactions(guild, roleSelectionMsg); + + const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); + + mentorCaveDoc.set({ + roleReactions: { + channelId: roleSelectionMsg.channelId, + messageId: roleSelectionMsg.id + } + }, {merge: true}); + + // channel.guild.channels.create('mentors-general', + // { + // type: 'GUILD_TEXT', + // topic: 'Private chat between all mentors and organizers', + // parent: mentorCategory + // } + // ); + + // const incomingTicketChannel = await channel.guild.channels.create('incoming-tickets', + // { + // type: 'GUILD_TEXT', + // topic: 'Tickets from hackers will come in here!', + // parent: mentorCategory + // } + // ); + + // const mentorHelpCategory = await channel.guild.channels.create('Mentor-help', + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: [ + // { + // id: this.botGuild.verification.guestRoleID, + // deny: ['VIEW_CHANNEL'], + // }, + // ] + // } + // ); + + // channel.guild.channels.create('quick-questions', + // { + // type: 'GUILD_TEXT', + // topic: 'ask questions for mentors here!', + // parent: mentorHelpCategory + // } + // ); + + // const requestTicketChannel = await channel.guild.channels.create('request-ticket', + // { + // type: 'GUILD_TEXT', + // topic: 'request 1-on-1 help from mentors here!', + // parent: mentorHelpCategory, + // permissionOverwrites: [ + // { + // id: publicRole, + // allow: ['VIEW_CHANNEL'], + // deny: ['SEND_MESSAGES'] + // }, + // { + // id: this.botGuild.roleIDs.staffRole, + // allow: ['VIEW_CHANNEL'] + // }, + // { + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'] + // } + // ] + // } + // ); + + const requestTicketEmbed = new MessageEmbed() + .setTitle('Need 1:1 mentor help?') + .setDescription('Select a technology you need help with and follow the instructions!'); + + const selectMenuRow = makeSelectMenuRow(); + + /** @type {Message} */ + const requestTicketConsole = await requestTicketChannel.send({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + + listenToRequestConsole( + this.initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel + ); + + mentorCaveDoc.set({ + requestTicketConsole: { + channelId: requestTicketConsole.channelId, + messageId: requestTicketConsole.id, + publicRoleId: publicRole.id, + reminderTime + } + }, {merge: true}); + + const adminEmbed = new MessageEmbed() + .setTitle('Mentor Cave Console') + .setColor(this.initBotInfo.embedColor); + + const adminRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('addRole') + .setLabel('Add Mentor Role') + .setStyle('PRIMARY'), + ); + + + const adminConsole = await guild.channels.fetch(this.initBotInfo.channelIDs.adminConsole); + + /** @type {Message} */ + const adminControls = await adminConsole.send({ embeds: [adminEmbed], components: [adminRow] }); + + listenToAdminControls( + this.initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + requestTicketConsole + ); + + mentorCaveDoc.set({ + adminControls: { + channelId: adminControls.channelId, + messageId: adminControls.id, + } + }, {merge: true}); + + } catch (error) { + // winston.loggers.get(interaction.guild.id).warning(`An error was found but it was handled by not setting up the mentor cave. Error: ${error}`, { event: 'StartMentorCave Command' }); + } + } + + async deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, embed) { + await ticketMsg.edit({ embeds: [embed] }); + ticketText.delete(); + ticketVoice.delete(); + ticketCategory.delete(); + } + + async startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime) { + // message collector that stops when there are no messages for inactivePeriod minutes + if (ticketText.parentId && ticketVoice.parentId) { + const activityListener = ticketText.createMessageCollector({ filter: m => !m.author.bot, idle: inactivePeriod * 60 * 1000 }); + activityListener.on('end', async collected => { + if (!ticketText.parentId || !ticketVoice.parentId) return; + if (collected.size === 0 && ticketVoice.members.size === 0 && ticketMsg.embeds[0].color != '#90EE90') { + const remainingMembers = await ticketCategory.members.filter(member => !member.roles.cache.has(this.initBotInfo.roleIDs.adminRole) && !member.user.bot).map(member => member.id); + const msgText = '<@' + remainingMembers.join('><@') + '> Hello! I detected some inactivity in this channel. If you are done and would like to leave this ticket, please go to the pinned message and click the "Leave" button. If you would like to keep this channel a little longer, please click the button below.\n**If no action is taken in the next ' + bufferTime + ' minutes, the channels for this ticket will be deleted automatically.**'; + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('keepChannels') + .setLabel('Keep Channels') + .setStyle('PRIMARY'), + ); + const warning = await ticketText.send({ content: msgText, components: [row] }); + + warning.awaitMessageComponent({ filter: i => !i.user.bot, time: bufferTime * 60 * 1000 }) + .then(interaction => { + interaction.reply('You have indicated that you need more time. I\'ll check in with you later!'); + const disabledButtonRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('keepChannels') + .setLabel('Keep Channels') + .setDisabled(true) + .setStyle('PRIMARY'), + ); + warning.edit({ components: [disabledButtonRow] }); + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + }) + .catch(() => { + if (!ticketText.parentId || !ticketVoice.parentId || ticketMsg.embeds[0].color == '#90EE90') return; + if (ticketVoice.members.size === 0) { + this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Deleted due to inactivity' }])); + } else { + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + } + }); + } else { + this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + } + }); + } + } + + /** + * Checks Firebase for existing stored listeners - + * restores the listeners if they exist, otherwise does nothing + * @param {Guild} guild + */ + async tryRestoreReactionListeners(guild) { + const savedMessagesSubCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const mentorCaveDoc = await savedMessagesSubCol.doc('mentor-cave').get(); + if (!mentorCaveDoc.exists) return 'Saved messages doc for mentor cave does not exist'; + + const mentorCaveData = mentorCaveDoc.data(); + if (mentorCaveData.extraEmojis) { + for (const [emoji, name] of Object.entries(mentorCaveData.extraEmojis)) { + emojisMap.set(emoji, name); + } + } + + // Get role reaction listener saved details + const { + messageId: reactionMessageId, + channelId: reactionChannelId + } = mentorCaveData.roleReactions; + const reactionChannel = await guild.channels.fetch(reactionChannelId); + if (!reactionChannel) return 'Saved role reactions message info not found'; + + // Get request ticket console saved details + const { + channelId: consoleChannelId, + messageId: consoleMessageId, + publicRoleId, + reminderTime + } = mentorCaveData.requestTicketConsole; + const consoleChannel = await guild.channels.fetch(consoleChannelId); + if (!consoleChannel) return 'Saved request ticket console info not found'; + + // Get admin controls saved details + const { + channelId: controlsChannelId, + messageId: controlsMessageId + } = mentorCaveData.adminControls; + const adminControlsChannel = await guild.channels.fetch(controlsChannelId); + if (!adminControlsChannel) return 'Saved admin controls info not found'; + + + try { + // Restore role reactions listener + /** @type {Message} */ + const roleSelectionMsg = await reactionChannel.messages.fetch(reactionMessageId); + await listenToRoleReactions(guild, roleSelectionMsg); + + // Restore request console listener + /** @type {Message} */ + const requestTicketConsole = await consoleChannel.messages.fetch(consoleMessageId); + const publicRole = await guild.roles.fetch(publicRoleId); + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + const incomingTicketsChannelId = initBotInfo.mentorTickets?.incomingTicketsChannel; + const incomingTicketsChannel = await guild.channels.fetch(incomingTicketsChannelId); + if (incomingTicketsChannelId && incomingTicketsChannel) { + listenToRequestConsole( + initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel + ); + } + + // Restore admin controls listener + const adminControls = await adminControlsChannel.messages.fetch(controlsMessageId); + const adminConsole = await guild.channels.fetch(initBotInfo.channelIDs.adminConsole); + listenToAdminControls( + initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + requestTicketConsole + ); + } catch (e) { + // message doesn't exist anymore + return 'Error: ' + e; + } + } +} + +function makeRoleSelectionEmbed() { + const fields = []; + for (const [key, value] of emojisMap) { + fields.push({ name: key + ' --> ' + value, value: '\u200b' }); + } + + const roleSelection = new MessageEmbed() + .setTitle('Choose what you would like to help hackers with! You can un-react to deselect a role.') + .setDescription('Note: You will be notified every time a hacker creates a ticket in one of your selected categories!') + .addFields(fields); + + return roleSelection; +} + +function makeSelectMenuRow() { + const options = []; + for (const value of emojisMap.values()) { + options.push({ label: value, value: value }); + } + options.push({ label: 'None of the above', value: 'None of the above' }); + + const selectMenuRow = new MessageActionRow() + .addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(options) + ); + return selectMenuRow; +} + +/** + * + * @param {Guild} guild + * @param {Message} roleSelectionMsg + */ +async function listenToRoleReactions(guild, roleSelectionMsg) { + const collector = roleSelectionMsg.createReactionCollector({ filter: (reaction, user) => !user.bot, dispose: true }); + collector.on('collect', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const value = emojisMap.get(reaction.emoji.name); + const findRole = guild.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + await guild.members.cache.get(user.id).roles.add(findRole); + } + }); + + collector.on('remove', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const member = guild.members.cache.get(user.id); + const value = emojisMap.get(reaction.emoji.name); + const findRole = member.roles.cache.find(role => role.name.toLowerCase() === `M-${value}`.toLowerCase()); + if (findRole) await guild.members.cache.get(user.id).roles.remove(findRole); + } + }); +} + +/** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {Guild} guild + * @param {Message} requestTicketConsole + * @param {MessageEmbed} requestTicketEmbed + * @param {MessageActionRow} selectMenuRow + * @param {Role} publicRole + * @param {number} reminderTime + * @param {GuildBasedChannel} incomingTicketsChannel + */ +function listenToRequestConsole( + initBotInfo, + guild, + requestTicketConsole, + publicRole, + reminderTime, + incomingTicketsChannel +) { + const selectMenuFilter = i => !i.user.bot; + const selectMenuCollector = requestTicketConsole.createMessageComponentCollector({filter: selectMenuFilter}); + selectMenuCollector.on('collect', async (i) => { + if (i.customId === 'ticketType') { + // requestTicketConsole.edit({ embeds: [requestTicketEmbed], components: [selectMenuRow] }); + if (!guild.members.cache.get(i.user.id).roles.cache.has(publicRole.id)) { + await i.reply({ content: 'You do not have permissions to request tickets!', ephemeral: true }); + return; + } + const modal = new Modal() + .setCustomId('ticketSubmitModal') + .setTitle(i.values[0] === 'None of the above' ? 'Request a general mentor ticket' : 'Request a ticket for ' + i.values[0]) + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('ticketDescription') + .setLabel('Brief description of your problem') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setPlaceholder('Describe your problem here') + .setRequired(true), + ), + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('location') + .setLabel('Where would you like to meet your mentor?') + .setPlaceholder('Help your mentor find you!') + .setMaxLength(300) + .setStyle('PARAGRAPH') + .setRequired(true), + ) + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(() => { + }); + + if (submitted) { + const role = i.values[0] === 'None of the above' ? initBotInfo.roleIDs.mentorRole : guild.roles.cache.find(role => role.name.toLowerCase() === `M-${i.values[0]}`.toLowerCase()).id; + const description = submitted.fields.getTextInputValue('ticketDescription'); + const location = submitted.fields.getTextInputValue('location'); + // const helpFormat = submitted.fields.getTextInputValue('helpFormat'); + const mentorCaveDoc = firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave'); + const ticketNumber = (await mentorCaveDoc.get()).data()?.ticketCount ?? 1; + await mentorCaveDoc.set({ ticketCount: ticketNumber + 1 }, { merge: true }); + const newTicketEmbed = new MessageEmbed() + .setTitle('Ticket #' + ticketNumber) + .setColor('#d3d3d3') + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + }, + // { + // name: 'OK with being helped online?', + // value: helpFormat + // } + ]); + const ticketAcceptanceRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('acceptIrl') + .setLabel('Accept ticket (in-person)') + .setStyle('PRIMARY'), + ); + // .addComponents( + // new MessageButton() + // .setCustomId('acceptOnline') + // .setLabel('Accept ticket (online) - Only use if hackers are OK with it!') + // .setStyle('PRIMARY'), + // ); + + const ticketMsg = await incomingTicketsChannel.send({ content: '<@&' + role + '>, requested by <@' + submitted.user.id + '>', embeds: [newTicketEmbed], components: [ticketAcceptanceRow] }); + submitted.reply({ content: 'Your ticket has been submitted!', ephemeral: true }); + const ticketReminder = setTimeout(() => { + ticketMsg.reply('<@&' + role + '> ticket ' + ticketNumber + ' still needs help!'); + }, reminderTime * 60000); + + const confirmationEmbed = new MessageEmbed() + .setTitle('Your ticket is number ' + ticketNumber) + .addFields([ + { + name: 'Problem description', + value: description + }, + { + name: 'Where to meet', + value: location + } + ]); + const deleteTicketRow = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('deleteTicket') + .setLabel('Delete ticket') + .setStyle('DANGER'), + ); + const ticketReceipt = await submitted.user.send({ embeds: [confirmationEmbed], content: 'You will be notified when a mentor accepts your ticket!', components: [deleteTicketRow] }); + const deleteTicketCollector = ticketReceipt.createMessageComponentCollector({ filter: i => !i.user.bot, max: 1 }); + deleteTicketCollector.on('collect', async deleteInteraction => { + await ticketMsg.edit({ embeds: [ticketMsg.embeds[0].setColor('#FFCCCB').addFields([{ name: 'Ticket closed', value: 'Deleted by hacker' }])], components: [] }); + clearTimeout(ticketReminder); + deleteInteraction.reply('Ticket deleted!'); + ticketReceipt.edit({ components: [] }); + }); + + const ticketAcceptFilter = i => !i.user.bot && i.isButton(); + const ticketAcceptanceCollector = ticketMsg.createMessageComponentCollector({ filter: ticketAcceptFilter }); + ticketAcceptanceCollector.on('collect', async acceptInteraction => { + const inProgressTicketEmbed = ticketMsg.embeds[0].setColor('#0096FF').addFields([{ name: 'Helped by:', value: '<@' + acceptInteraction.user.id + '>' }]); + if (acceptInteraction.customId === 'acceptIrl' || acceptInteraction.customId === 'acceptOnline') { + await ticketReceipt.edit({ components: [] }); + clearTimeout(ticketReminder); + ticketMsg.edit({ embeds: [inProgressTicketEmbed], components: [] }); + } + if (acceptInteraction.customId === 'acceptIrl') { + // TODO: mark as complete? + submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! They will be making their way to you shortly.'); + acceptInteraction.reply({ content: 'Thanks for accepting their ticket! Please head to their stated location. If you need to contact them, you can click on their username above to DM them!', ephemeral: true }); + } + // if (acceptInteraction.customId === 'acceptOnline') { + // submitted.user.send('Your ticket number ' + ticketNumber + ' has been accepted by a mentor! You should have gotten a ping from a new private channel. You can talk to your mentor there!'); + // acceptInteraction.reply({ content: 'Thanks for accepting their ticket! You should get a ping from a private channel for this ticket! You can help them there.', ephemeral: true }); + // let ticketChannelOverwrites = + // [{ + // id: this.botGuild.roleIDs.everyoneRole, + // deny: ['VIEW_CHANNEL'], + // }, + // { + // id: acceptInteraction.user.id, + // allow: ['VIEW_CHANNEL'], + // }, + // { + // id: submitted.user.id, + // allow: ['VIEW_CHANNEL'], + // }]; + + // let ticketCategory = await channel.guild.channels.create('Ticket-#' + ticketNumber, + // { + // type: 'GUILD_CATEGORY', + // permissionOverwrites: ticketChannelOverwrites + // } + // ); + + // const ticketText = await channel.guild.channels.create('ticket-' + ticketNumber, + // { + // type: 'GUILD_TEXT', + // parent: ticketCategory + // } + // ); + + // const ticketVoice = await channel.guild.channels.create('ticket-' + ticketNumber + '-voice', + // { + // type: 'GUILD_VOICE', + // parent: ticketCategory + // } + // ); + + // const ticketChannelEmbed = new MessageEmbed() + // .setColor(this.botGuild.colors.embedColor) + // .setTitle('Ticket description') + // .setDescription(submitted.fields.getTextInputValue('ticketDescription')); + + // const ticketChannelButtons = new MessageActionRow() + // .addComponents( + // new MessageButton() + // .setCustomId('addMembers') + // .setLabel('Add Members to Channels') + // .setStyle('PRIMARY'), + // ) + // .addComponents( + // new MessageButton() + // .setCustomId('leaveTicket') + // .setLabel('Leave') + // .setStyle('DANGER'), + // ); + // const ticketChannelInfoMsg = await ticketText.send({ content: `<@${acceptInteraction.user.id}><@${submitted.user.id}> These are your very own private channels! It is only visible to the admins of the server and any other users (i.e. teammates) you add to this channel with the button labeled "Add Members to Channels" below ⬇️. Feel free to discuss anything in this channel or the attached voice channel. **Please click the "Leave" button below when you are done to leave these channels**\n\n**Note: these channels may be deleted if they appear to be inactive for a significant period of time, even if not everyone has left**`, embeds: [ticketChannelEmbed], components: [ticketChannelButtons] }); + // ticketChannelInfoMsg.pin(); + + // const ticketChannelCollector = ticketChannelInfoMsg.createMessageComponentCollector({ filter: notBotFilter }); + // ticketChannelCollector.on('collect', async ticketInteraction => { + // if (ticketInteraction.customId === 'addMembers') { + // ticketInteraction.reply({ content: 'Tag the users you would like to add to the channel! (You can mention them by typing @ and then paste in their username with the tag)', ephemeral: true, fetchReply: true }) + // .then(() => { + // const awaitMessageFilter = i => i.user.id === ticketInteraction.user.id; + // ticketInteraction.channel.awaitMessages({ awaitMessageFilter, max: 1, time: 60000, errors: ['time'] }) + // .then(async collected => { + // if (collected.first().mentions.members.size === 0) { + // await ticketInteraction.followUp({ content: 'You have not mentioned any users! Click the button again to try again.' }); + // } else { + // var newMembersArray = []; + // collected.first().mentions.members.forEach(member => { + // ticketCategory.permissionOverwrites.edit(member.id, { VIEW_CHANNEL: true }); + // newMembersArray.push(member.id); + // }); + // ticketInteraction.channel.send('<@' + newMembersArray.join('><@') + '> Welcome to the channel! You have been invited to join the discussion for this ticket. Check the pinned message for more details.'); + // } + // }) + // .catch(collected => { + // ticketInteraction.followUp({ content: 'Timed out. Click the button again to try again.', ephemeral: true }); + // }); + // }); + // } else if (ticketInteraction.customId === 'leaveTicket' && guild.members.cache.get(ticketInteraction.user.id).roles.cache.has(this.botGuild.roleIDs.adminRole) ) { + // await ticketCategory.permissionOverwrites.edit(ticketInteraction.user.id, { VIEW_CHANNEL: false }); + // ticketInteraction.reply({ content: 'Successfully left the channel!', ephemeral: true }); + // if (ticketCategory.members.filter(member => !member.roles.cache.has(this.botGuild.roleIDs.adminRole) && !member.user.bot).size === 0) { + // const leftTicketEmbed = ticketMsg.embeds[0].setColor('#90EE90').addFields([{ name: 'Ticket closed', value: 'Everyone has left the ticket' }]); + // await this.deleteTicketChannels(ticketText, ticketVoice, ticketCategory, ticketMsg, leftTicketEmbed); + // } + // } else { + // ticketInteraction.reply({ content: 'You are an admin, you cannot leave this channel!', ephemeral: true }); + // } + // }); + // this.startChannelActivityListener(ticketText, ticketVoice, ticketCategory, ticketMsg, inactivePeriod, bufferTime); + // } + }); + } + + } + }); +} + +/** + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {Guild} guild + * @param {Message} adminControls + * @param {GuildBasedChannel} adminConsole + * @param {Message} roleSelectionMsg + * @param {Message} requestTicketConsole + */ +function listenToAdminControls( + initBotInfo, + guild, + adminControls, + adminConsole, + roleSelectionMsg, + requestTicketConsole +) { + const mentorRoleColour = guild.roles.cache.find(role => role.id === initBotInfo.roleIDs.mentorRole).hexColor; + const adminCollector = adminControls.createMessageComponentCollector({ filter: i => !i.user.bot && i.member.roles.cache.has(initBotInfo.roleIDs.adminRole) }); + adminCollector.on('collect', async (adminInteraction) => { + if (adminInteraction.customId === 'addRole') { + const askForRoleName = await adminInteraction.reply({ content: `<@${adminInteraction.user.id}> name of role to add? Type "cancel" to cancel this operation.`, fetchReply: true }); + const roleNameCollector = adminConsole.createMessageCollector({ filter: m => m.author.id === adminInteraction.user.id, max: 1 }); + let roleName; + roleNameCollector.on('collect', async collected => { + if (collected.content.toLowerCase() != 'cancel') { + roleName = collected.content.replace(/\s+/g, '-').toLowerCase(); + const roleExists = guild.roles.cache.filter(role => { + role.name === `M-${roleName}`; + }).size != 0; + if (!roleExists) { + await guild.roles.create( + { + name: `M-${roleName}`, + color: mentorRoleColour, + } + ); + } + + const askForEmoji = await adminConsole.send(`<@${adminInteraction.user.id}> React to this message with the emoji for the role!`); + const emojiCollector = askForEmoji.createReactionCollector({ filter: (reaction, user) => user.id === adminInteraction.user.id }); + emojiCollector.on('collect', (collected) => { + if (emojisMap.has(collected.emoji.name)) { + adminConsole.send(`<@${adminInteraction.user.id}> Emoji is already used in another role. Please react again.`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + } else { + emojiCollector.stop(); + firebaseUtil.getSavedMessagesSubCol(guild.id).doc('mentor-cave').set({ + extraEmojis: { + [collected.emoji.name]: roleName + } + }, { merge: true }); + emojisMap.set(collected.emoji.name, roleName); + adminConsole.send(`<@${adminInteraction.user.id}> Role added!`).then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + roleSelectionMsg.edit({ embeds: [new MessageEmbed(makeRoleSelectionEmbed())] }); + roleSelectionMsg.react(collected.emoji.name); + + const selectMenuOptions = makeSelectMenuRow().components[0].options; + const newSelectMenuRow = + new MessageActionRow().addComponents( + new MessageSelectMenu() + .setCustomId('ticketType') + .addOptions(selectMenuOptions) + ); + requestTicketConsole.edit({ components: [newSelectMenuRow] }); + askForEmoji.delete(); + } + }); + } + askForRoleName.delete(); + collected.delete(); + + }); + } + }); +} + module.exports = StartMentorCave; \ No newline at end of file diff --git a/commands/a_utility/change-pre-fix.js b/commands/a_utility/change-pre-fix.js index 79ea150a..be1d6eee 100644 --- a/commands/a_utility/change-pre-fix.js +++ b/commands/a_utility/change-pre-fix.js @@ -1,3 +1,5 @@ +/** DEPRECATED */ + const { Message } = require('discord.js'); const PermissionCommand = require('../../classes/permission-command'); const { StringPrompt } = require('advanced-discord.js-prompts'); diff --git a/commands/a_utility/pronouns.js b/commands/a_utility/pronouns.js index ade7c27a..8a9423cf 100644 --- a/commands/a_utility/pronouns.js +++ b/commands/a_utility/pronouns.js @@ -22,15 +22,18 @@ class Pronouns extends Command { }); } + /** + * + * @param {Command.Registry} registry + */ registerApplicationCommands(registry) { registry.registerChatInputCommand((builder) => builder .setName(this.name) - .setDescription(this.description) - ), + .setDescription(this.description), { idHints: '1051737347441569813' - }; + }); } /** @@ -69,7 +72,7 @@ class Pronouns extends Command { listenToReactions(guild, messageEmbed); - const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(); + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(guild.id); await savedMessagesCol.doc('pronouns').set({ messageId: messageEmbed.id, channelId: messageEmbed.channel.id, @@ -123,7 +126,7 @@ async function getPronounRoles(guild) { * @param {string} name * @param {string} color */ - async function createRole(guild, name, color) { +async function createRole(guild, name, color) { try { const role = await guild.roles.create({ name: name, diff --git a/commands/a_utility/role-selector.js b/commands/a_utility/role-selector.js index b1951374..421b6236 100644 --- a/commands/a_utility/role-selector.js +++ b/commands/a_utility/role-selector.js @@ -1,10 +1,11 @@ // Discord.js commando requirements const PermissionCommand = require('../../classes/permission-command'); -const { checkForRole, addRoleToMember, removeRolToMember } = require('../../discord-services'); -const { Message, Role, Collection } = require('discord.js'); -const { SpecialPrompt, RolePrompt, StringPrompt } = require('advanced-discord.js-prompts'); -const Console = require('../../classes/UI/Console/console'); -const Feature = require('../../classes/UI/Console/feature'); +const { Command } = require('@sapphire/framework'); +const { Message, MessageEmbed, MessageActionRow, Modal, TextInputComponent, MessageSelectMenu, Guild } = require('discord.js'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); + +const newTransferEmoji = '🆕'; +const emojisMap = new Map(); /** * Make a message embed (console) available on the channel for users to react and un-react for roles. Staff can dynamically add @@ -14,110 +15,285 @@ const Feature = require('../../classes/UI/Console/feature'); * @extends PermissionCommand */ class RoleSelector extends PermissionCommand { - constructor(client) { - super(client, { + constructor(context, options) { + super(context, { + ...options, name: 'role-selector', - group: 'a_utility', - memberName: 'transfer role', description: 'Will let users transfer roles. Useful for sponsor reps that are also mentors!', - guildOnly: true, - }, - { + }, { role: PermissionCommand.FLAGS.STAFF_ROLE, roleMessage: 'Hey there, the command !role-selector is only available to staff!', }); } + /** + * + * @param {Command.Registry} registry + */ + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description), + { idHints: '1262581324657725521' }); + } /** - * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo - * @param {Message} message - the command message + * + * @param {Command.ChatInputInteraction} interaction */ - async runCommand(initBotInfo, message) { - - // the emoji for staff to add new transfers - let newTransferEmoji = '🆕'; - - /** - * @typedef Transfer - * @property {String} name - the transfer name - * @property {String} description - the transfer description - * @property {Role} role - the transfer role - */ - - /** - * The transfers on this role transfer card. - * @type {Collection} - */ - let transfers = new Collection(); - - let addTransferFeature = Feature.create({ - name: 'Add a Role!', - emoji: newTransferEmoji, - description: 'Add a new emoji to this transfer console! Only staff can select this option!', - callback: async (user, reaction, stopInteracting, console) => { - let channel = console.channel; - // staff add new transfer - if (checkForRole(message.guild.member(user), initBotInfo.roleIDs.staffRole)) { - - try { - var role = await RolePrompt.single({ - prompt: 'What role do you want to add?', - channel: channel, - userId: user.id - }); + async chatInputRun(interaction) { + const guild = interaction.guild; + const userId = interaction.user.id; + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); - var title = await StringPrompt.single({ - prompt: 'What is the transfer title?', - channel: channel, - userId: user.id - }); + if (!guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(initBotInfo.roleIDs.adminRole)) { + await interaction.reply({ content: 'You do not have permissions to run this command!', ephemeral: true }); + return; + } - var description = await StringPrompt.single({ - prompt: 'What is the transfer description?', - channel: channel, - userId: user.id - }); + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const roleSelectorDoc = await savedMessagesCol.doc('role-selector').get(); + const roleSelectorData = roleSelectorDoc.data(); + if (roleSelectorData?.emojis) { + for (const [emoji, name] of Object.entries( + roleSelectorData.emojis + )) { + emojisMap.set(emoji, name); + } + } - } catch (error) { - stopInteracting(user); - return; - } - - let emoji = await SpecialPrompt.singleEmoji({prompt: 'What emoji to you want to use for this transfer?', channel: message.channel, userId: message.author.id}); - - // new feature will add the emoji transfer to the embed - let newFeature = Feature.create({ - name: title, - description: description, - emoji: emoji, - callback: (user, reaction, stopInteracting, console) => { - addRoleToMember(console.channel.guild.member(user), role); - stopInteracting(user); - console.channel.send('<@' + user.id + '> You have been given the role: ' + role.name).then(msg => msg.delete({timeout: 4000})); - }, - removeCallback: (user, reaction, stopInteracting, console) => { - removeRolToMember(console.channel.guild.member(user), role); - stopInteracting(user); - console.channel.send('<@' + user.id + '> You have lost the role: ' + role.name).then(msg => msg.delete({timeout: 4000})); - } - }); - console.addFeature(newFeature); + let embed = await makeRoleSelectionEmbed(); + + let messageEmbed = await interaction.channel.send({embeds: [embed] }); + messageEmbed.react(newTransferEmoji); + + for (const key of emojisMap.keys()) { + messageEmbed.react(key); + } + + listenToRoleSelectorReaction(initBotInfo, guild, messageEmbed); + + savedMessagesCol.doc('role-selector').set({ + messageId: messageEmbed.id, + channelId: messageEmbed.channelId + }, { merge: true }); + + interaction.reply({content: 'Role selector created!', ephemeral: true}); + } + + /** + * Checks Firebase for an existing stored reaction listener - + * restores the listeners for the reaction if it exists, otherwise does nothing + * @param {Guild} guild + */ + async tryRestoreReactionListeners(guild) { + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const roleSelectorDoc = await savedMessagesCol.doc('role-selector').get(); + if (roleSelectorDoc.exists) { + const { messageId, channelId, emojis } = roleSelectorDoc.data(); + + if (emojis) { + for (const [emoji, name] of Object.entries(emojis)) { + emojisMap.set(emoji, name); } - - reaction.users.remove(user); - stopInteracting(user); - }, - }); + } + const channel = await this.container.client.channels.fetch(channelId); + if (channel) { + try { + /** @type {Message} */ + const message = await channel.messages.fetch(messageId); + const initBotInfo = await firebaseUtil.getInitBotInfo(guild.id); + listenToRoleSelectorReaction(initBotInfo, guild, message); + } catch (e) { + // message doesn't exist anymore + return e; + } + } else { + return 'Saved message channel does not exist'; + } + } else { + return 'No existing saved message for role-selector command'; + } + } +} - let console = new Console({ - title: 'Role Selector!', - description: 'React to the specified emoji to get the role, un-react to remove the role.', - channel: message.channel, - }); - console.addFeature(addTransferFeature); +async function makeRoleSelectionEmbed() { + const roleSelectionEmbed = new MessageEmbed() + .setTitle('Role Selector!') + .setDescription( + 'React to the specified emoji to get the role, un-react to remove the role.' + ); - await console.sendConsole(); + if (emojisMap.size > 0) { + roleSelectionEmbed.setFields( + Array.from(emojisMap).map(([k, v]) => ({name: k, value: `${v.title} - ${v.description}`})) + ); } + + return roleSelectionEmbed; } + +/** + * + * @param {FirebaseFirestore.DocumentData | null | undefined} initBotInfo + * @param {Guild} guild + * @param {Message} messageEmbed + */ +async function listenToRoleSelectorReaction( + initBotInfo, + guild, + messageEmbed +) { + const reactionCollector = messageEmbed.createReactionCollector({ + dispose: true, + }); + + reactionCollector.on('collect', async (reaction, user) => { + if (user.bot) return; + if ( + !guild.members.cache + .get(user.id) + .roles.cache.has(initBotInfo.roleIDs.staffRole) && + !guild.members.cache + .get(user.id) + .roles.cache.has(initBotInfo.roleIDs.adminRole) + ) { + await user.send({ + content: 'You do not have permissions to run this command!', + ephemeral: true, + }); + return; + } + if (reaction.emoji.name === newTransferEmoji) { + const allRoles = await guild.roles.fetch(); + const roleSelectRow = new MessageActionRow().setComponents( + new MessageSelectMenu() + .setOptions( + ...allRoles + .map((r) => ({ + label: r.name, + value: r.id, + })) + .slice(-25) + ) + .setCustomId('transfer_role') + ); + const roleSelectEmbed = new MessageEmbed() + .setTitle('New role selector') + .setDescription( + 'Select the role that you want to add to the role selector embed:' + ); + + const roleSelectMenu = await user.send({ + components: [roleSelectRow], + embeds: [roleSelectEmbed], + }); + + const roleSelectListener = roleSelectMenu.createMessageComponentCollector(); + roleSelectListener.on('collect', async (i) => { + const role = await guild.roles.fetch(i.values[0]); + if (i.customId === 'transfer_role') { + if ( + !guild.members.cache + .get(i.user.id) + .roles.cache.has(initBotInfo.roleIDs.staffRole) && + !guild.members.cache + .get(i.user.id) + .roles.cache.has(initBotInfo.roleIDs.adminRole) + ) { + await i.reply({ + content: 'You do not have permissions to run this command!', + ephemeral: true, + }); + return; + } + const newReactionModal = new Modal() + .setTitle('New role reaction for ' + role.name) + .setCustomId('new_role') + .setComponents( + new MessageActionRow().setComponents( + new TextInputComponent() + .setLabel('What is the transfer title?') + .setStyle('SHORT') + .setRequired(true) + .setCustomId('transfer_title') + ), + new MessageActionRow().setComponents( + new TextInputComponent() + .setLabel('What is the transfer description?') + .setStyle('SHORT') + .setRequired(true) + .setCustomId('transfer_desc') + ) + ); + await i.showModal(newReactionModal); + + const submitted = await i + .awaitModalSubmit({ + time: 300000, + filter: (j) => j.user.id === i.user.id, + }) + .catch(() => {}); + + if (submitted) { + await roleSelectMenu.delete(); + const title = submitted.fields.getTextInputValue('transfer_title'); + const description = submitted.fields.getTextInputValue('transfer_desc'); + + await submitted.reply('New role selector details successfully submitted!'); + + const askForEmoji = await user.send('React to this message with the emoji for the role!'); + const emojiCollector = askForEmoji.createReactionCollector(); + emojiCollector.on('collect', async (reaction, user) => { + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const roleSelectorData = (await savedMessagesCol.doc('role-selector').get()).data(); + if (roleSelectorData?.emojis && reaction.emoji.name in roleSelectorData.emojis) { + user.send('Emoji is already used in another role. Please react again.').then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + } else { + emojiCollector.stop(); + firebaseUtil.getSavedMessagesSubCol(guild.id).doc('role-selector').set({ + emojis: { + [reaction.emoji.name]: { + title, + description, + roleId: role.id + } + } + }, { merge: true }); + emojisMap.set(reaction.emoji.name, { title, description, roleId: role.id }); + user.send('Role added!').then(msg => { + setTimeout(() => msg.delete(), 5000); + }); + messageEmbed.edit({ embeds: [new MessageEmbed(await makeRoleSelectionEmbed())] }); + messageEmbed.react(reaction.emoji.name); + + askForEmoji.delete(); + } + }); + } + } + }); + } else { + if (emojisMap.has(reaction.emoji.name)) { + const value = emojisMap.get(reaction.emoji.name); + const findRole = await guild.roles.cache.get(value.roleId); + await guild.members.cache.get(user.id).roles.add(findRole); + } + } + }); + reactionCollector.on('remove', async (reaction, user) => { + if (emojisMap.has(reaction.emoji.name)) { + const member = guild.members.cache.get(user.id); + const value = emojisMap.get(reaction.emoji.name); + const findRole = await member.roles.cache.get(value.roleId); + if (findRole) + await guild.members.cache.get(user.id).roles.remove(findRole); + } + }); +} + module.exports = RoleSelector; + From 95d96f5cdf5672cd7d641daeea325c947af06be9 Mon Sep 17 00:00:00 2001 From: byronwang93 Date: Fri, 18 Oct 2024 00:02:32 -0700 Subject: [PATCH 65/67] add members + verify for hackers + other roles --- commands/verification/add-members.js | 26 ++++-- db/firebase/firebaseUtil.js | 126 +++++++++++++++++++-------- 2 files changed, 109 insertions(+), 43 deletions(-) diff --git a/commands/verification/add-members.js b/commands/verification/add-members.js index 7a82a973..a47fad1f 100644 --- a/commands/verification/add-members.js +++ b/commands/verification/add-members.js @@ -33,16 +33,31 @@ class AddMembers extends Command { const guild = interaction.guild; const participantsType = interaction.options.getString('participantstype'); const overwrite = interaction.options.getBoolean('overwrite') ?? false; + + const userRoles = guild.members.cache.get(userId).roles.cache; + const staffRoleID = this.initBotInfo.roleIDs.staffRole; + const adminRoleID = this.initBotInfo.roleIDs.adminRole; + console.log(this.initBotInfo, ' is the initBotInfo'); + console.log(this.initBotInfo.roleIDs, ' is the roleIds'); + console.log(userId); + console.log(guild); + console.log(participantsType); + console.log(overwrite); + console.log(' '); - if (!guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.staffRole) && !guild.members.cache.get(userId).roles.cache.has(this.initBotInfo.roleIDs.adminRole)) { - return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }) + if (!userRoles.has(staffRoleID) && !userRoles.has(adminRoleID)) { + return this.error({ message: 'You do not have permissions to run this command!', ephemeral: true }); } - if (!this.initBotInfo.verification.verificationRoles.has(participantsType)) { + console.log('PASSED'); + + const roles = this.initBotInfo.verification.roles; + const roleExists = roles.some(role => role.name === participantsType); + if (!roleExists) { await interaction.reply({ content: 'The role you entered does not exist!', ephemeral: true }); return; } - + console.log('PASSED AGAIN'); const modal = new Modal() .setCustomId('emailsModal') .setTitle('Enter all emails to be added as ' + participantsType) @@ -63,7 +78,8 @@ class AddMembers extends Command { if (submitted) { const emailsRaw = submitted.fields.getTextInputValue('emails'); - const emails = emailsRaw.split(/\r?\n|\r|\n/g); + console.log(emailsRaw, ' is the raw emails'); + const emails = emailsRaw.split(/[\r?\n|\r|\n|,]+/g).map(email => email.trim()).filter(Boolean); emails.forEach(email => { addUserData(email, participantsType, interaction.guild.id, overwrite); }); diff --git a/db/firebase/firebaseUtil.js b/db/firebase/firebaseUtil.js index 56fb684d..95261dad 100644 --- a/db/firebase/firebaseUtil.js +++ b/db/firebase/firebaseUtil.js @@ -235,72 +235,122 @@ module.exports = { * @param {String} [lastName=''] - users last name * @async */ - async addUserData(email, type, guildId, overwrite) { + async addUserData(email, type, guildId, overwrite) { const cleanEmail = email.trim().toLowerCase(); - const documentRef = getFactotumDoc().collection('guilds').doc(guildId).collection('members').doc(cleanEmail); - const doc = await documentRef.get(); - - if (doc.exists && !overwrite) { + const querySnapshot = type === "hacker" ? await getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('applicants') + .where('email', '==', cleanEmail) + .limit(1) + .get() + : await getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('otherRoles') + .where('email', '==', cleanEmail) + .limit(1) + .get() + + let docRef; + + if (!querySnapshot.empty) { + const doc = querySnapshot.docs[0]; + console.log(doc, ' is doc data'); + docRef = doc.ref; + const types = doc.data().types || []; const containsType = types.some(existingType => existingType.type === type); - if (!containsType) { - types.push({ type, isVerified: false }); + + if (!containsType || overwrite) { + if (!containsType) { + types.push({ type, isVerified: false }); + } + await docRef.update({ types }); } - await documentRef.update({ types }); } else { + docRef = type=== "hacker" ? getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('applicants') + .doc() + : getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('otherRoles') + .doc(); + const data = { email: cleanEmail, types: [{ isVerified: false, type }] }; - await documentRef.set(data); + await docRef.set(data); } }, /** - * Verifies the any event member via their email. - * @param {String} email - the user email - * @param {String} id - the user's discord snowflake - * @param {String} guildId - the guild id - * @returns {Promise} - the types this user is verified + * Verifies any event member via their email. + * @param {String} email - The user's email. + * @param {String} id - The user's Discord ID. + * @param {String} guildId - The guild ID. + * @returns {Promise} - The types this user is verified for. * @async * @throws Error if the email provided was not found. */ async verify(email, id, guildId) { - let emailLowerCase = email.trim().toLowerCase(); - let userRef = getFactotumDoc().collection('InitBotInfo').doc(guildId).collection('applicants').where('email', '==', emailLowerCase).limit(1); - let user = (await userRef.get()).docs[0]; - let otherRolesRef = getFactotumDoc().collection('InitBotInfo').doc(guildId).collection('otherRoles').where('email', '==', emailLowerCase).limit(1); - let otherRolesUser = (await otherRolesRef.get()).docs[0]; - if (user) { + const emailLowerCase = email.trim().toLowerCase(); + + const userRef = getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('applicants') + .where('email', '==', emailLowerCase) + .limit(1); + const userSnapshot = await userRef.get(); + const user = userSnapshot.docs[0]; + + const otherRolesRef = getFactotumDoc() + .collection('InitBotInfo') + .doc(guildId) + .collection('otherRoles') + .where('email', '==', emailLowerCase) + .limit(1); + const otherRolesUserSnapshot = await otherRolesRef.get(); + const otherRolesUser = otherRolesUserSnapshot.docs[0]; + + if (user || otherRolesUser) { let returnTypes = []; + let data; - /** @type {FirebaseUser} */ - let data = user.data(); - if (!data?.isVerified) { - data.isVerified = true; - data.VerifiedTimestamp = admin.firestore.Timestamp.now(); - data.discordId = id; - await user.ref.update(data); - returnTypes.push("hacker") + if (user) { + data = user.data(); + } else { + data = otherRolesUser.data(); } - return returnTypes; - } else if (otherRolesUser) { - let returnTypes = []; + if (data?.types) { + data.types = data.types.map((role) => { + if (!role.isVerified) { + role.isVerified = true; + returnTypes.push(role.type); + } + return role; + }); - /** @type {FirebaseUser} */ - let data = otherRolesUser.data(); - if (!data?.isVerified) { - data.isVerified = true; data.VerifiedTimestamp = admin.firestore.Timestamp.now(); data.discordId = id; - await otherRolesUser.ref.update(data); - returnTypes.push(data?.role) + + if (user) { + await user.ref.update(data); + } else if (otherRolesUser) { + await otherRolesUser.ref.update(data); + } } - return returnTypes; + return returnTypes; } else { throw new Error('The email provided was not found!'); } }, + /** * Attends the user via their discord id * @param {String} id - the user's discord snowflake From ec534f1508367dc7d5135ebd7e4b2a7ecbf68765 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Sun, 20 Oct 2024 09:33:03 -0700 Subject: [PATCH 66/67] Save start-report messages --- app.js | 10 + commands/hacker_utility/start-report.js | 321 +++++++++++------------- 2 files changed, 155 insertions(+), 176 deletions(-) diff --git a/app.js b/app.js index d90c7727..bdf8d268 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,7 @@ const Tracing = require('@sentry/tracing'); const { LogLevel, SapphireClient } = require('@sapphire/framework'); const Pronouns = require('./commands/a_utility/pronouns'); const RoleSelector = require('./commands/a_utility/role-selector'); +const StartReport = require('./commands/hacker_utility/start-report'); /** * The Main App module houses the bot events, process events, and initializes @@ -204,6 +205,15 @@ bot.once('ready', async () => { } else { mainLogger.verbose('Restored role selector command message'); } + + /** @type {StartReport} */ + const startReportCommand = bot.stores.get('commands').get('start-report'); + const startReportError = await startReportCommand.tryRestoreReactionListeners(guild); + if (startReportError) { + mainLogger.warning(roleSelectorError); + } else { + mainLogger.verbose('Restored role selector command message'); + } } guild.commandPrefix = botGuild.prefix; diff --git a/commands/hacker_utility/start-report.js b/commands/hacker_utility/start-report.js index 0a618e51..7411fb27 100644 --- a/commands/hacker_utility/start-report.js +++ b/commands/hacker_utility/start-report.js @@ -1,177 +1,146 @@ -// const { Command } = require('discord.js-commando'); -// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); -// const { MessageEmbed, Message } = require('discord.js'); -// const BotGuild = require('../../db/mongo/BotGuild'); - -// /** -// * The report command allows users to report incidents from the server to the admins. Reports are made -// * via the bot's DMs and are 100% anonymous. -// * @category Commands -// * @subcategory Hacker-Utility -// * @extends Command -// */ -// class Report extends Command { -// constructor(client) { -// super(client, { -// name: 'report', -// group: 'hacker_utility', -// memberName: 'report to admins', -// description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', -// // not guild only! -// args: [], -// }); -// } - -// /** -// * @param {Message} message -// */ -// async run (message) { -// let botGuild = await BotGuild.findById(message.guild.id); - -// deleteMessage(message); - -// if (!botGuild.report.isEnabled) { -// sendMessageToMember(message.author, 'The report functionality is disabled for this guild.'); -// return; -// } - -// const embed = new MessageEmbed() -// .setColor(botGuild.colors.embedColor) -// .setTitle('Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!') -// .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + -// 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + -// 'Copy paste the format and send it to me in this channel!') -// .addField('Format:', 'User(s) discord username(s) (including discord id number(s)):\n' + -// 'Reason for report (one line):\n' + -// 'Detailed Explanation:\n' + -// 'Name of channel where the incident occurred (if possible):'); - -// // send message to user with report format -// var msgEmbed = await message.author.send(embed); - -// // await response -// msgEmbed.channel.awaitMessages(m => true, {max: 1}).then(async msgs => { -// var msg = msgs.first(); - -// msgEmbed.delete(); -// message.author.send('Thank you for the report! Our admin team will look at it ASAP!'); - -// // send the report content to the admin report channel! -// var incomingReportChn = await message.guild.channels.resolve(botGuild.report.incomingReportChannelID); - -// const adminMsgEmbed = new MessageEmbed() -// .setColor(botGuild.colors.embedColor) -// .setTitle('There is a new report that needs your attention!') -// .setDescription(msg.content); - -// // send embed with text message to ping admin -// incomingReportChn.send('<@&' + botGuild.roleIDs.adminRole + '> Incoming Report', {embed: adminMsgEmbed}); -// }); - -// } -// } -// module.exports = Report; - -const { Command } = require('@sapphire/framework'); -// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); -const { Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); -const { discordLog } = require('../../discord-services'); - -/** - * The report command allows users to report incidents from the server to the admins. Reports are made - * via the bot's DMs and are 100% anonymous. - * @category Commands - * @subcategory Hacker-Utility - * @extends Command - */ -class StartReport extends Command { - constructor(context, options) { - super(context, { - ...options, - description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', - }); - } - - registerApplicationCommands(registry) { - registry.registerChatInputCommand((builder) => - builder - .setName(this.name) - .setDescription(this.description) - ), - { - idHints: '1214159059880517652' - }; - } - - /** - * - * @param {Command.ChatInputInteraction} interaction - */ - async chatInputRun(interaction) { - // const userId = interaction.user.id; - - // const embed = new MessageEmbed() - // .setTitle(`See an issue you'd like to annoymously report at ${interaction.guild.name}? Let our organizers know!`); - - const embed = new MessageEmbed() - .setTitle('Anonymously report users who are not following server or MLH rules. Help makes our community safer!') - .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + - 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + - 'Copy paste the format and send it to me in this channel!') - .addFields({ - name: 'Format:', - value: 'User(s) discord username(s) (including discord id number(s)):\n' + - 'Reason for report (one line):\n' + - 'Detailed Explanation:\n' + - 'Name of channel where the incident occurred (if possible):' - }); - // modal timeout warning? - const row = new MessageActionRow() - .addComponents( - new MessageButton() - .setCustomId('report') - .setLabel('Report an issue') - .setStyle('PRIMARY'), - ); - interaction.reply({ content: 'Report started!', ephemeral: true }); - const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); - - const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); - - checkInCollector.on('collect', async i => { - const modal = new Modal() - .setCustomId('reportModal') - .setTitle('Report an issue') - .addComponents([ - new MessageActionRow().addComponents( - new TextInputComponent() - .setCustomId('issueMessage') - .setLabel('Reason for report:') - .setMinLength(3) - .setMaxLength(1000) - .setStyle(2) - .setPlaceholder('Type your issue here...') - .setRequired(true), - ), - ]); - await i.showModal(modal); - - const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) - .catch(error => { - }); - - if (submitted) { - const issueMessage = submitted.fields.getTextInputValue('issueMessage'); - - try { - discordLog(interaction.guild, `<@&${interaction.guild.roleIDs.staffRole}> New anonymous report:\n\n ${issueMessage}`); - } catch { - discordLog(interaction.guild, `New anonymous report:\n\n ${issueMessage}`); - } - submitted.reply({ content: 'Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!', ephemeral: true }); - return; - } - }); - } -} +const { Command } = require('@sapphire/framework'); +// const { deleteMessage, sendMessageToMember, } = require('../../discord-services'); +const { Guild, Message, MessageEmbed, Modal, MessageActionRow, MessageButton, TextInputComponent } = require('discord.js'); +const { discordLog } = require('../../discord-services'); +const firebaseUtil = require('../../db/firebase/firebaseUtil'); + +/** + * The report command allows users to report incidents from the server to the admins. Reports are made + * via the bot's DMs and are 100% anonymous. + * @category Commands + * @subcategory Hacker-Utility + * @extends Command + */ +class StartReport extends Command { + constructor(context, options) { + super(context, { + ...options, + description: 'Will send report format to user via DM for user to send back via DM. Admins will get the report!', + }); + } + + registerApplicationCommands(registry) { + registry.registerChatInputCommand((builder) => + builder + .setName(this.name) + .setDescription(this.description) + ), + { + idHints: '1214159059880517652' + }; + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + */ + async chatInputRun(interaction) { + // const userId = interaction.user.id; + + // const embed = new MessageEmbed() + // .setTitle(`See an issue you'd like to annoymously report at ${interaction.guild.name}? Let our organizers know!`); + + const embed = new MessageEmbed() + .setTitle('Anonymously report users who are not following server or MLH rules. Help makes our community safer!') + .setDescription('Please use the format below, be as precise and accurate as possible. \n ' + + 'Everything you say will be 100% anonymous. We have no way of reaching back to you so again, be as detailed as possible!\n' + + 'Copy paste the format and send it to me in this channel!') + .addFields({ + name: 'Format:', + value: 'User(s) discord username(s) (including discord id number(s)):\n' + + 'Reason for report (one line):\n' + + 'Detailed Explanation:\n' + + 'Name of channel where the incident occurred (if possible):' + }); + // modal timeout warning? + const row = new MessageActionRow() + .addComponents( + new MessageButton() + .setCustomId('report') + .setLabel('Report an issue') + .setStyle('PRIMARY'), + ); + interaction.reply({ content: 'Report started!', ephemeral: true }); + const msg = await interaction.channel.send({ embeds: [embed], components: [row] }); + + await this.listenToReports(interaction, msg); + + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(interaction.guild.id); + await savedMessagesCol.doc('report').set({ + messageId: msg.id, + channelId: msg.channel.id, + }); + } + + /** + * + * @param {Command.ChatInputInteraction} interaction + * @param {Message} msg + */ + async listenToReports(interaction, msg) { + const checkInCollector = msg.createMessageComponentCollector({ filter: i => !i.user.bot}); + + checkInCollector.on('collect', async i => { + const modal = new Modal() + .setCustomId('reportModal') + .setTitle('Report an issue') + .addComponents([ + new MessageActionRow().addComponents( + new TextInputComponent() + .setCustomId('issueMessage') + .setLabel('Reason for report:') + .setMinLength(3) + .setMaxLength(1000) + .setStyle(2) + .setPlaceholder('Type your issue here...') + .setRequired(true), + ), + ]); + await i.showModal(modal); + + const submitted = await i.awaitModalSubmit({ time: 300000, filter: j => j.user.id === i.user.id }) + .catch(error => { + }); + + if (submitted) { + const issueMessage = submitted.fields.getTextInputValue('issueMessage'); + + try { + discordLog(interaction.guild, `<@&${interaction.guild.roleIDs.staffRole}> New anonymous report:\n\n ${issueMessage}`); + } catch { + discordLog(interaction.guild, `New anonymous report:\n\n ${issueMessage}`); + } + submitted.reply({ content: 'Thank you for taking the time to report users who are not following server or MLH rules. You help makes our community safer!', ephemeral: true }); + return; + } + }); + } + + /** + * + * @param {Guild} guild + */ + async tryRestoreReactionListeners(guild) { + const savedMessagesCol = firebaseUtil.getSavedMessagesSubCol(guild.id); + const reportDoc = await savedMessagesCol.doc('report').get(); + if (reportDoc.exists) { + const { messageId, channelId } = reportDoc.data(); + const channel = await this.container.client.channels.fetch(channelId); + if (channel) { + try { + /** @type {Message} */ + const message = await channel.messages.fetch(messageId); + this.listenToReports(guild, message); + } catch (e) { + // message doesn't exist anymore + return e; + } + } else { + return 'Saved message channel does not exist'; + } + } else { + return 'No existing saved message for pronouns command'; + } + } +} module.exports = StartReport; \ No newline at end of file From bcb310b0d8a9c930b3e42e1f2a3878ffd8c03158 Mon Sep 17 00:00:00 2001 From: Daniel Pan <57362494+daniel-panhead@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:18:12 -0700 Subject: [PATCH 67/67] Fix jsdoc error in mentor cave function --- commands/a_start_commands/start-mentor-cave.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/commands/a_start_commands/start-mentor-cave.js b/commands/a_start_commands/start-mentor-cave.js index e76ba5fc..7c50fd4d 100644 --- a/commands/a_start_commands/start-mentor-cave.js +++ b/commands/a_start_commands/start-mentor-cave.js @@ -99,8 +99,7 @@ class StartMentorCave extends Command { /** * - * @param {Command.ChatInputInteraction} interaction - * @returns + * @param {Command.ChatInputInteraction} interaction */ async chatInputRun(interaction) { try {