diff --git a/.env.example b/.env.example index 98d0504..67ef6c4 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,8 @@ EMAIL_DOMAINS=example@example.com // or it can be a list, example: example.com,e GUILD_ID=YOUR_GUILD_ID VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME VERIFIED_ROLE_NAME=YOUR_VERIFIED_ROLE_NAME -ADMIN_LOG_CHANNEL_ID=YOUR_ADMIN_LOG_CHANNEL_ID +LOG_CHANNEL_ID=YOUR_LOG_CHANNEL_ID +MOD_ROLE_ID=YOUR_MOD_ROLE_ID # Database MONGODB_URI=YOUR_MONGODB_URI \ No newline at end of file diff --git a/README.md b/README.md index fc09bee..55b6cc5 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,14 @@ -# Discord Verification Bot +# Discord Multipurpose Bot -Welcome to the **Discord Verification Bot**! This bot is designed to handle user verification for Discord servers. It verifies users through their student email addresses and manages roles based on their verification status. +Welcome to the **Discord Multipurpose Bot**! This bot manages user verification for Discord servers through email authentication, includes a fun trivia game feature, and provides role management and leaderboard tracking functionalities. ## Features - **Email Verification**: Users receive a verification code via email and must enter it in Discord to verify their account. - **Role Management**: Automatically assigns a specific role to users once they have been verified. -- **Customizable**: Easy to configure email domains, roles, and channels for different needs. -- **Expiration Handling**: Verification codes expire after 10 minutes for added security. - -## Getting Started - -### Prerequisites - -Before you begin, ensure you have: - -- A Discord bot token (create a bot on the [Discord Developer Portal](https://discord.com/developers/applications)). -- Access to a MongoDB database (you can use [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) for a free tier). -- A Gmail account for sending emails (you can use any email service, but make sure to adjust the Nodemailer configuration). +- **Trivia Game**: Play a video game-themed trivia game and compete with others in the server. +- **Leaderboard**: Displays the top players based on correct trivia answers. +- **Customizable**: Configure email domains, roles, trivia settings, and more to suit your server. ### Installation @@ -25,6 +16,9 @@ Before you begin, ensure you have: ```sh git clone git@github.com:aydenjahola/esports-verification-bot.git +``` + +```sh cd esports-verification-bot ``` @@ -48,13 +42,14 @@ EMAIL_USER=example@example.com EMAIL_PASS=YOUR_EMAIL_PASS # Allowed domains for email verification -EMAIL_DOMAINS=example@example.com // or it can be a list, example: "example.com,example2.com" +EMAIL_DOMAINS=example@example.com // or it can be a list, example: example.com,example2.com # Discord GUILD_ID=YOUR_GUILD_ID VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME VERIFIED_ROLE_NAME=YOUR_VERIFIED_ROLE_NAME -ADMIN_LOG_CHANNEL_ID=YOUR_ADMIN_LOG_CHANNEL_ID +LOG_CHANNEL_ID=YOUR_LOG_CHANNEL_ID +MOD_ROLE_ID=YOUR_MOD_ROLE_ID # Database MONGODB_URI=YOUR_MONGODB_URI @@ -68,7 +63,37 @@ this can also be seen in in the [.env.example](./.env.example) node bot.js ``` -### Usage +## Usage + +### Information Commands + +- **/botinfo**: Displays information about the bot +- **/serverinfo**: Displays information about the server + +### Utility Commands + +- **/help**: Lists all available commands +- **/ping**: Replies with Pong! and bot latency +- **/uptime**: Shows how long the bot has been running + +### Email Verification Commands - **/verify your_email@example.com**: Sends a verification code to the provided email. -- **/code your_code**: Validates the provided verification code. +- **/code your_code**: Validates the provided verification code and completes the verification process. + +### Moderation Commands + +- **/purge**: Deletes messages from the channel +- **/userinfo**: Displays information about a user +- **/warn**: Issue a warning to a user + +### Fun Commands + +- **/trivia**: Starts a trivia game with video game-themed questions. Players have 30 seconds to answer. + - Accepts both number answers (1-4) **and** the correct answer +- **/leaderboard**: Displays the top 10 players on the trivia leaderboard based on their correct answers. + +### Other Functionalities + +- **Role Management**: Once a user is verified, they are automatically assigned a predefined role. +- **Admin Log**: Admins can review logs of verification attempts and trivia games in a designated channel. diff --git a/commands/fun/leaderboard.js b/commands/fun/leaderboard.js new file mode 100644 index 0000000..00e29ad --- /dev/null +++ b/commands/fun/leaderboard.js @@ -0,0 +1,52 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const Leaderboard = require("../../models/Leaderboard"); + +module.exports = { + data: new SlashCommandBuilder() + .setName("leaderboard") + .setDescription("Displays the trivia leaderboard"), + + async execute(interaction, client) { + const guild = interaction.guild; + + try { + const scores = await Leaderboard.find() + .sort({ correctAnswers: -1 }) + .limit(10); + + const leaderboardEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Trivia Leaderboard") + .setDescription( + scores + .map( + (entry, index) => + `${index + 1}. ${entry.username}: ${ + entry.correctAnswers + } correct answers in ${entry.gamesPlayed} games` + ) + .join("\n") + ) + .setTimestamp(); + + if (guild.iconURL()) { + leaderboardEmbed.setFooter({ + text: guild.name, + iconURL: guild.iconURL(), + }); + } else { + leaderboardEmbed.setFooter({ + text: guild.name, + }); + } + + await interaction.reply({ embeds: [leaderboardEmbed] }); + } catch (error) { + console.error("Error executing leaderboard command:", error); + await interaction.reply({ + content: "There was an error while executing this command!", + ephemeral: true, + }); + } + }, +}; diff --git a/commands/fun/trivia.js b/commands/fun/trivia.js new file mode 100644 index 0000000..b06720e --- /dev/null +++ b/commands/fun/trivia.js @@ -0,0 +1,170 @@ +const { SlashCommandBuilder, EmbedBuilder } = require("discord.js"); +const axios = require("axios"); +const TriviaQuestion = require("../../models/TriviaQuestion"); +const Leaderboard = require("../../models/Leaderboard"); +const { decode } = require("html-entities"); + +const API_INTERVAL = 5000; // 5 seconds +const QUESTION_EXPIRY = 30 * 24 * 60 * 60 * 1000; // 1 month + +let lastApiCall = 0; + +module.exports = { + data: new SlashCommandBuilder() + .setName("trivia") + .setDescription("Play a trivia game about video games"), + + async execute(interaction, client) { + const userId = interaction.user.id; + const username = interaction.user.username; + const guild = interaction.guild; + const timeLimit = 30000; // Time limit for answering in milliseconds + + try { + // Fetch a trivia question from the cache or the API + let triviaQuestion = await TriviaQuestion.findOne({ + last_served: { $lt: new Date(Date.now() - QUESTION_EXPIRY) }, // Fetch questions not served recently + }).sort({ last_served: 1 }); + + if (!triviaQuestion || Date.now() - lastApiCall >= API_INTERVAL) { + // Fetch a new trivia question from OTDB + const response = await axios.get( + "https://opentdb.com/api.php?amount=1&category=15&type=multiple" // Category 15 is for Video Games + ); + + triviaQuestion = response.data.results[0]; + lastApiCall = Date.now(); + + // Save the new trivia question to MongoDB + await TriviaQuestion.create({ + question: decode(triviaQuestion.question), + correct_answer: decode(triviaQuestion.correct_answer), + incorrect_answers: triviaQuestion.incorrect_answers.map(decode), + last_served: null, // Initially not served + }); + + // Fetch the newly created question + triviaQuestion = await TriviaQuestion.findOne({ + question: decode(triviaQuestion.question), + }); + } + + if (triviaQuestion) { + triviaQuestion.last_served = new Date(); + await triviaQuestion.save(); + } + + const question = decode(triviaQuestion.question); + const correctAnswer = decode(triviaQuestion.correct_answer); + const incorrectAnswers = triviaQuestion.incorrect_answers.map(decode); + const allAnswers = [...incorrectAnswers, correctAnswer].sort( + () => Math.random() - 0.5 + ); + + // Create a mapping of numbers to answers + const answerMap = allAnswers.reduce((map, answer, index) => { + map[index + 1] = answer; + return map; + }, {}); + + // Create an embed with the trivia question and numbered options + const triviaEmbed = new EmbedBuilder() + .setColor("#0099ff") + .setTitle("Trivia Question") + .setDescription(question) + .addFields( + Object.entries(answerMap).map(([number, answer]) => ({ + name: `Option ${number}`, + value: answer, + inline: true, + })) + ) + .setTimestamp() + .setFooter({ + text: `${guild.name} | Answer within ${timeLimit / 1000} seconds`, + iconURL: guild.iconURL(), + }); + + await interaction.reply({ + content: `<@${userId}>`, + embeds: [triviaEmbed], + }); + + // Create a message collector specific to the user + const filter = (response) => { + const userInput = response.content.trim(); + const userAnswerNumber = parseInt(userInput, 10); + const userAnswerText = + allAnswers.includes(userInput) || + (answerMap[userAnswerNumber] && + answerMap[userAnswerNumber] === correctAnswer); + + // Check if the input is a number within valid range or a text that matches one of the options + return ( + response.author.id === userId && + (userAnswerText || (userAnswerNumber >= 1 && userAnswerNumber <= 4)) + ); + }; + + const collector = interaction.channel.createMessageCollector({ + filter, + max: 1, + time: timeLimit, + }); + + collector.on("collect", async (message) => { + const userInput = message.content.trim(); + const userAnswerNumber = parseInt(userInput, 10); + const userAnswer = answerMap[userAnswerNumber] || userInput; + + let resultMessage = "Incorrect! Better luck next time."; + + if (userAnswer === correctAnswer) { + resultMessage = "Correct!"; + } + + // Update leaderboard + let userScore = await Leaderboard.findOne({ userId }); + if (!userScore) { + userScore = new Leaderboard({ + userId, + username, + gamesPlayed: 1, + correctAnswers: userAnswer === correctAnswer ? 1 : 0, + }); + } else { + userScore.gamesPlayed += 1; + if (userAnswer === correctAnswer) { + userScore.correctAnswers += 1; + } + } + await userScore.save(); + + await interaction.followUp( + `${resultMessage} <@${userId}> You've answered ${userScore.correctAnswers} questions correctly out of ${userScore.gamesPlayed} games.` + ); + }); + + collector.on("end", (collected, reason) => { + if (reason === "time") { + interaction.followUp( + `<@${userId}> Time's up! You didn't answer in time.` + ); + } + }); + } catch (error) { + console.error("Error executing trivia command:", error); + if (error.response && error.response.status === 429) { + await interaction.reply({ + content: `<@${userId}> The trivia API rate limit has been exceeded. Please try again later.`, + ephemeral: true, + }); + } else { + await interaction.reply({ + content: `<@${userId}> There was an error while executing this command!`, + ephemeral: true, + }); + } + } + }, +}; diff --git a/models/Leaderboard.js b/models/Leaderboard.js new file mode 100644 index 0000000..29bc347 --- /dev/null +++ b/models/Leaderboard.js @@ -0,0 +1,12 @@ +const mongoose = require("mongoose"); + +const leaderboardSchema = new mongoose.Schema({ + userId: { type: String, required: true }, + username: { type: String, required: true }, + gamesPlayed: { type: Number, default: 0 }, + correctAnswers: { type: Number, default: 0 }, +}); + +const Leaderboard = mongoose.model("Leaderboard", leaderboardSchema); + +module.exports = Leaderboard; diff --git a/models/TriviaQuestion.js b/models/TriviaQuestion.js new file mode 100644 index 0000000..efe588c --- /dev/null +++ b/models/TriviaQuestion.js @@ -0,0 +1,11 @@ +const mongoose = require("mongoose"); + +const triviaQuestionSchema = new mongoose.Schema({ + question: String, + correct_answer: String, + incorrect_answers: [String], + last_served: Date, // Track when the question was last served + timestamp: { type: Date, default: Date.now }, +}); + +module.exports = mongoose.model("TriviaQuestion", triviaQuestionSchema); diff --git a/package.json b/package.json index 27d9b39..986163b 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,11 @@ "license": "ISC", "description": "", "dependencies": { + "axios": "^1.7.7", "discord.js": "^14.15.3", "dotenv": "^16.4.5", "express": "^4.19.2", + "html-entities": "^2.5.2", "mongoose": "^8.6.0", "nodemailer": "^6.9.14" }