diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java index 8604ac631b..9d987954ac 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpSystemHelper.java @@ -1,10 +1,7 @@ package org.togetherjava.tjbot.features.help; import net.dv8tion.jda.api.EmbedBuilder; -import net.dv8tion.jda.api.entities.Guild; -import net.dv8tion.jda.api.entities.Message; -import net.dv8tion.jda.api.entities.MessageEmbed; -import net.dv8tion.jda.api.entities.Role; +import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.channel.attribute.IThreadContainer; import net.dv8tion.jda.api.entities.channel.concrete.ForumChannel; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; @@ -12,6 +9,7 @@ import net.dv8tion.jda.api.entities.channel.forums.ForumTagSnowflake; import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; +import net.dv8tion.jda.api.interactions.components.buttons.Button; import net.dv8tion.jda.api.requests.RestAction; import net.dv8tion.jda.api.requests.restaction.MessageCreateAction; import net.dv8tion.jda.api.utils.FileUpload; @@ -26,6 +24,7 @@ import org.togetherjava.tjbot.db.generated.tables.records.HelpThreadsRecord; import org.togetherjava.tjbot.features.chatgpt.ChatGptCommand; import org.togetherjava.tjbot.features.chatgpt.ChatGptService; +import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import javax.annotation.Nullable; @@ -40,6 +39,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -59,6 +59,7 @@ public final class HelpSystemHelper { private static final String CODE_SYNTAX_EXAMPLE_PATH = "codeSyntaxExample.png"; + private final Predicate hasTagManageRole; private final Predicate isHelpForumName; private final String helpForumPattern; /** @@ -88,6 +89,7 @@ public HelpSystemHelper(Config config, Database database, ChatGptService chatGpt this.database = database; this.chatGptService = chatGptService; + hasTagManageRole = Pattern.compile(config.getTagManageRolePattern()).asMatchPredicate(); helpForumPattern = helpConfig.getHelpForumPattern(); isHelpForumName = Pattern.compile(helpForumPattern).asMatchPredicate(); @@ -161,7 +163,7 @@ private RestAction sendExplanationMessage(GuildMessageChannel threadCha * why the message wasn't used. */ RestAction constructChatGptAttempt(ThreadChannel threadChannel, - String originalQuestion) { + String originalQuestion, ComponentIdInteractor componentIdInteractor) { Optional questionOptional = prepareChatGptQuestion(threadChannel, originalQuestion); Optional chatGPTAnswer; @@ -176,6 +178,7 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, return useChatGptFallbackMessage(threadChannel); } + List ids = new CopyOnWriteArrayList<>(); RestAction message = mentionGuildSlashCommand(threadChannel.getGuild(), ChatGptCommand.COMMAND_NAME) .map(""" @@ -183,15 +186,31 @@ RestAction constructChatGptAttempt(ThreadChannel threadChannel, In any case, a human is on the way 👍. To continue talking to the AI, you can use \ %s. """::formatted) - .flatMap(threadChannel::sendMessage); + .flatMap(threadChannel::sendMessage) + .onSuccess(m -> ids.add(m.getId())); + String[] answers = chatGPTAnswer.orElseThrow(); + + for (int i = 0; i < answers.length; i++) { + MessageCreateAction answer = threadChannel.sendMessage(answers[i]); + + if (i == answers.length - 1) { + message = message.flatMap(any -> answer + .addActionRow(generateDismissButton(componentIdInteractor, ids))); + continue; + } - for (String aiResponse : chatGPTAnswer.get()) { - message = message.map(aiResponse::formatted).flatMap(threadChannel::sendMessage); + message = message.flatMap(ignored -> answer.onSuccess(m -> ids.add(m.getId()))); } return message; } + private Button generateDismissButton(ComponentIdInteractor componentIdInteractor, + List ids) { + String buttonId = componentIdInteractor.generateComponentId(ids.toArray(String[]::new)); + return Button.danger(buttonId, "Dismiss"); + } + private Optional prepareChatGptQuestion(ThreadChannel threadChannel, String originalQuestion) { String questionTitle = threadChannel.getName(); @@ -344,6 +363,10 @@ private static ForumTag requireTag(String tagName, ForumChannel forumChannel) { return matchingTags.get(0); } + boolean hasTagManageRole(Member member) { + return member.getRoles().stream().map(Role::getName).anyMatch(hasTagManageRole); + } + boolean isHelpForumName(String channelName) { return isHelpForumName.test(channelName); } diff --git a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java index 210b279779..8e5db87dc0 100644 --- a/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java +++ b/application/src/main/java/org/togetherjava/tjbot/features/help/HelpThreadCreatedListener.java @@ -2,18 +2,28 @@ import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; +import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Role; import net.dv8tion.jda.api.entities.channel.concrete.ThreadChannel; import net.dv8tion.jda.api.entities.channel.forums.ForumTag; import net.dv8tion.jda.api.events.channel.ChannelCreateEvent; +import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent; +import net.dv8tion.jda.api.events.interaction.component.SelectMenuInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import net.dv8tion.jda.api.requests.RestAction; import org.togetherjava.tjbot.features.EventReceiver; +import org.togetherjava.tjbot.features.UserInteractionType; +import org.togetherjava.tjbot.features.UserInteractor; +import org.togetherjava.tjbot.features.componentids.ComponentIdGenerator; +import org.togetherjava.tjbot.features.componentids.ComponentIdInteractor; import java.time.Instant; import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -22,13 +32,16 @@ * Will for example record thread metadata in the database and send an explanation message to the * user. */ -public final class HelpThreadCreatedListener extends ListenerAdapter implements EventReceiver { +public final class HelpThreadCreatedListener extends ListenerAdapter + implements EventReceiver, UserInteractor { private final HelpSystemHelper helper; private final Cache threadIdToCreatedAtCache = Caffeine.newBuilder() .maximumSize(1_000) .expireAfterAccess(2, TimeUnit.of(ChronoUnit.MINUTES)) .build(); + private final ComponentIdInteractor componentIdInteractor = + new ComponentIdInteractor(getInteractionType(), getName()); /** * Creates a new instance. @@ -78,8 +91,8 @@ private void handleHelpThreadCreated(ThreadChannel threadChannel) { private RestAction createAIResponse(ThreadChannel threadChannel) { RestAction originalQuestion = threadChannel.retrieveMessageById(threadChannel.getIdLong()); - return originalQuestion.flatMap( - message -> helper.constructChatGptAttempt(threadChannel, message.getContentRaw())); + return originalQuestion.flatMap(message -> helper.constructChatGptAttempt(threadChannel, + message.getContentRaw(), componentIdInteractor)); } private RestAction pinOriginalQuestion(ThreadChannel threadChannel) { @@ -112,4 +125,48 @@ private RestAction sendHelperHeadsUp(ThreadChannel threadChannel) { return threadChannel.sendMessage(headsUpWithoutRole) .flatMap(message -> message.editMessage(headsUpWithRole)); } + + @Override + public String getName() { + return "chatpgt-answer"; + } + + @Override + public UserInteractionType getInteractionType() { + return UserInteractionType.OTHER; + } + + @Override + public void acceptComponentIdGenerator(ComponentIdGenerator generator) { + componentIdInteractor.acceptComponentIdGenerator(generator); + } + + @Override + public void onButtonClick(ButtonInteractionEvent event, List args) { + // This method handles chatgpt's automatic response "dismiss" button + ThreadChannel channel = event.getChannel().asThreadChannel(); + Member interactionUser = Objects.requireNonNull(event.getMember()); + if (channel.getOwnerIdLong() != interactionUser.getIdLong() + && !helper.hasTagManageRole(interactionUser)) { + event.reply("You do not have permission for this action.").setEphemeral(true).queue(); + return; + } + + RestAction deleteMessages = event.getMessage().delete(); + for (String id : args) { + deleteMessages = deleteMessages.and(channel.deleteMessageById(id)); + } + deleteMessages.queue(); + } + + @Override + public void onSelectMenuSelection(SelectMenuInteractionEvent event, List args) { + throw new UnsupportedOperationException("Not used"); + } + + @Override + public void onModalSubmitted(ModalInteractionEvent event, List args) { + throw new UnsupportedOperationException("Not used"); + } + }