mirror of
				https://github.com/aydenjahola/discord-multipurpose-bot.git
				synced 2025-10-31 14:21:36 +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 | GUILD_ID=YOUR_GUILD_ID | ||||||
| VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME | VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME | ||||||
| VERIFIED_ROLE_NAME=YOUR_VERIFIED_ROLE_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 | # Database | ||||||
| MONGODB_URI=YOUR_MONGODB_URI | 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 | ## Features | ||||||
| 
 | 
 | ||||||
| - **Email Verification**: Users receive a verification code via email and must enter it in Discord to verify their account. | - **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. | - **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. | - **Trivia Game**: Play a video game-themed trivia game and compete with others in the server. | ||||||
| - **Expiration Handling**: Verification codes expire after 10 minutes for added security. | - **Leaderboard**: Displays the top players based on correct trivia answers. | ||||||
| 
 | - **Customizable**: Configure email domains, roles, trivia settings, and more to suit your server. | ||||||
| ## 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). |  | ||||||
| 
 | 
 | ||||||
| ### Installation | ### Installation | ||||||
| 
 | 
 | ||||||
|  | @ -25,6 +16,9 @@ Before you begin, ensure you have: | ||||||
| 
 | 
 | ||||||
| ```sh | ```sh | ||||||
| git clone git@github.com:aydenjahola/esports-verification-bot.git | git clone git@github.com:aydenjahola/esports-verification-bot.git | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ```sh | ||||||
| cd esports-verification-bot | cd esports-verification-bot | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | @ -48,13 +42,14 @@ EMAIL_USER=example@example.com | ||||||
| EMAIL_PASS=YOUR_EMAIL_PASS | EMAIL_PASS=YOUR_EMAIL_PASS | ||||||
| 
 | 
 | ||||||
| # Allowed domains for email verification | # 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 | # Discord | ||||||
| GUILD_ID=YOUR_GUILD_ID | GUILD_ID=YOUR_GUILD_ID | ||||||
| VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME | VERIFICATION_CHANNEL_NAME=YOUR_VERIFICATION_CHANNEL_NAME | ||||||
| VERIFIED_ROLE_NAME=YOUR_VERIFIED_ROLE_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 | # Database | ||||||
| MONGODB_URI=YOUR_MONGODB_URI | MONGODB_URI=YOUR_MONGODB_URI | ||||||
|  | @ -68,7 +63,37 @@ this can also be seen in in the [.env.example](./.env.example) | ||||||
| node bot.js | 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. | - **/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", |   "license": "ISC", | ||||||
|   "description": "", |   "description": "", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|  |     "axios": "^1.7.7", | ||||||
|     "discord.js": "^14.15.3", |     "discord.js": "^14.15.3", | ||||||
|     "dotenv": "^16.4.5", |     "dotenv": "^16.4.5", | ||||||
|     "express": "^4.19.2", |     "express": "^4.19.2", | ||||||
|  |     "html-entities": "^2.5.2", | ||||||
|     "mongoose": "^8.6.0", |     "mongoose": "^8.6.0", | ||||||
|     "nodemailer": "^6.9.14" |     "nodemailer": "^6.9.14" | ||||||
|   } |   } | ||||||
|  |  | ||||||
		Loading…
	
		Reference in a new issue