2024-09-05 18:49:18 +01:00
const { SlashCommandBuilder , EmbedBuilder } = require ( "discord.js" ) ;
const axios = require ( "axios" ) ;
2024-09-08 18:34:21 +01:00
const { decode } = require ( "html-entities" ) ;
2024-09-05 18:49:18 +01:00
const TriviaQuestion = require ( "../../models/TriviaQuestion" ) ;
const Leaderboard = require ( "../../models/Leaderboard" ) ;
2024-09-10 13:43:19 +01:00
// WARNING: this code is by no means perfect, and it might have questionable implementation but it's still a good starting point and under development. Feel free to suggest improvements.
2024-09-10 12:30:08 +01:00
2024-09-05 18:49:18 +01:00
const API _INTERVAL = 5000 ; // 5 seconds
const QUESTION _EXPIRY = 30 * 24 * 60 * 60 * 1000 ; // 1 month
2024-09-07 23:49:03 +01:00
const ACTIVE _GAMES = new Set ( ) ; // Track users with ongoing trivia
2024-09-06 02:23:33 +01:00
const LAST _API _CALL = { time : 0 } ; // Track last API call
2024-09-05 18:49:18 +01:00
2024-09-10 13:43:19 +01:00
const USER _TOKENS = { } ; // Track tokens for each user
2024-09-06 02:11:06 +01:00
const CATEGORY _MAP = {
15 : "Video Games" ,
31 : "Anime & Manga" ,
18 : "Computers" ,
16 : "Board Games" ,
29 : "Comics" ,
32 : "Cartoons & Animations" ,
11 : "Film" ,
9 : "General Knowledge" ,
17 : "Science & Nature" ,
27 : "Animals" ,
12 : "Music" ,
23 : "History" ,
22 : "Geography" ,
20 : "Mythology" ,
} ;
2024-09-10 12:28:56 +01:00
// Generate a new session token for each user
const generateSessionToken = async ( ) => {
2024-09-08 18:34:21 +01:00
const response = await axios . get (
2024-09-10 12:28:56 +01:00
"https://opentdb.com/api_token.php?command=request"
2024-09-08 18:34:21 +01:00
) ;
2024-09-10 12:28:56 +01:00
return response . data . token ;
2024-09-08 18:34:21 +01:00
} ;
2024-09-10 13:43:19 +01:00
const fetchTriviaQuestion = async ( userId , categoryId , categoryName ) => {
2024-09-06 02:11:06 +01:00
try {
2024-09-07 21:39:45 +01:00
let triviaQuestion ;
let source = "API" ; // Default to API
2024-09-10 13:43:19 +01:00
// Get or generate a session token for the user
let sessionToken = USER _TOKENS [ userId ] || ( await generateSessionToken ( ) ) ;
USER _TOKENS [ userId ] = sessionToken ;
2024-09-08 18:34:21 +01:00
2024-09-07 21:39:45 +01:00
// Attempt to find a question in the database that hasn't been served recently
triviaQuestion = await TriviaQuestion . findOne ( {
2024-09-06 02:11:06 +01:00
last _served : { $lt : new Date ( Date . now ( ) - QUESTION _EXPIRY ) } ,
category : categoryName ,
} ) . sort ( { last _served : 1 } ) ;
2024-09-06 02:23:33 +01:00
if ( ! triviaQuestion || Date . now ( ) - LAST _API _CALL . time >= API _INTERVAL ) {
2024-09-07 21:39:45 +01:00
// If no question was found in the database or API cooldown is over, fetch from API
2024-09-06 02:11:06 +01:00
const response = await axios . get (
2024-09-09 11:48:02 +01:00
` https://opentdb.com/api.php?amount=1&category= ${ categoryId } &token= ${ sessionToken } `
2024-09-06 02:11:06 +01:00
) ;
2024-09-07 21:39:45 +01:00
const apiQuestion = response . data . results [ 0 ] ;
2024-09-06 02:11:06 +01:00
2024-09-09 15:18:00 +01:00
// Check if the token is exhausted (response code 3 indicates this)
2024-09-09 15:17:03 +01:00
if ( response . data . response _code === 3 ) {
2024-09-10 12:28:56 +01:00
// Token exhausted, generate a new one
sessionToken = await generateSessionToken ( ) ;
2024-09-10 13:43:19 +01:00
USER _TOKENS [ userId ] = sessionToken ; // Update the user's token
2024-09-09 15:17:03 +01:00
// Retry fetching the question
2024-09-08 18:34:21 +01:00
const retryResponse = await axios . get (
2024-09-09 11:48:02 +01:00
` https://opentdb.com/api.php?amount=1&category= ${ categoryId } &token= ${ sessionToken } `
2024-09-08 18:34:21 +01:00
) ;
const retryApiQuestion = retryResponse . data . results [ 0 ] ;
triviaQuestion = await TriviaQuestion . findOne ( {
question : decode ( retryApiQuestion . question ) ,
category : categoryName ,
} ) ;
2024-09-08 18:38:08 +01:00
if ( ! triviaQuestion ) {
await TriviaQuestion . create ( {
question : decode ( retryApiQuestion . question ) ,
correct _answer : decode ( retryApiQuestion . correct _answer ) ,
incorrect _answers : retryApiQuestion . incorrect _answers . map ( decode ) ,
category : categoryName ,
last _served : null ,
} ) ;
triviaQuestion = await TriviaQuestion . findOne ( {
question : decode ( retryApiQuestion . question ) ,
category : categoryName ,
} ) ;
}
} else {
2024-09-08 18:34:21 +01:00
triviaQuestion = await TriviaQuestion . findOne ( {
question : decode ( apiQuestion . question ) ,
category : categoryName ,
} ) ;
2024-09-08 18:38:08 +01:00
if ( ! triviaQuestion ) {
await TriviaQuestion . create ( {
question : decode ( apiQuestion . question ) ,
correct _answer : decode ( apiQuestion . correct _answer ) ,
incorrect _answers : apiQuestion . incorrect _answers . map ( decode ) ,
category : categoryName ,
last _served : null ,
} ) ;
triviaQuestion = await TriviaQuestion . findOne ( {
question : decode ( apiQuestion . question ) ,
category : categoryName ,
} ) ;
}
2024-09-08 18:34:21 +01:00
}
2024-09-07 21:39:45 +01:00
LAST _API _CALL . time = Date . now ( ) ; // Update the last API call time
} else {
// If found in the database, set source to "Database"
source = "Database" ;
2024-09-06 02:11:06 +01:00
}
if ( triviaQuestion ) {
2024-09-07 21:39:45 +01:00
// Update the `last_served` timestamp when serving the question
2024-09-06 02:11:06 +01:00
triviaQuestion . last _served = new Date ( ) ;
await triviaQuestion . save ( ) ;
}
2024-09-07 21:39:45 +01:00
return { triviaQuestion , source } ; // Return both the question and its source
2024-09-06 02:11:06 +01:00
} catch ( error ) {
console . error ( "Error fetching or saving trivia question:" , error ) ;
throw new Error ( "Error fetching trivia question" ) ;
}
} ;
const createTriviaEmbed = (
categoryName ,
question ,
answerMap ,
guild ,
2024-09-07 21:39:45 +01:00
timeLimit ,
source
2024-09-06 02:11:06 +01:00
) => {
return new EmbedBuilder ( )
. setColor ( "#0099ff" )
. setTitle ( ` ${ categoryName } Trivia Question ` )
. setDescription ( question )
. addFields (
Object . entries ( answerMap ) . map ( ( [ number , answer ] ) => ( {
name : ` Option ${ number } ` ,
value : answer ,
inline : true ,
} ) )
)
. setTimestamp ( )
. setFooter ( {
2024-09-07 21:39:45 +01:00
text : ` ${ guild . name } | Answer within ${
timeLimit / 1000
} seconds | Source : $ { source } ` ,
2024-09-06 02:11:06 +01:00
iconURL : guild . iconURL ( ) ,
} ) ;
} ;
const handleAnswerCollection = async (
interaction ,
triviaQuestion ,
answerMap ,
correctAnswer ,
allAnswers ,
timeLimit ,
userId ,
username
) => {
try {
const answerFilter = ( response ) => {
const userInput = response . content . trim ( ) ;
const userAnswerNumber = parseInt ( userInput , 10 ) ;
const userAnswerText =
allAnswers . includes ( userInput ) ||
( answerMap [ userAnswerNumber ] &&
answerMap [ userAnswerNumber ] === correctAnswer ) ;
return (
response . author . id === userId &&
( userAnswerText || ( userAnswerNumber >= 1 && userAnswerNumber <= 4 ) )
) ;
} ;
const answerCollector = interaction . channel . createMessageCollector ( {
filter : answerFilter ,
max : 1 ,
time : timeLimit ,
} ) ;
answerCollector . on ( "collect" , async ( message ) => {
try {
const userInput = message . content . trim ( ) ;
const userAnswerNumber = parseInt ( userInput , 10 ) ;
const userAnswer = answerMap [ userAnswerNumber ] || userInput ;
let resultMessage =
userAnswer === correctAnswer
2024-09-07 23:49:03 +01:00
? "🎉 Correct!"
: ` ❌ Incorrect! the correct answer is ** ${ correctAnswer } .** ` ;
2024-09-06 02:11:06 +01:00
let userScore = await Leaderboard . findOne ( { userId } ) ;
if ( ! userScore ) {
userScore = new Leaderboard ( {
userId ,
username ,
gamesPlayed : 1 ,
correctAnswers : userAnswer === correctAnswer ? 1 : 0 ,
2024-09-07 11:32:03 +01:00
streak : userAnswer === correctAnswer ? 1 : 0 , // Start streak
2024-09-06 02:11:06 +01:00
} ) ;
} else {
userScore . gamesPlayed += 1 ;
if ( userAnswer === correctAnswer ) {
userScore . correctAnswers += 1 ;
2024-09-07 11:32:03 +01:00
userScore . streak += 1 ; // Increment streak
} else {
userScore . streak = 0 ; // Reset streak
2024-09-06 02:11:06 +01:00
}
}
await userScore . save ( ) ;
await interaction . followUp (
2024-09-07 11:32:03 +01:00
` ${ resultMessage } <@ ${ userId } > You've answered ${ userScore . correctAnswers } questions correctly out of ${ userScore . gamesPlayed } games. Your current streak is ** ${ userScore . streak } **. `
2024-09-06 02:11:06 +01:00
) ;
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . delete ( userId ) ;
2024-09-06 02:11:06 +01:00
} catch ( error ) {
console . error ( "Error processing collected answer:" , error ) ;
await interaction . followUp ( {
content : "There was an error processing your answer." ,
ephemeral : true ,
} ) ;
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . delete ( userId ) ;
2024-09-06 02:11:06 +01:00
}
} ) ;
2024-09-07 12:35:19 +01:00
answerCollector . on ( "end" , async ( collected , reason ) => {
2024-09-06 02:11:06 +01:00
if ( reason === "time" ) {
2024-09-07 12:35:19 +01:00
// Reset the user's streak when time runs out
try {
let userScore = await Leaderboard . findOne ( { userId } ) ;
if ( userScore ) {
userScore . streak = 0 ; // Reset streak
await userScore . save ( ) ;
}
await interaction . followUp (
2024-09-07 23:49:03 +01:00
` ⏰ <@ ${ userId } > Time's up! The correct answer is ** ${ correctAnswer } **. Your current streak is ** ${ userScore . streak } **. `
2024-09-07 12:35:19 +01:00
) ;
} catch ( error ) {
console . error ( "Error resetting streak after time limit:" , error ) ;
await interaction . followUp ( {
content : "There was an error resetting your streak." ,
ephemeral : true ,
} ) ;
}
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . delete ( userId ) ;
2024-09-06 02:11:06 +01:00
}
} ) ;
} catch ( error ) {
console . error ( "Error handling answer collection:" , error ) ;
await interaction . followUp ( {
content : "There was an error handling your response." ,
ephemeral : true ,
} ) ;
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . delete ( userId ) ;
2024-09-06 02:11:06 +01:00
}
} ;
2024-09-05 18:49:18 +01:00
module . exports = {
data : new SlashCommandBuilder ( )
. setName ( "trivia" )
2024-09-05 21:29:50 +01:00
. setDescription ( "Play a trivia game" )
. addStringOption ( ( option ) =>
option
. setName ( "category" )
2024-09-24 12:25:21 +01:00
. setDescription ( "Choose a trivia category or random" )
2024-09-05 21:29:50 +01:00
. setRequired ( true )
. addChoices (
2024-09-24 12:25:21 +01:00
{ name : "Random" , value : "random" } ,
2024-09-06 02:11:06 +01:00
... Object . entries ( CATEGORY _MAP ) . map ( ( [ value , name ] ) => ( {
name ,
value ,
} ) )
2024-09-05 21:29:50 +01:00
)
) ,
2024-09-05 18:49:18 +01:00
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
2024-09-07 23:49:03 +01:00
if ( ACTIVE _GAMES . has ( userId ) ) {
2024-09-05 23:55:49 +01:00
return interaction . reply ( {
content :
"You already have an ongoing trivia game. Please finish it before starting a new one." ,
2024-09-06 02:11:06 +01:00
ephemeral : true ,
2024-09-05 23:55:49 +01:00
} ) ;
}
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . add ( userId ) ;
2024-09-05 23:55:49 +01:00
2024-09-06 00:00:30 +01:00
try {
2024-09-24 12:25:21 +01:00
let categoryId = interaction . options . getString ( "category" ) ;
let categoryName ;
if ( categoryId === "random" ) {
// Choose a random category from CATEGORY_MAP
const categoryKeys = Object . keys ( CATEGORY _MAP ) ;
const randomKey =
categoryKeys [ Math . floor ( Math . random ( ) * categoryKeys . length ) ] ;
categoryId = randomKey ; // This is now valid
categoryName = CATEGORY _MAP [ randomKey ] ;
} else {
categoryName = CATEGORY _MAP [ categoryId ] || "Video Games" ;
}
2024-09-05 21:29:50 +01:00
2024-09-07 21:39:45 +01:00
const { triviaQuestion , source } = await fetchTriviaQuestion (
2024-09-10 13:43:19 +01:00
userId ,
2024-09-07 21:39:45 +01:00
categoryId ,
categoryName
) ;
2024-09-06 02:27:50 +01:00
if ( ! triviaQuestion ) {
2024-09-07 21:39:45 +01:00
throw new Error ( "No questions available." ) ;
2024-09-06 02:27:50 +01:00
}
2024-09-06 00:00:30 +01:00
const question = decode ( triviaQuestion . question ) ;
const correctAnswer = decode ( triviaQuestion . correct _answer ) ;
const incorrectAnswers = triviaQuestion . incorrect _answers . map ( decode ) ;
let allAnswers = [ ... incorrectAnswers , correctAnswer ] ;
2024-09-06 00:22:36 +01:00
let answerMap = { } ;
2024-09-06 00:00:30 +01:00
if ( triviaQuestion . type === "boolean" ) {
2024-09-06 00:18:16 +01:00
answerMap = { 1 : "True" , 2 : "False" } ;
} else {
allAnswers = allAnswers . sort ( ( ) => Math . random ( ) - 0.5 ) ;
answerMap = allAnswers . reduce ( ( map , answer , index ) => {
map [ index + 1 ] = answer ;
return map ;
} , { } ) ;
2024-09-06 00:00:30 +01:00
}
2024-09-06 02:11:06 +01:00
const triviaEmbed = createTriviaEmbed (
categoryName ,
question ,
answerMap ,
guild ,
2024-09-07 21:39:45 +01:00
timeLimit ,
source // Pass the source flag (API or Database)
2024-09-06 02:11:06 +01:00
) ;
await interaction . reply ( { embeds : [ triviaEmbed ] } ) ;
await handleAnswerCollection (
interaction ,
triviaQuestion ,
answerMap ,
correctAnswer ,
allAnswers ,
timeLimit ,
userId ,
username
) ;
2024-09-06 00:00:30 +01:00
} catch ( error ) {
2024-09-06 02:11:06 +01:00
console . error ( "Error executing trivia command:" , error ) ;
2024-09-06 00:00:30 +01:00
await interaction . reply ( {
content :
2024-09-06 02:23:33 +01:00
"Trivia API hit the rate limit or encountered an issue. Please try again in 5 seconds." ,
2024-09-06 00:00:30 +01:00
ephemeral : true ,
} ) ;
2024-09-07 23:49:03 +01:00
ACTIVE _GAMES . delete ( userId ) ;
2024-09-06 00:00:30 +01:00
}
2024-09-05 18:49:18 +01:00
} ,
} ;