diff --git a/commands/help.js b/commands/help.js new file mode 100644 index 0000000..154795e --- /dev/null +++ b/commands/help.js @@ -0,0 +1,14 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { MessageEmbed } = require('discord.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('help') + .setDescription('Replies with a help message explaining what the bot can do'), + async execute(interaction) { + const helpEmbed = new MessageEmbed() + .setDescription('AwesomeSciBo has migrated to using slash commands! You can take a look at the different commands by typing `/` and clicking on the AwesomeSciBo icon.') + .setColor('#ffffff'); + interaction.reply({ embeds: [helpEmbed] }); + }, +}; diff --git a/commands/top.js b/commands/top.js new file mode 100644 index 0000000..bb5abb9 --- /dev/null +++ b/commands/top.js @@ -0,0 +1,39 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { MessageEmbed } = require('discord.js'); + +const userScore = require('../models/userScore'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('top') + .setDescription('Lists top ten scores across servers (server specific leaderboard WIP)'), + async execute(interaction) { + let messageContent = ''; + userScore + .find({}) + .sort({ score: -1 }) // Sort by descending order + .exec((err, obj) => { + if (err) { + console.log(err); + return interaction.reply( + 'Uh oh! :( There was an internal error. Please try again.', + ); + } + if (obj.length < 10) { + // Need at least 10 scores for top 10 + return interaction.reply( + `There are only ${obj.length} users, we need at least 10!`, + ); + } + for (let i = 0; i < 10; i++) { + messageContent += `${i + 1}: <@${obj[i].authorID}>: ${obj[i].score}\n`; // Loop through each user and add their name and score to leaderboard content + } + const leaderboardEmbed = new MessageEmbed() + .setTitle('Top Ten!') + .setDescription(messageContent) + .setColor('#ffffff'); + + interaction.reply({ embeds: [leaderboardEmbed] }); + }); + }, +}; diff --git a/commands/train.js b/commands/train.js new file mode 100644 index 0000000..d050dbd --- /dev/null +++ b/commands/train.js @@ -0,0 +1,169 @@ +const { SlashCommandBuilder } = require('@discordjs/builders'); +const { MessageEmbed } = require('discord.js'); + +const decode = require('html-entities').decode; +const axios = require('axios'); + +const userScore = require('../models/userScore'); + +const { updateScore } = require('../helpers/db.js'); + +module.exports = { + data: new SlashCommandBuilder() + .setName('train') + .setDescription('Sends a training question to be answered') + .addStringOption(option => { + option + .setName('subject') + .setDescription('Optional subject to be used as a filter') + .setRequired(false) + .addChoice('astro', 'astro') + .addChoice('bio', 'bio') + .addChoice('ess', 'ess') + .addChoice('chem', 'chem') + .addChoice('phys', 'phys') + .addChoice('math', 'math') + .addChoice('energy', 'energy') + .setRequired(false); + return option; + }), + async execute(interaction) { + const subject = interaction.options.get('subject') ? interaction.options.get('subject').value : null; + const authorId = interaction.user.id; + let score; + userScore + .findOne({ authorID: authorId }) + .lean() + .then((obj, err) => { + if (!obj) { + score = 0; + } + else if (obj) { + score = obj.score; + } + else { + console.log(err); + } + }); + + let categoryArray = []; + + switch (subject) { + case null: + categoryArray = ['BIOLOGY', 'PHYSICS', 'CHEMISTRY', 'EARTH AND SPACE', 'ASTRONOMY', 'MATH']; + break; + case 'astro': + case 'astronomy': + categoryArray = ['ASTRONOMY']; + break; + case 'bio': + case 'biology': + categoryArray = ['BIOLOGY']; + break; + case 'ess': + case 'earth science': + case 'es': + categoryArray = ['EARTH SCIENCE']; + break; + case 'chem': + case 'chemistry': + categoryArray = ['CHEMISTRY']; + break; + case 'phys': + case 'physics': + categoryArray = ['PHYSICS']; + break; + case 'math': + categoryArray = ['MATH']; + break; + case 'energy': + categoryArray = ['ENERGY']; + break; + default: + interaction.reply( + new MessageEmbed() + .setDescription('<:red_x:816791117671825409> Not a valid subject!') + .setColor('#ffffff'), + ); + return; + } + + axios + .post('https://scibowldb.com/api/questions/random', { categories: categoryArray }) + .then((res) => { + const data = res.data.question; + const tossupQuestion = data.tossup_question; + const tossupAnswer = data.tossup_answer; + const messageFilter = message => message.author.id === interaction.author.id; + interaction.reply({ content: decode(tossupQuestion) + `\n\n||Source: ${data.uri}||` }) + .then(() => { + interaction.channel.awaitMessages({ + messageFilter, + max: 1, + }) + .then(collected => { + console.log(collected.first()); + const answerMsg = collected.first(); + + let predicted = null; + if (data.tossup_format === 'Multiple Choice') { + if ( + answerMsg.content.charAt(0).toLowerCase() === + tossupAnswer.charAt(0).toLowerCase() + ) { + predicted = 'correct'; + } + else { + predicted = 'incorrect'; + } + } + else if ( + answerMsg.content.toLowerCase() === + tossupAnswer.toLowerCase() + ) { + predicted = 'correct'; + } + else { + predicted = 'incorrect'; + } + + if (predicted === 'correct') { + updateScore(true, score, authorId).then((msgToReply) => + answerMsg.reply(msgToReply), + ); + } + else { + const overrideEmbed = new MessageEmbed() + .setAuthor({ name: answerMsg.author.tag, iconURL: answerMsg.author.displayAvatarURL() }) + .addField('Correct answer', `\`${tossupAnswer}\``) + .setDescription('It seems your answer was incorrect. Please react with <:override:842778128966615060> to override your answer if you think you got it right.') + .setColor('#ffffff') + .setTimestamp(); + answerMsg.channel.send({ + embeds: [overrideEmbed], + }) + .then(overrideMsg => { + overrideMsg.react('<:override:842778128966615060>'); + const filter = (reaction, user) => { + return ( + ['override'].includes(reaction.emoji.name) && + user.id === answerMsg.author.id + ); + }; + overrideMsg + .awaitReactions({ + filter, + max: 1, + }) + .then(() => { + updateScore(true, score, authorId).then((msgToReply) => + answerMsg.reply(msgToReply), + ); + }).catch(console.error); + }).catch(console.error); + } + }).catch(console.error); + }).catch(console.error); + }).catch(console.error); + }, +}; diff --git a/deploy-commands.js b/deploy-commands.js new file mode 100755 index 0000000..0b7b816 --- /dev/null +++ b/deploy-commands.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); +const { REST } = require('@discordjs/rest'); +const { Routes } = require('discord-api-types/v9'); +const { clientId, token } = require('./helpers/env'); + +const commands = []; +const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); + +for (const file of commandFiles) { + const command = require(`./commands/${file}`); + commands.push(command.data.toJSON()); +} + +const rest = new REST({ version: '9' }).setToken(token); + +rest.put(Routes.applicationCommands(clientId), { body: commands }) + .then(() => console.log('Successfully registered application commands.')) + .catch(console.error); diff --git a/helpers/db.js b/helpers/db.js new file mode 100644 index 0000000..4754b42 --- /dev/null +++ b/helpers/db.js @@ -0,0 +1,41 @@ +const mongoose = require('mongoose'); + +const userScore = require('../models/userScore'); + +module.exports = { + async updateScore(isCorrect, score, authorId) { + if (!isCorrect) { + return `Nice try! Your score is still ${score}.`; + } + else { + score += 4; + if (score == 4) { + const newUserScore = new userScore({ + authorID: authorId, + score: score, + }); + newUserScore.save((err) => + err + ? console.log('Error creating new user for scoring') + : console.log('Sucessfully created user to score.'), + ); + } + else { + const doc = await userScore.findOne({ + authorID: authorId, + }); + doc.score = doc.score + 4; + doc.save(); + } + + return `Great job! Your score is now ${score}.`; + } + }, + async connect(mongoUri) { + mongoose + .connect(mongoUri, { + useUnifiedTopology: true, + useNewUrlParser: true, + }); + }, +}; diff --git a/helpers/env.js b/helpers/env.js new file mode 100644 index 0000000..69f0af6 --- /dev/null +++ b/helpers/env.js @@ -0,0 +1,8 @@ +require('dotenv').config(); + +module.exports = { + clientId: process.env.CLIENT_ID, + testingGuild: process.env.TESTING_GUILD, + token: process.env.TOKEN, + mongoUri: process.env.MONGO_URI, +}; diff --git a/index.js b/index.js index 6bd39b0..d56cca6 100755 --- a/index.js +++ b/index.js @@ -1,437 +1,41 @@ #!/usr/bin/env node -require('dotenv').config(); +const fs = require('node:fs'); +const db = require('./helpers/db'); +const { Client, Collection, Intents } = require('discord.js'); +const { token, mongoUri } = require('./helpers/env'); -const Discord = require('discord.js'); -const client = new Discord.Client({ - intents: ['GUILDS', 'GUILD_MESSAGES', 'GUILD_MESSAGE_REACTIONS', 'DIRECT_MESSAGES', 'DIRECT_MESSAGE_REACTIONS'], - partials: ['MESSAGE', 'CHANNEL', 'REACTION'], +const client = new Client({ + intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES, Intents.FLAGS.GUILD_MESSAGE_REACTIONS, Intents.FLAGS.DIRECT_MESSAGES, Intents.FLAGS.DIRECT_MESSAGE_REACTIONS], }); -const axios = require('axios'); -const mongoose = require('mongoose'); -const gitlog = require('gitlog').default; -const decode = require('html-entities').decode; -const userScore = require('./models/userScore'); -const generatedRound = require('./models/generateRound'); +client.commands = new Collection(); -const helpMessage = 'AwesomeSciBo has migrated to using slash commands! You can take a look at the different commands by typing `/` and clicking on the AwesomeSciBo icon.'; -const slashCommands = require('./slashCommands.json'); -const config = { - topggauth: process.env['TOPGGAUTH'], -}; +const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); -client.once('ready', () => { - client.application.commands.set(slashCommands); - - // Connect to MongoDB using mongoose - if (!process.env.CI) { - mongoose - .connect(process.env.MONGO_URI, { - useUnifiedTopology: true, - useNewUrlParser: true, - }) - .then(() => { - // Log client tag and set status - console.log(`Logged in as: ${client.user.username}!`); - client.user.setActivity( - 'for /help', - { type: 'WATCHING' }, - ); - }) - .catch((err) => console.log(err)); - } -}); - -client.on('guildCreate', () => { - const topggAuthHeader = { - headers: { - 'Authorization': config.topggauth, - }, - }; - axios.post(`https://top.gg/api/bots/${client.user.id}/stats`, { server_count: client.guilds.cache.size }, topggAuthHeader).then(response => { console.log(response); }); -}); - -client.on('guildDelete', () => { - const topggAuthHeader = { - headers: { - 'Authorization': config.topggauth, - }, - }; - axios.post(`https://top.gg/api/bots/${client.user.id}/stats`, { server_count: client.guilds.cache.size }, topggAuthHeader); -}); - -async function updateScore(isCorrect, score, authorId) { - if (!isCorrect) { - return `Nice try! Your score is still ${score}.`; - } - else { - score += 4; - if (score == 4) { - const newUserScore = new userScore({ - authorID: authorId, - score: score, - }); - newUserScore.save((err) => - err - ? console.log('Error creating new user for scoring') - : console.log('Sucessfully created user to score.'), - ); - } - else { - const doc = await userScore.findOne({ - authorID: authorId, - }); - doc.score = doc.score + 4; - doc.save(); - } - - return `Great job! Your score is now ${score}.`; - } -} - -async function training(subject, interaction) { - const authorId = interaction.user.id; - let score; - userScore - .findOne({ authorID: authorId }) - .lean() - .then((obj, err) => { - if (!obj) { - score = 0; - } - else if (obj) { - score = obj.score; - } - else { - console.log(err); - } - }); - - let categoryArray = []; - - switch (subject) { - case null: - categoryArray = ['BIOLOGY', 'PHYSICS', 'CHEMISTRY', 'EARTH AND SPACE', 'ASTRONOMY', 'MATH']; - break; - case 'astro': - case 'astronomy': - categoryArray = ['ASTRONOMY']; - break; - case 'bio': - case 'biology': - categoryArray = ['BIOLOGY']; - break; - case 'ess': - case 'earth science': - case 'es': - categoryArray = ['EARTH SCIENCE']; - break; - case 'chem': - case 'chemistry': - categoryArray = ['CHEMISTRY']; - break; - case 'phys': - case 'physics': - categoryArray = ['PHYSICS']; - break; - case 'math': - categoryArray = ['MATH']; - break; - case 'energy': - categoryArray = ['ENERGY']; - break; - default: - interaction.reply( - new Discord.MessageEmbed() - .setDescription('<:red_x:816791117671825409> Not a valid subject!') - .setColor('#ffffff'), - ); - return; - } - - axios - .post('https://scibowldb.com/api/questions/random', { categories: categoryArray }) - .then((res) => { - const data = res.data.question; - const tossupQuestion = data.tossup_question; - const tossupAnswer = data.tossup_answer; - const messageFilter = message => message.author.id === interaction.author.id; - interaction.reply({ content: decode(tossupQuestion) + `\n\n||Source: ${data.uri}||` }) - .then(() => { - interaction.channel.awaitMessages({ - messageFilter, - max: 1, - }) - .then(collected => { - console.log(collected.first()); - const answerMsg = collected.first(); - - let predicted = null; - if (data.tossup_format === 'Multiple Choice') { - if ( - answerMsg.content.charAt(0).toLowerCase() === - tossupAnswer.charAt(0).toLowerCase() - ) { - predicted = 'correct'; - } - else { - predicted = 'incorrect'; - } - } - else if ( - answerMsg.content.toLowerCase() === - tossupAnswer.toLowerCase() - ) { - predicted = 'correct'; - } - else { - predicted = 'incorrect'; - } - - if (predicted === 'correct') { - updateScore(true, score, authorId).then((msgToReply) => - answerMsg.reply(msgToReply), - ); - } - else { - const overrideEmbed = new Discord.MessageEmbed() - .setAuthor({ name: answerMsg.author.tag, iconURL: answerMsg.author.displayAvatarURL() }) - .addField('Correct answer', `\`${tossupAnswer}\``) - .setDescription('It seems your answer was incorrect. Please react with <:override:842778128966615060> to override your answer if you think you got it right.') - .setColor('#ffffff') - .setTimestamp(); - answerMsg.channel.send({ - embeds: [overrideEmbed], - }) - .then(overrideMsg => { - overrideMsg.react('<:override:842778128966615060>'); - const filter = (reaction, user) => { - return ( - ['override'].includes(reaction.emoji.name) && - user.id === answerMsg.author.id - ); - }; - overrideMsg - .awaitReactions({ - filter, - max: 1, - }) - .then(() => { - updateScore(true, score, authorId).then((msgToReply) => - answerMsg.reply(msgToReply), - ); - }).catch(console.error); - }).catch(console.error); - } - }).catch(console.error); - }).catch(console.error); - }).catch(console.error); -} - -function sendHelpMessage(interaction) { - const helpEmbed = new Discord.MessageEmbed().setDescription(helpMessage).setColor('ffffff'); - interaction.reply({ embeds: [helpEmbed] }); -} - -function showLeaderboard(interaction) { - let messageContent = ''; - userScore - .find({}) - .sort({ score: -1 }) // Sort by descending order - .exec((err, obj) => { - if (err) { - console.log(err); - return interaction.reply( - 'Uh oh! :( There was an internal error. Please try again.', - ); - } - if (obj.length < 10) { - // Need at least 10 scores for top 10 - return interaction.reply( - `There are only ${obj.length} users, we need at least 10!`, - ); - } - for (let i = 0; i < 10; i++) { - messageContent += `${i + 1}: <@${obj[i].authorID}>: ${obj[i].score}\n`; // Loop through each user and add their name and score to leaderboard content - } - const leaderboardEmbed = new Discord.MessageEmbed() - .setTitle('Top Ten!') - .setDescription(messageContent) - .setColor('#ffffff'); - - interaction.reply({ embeds: [leaderboardEmbed] }); - }); -} - -async function about(action, interaction) { - if (action === 'contributors') { - const contributorEmbed = new Discord.MessageEmbed().setTitle('Contributors') - .addField('Creator', '<@745063586422063214> [ADawesomeguy#3602]', true) - .addField('Contributors', '<@650525101048987649> [tEjAs#8127]\n<@426864344463048705> [tetrident#9396]', true) // Add more contributors here, first one is Abheek, second one is Tejas - .setTimestamp() - .setColor('#ffffff'); - - interaction.reply({ embeds: [contributorEmbed] }); - } - else if (action === 'changelog') { - const gitRepoLocation = __dirname; - - const commits = gitlog({ - repo: gitRepoLocation, - number: 5, - fields: ['hash', 'abbrevHash', 'subject', 'authorName', 'authorDateRel'], - }); - - const changelogEmbed = new Discord.MessageEmbed() - .setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL() }) - .setTitle('Changelog') - .setColor('#ffffff') - .setTimestamp(); - - commits.forEach(commit => { - changelogEmbed.addField(commit.abbrevHash, `> \`Hash:\`${commit.hash}\n> \`Subject:\`${commit.subject}\n> \`Author:\`${commit.authorName}\n> \`Date:\`${commit.authorDateRel}\n> \`Link\`: [GitHub](https://github.com/ADawesomeguy/AwesomeSciBo/commit/${commit.hash})\n`); - }); - - interaction.reply({ embeds: [changelogEmbed] }); - } - else if (action === 'bot') { - await client.guilds.fetch(); - const trainingDocuments = await userScore.countDocuments({}); - const aboutBotEmbed = new Discord.MessageEmbed() - .setAuthor({ name: interaction.user.tag, iconURL: interaction.user.displayAvatarURL() }) - .setTitle('About AwesomeSciBo') - .addField('Servers', `${client.guilds.cache.size}`, true) - .addField('Training Users', `${trainingDocuments}`, true) - .setTimestamp(); - - interaction.reply({ embeds: [aboutBotEmbed] }); - } +for (const file of commandFiles) { + const command = require(`./commands/${file}`); + client.commands.set(command.data.name, command); } -async function rounds(action, interaction) { - if (action === 'generate') { - let i; - let finalizedHTML = '