[AB-xxx] adding ability to track emotes used in reactions

adding ability to have confirmations for slash commands
This commit is contained in:
Sheldan
2025-01-27 01:31:56 +01:00
parent 2c3b16879e
commit ed42940e29
66 changed files with 1630 additions and 266 deletions

View File

@@ -51,7 +51,7 @@ public class SetEmote extends AbstractConditionableCommand {
public CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) {
String emoteKey = slashCommandParameterService.getCommandOption(EMOTE_KEY_PARAMETER, event, String.class);
String emote = slashCommandParameterService.getCommandOption(EMOTE_PARAMETER, event, String.class);
AEmote aEmote = slashCommandParameterService.loadAEmoteFromString(emote, event);
AEmote aEmote = slashCommandParameterService.loadAEmoteFromString(emote, event.getGuild());
emoteManagementService.setEmoteToAEmote(emoteKey, aEmote, event.getGuild().getIdLong());
return interactionService.replyEmbed(RESPONSE_TEMPLATE, new Object(), event)
.thenApply(interactionHook -> CommandResult.fromSuccess());

View File

@@ -10,7 +10,6 @@ import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.core.templating.model.AttachedFile;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FileService;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.MessageEmbed;
@@ -167,7 +166,7 @@ public class InteractionServiceBean implements InteractionService {
}
@Override
public CompletableFuture<Message> editOriginal(MessageToSend messageToSend, InteractionHook interactionHook) {
public CompletableFuture<Message> replaceOriginal(MessageToSend messageToSend, InteractionHook interactionHook) {
Long serverId = interactionHook.getInteraction().getGuild().getIdLong();
if(messageToSend.getEphemeral()) {
@@ -231,9 +230,17 @@ public class InteractionServiceBean implements InteractionService {
if(action == null) {
throw new AbstractoRunTimeException("The callback did not result in any message.");
}
action.setReplace(true);
return action.submit();
}
@Override
public CompletableFuture<Message> replaceOriginal(String template, Object model, InteractionHook interactionHook) {
Long serverId = interactionHook.getInteraction().getGuild().getIdLong();
MessageToSend messageToSend = templateService.renderEmbedTemplate(template, new Object(), serverId);
return replaceOriginal(messageToSend, interactionHook);
}
public CompletableFuture<InteractionHook> replyMessageToSend(MessageToSend messageToSend, IReplyCallback callback) {
Long serverId = callback.getGuild().getIdLong();
ReplyCallbackAction action = null;

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.interaction.job;
import dev.sheldan.abstracto.core.command.CommandReceivedHandler;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandListenerBean;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
@@ -23,18 +24,28 @@ public class ConfirmationCleanupJob extends QuartzJobBean {
private Long messageId;
private String confirmationPayloadId;
private String abortPayloadId;
private Long interactionId;
@Autowired
private CommandReceivedHandler commandReceivedHandler;
@Autowired
private SlashCommandListenerBean slashCommandListenerBean;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("Cleaning up confirmation message {} in server {} in channel {}.", messageId, serverId, channelId);
commandReceivedHandler.cleanupConfirmationMessage(serverId, channelId, messageId, confirmationPayloadId, abortPayloadId)
.thenAccept(unused -> log.info("Deleted confirmation message {}", messageId))
.exceptionally(throwable -> {
log.warn("Failed to cleanup confirmation message {}.", messageId);
return null;
});
// we either clean up a slash command confirmation or a message command interaction
if(interactionId == null) {
log.info("Cleaning up confirmation message {} in server {} in channel {}.", messageId, serverId, channelId);
commandReceivedHandler.cleanupConfirmationMessage(serverId, channelId, messageId, confirmationPayloadId, abortPayloadId)
.thenAccept(unused -> log.info("Deleted confirmation message {}", messageId))
.exceptionally(throwable -> {
log.warn("Failed to cleanup confirmation message {}.", messageId);
return null;
});
} else {
log.info("Cleaning up slash command confirmation message in server {}.", serverId);
slashCommandListenerBean.removeSlashCommandConfirmationInteraction(interactionId, confirmationPayloadId, abortPayloadId);
}
}
}

View File

@@ -0,0 +1,13 @@
package dev.sheldan.abstracto.core.interaction.slash;
import dev.sheldan.abstracto.core.command.Command;
import lombok.Builder;
import lombok.Getter;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
@Getter
@Builder
public class DriedSlashCommand {
private Command command;
private SlashCommandInteractionEvent event;
}

View File

@@ -1,19 +1,41 @@
package dev.sheldan.abstracto.core.interaction.slash;
import static dev.sheldan.abstracto.core.command.CommandReceivedHandler.COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY;
import dev.sheldan.abstracto.core.command.Command;
import dev.sheldan.abstracto.core.command.CommandReceivedHandler;
import dev.sheldan.abstracto.core.command.condition.ConditionResult;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureConfig;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.PostCommandExecution;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.interaction.ComponentPayloadService;
import dev.sheldan.abstracto.core.interaction.ComponentService;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.button.CommandConfirmationModel;
import dev.sheldan.abstracto.core.interaction.slash.payload.SlashCommandConfirmationPayload;
import dev.sheldan.abstracto.core.metric.service.CounterMetric;
import dev.sheldan.abstracto.core.metric.service.MetricService;
import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.ConfigService;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
import dev.sheldan.abstracto.scheduling.model.JobParameters;
import dev.sheldan.abstracto.scheduling.service.SchedulerService;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.build.OptionData;
import org.springframework.beans.factory.annotation.Autowired;
@@ -60,6 +82,30 @@ public class SlashCommandListenerBean extends ListenerAdapter {
@Autowired
private MetricService metricService;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadService componentPayloadService;
@Autowired
private ServerManagementService serverManagementService;
@Autowired
private InteractionService interactionService;
@Autowired
private ConfigService configService;
@Autowired
private SchedulerService schedulerService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
private static final Map<Long, DriedSlashCommand> COMMANDS_WAITING_FOR_CONFIRMATION = new ConcurrentHashMap<>();
public static final String SLASH_COMMAND_CONFIRMATION_ORIGIN = "SLASH_COMMAND_CONFIRMATION";
public static final CounterMetric SLASH_COMMANDS_PROCESSED_COUNTER = CounterMetric
.builder()
.name(CommandReceivedHandler.COMMAND_PROCESSED)
@@ -140,25 +186,118 @@ public class SlashCommandListenerBean extends ListenerAdapter {
});
}
@Transactional(rollbackFor = AbstractoRunTimeException.class)
public void executeCommand(SlashCommandInteractionEvent event, Command command, ConditionResult conditionResult) {
CompletableFuture<CommandResult> commandOutput;
if(conditionResult.isResult()) {
commandOutput = command.executeSlash(event).thenApply(commandResult -> {
log.info("Command {} in server {} was executed.", command.getConfiguration().getName(), event.getGuild().getIdLong());
@Transactional
public void continueSlashCommand(Long interactionId, ButtonInteractionEvent buttonInteractionEvent) {
if(COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
DriedSlashCommand driedSlashCommand = COMMANDS_WAITING_FOR_CONFIRMATION.get(interactionId);
Command commandInstance = driedSlashCommand.getCommand();
String commandName = commandInstance.getConfiguration().getName();
log.info("Continuing slash command {}", commandName);
commandInstance.executeSlash(driedSlashCommand.getEvent()).thenApply(commandResult -> {
log.info("Command {} in server {} was executed after confirmation.", commandName, buttonInteractionEvent.getGuild().getIdLong());
return commandResult;
}).thenAccept(commandResult -> {
self.executePostCommandListener(commandInstance, driedSlashCommand.getEvent(), commandResult);
COMMANDS_WAITING_FOR_CONFIRMATION.remove(interactionId);
}).exceptionally(throwable -> {
log.error("Error while handling post execution of command with confirmation {}", commandName, throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
self.executePostCommandListener(commandInstance, driedSlashCommand.getEvent(), commandResult);
COMMANDS_WAITING_FOR_CONFIRMATION.remove(interactionId);
return null;
});
} else {
commandOutput = CompletableFuture.completedFuture(CommandResult.fromCondition(conditionResult));
log.warn("Interaction was not found in internal map - not continuing interaction from user {} in server {}.", buttonInteractionEvent.getUser().getIdLong(), buttonInteractionEvent.getGuild().getIdLong());
}
commandOutput.thenAccept(commandResult -> {
self.executePostCommandListener(command, event, commandResult);
}).exceptionally(throwable -> {
log.error("Error while handling post execution of command {}", command.getConfiguration().getName(), throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
self.executePostCommandListener(command, event, commandResult);
return null;
});
}
@Transactional
public void removeSlashCommandConfirmationInteraction(Long interactionId, String confirmationPayload, String abortPayload) {
if(COMMANDS_WAITING_FOR_CONFIRMATION.containsKey(interactionId)) {
DriedSlashCommand removedSlashCommand = COMMANDS_WAITING_FOR_CONFIRMATION.remove(interactionId);
SlashCommandInteractionEvent event = removedSlashCommand.getEvent();
event.getInteraction().getHook().deleteOriginal().queue();
log.info("Remove interaction for command {} in server {} from user {}.", removedSlashCommand.getCommand().getConfiguration().getName(), event.getGuild().getIdLong(), event.getUser().getIdLong());
} else {
log.info("Did not find interaction to clean up.");
}
componentPayloadManagementService.deletePayloads(Arrays.asList(confirmationPayload, abortPayload));
}
@Transactional(rollbackFor = AbstractoRunTimeException.class)
public void executeCommand(SlashCommandInteractionEvent event, Command command, ConditionResult conditionResult) {
String commandName = command.getConfiguration().getName();
if(command.getConfiguration().isRequiresConfirmation() && conditionResult.isResult()) {
DriedSlashCommand slashCommand = DriedSlashCommand
.builder()
.command(command)
.event(event)
.build();
COMMANDS_WAITING_FOR_CONFIRMATION.put(event.getIdLong(), slashCommand);
String confirmationId = componentService.generateComponentId();
String abortId = componentService.generateComponentId();
SlashCommandConfirmationPayload confirmPayload = SlashCommandConfirmationPayload
.builder()
.action(SlashCommandConfirmationPayload.CommandConfirmationAction.CONFIRM)
.interactionId(event.getIdLong())
.build();
Long serverId = event.getGuild().getIdLong();
AServer server = serverManagementService.loadServer(event.getGuild());
componentPayloadService.createButtonPayload(confirmationId, confirmPayload, SLASH_COMMAND_CONFIRMATION_ORIGIN, server);
SlashCommandConfirmationPayload denialPayload = SlashCommandConfirmationPayload
.builder()
.action(SlashCommandConfirmationPayload.CommandConfirmationAction.ABORT)
.interactionId(event.getIdLong())
.build();
componentPayloadService.createButtonPayload(abortId, denialPayload, SLASH_COMMAND_CONFIRMATION_ORIGIN, server);
CommandConfirmationModel model = CommandConfirmationModel
.builder()
.abortButtonId(abortId)
.confirmButtonId(confirmationId)
.commandName(commandName)
.build();
Long userId = event.getUser().getIdLong();
interactionService.replyEmbed(COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY, model, event).thenAccept(interactionHook -> {
log.info("Sent confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
}).exceptionally(throwable -> {
log.warn("Failed to send confirmation for command {} in server {} for user {}.", commandName, serverId, userId);
return null;
});
scheduleConfirmationDeletion(event.getIdLong(), confirmationId, abortId, serverId);
} else {
CompletableFuture<CommandResult> commandOutput;
if(conditionResult.isResult()) {
commandOutput = command.executeSlash(event).thenApply(commandResult -> {
log.info("Command {} in server {} was executed.", commandName, event.getGuild().getIdLong());
return commandResult;
});
} else {
commandOutput = CompletableFuture.completedFuture(CommandResult.fromCondition(conditionResult));
}
commandOutput.thenAccept(commandResult -> {
self.executePostCommandListener(command, event, commandResult);
}).exceptionally(throwable -> {
log.error("Error while handling post execution of command {}", commandName, throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
self.executePostCommandListener(command, event, commandResult);
return null;
});
}
}
private void scheduleConfirmationDeletion(Long interactionId, String confirmationPayloadId, String abortPayloadId, Long serverId) {
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("interactionId", interactionId.toString());
parameters.put("confirmationPayloadId", confirmationPayloadId);
parameters.put("abortPayloadId", abortPayloadId);
JobParameters jobParameters = JobParameters
.builder()
.parameters(parameters)
.build();
Long confirmationTimeout = configService.getLongValueOrConfigDefault(CoreFeatureConfig.CONFIRMATION_TIMEOUT, serverId);
Instant targetDate = Instant.now().plus(confirmationTimeout, ChronoUnit.SECONDS);
log.info("Scheduling job to delete slash command confirmation in server {} at {}.", serverId, targetDate);
schedulerService.executeJobWithParametersOnce("confirmationCleanupJob", "core", jobParameters, Date.from(targetDate));
}
@Transactional(propagation = Propagation.REQUIRES_NEW)

View File

@@ -2,17 +2,20 @@ package dev.sheldan.abstracto.core.interaction.slash;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.model.database.ACommandInAServer;
import dev.sheldan.abstracto.core.command.service.management.CommandInServerManagementService;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService;
import dev.sheldan.abstracto.core.service.FeatureConfigService;
import dev.sheldan.abstracto.core.service.FeatureFlagService;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.*;
@@ -52,6 +55,9 @@ public class SlashCommandServiceBean implements SlashCommandService {
@Autowired
private FeatureFlagService featureFlagService;
@Autowired
private InteractionService interactionService;
@Override
public void convertCommandConfigToCommandData(CommandConfiguration commandConfiguration, List<Pair<List<CommandConfiguration>, SlashCommandData>> existingCommands, Long serverId) {
boolean isTemplated = commandConfiguration.isTemplated();
@@ -107,6 +113,22 @@ public class SlashCommandServiceBean implements SlashCommandService {
}
}
@Override
public CompletableFuture<CommandResult> completeConfirmableCommand(SlashCommandInteractionEvent event, String template) {
return completeConfirmableCommand(event, template, new Object());
}
@Override
public CompletableFuture<CommandResult> completeConfirmableCommand(SlashCommandInteractionEvent event, String template, Object parameter) {
if(event.isAcknowledged()) {
return interactionService.replaceOriginal(template, parameter, event.getInteraction().getHook())
.thenApply(interactionHook -> CommandResult.fromIgnored());
} else {
return interactionService.replyMessage(template, parameter, event)
.thenApply(interactionHook -> CommandResult.fromIgnored());
}
}
@Override
public void convertCommandConfigToCommandData(CommandConfiguration commandConfiguration, List<Pair<List<CommandConfiguration>, SlashCommandData>> existingCommands) {
convertCommandConfigToCommandData(commandConfiguration, existingCommands, null);

View File

@@ -0,0 +1,50 @@
package dev.sheldan.abstracto.core.interaction.slash.listener;
import static dev.sheldan.abstracto.core.interaction.slash.SlashCommandListenerBean.SLASH_COMMAND_CONFIRMATION_ORIGIN;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListener;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.interaction.button.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.interaction.slash.SlashCommandListenerBean;
import dev.sheldan.abstracto.core.interaction.slash.payload.SlashCommandConfirmationPayload;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class SlashCommandConfirmationGivenButtonListener implements ButtonClickedListener {
@Autowired
private SlashCommandListenerBean slashCommandListenerBean;
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
SlashCommandConfirmationPayload payload = (SlashCommandConfirmationPayload) model.getDeserializedPayload();
if(payload.getAction().equals(SlashCommandConfirmationPayload.CommandConfirmationAction.CONFIRM)) {
slashCommandListenerBean.continueSlashCommand(payload.getInteractionId(), model.getEvent());
return ButtonClickedListenerResult.ACKNOWLEDGED;
} else {
return ButtonClickedListenerResult.IGNORED;
}
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
return model.getOrigin().equals(SLASH_COMMAND_CONFIRMATION_ORIGIN);
}
@Override
public FeatureDefinition getFeature() {
return CoreFeatureDefinition.CORE_FEATURE;
}
@Override
public Integer getPriority() {
return ListenerPriority.HIGHEST;
}
}

View File

@@ -5,6 +5,7 @@ import dev.sheldan.abstracto.core.interaction.slash.parameter.provider.SlashComm
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.AEmote;
import dev.sheldan.abstracto.core.service.EmoteService;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.emoji.Emoji;
import net.dv8tion.jda.api.interactions.commands.CommandInteractionPayload;
@@ -138,16 +139,16 @@ public class SlashCommandParameterServiceBean implements SlashCommandParameterSe
}
@Override
public AEmote loadAEmoteFromString(String input, CommandInteractionPayload event) {
Emoji emoji = loadEmoteFromString(input, event);
public AEmote loadAEmoteFromString(String input, Guild guild) {
Emoji emoji = loadEmoteFromString(input, guild);
return emoteService.getFakeEmoteFromEmoji(emoji);
}
@Override
public Emoji loadEmoteFromString(String input, CommandInteractionPayload event) {
public Emoji loadEmoteFromString(String input, Guild guild) {
if(StringUtils.isNumeric(input)) {
long emoteId = Long.parseLong(input);
return event.getGuild().getEmojiById(emoteId);
return guild.getEmojiById(emoteId);
}
return Emoji.fromFormatted(input);
}

View File

@@ -0,0 +1,17 @@
package dev.sheldan.abstracto.core.interaction.slash.payload;
import dev.sheldan.abstracto.core.interaction.button.ButtonPayload;
import lombok.Builder;
import lombok.Getter;
@Builder
@Getter
public class SlashCommandConfirmationPayload implements ButtonPayload {
private CommandConfirmationAction action;
private Long interactionId;
public enum CommandConfirmationAction {
CONFIRM, ABORT
}
}