mirror of
				https://github.com/aydenjahola/discord-multipurpose-bot.git
				synced 2025-10-31 06:11:34 +00:00 
			
		
		
		
	add trivia command as well as leaderboard command, and update README
This commit is contained in:
		
							parent
							
								
									2ec7c73709
								
							
						
					
					
						commit
						77e5489269
					
				
					 7 changed files with 292 additions and 19 deletions
				
			
		|  | @ -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 | ||||
							
								
								
									
										61
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										61
									
								
								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. | ||||
|  |  | |||
							
								
								
									
										52
									
								
								commands/fun/leaderboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								commands/fun/leaderboard.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|       }); | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										170
									
								
								commands/fun/trivia.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								commands/fun/trivia.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
|         }); | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
| }; | ||||
							
								
								
									
										12
									
								
								models/Leaderboard.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								models/Leaderboard.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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; | ||||
							
								
								
									
										11
									
								
								models/TriviaQuestion.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								models/TriviaQuestion.js
									
									
									
									
									
										Normal file
									
								
							|  | @ -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); | ||||
|  | @ -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" | ||||
|   } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue