add trivia command as well as leaderboard command, and update README

This commit is contained in:
Ayden Jahola 2024-09-05 18:49:18 +01:00
parent 2ec7c73709
commit 77e5489269
No known key found for this signature in database
GPG key ID: 71DD90AE4AE92742
7 changed files with 292 additions and 19 deletions

View file

@ -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

View file

@ -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.

View 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
View 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
View 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
View 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);

View file

@ -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"
} }