Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions app/src/main/java/com/togetherjava/tjplays/Bot.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.nio.file.Path;
import java.util.List;
import com.togetherjava.tjplays.listeners.commands.*;
import com.togetherjava.tjplays.trivia.TriviaManager;

import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
Expand Down Expand Up @@ -32,9 +33,12 @@ private static JDA createBot(Config config) {
}

private static List<SlashCommand> getCommands(Config config) {
return List.of(
// Construct JavaQuizCommand with TriviaManager and ChatGptService
TriviaManager triviaManager = new TriviaManager(new com.togetherjava.tjplays.services.chatgpt.ChatGptService(config.openAIApiKey()));
return java.util.Arrays.asList(
new PingCommand(),
new Game2048Command()
new Game2048Command(),
new JavaQuizCommand(triviaManager)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com.togetherjava.tjplays.listeners.commands;

import com.togetherjava.tjplays.trivia.QuizQuestion;
import com.togetherjava.tjplays.trivia.TriviaManager;

import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.components.buttons.Button;

import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

/**
* Handles the /javaquiz command, presenting Java trivia questions to users.
* Uses TriviaManager to fetch questions and manages user quiz state.
* Future contributors can extend this for more quiz features.
*/
public final class JavaQuizCommand extends SlashCommand {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, a JavaDoc would be useful here for future contributors as this is public facing.

private static final String COMMAND_NAME = "javaquiz";

private final TriviaManager triviaManager;
// messageId -> QuizQuestion
private final ConcurrentHashMap<String, QuizQuestion> activeQuestions = new ConcurrentHashMap<>();
// userId -> messageId
private final ConcurrentHashMap<String, String> userActiveQuiz = new ConcurrentHashMap<>();

/**
* Constructs a JavaQuizCommand with a TriviaManager instance.
* @param triviaManager the TriviaManager to use for fetching questions
*/
public JavaQuizCommand(TriviaManager triviaManager) {
super(Commands.slash(COMMAND_NAME, "Get a random Java trivia question"));
this.triviaManager = triviaManager;
}

@Override
public void onSlashCommand(SlashCommandInteractionEvent event) {
String userId = event.getUser().getId();
// Prevent new quiz if user has an active one
if (userActiveQuiz.containsKey(userId)) {
event.reply("You already have an active quiz. Please answer it before starting a new one.").setEphemeral(true).queue();
return;
}

event.deferReply().queue();

Optional<QuizQuestion> question = triviaManager.fetchRandomQuestion();
if (question.isEmpty()) {
event.getHook().editOriginal("Could not fetch a quiz question. Try again later.").queue();
return;
}

QuizQuestion quizQuestion = question.get();
List<String> choices = quizQuestion.getChoices();

String quizContent = String.format("**Java Quiz**\n\n%s\n\n%s",
quizQuestion.getQuestion(),
java.util.stream.IntStream.range(0, choices.size())
.mapToObj(i -> String.format("`%d`) %s", i + 1, choices.get(i)))
.collect(java.util.stream.Collectors.joining("\n"))
);

String messageId = event.getHook().editOriginal(quizContent)
.setActionRow(
Button.primary(COMMAND_NAME + "-1-" + userId, "1"),
Button.primary(COMMAND_NAME + "-2-" + userId, "2"),
Button.primary(COMMAND_NAME + "-3-" + userId, "3"),
Button.primary(COMMAND_NAME + "-4-" + userId, "4")
)
.complete()
.getId();

activeQuestions.put(messageId, quizQuestion);
userActiveQuiz.put(userId, messageId);
}

@Override
public void onButtonInteraction(ButtonInteractionEvent event) {
String buttonId = event.getButton().getId();
if (buttonId == null || !buttonId.startsWith(COMMAND_NAME)) {
return;
}

if (!buttonId.contains(event.getUser().getId())) {
event.reply("This isn't your quiz!").setEphemeral(true).queue();
return;
}

String userId = event.getUser().getId();
QuizQuestion quizQuestion = activeQuestions.remove(event.getMessageId());
// Remove user's active quiz
userActiveQuiz.remove(userId);
if (quizQuestion == null) {
event.reply("This quiz has already been answered.").setEphemeral(true).queue();
return;
}

int chosen = Character.getNumericValue(buttonId.charAt(COMMAND_NAME.length() + 1)) - 1;
boolean correct = chosen == quizQuestion.getCorrectAnswerIndex();

String correctAnswer = quizQuestion.getChoices().get(quizQuestion.getCorrectAnswerIndex());
String result = correct
? "Correct! The answer is: **" + correctAnswer + "**"
: "Wrong! The correct answer was: **" + correctAnswer + "**";

event.editMessage(event.getMessage().getContentRaw() + "\n\n" + result)
.setActionRow(
Button.primary(COMMAND_NAME + "-1-" + userId, "1").asDisabled(),
Button.primary(COMMAND_NAME + "-2-" + userId, "2").asDisabled(),
Button.primary(COMMAND_NAME + "-3-" + userId, "3").asDisabled(),
Button.primary(COMMAND_NAME + "-4-" + userId, "4").asDisabled()
)
.queue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.togetherjava.tjplays.trivia;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;

/**
* Represents a single quiz question for Java trivia.
* Used by TriviaManager and JavaQuizCommand to display questions and validate answers.
* Future contributors can use this for any quiz/trivia feature needing a question, choices, and answer index.
*/
public record QuizQuestion(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is public-facing, it should have a JavaDoc explaining what it is, where it's used in and where it could be used by future contributors.

@JsonProperty("question") String question,
@JsonProperty("choices") List<String> choices,
@JsonProperty("correct") int correctAnswerIndex
) {
@JsonCreator
public QuizQuestion {}

public String getQuestion() {
return question;
}

public List<String> getChoices() {
return choices;
}

/**
* Returns the index of the correct answer in the choices list.
*/
public int getCorrectAnswerIndex() {
return correctAnswerIndex;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.togetherjava.tjplays.trivia;

import com.togetherjava.tjplays.services.chatgpt.ChatGptService;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Optional;

/**
* Manages Java trivia questions using ChatGptService.
* Used by quiz commands to fetch questions for users.
* Future contributors can use this for any trivia/quiz feature.
*/
public class TriviaManager {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is public-facing, it should have a JavaDoc explaining what it is, where it's used in and where it could be used by future contributors.

private final ChatGptService chatGptService;
private final ObjectMapper mapper = new ObjectMapper();

public TriviaManager(ChatGptService chatGptService) {
this.chatGptService = chatGptService;
}

public Optional<QuizQuestion> fetchRandomQuestion() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To respect the principle of least privilege, let's reduce this method's visibility into protected.

String prompt = "Provide one Java trivia question as a JSON object like"
+ " {\"question\":\"...\",\"choices\":[\"A\",\"B\",\"C\",\"D\"],\"correct\":<index>}";
Optional<String> raw = chatGptService.ask(prompt, "java quiz");
if (raw.isEmpty()) {
return Optional.empty();
}
try {
QuizQuestion question = mapper.readValue(raw.get(), QuizQuestion.class);
return Optional.of(question);
} catch (Exception e) {
return Optional.empty();
}
}
}
Empty file added gradle.properties
Empty file.