[AB-196] adding confirmation requirement to various commands

refactoring command received handler in order to re-use when confirmation has been given
removing reaction from showSuggestion and remind command
adding log message for message deleted message, in case the member left
all commands from now on must work without the member field from the message, as this is not available when retrieving the message
This commit is contained in:
Sheldan
2021-09-10 21:40:54 +02:00
parent da1a71ecdc
commit 16e6caa1f0
51 changed files with 615 additions and 109 deletions

View File

@@ -50,6 +50,7 @@ public class DeleteAssignableRolePlace extends AbstractConditionableCommand {
.templated(true)
.causesReaction(true)
.async(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)

View File

@@ -66,6 +66,7 @@ public class SetExpRole extends AbstractConditionableCommand {
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.requiresConfirmation(true)
.parameters(parameters)
.help(helpInfo)
.build();

View File

@@ -52,6 +52,7 @@ public class SyncRoles extends AbstractConditionableCommand {
.module(ExperienceModuleDefinition.EXPERIENCE)
.templated(true)
.async(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)

View File

@@ -67,6 +67,7 @@ public class UnSetExpRole extends AbstractConditionableCommand {
.templated(true)
.async(true)
.causesReaction(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.parameters(parameters)
.help(helpInfo)

View File

@@ -51,6 +51,7 @@ public class RemoveTrackedInviteLinks extends AbstractConditionableCommand {
.module(InviteFilterModerationModuleDefinition.MODERATION)
.templated(true)
.async(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)

View File

@@ -65,7 +65,10 @@ public class MessageDeleteLogListener implements AsyncMessageDeletedListener {
CachedMessage message = model.getCachedMessage();
memberService.getMemberInServerAsync(model.getServerId(), message.getAuthor().getAuthorId()).thenAccept(member ->
self.executeListener(message, member)
);
).exceptionally(throwable -> {
log.warn("Could not retrieve member {} for message deleted event in guild {}.", message.getAuthor().getAuthorId(), model.getServerId(), throwable);
return null;
});
return DefaultListenerResult.PROCESSED;
}

View File

@@ -44,6 +44,7 @@ public class DecayAllWarnings extends AbstractConditionableCommand {
.module(ModerationModuleDefinition.MODERATION)
.templated(true)
.async(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)

View File

@@ -43,6 +43,7 @@ public class DecayWarnings extends AbstractConditionableCommand {
.name("decayWarnings")
.module(ModerationModuleDefinition.MODERATION)
.templated(true)
.requiresConfirmation(true)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)

View File

@@ -54,7 +54,7 @@ public class AnonReply extends AbstractConditionableCommand {
ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
Long threadId = thread.getId();
return memberService.getMemberInServerAsync(thread.getUser()).thenCompose(member ->
modMailThreadService.relayMessageToDm(threadId, text, commandContext.getMessage(), true, commandContext.getChannel(), commandContext.getUndoActions(), member)
modMailThreadService.loadExecutingMemberAndRelay(threadId, text, commandContext.getMessage(), true, member)
).thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -52,7 +52,7 @@ public class Reply extends AbstractConditionableCommand {
ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
Long threadId = thread.getId();
return memberService.getMemberInServerAsync(thread.getUser()).thenCompose(member ->
modMailThreadService.relayMessageToDm(threadId, text, commandContext.getMessage(), false, commandContext.getChannel(), commandContext.getUndoActions(), member)
modMailThreadService.loadExecutingMemberAndRelay(threadId, text, commandContext.getMessage(), false, member)
).thenApply(aVoid -> CommandResult.fromSuccess());
}

View File

@@ -553,8 +553,14 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Override
@Transactional
public CompletableFuture<Void> relayMessageToDm(Long modmailThreadId, String text, Message replyCommandMessage, boolean anonymous, MessageChannel feedBack, List<UndoActionInstance> undoActions, Member targetMember) {
public CompletableFuture<Void> loadExecutingMemberAndRelay(Long modmailThreadId, String text, Message replyCommandMessage, boolean anonymous, Member targetMember) {
log.info("Relaying message {} to user {} in modmail thread {} on server {}.", replyCommandMessage.getId(), targetMember.getId(), modmailThreadId, targetMember.getGuild().getId());
return memberService.getMemberInServerAsync(replyCommandMessage.getGuild().getIdLong(), replyCommandMessage.getAuthor().getIdLong())
.thenCompose(executingMember -> self.relayMessageToDm(modmailThreadId, text, replyCommandMessage, anonymous, targetMember, executingMember));
}
@Transactional
public CompletableFuture<Void> relayMessageToDm(Long modmailThreadId, String text, Message replyCommandMessage, boolean anonymous, Member targetMember, Member executingMember) {
metricService.incrementCounter(MDOMAIL_THREAD_MESSAGE_SENT);
ModMailThread modMailThread = modMailThreadManagementService.getById(modmailThreadId);
FullUserInServer fullThreadUser = FullUserInServer
@@ -573,7 +579,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
log.debug("Message is sent anonymous.");
modMailModeratorReplyModelBuilder.moderator(memberService.getBotInGuild(modMailThread.getServer()));
} else {
modMailModeratorReplyModelBuilder.moderator(replyCommandMessage.getMember());
modMailModeratorReplyModelBuilder.moderator(executingMember);
}
ModMailModeratorReplyModel modMailUserReplyModel = modMailModeratorReplyModelBuilder.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(MODMAIL_STAFF_MESSAGE_TEMPLATE_KEY, modMailUserReplyModel, modMailThread.getServer().getId());

View File

@@ -67,12 +67,10 @@ public interface ModMailThreadService {
* @param text The parsed text of the reply
* @param message The pure {@link Message} containing the command which caused the reply
* @param anonymous Whether or nor the message should be send anonymous
* @param feedBack The {@link MessageChannel} in which feedback about possible exceptions should be sent to
* @param undoActions A list of {@link dev.sheldan.abstracto.core.models.UndoAction actions} to be undone in case the operation fails. This list will be filled in the method.
* @param targetMember The {@link Member} the {@link ModMailThread} is about.
* @return A {@link CompletableFuture future} which completes when the message has been relayed to the DM
*/
CompletableFuture<Void> relayMessageToDm(Long threadId, String text, Message message, boolean anonymous, MessageChannel feedBack, List<UndoActionInstance> undoActions, Member targetMember);
CompletableFuture<Void> loadExecutingMemberAndRelay(Long threadId, String text, Message message, boolean anonymous, Member targetMember);
/**
* Closes the mod mail thread which means: deletes the {@link net.dv8tion.jda.api.entities.TextChannel} associated with the mod mail thread,

View File

@@ -70,7 +70,7 @@ public class Remind extends AbstractConditionableCommand {
.module(UtilityModuleDefinition.UTILITY)
.templated(true)
.supportsEmbedException(true)
.causesReaction(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();

View File

@@ -109,7 +109,10 @@ public class RemindServiceBean implements ReminderService {
} else {
HashMap<Object, Object> parameters = new HashMap<>();
parameters.put("reminderId", reminder.getId().toString());
JobParameters jobParameters = JobParameters.builder().parameters(parameters).build();
JobParameters jobParameters = JobParameters
.builder()
.parameters(parameters)
.build();
String triggerKey = schedulerService.executeJobWithParametersOnce("reminderJob", "utility", jobParameters, Date.from(reminder.getTargetDate()));
log.info("Starting scheduled job with trigger {} to execute reminder {}.", triggerKey, reminder.getId());
reminder.setJobTriggerKey(triggerKey);

View File

@@ -50,6 +50,7 @@ public class PurgeImagePosts extends AbstractConditionableCommand {
.module(RepostDetectionModuleDefinition.REPOST_DETECTION)
.templated(true)
.async(false)
.requiresConfirmation(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)

View File

@@ -50,6 +50,7 @@ public class PurgeReposts extends AbstractConditionableCommand {
.module(RepostDetectionModuleDefinition.REPOST_DETECTION)
.templated(true)
.async(false)
.requiresConfirmation(true)
.supportsEmbedException(true)
.causesReaction(true)
.parameters(parameters)

View File

@@ -51,6 +51,7 @@ public class DeleteTrackedEmote extends AbstractConditionableCommand {
.templated(true)
.supportsEmbedException(true)
.causesReaction(true)
.requiresConfirmation(true)
.parameters(parameters)
.help(helpInfo)
.build();

View File

@@ -60,6 +60,7 @@ public class PurgeEmoteStats extends AbstractConditionableCommand {
.module(EmoteTrackingModuleDefinition.EMOTE_TRACKING)
.templated(true)
.supportsEmbedException(true)
.requiresConfirmation(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)

View File

@@ -41,6 +41,7 @@ public class ResetEmoteStats extends AbstractConditionableCommand {
.module(EmoteTrackingModuleDefinition.EMOTE_TRACKING)
.templated(true)
.supportsEmbedException(true)
.requiresConfirmation(true)
.causesReaction(true)
.parameters(parameters)
.help(helpInfo)

View File

@@ -58,7 +58,7 @@ public class ShowSuggestion extends AbstractConditionableCommand {
.templated(true)
.async(true)
.supportsEmbedException(true)
.causesReaction(true)
.causesReaction(false)
.parameters(parameters)
.help(helpInfo)
.build();

View File

@@ -114,7 +114,13 @@ public class SuggestionServiceBean implements SuggestionService {
@Override
public CompletableFuture<Void> createSuggestionMessage(Message commandMessage, String text) {
Member suggester = commandMessage.getMember();
// it is done that way, because we cannot always be sure, that the message containsn the member
return memberService.getMemberInServerAsync(commandMessage.getGuild().getIdLong(), commandMessage.getAuthor().getIdLong())
.thenCompose(suggester -> self.createMessageWithSuggester(commandMessage, text, suggester));
}
@Transactional
public CompletableFuture<Void> createMessageWithSuggester(Message commandMessage, String text, Member suggester) {
Long serverId = suggester.getGuild().getIdLong();
AServer server = serverManagementService.loadServer(serverId);
AUserInAServer userSuggester = userInServerManagementService.loadOrCreateUser(suggester);
@@ -126,7 +132,7 @@ public class SuggestionServiceBean implements SuggestionService {
.state(SuggestionState.NEW)
.serverId(serverId)
.message(commandMessage)
.member(commandMessage.getMember())
.member(suggester)
.suggesterUser(userSuggester)
.useButtons(useButtons)
.suggester(suggester.getUser())
@@ -210,22 +216,24 @@ public class SuggestionServiceBean implements SuggestionService {
@Override
public CompletableFuture<Void> acceptSuggestion(Long suggestionId, Message commandMessage, String text) {
return memberService.getMemberInServerAsync(commandMessage.getGuild().getIdLong(), commandMessage.getAuthor().getIdLong())
.thenCompose(member -> self.setSuggestionToFinalState(member, suggestionId, commandMessage, text, SuggestionState.ACCEPTED));
}
@Transactional
public CompletableFuture<Void> setSuggestionToFinalState(Member executingMember, Long suggestionId, Message commandMessage, String text, SuggestionState state) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.ACCEPTED);
suggestionManagementService.setSuggestionState(suggestion, state);
cancelSuggestionReminder(suggestion);
log.info("Accepting suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(commandMessage.getMember(), text, suggestion);
log.info("Setting suggestion {} in server {} to state {}", suggestionId, suggestion.getServer().getId(), state);
return updateSuggestion(executingMember, text, suggestion);
}
@Override
public CompletableFuture<Void> vetoSuggestion(Long suggestionId, Message commandMessage, String text) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.VETOED);
cancelSuggestionReminder(suggestion);
log.info("Vetoing suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(commandMessage.getMember(), text, suggestion);
return memberService.getMemberInServerAsync(commandMessage.getGuild().getIdLong(), commandMessage.getAuthor().getIdLong())
.thenCompose(member -> self.setSuggestionToFinalState(member, suggestionId, commandMessage, text, SuggestionState.VETOED));
}
private CompletableFuture<Void> updateSuggestion(Member memberExecutingCommand, String reason, Suggestion suggestion) {
@@ -293,12 +301,8 @@ public class SuggestionServiceBean implements SuggestionService {
@Override
public CompletableFuture<Void> rejectSuggestion(Long suggestionId, Message commandMessage, String text) {
Long serverId = commandMessage.getGuild().getIdLong();
Suggestion suggestion = suggestionManagementService.getSuggestion(serverId, suggestionId);
suggestionManagementService.setSuggestionState(suggestion, SuggestionState.REJECTED);
cancelSuggestionReminder(suggestion);
log.info("Rejecting suggestion {} in server {}.", suggestionId, suggestion.getServer().getId());
return updateSuggestion(commandMessage.getMember(), text, suggestion);
return memberService.getMemberInServerAsync(commandMessage.getGuild().getIdLong(), commandMessage.getAuthor().getIdLong())
.thenCompose(member -> self.setSuggestionToFinalState(member, suggestionId, commandMessage, text, SuggestionState.REJECTED));
}
@Override

View File

@@ -1,11 +1,7 @@
package dev.sheldan.abstracto.suggestion.service;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.exception.ChannelNotInGuildException;
import dev.sheldan.abstracto.core.models.ServerSpecificId;
import dev.sheldan.abstracto.core.models.database.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.models.database.AUser;
import dev.sheldan.abstracto.core.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.ServerManagementService;
@@ -16,10 +12,8 @@ import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureDefinition;
import dev.sheldan.abstracto.suggestion.config.SuggestionFeatureMode;
import dev.sheldan.abstracto.suggestion.config.SuggestionPostTarget;
import dev.sheldan.abstracto.suggestion.exception.SuggestionNotFoundException;
import dev.sheldan.abstracto.suggestion.model.database.Suggestion;
import dev.sheldan.abstracto.suggestion.model.database.SuggestionState;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionUpdateModel;
import dev.sheldan.abstracto.suggestion.service.management.SuggestionManagementService;
import net.dv8tion.jda.api.entities.*;
import org.junit.Test;
@@ -113,17 +107,12 @@ public class SuggestionServiceBeanTest {
public void testCreateSuggestionMessage() {
String suggestionText = "text";
when(guild.getIdLong()).thenReturn(SERVER_ID);
when(serverManagementService.loadServer(SERVER_ID)).thenReturn(server);
MessageToSend messageToSend = Mockito.mock(MessageToSend.class);
when(templateService.renderEmbedTemplate(eq(SuggestionServiceBean.SUGGESTION_CREATION_TEMPLATE), any(SuggestionLog.class), eq(SERVER_ID))).thenReturn(messageToSend);
Message suggestionMessage = Mockito.mock(Message.class);
when(counterService.getNextCounterValue(server, SuggestionServiceBean.SUGGESTION_COUNTER_KEY)).thenReturn(SUGGESTION_ID);
List<CompletableFuture<Message>> postingFutures = Arrays.asList(CompletableFuture.completedFuture(suggestionMessage));
when(postTargetService.sendEmbedInPostTarget(messageToSend, SuggestionPostTarget.SUGGESTION, SERVER_ID)).thenReturn(postingFutures);
when(message.getMember()).thenReturn(member);
when(member.getGuild()).thenReturn(guild);
when(member.getIdLong()).thenReturn(SUGGESTER_ID);
when(message.getAuthor()).thenReturn(suggesterUser);
when(message.getGuild()).thenReturn(guild);
when(suggesterUser.getIdLong()).thenReturn(SUGGESTER_ID);
when(memberService.getMemberInServerAsync(SERVER_ID, SUGGESTER_ID)).thenReturn(CompletableFuture.completedFuture(member));
testUnit.createSuggestionMessage(message, suggestionText);
verify(self).createMessageWithSuggester(message, suggestionText, member);
}
@Test
@@ -139,20 +128,26 @@ public class SuggestionServiceBeanTest {
}
@Test(expected = SuggestionNotFoundException.class)
@Test
public void testAcceptNotExistingSuggestion() {
when(suggestionManagementService.getSuggestion(SERVER_ID, SUGGESTION_ID)).thenThrow(new SuggestionNotFoundException(SUGGESTION_ID));
when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getGuild()).thenReturn(guild);
when(message.getAuthor()).thenReturn(suggesterUser);
when(suggesterUser.getIdLong()).thenReturn(SUGGESTER_ID);
when(memberService.getMemberInServerAsync(SERVER_ID, SUGGESTER_ID)).thenReturn(CompletableFuture.completedFuture(member));
testUnit.acceptSuggestion(SUGGESTION_ID, message, CLOSING_TEXT);
verify(self).setSuggestionToFinalState(member, SUGGESTION_ID, message, CLOSING_TEXT, SuggestionState.ACCEPTED);
}
@Test(expected = SuggestionNotFoundException.class)
@Test
public void testRejectNotExistingSuggestion() {
when(suggestionManagementService.getSuggestion(SERVER_ID, SUGGESTION_ID)).thenThrow(new SuggestionNotFoundException(SUGGESTION_ID));
when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getGuild()).thenReturn(guild);
when(message.getAuthor()).thenReturn(suggesterUser);
when(suggesterUser.getIdLong()).thenReturn(SUGGESTER_ID);
when(memberService.getMemberInServerAsync(SERVER_ID, SUGGESTER_ID)).thenReturn(CompletableFuture.completedFuture(member));
testUnit.rejectSuggestion(SUGGESTION_ID, message, CLOSING_TEXT);
verify(self).setSuggestionToFinalState(member, SUGGESTION_ID, message, CLOSING_TEXT, SuggestionState.REJECTED);
}
}

View File

@@ -2,15 +2,15 @@ package dev.sheldan.abstracto.core.command;
import dev.sheldan.abstracto.core.command.condition.ConditionResult;
import dev.sheldan.abstracto.core.command.config.*;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureConfig;
import dev.sheldan.abstracto.core.command.exception.CommandParameterValidationException;
import dev.sheldan.abstracto.core.command.exception.IncorrectParameterException;
import dev.sheldan.abstracto.core.command.exception.InsufficientParametersException;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.UnParsedCommandParameter;
import dev.sheldan.abstracto.core.command.execution.UnparsedCommandParameterPiece;
import dev.sheldan.abstracto.core.command.execution.*;
import dev.sheldan.abstracto.core.command.handler.CommandParameterHandler;
import dev.sheldan.abstracto.core.command.handler.CommandParameterIterators;
import dev.sheldan.abstracto.core.command.model.CommandConfirmationModel;
import dev.sheldan.abstracto.core.command.model.CommandConfirmationPayload;
import dev.sheldan.abstracto.core.command.service.CommandManager;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.ExceptionService;
@@ -20,10 +20,16 @@ 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.context.UserInitiatedServerContext;
import dev.sheldan.abstracto.core.service.EmoteService;
import dev.sheldan.abstracto.core.service.RoleService;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.*;
import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import dev.sheldan.abstracto.scheduling.model.JobParameters;
import dev.sheldan.abstracto.scheduling.service.SchedulerService;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
@@ -35,6 +41,8 @@ import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
@@ -88,6 +96,32 @@ public class CommandReceivedHandler extends ListenerAdapter {
@Autowired
private MetricService metricService;
@Autowired
private ComponentService componentService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private ComponentPayloadService componentPayloadService;
@Autowired
private TemplateService templateService;
@Autowired
private ChannelService channelService;
@Autowired
private SchedulerService schedulerService;
@Autowired
private ConfigService configService;
@Autowired
private MessageService messageService;
public static final String COMMAND_CONFIRMATION_ORIGIN = "commandConfirmation";
public static final String COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY = "command_confirmation_message";
public static final String COMMAND_PROCESSED = "command.processed";
public static final String STATUS_TAG = "status";
public static final CounterMetric COMMANDS_PROCESSED_COUNTER = CounterMetric
@@ -107,38 +141,105 @@ public class CommandReceivedHandler extends ListenerAdapter {
if(!event.isFromGuild()) {
return;
}
if(!commandManager.isCommand(event.getMessage())) {
Message message = event.getMessage();
if(!commandManager.isCommand(message)) {
return;
}
metricService.incrementCounter(COMMANDS_PROCESSED_COUNTER);
final Command foundCommand;
try {
String contentStripped = event.getMessage().getContentRaw();
List<String> parameters = Arrays.asList(contentStripped.split(" "));
UnParsedCommandParameter unParsedParameter = new UnParsedCommandParameter(contentStripped, event.getMessage());
String commandName = commandManager.getCommandName(parameters.get(0), event.getGuild().getIdLong());
foundCommand = commandManager.findCommandByParameters(commandName, unParsedParameter, event.getGuild().getIdLong());
tryToExecuteFoundCommand(event, foundCommand, unParsedParameter);
UnParsedCommandResult result = getUnparsedCommandResult(message);
CompletableFuture<CommandParseResult> parsingFuture = getParametersFromMessage(message, result);
parsingFuture.thenAccept(parsedParameters ->
self.executeCommand(event, parsedParameters.getCommand(), parsedParameters.getParameters())
).exceptionally(throwable -> {
self.reportException(event, result.getCommand(), throwable, "Exception when executing or parsing command.");
return null;
});
} catch (Exception e) {
reportException(event, null, e, String.format("Exception when executing command from message %d in message %d in guild %d."
,event.getMessage().getIdLong(), event.getChannel().getIdLong(), event.getGuild().getIdLong()));
, message.getIdLong(), event.getChannel().getIdLong(), event.getGuild().getIdLong()));
}
}
private void tryToExecuteFoundCommand(MessageReceivedEvent event, Command foundCommand, UnParsedCommandParameter unParsedParameter) {
CompletableFuture<Parameters> parsingFuture = getParsedParameters(unParsedParameter, foundCommand, event.getMessage());
parsingFuture.thenAccept(parsedParameters ->
self.executeCommand(event, foundCommand, parsedParameters)
).exceptionally(throwable -> {
self.reportException(event, foundCommand, throwable, "Exception when executing or parsing command.");
return null;
});
public UnParsedCommandResult getUnparsedCommandResult(Message message) {
String contentStripped = message.getContentRaw();
List<String> parameters = Arrays.asList(contentStripped.split(" "));
UnParsedCommandParameter unParsedParameter = new UnParsedCommandParameter(contentStripped, message);
String commandName = commandManager.getCommandName(parameters.get(0), message.getGuild().getIdLong());
Command foundCommand = commandManager.findCommandByParameters(commandName, unParsedParameter, message.getGuild().getIdLong());
return UnParsedCommandResult
.builder()
.command(foundCommand)
.parameter(unParsedParameter)
.build();
}
public CompletableFuture<CommandParseResult> getParametersFromMessage(Message message) {
UnParsedCommandResult result = getUnparsedCommandResult(message);
return getParsedParameters(result.getParameter(), result.getCommand(), message).thenApply(foundParameters -> CommandParseResult
.builder()
.command(result.getCommand())
.parameters(foundParameters)
.build());
}
public CompletableFuture<CommandParseResult> getParametersFromMessage(Message message, UnParsedCommandResult result) {
return getParsedParameters(result.getParameter(), result.getCommand(), message).thenApply(foundParameters -> CommandParseResult
.builder()
.command(result.getCommand())
.parameters(foundParameters)
.build());
}
@Transactional
public CompletableFuture<Void> cleanupConfirmationMessage(Long server, Long channelId, Long messageId, String confirmationPayloadId, String abortPayloadId) {
componentPayloadManagementService.deletePayloads(Arrays.asList(confirmationPayloadId, abortPayloadId));
return messageService.deleteMessageInChannelInServer(server, channelId, messageId);
}
@Transactional
public void persistConfirmationCallbacks(CommandConfirmationModel model, Message createdMessage) {
AServer server = serverManagementService.loadServer(model.getDriedCommandContext().getServerId());
CommandConfirmationPayload confirmPayload = CommandConfirmationPayload
.builder()
.commandContext(model.getDriedCommandContext())
.otherButtonComponentId(model.getAbortButtonId())
.action(CommandConfirmationPayload.CommandConfirmationAction.CONFIRM)
.build();
componentPayloadService.createButtonPayload(model.getConfirmButtonId(), confirmPayload, COMMAND_CONFIRMATION_ORIGIN, server);
CommandConfirmationPayload abortPayload = CommandConfirmationPayload
.builder()
.commandContext(model.getDriedCommandContext())
.otherButtonComponentId(model.getConfirmButtonId())
.action(CommandConfirmationPayload.CommandConfirmationAction.ABORT)
.build();
componentPayloadService.createButtonPayload(model.getAbortButtonId(), abortPayload, COMMAND_CONFIRMATION_ORIGIN, server);
scheduleConfirmationDeletion(createdMessage, model.getConfirmButtonId(), model.getAbortButtonId());
}
private void scheduleConfirmationDeletion(Message createdMessage, String confirmationPayloadId, String abortPayloadId) {
HashMap<Object, Object> parameters = new HashMap<>();
Long serverId = createdMessage.getGuild().getIdLong();
parameters.put("serverId", serverId.toString());
parameters.put("channelId", createdMessage.getChannel().getId());
parameters.put("messageId", createdMessage.getId());
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);
long channelId = createdMessage.getChannel().getIdLong();
log.info("Scheduling job to delete confirmation message {} in channel {} in server {} at {}.", createdMessage.getIdLong(), channelId, serverId, targetDate);
schedulerService.executeJobWithParametersOnce("confirmationCleanupJob", "core", jobParameters, Date.from(targetDate));
}
@Transactional
public void executeCommand(MessageReceivedEvent event, Command foundCommand, Parameters parsedParameters) {
UserInitiatedServerContext userInitiatedContext = buildTemplateParameter(event);
UserInitiatedServerContext userInitiatedContext = buildUserInitiatedServerContext(event);
CommandContext.CommandContextBuilder commandContextBuilder = CommandContext.builder()
.author(event.getMember())
.guild(event.getGuild())
@@ -153,9 +254,27 @@ public class CommandReceivedHandler extends ListenerAdapter {
conditionResultFuture.thenAccept(conditionResult -> {
CommandResult commandResult = null;
if(conditionResult.isResult()) {
if(foundCommand.getConfiguration().isAsync()) {
CommandConfiguration commandConfiguration = foundCommand.getConfiguration();
if(commandConfiguration.isRequiresConfirmation()) {
DriedCommandContext driedCommandContext = DriedCommandContext.buildFromCommandContext(commandContext);
driedCommandContext.setCommandName(commandConfiguration.getName());
String confirmId = componentService.generateComponentId();
String abortId = componentService.generateComponentId();
CommandConfirmationModel model = CommandConfirmationModel
.builder()
.abortButtonId(abortId)
.confirmButtonId(confirmId)
.driedCommandContext(driedCommandContext)
.commandName(commandConfiguration.getName())
.build();
MessageToSend message = templateService.renderEmbedTemplate(COMMAND_CONFIRMATION_MESSAGE_TEMPLATE_KEY, model, event.getGuild().getIdLong());
List<CompletableFuture<Message>> confirmationMessageFutures = channelService.sendMessageToSendToChannel(message, event.getChannel());
FutureUtils.toSingleFutureGeneric(confirmationMessageFutures)
.thenAccept(unused -> self.persistConfirmationCallbacks(model, confirmationMessageFutures.get(0).join()))
.exceptionally(throwable -> self.failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable));
} else if(commandConfiguration.isAsync()) {
log.info("Executing async command {} for server {} in channel {} based on message {} by user {}.",
foundCommand.getConfiguration().getName(), commandContext.getGuild().getId(), commandContext.getChannel().getId(), commandContext.getMessage().getId(), commandContext.getAuthor().getId());
commandConfiguration.getName(), commandContext.getGuild().getId(), commandContext.getChannel().getId(), commandContext.getMessage().getId(), commandContext.getAuthor().getId());
self.executeAsyncCommand(foundCommand, commandContext)
.exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable));
@@ -174,7 +293,7 @@ public class CommandReceivedHandler extends ListenerAdapter {
private Void failedCommandHandling(MessageReceivedEvent event, Command foundCommand, Parameters parsedParameters, CommandContext commandContext, Throwable throwable) {
log.error("Asynchronous command {} failed.", foundCommand.getConfiguration().getName(), throwable);
UserInitiatedServerContext rebuildUserContext = buildTemplateParameter(event);
UserInitiatedServerContext rebuildUserContext = buildUserInitiatedServerContext(commandContext);
CommandContext rebuildContext = CommandContext.builder()
.author(event.getMember())
.guild(event.getGuild())
@@ -197,15 +316,20 @@ public class CommandReceivedHandler extends ListenerAdapter {
}
@Transactional
public void reportException(MessageReceivedEvent event, Command foundCommand, Throwable throwable, String s) {
UserInitiatedServerContext userInitiatedContext = buildTemplateParameter(event);
public void reportException(CommandContext context, Command foundCommand, Throwable throwable, String s) {
reportException(context.getMessage(), context.getChannel(), context.getAuthor(), foundCommand, throwable, s);
}
@Transactional
public void reportException(Message message, TextChannel textChannel, Member member, Command foundCommand, Throwable throwable, String s) {
UserInitiatedServerContext userInitiatedContext = buildUserInitiatedServerContext(member, textChannel, member.getGuild());
CommandContext.CommandContextBuilder commandContextBuilder = CommandContext.builder()
.author(event.getMember())
.guild(event.getGuild())
.author(member)
.guild(message.getGuild())
.undoActions(new ArrayList<>())
.channel(event.getTextChannel())
.message(event.getMessage())
.jda(event.getJDA())
.channel(message.getTextChannel())
.message(message)
.jda(message.getJDA())
.userInitiatedContext(userInitiatedContext);
log.error(s, throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
@@ -213,6 +337,11 @@ public class CommandReceivedHandler extends ListenerAdapter {
self.executePostCommandListener(foundCommand, commandContext, commandResult);
}
@Transactional
public void reportException(MessageReceivedEvent event, Command foundCommand, Throwable throwable, String s) {
reportException(event.getMessage(), event.getTextChannel(), event.getMember(), foundCommand, throwable, s);
}
private void validateCommandParameters(Parameters parameters, Command foundCommand) {
CommandConfiguration commandConfiguration = foundCommand.getConfiguration();
List<Parameter> parameterList = commandConfiguration.getParameters();
@@ -249,15 +378,23 @@ public class CommandReceivedHandler extends ListenerAdapter {
return foundCommand.execute(commandContext);
}
private UserInitiatedServerContext buildTemplateParameter(MessageReceivedEvent event) {
private UserInitiatedServerContext buildUserInitiatedServerContext(Member member, TextChannel textChannel, Guild guild) {
return UserInitiatedServerContext
.builder()
.member(event.getMember())
.messageChannel(event.getTextChannel())
.guild(event.getGuild())
.member(member)
.messageChannel(textChannel)
.guild(guild)
.build();
}
private UserInitiatedServerContext buildUserInitiatedServerContext(MessageReceivedEvent event) {
return buildUserInitiatedServerContext(event.getMember(), event.getTextChannel(), event.getGuild());
}
private UserInitiatedServerContext buildUserInitiatedServerContext(CommandContext context) {
return buildUserInitiatedServerContext(context.getAuthor(), context.getChannel(), context.getGuild());
}
public CompletableFuture<Parameters> getParsedParameters(UnParsedCommandParameter unParsedCommandParameter, Command command, Message message){
List<ParseResult> parsedParameters = new ArrayList<>();
List<Parameter> parameters = command.getConfiguration().getParameters();
@@ -405,4 +542,18 @@ public class CommandReceivedHandler extends ListenerAdapter {
metricService.registerCounter(COMMANDS_WRONG_PARAMETER_COUNTER, "Commands with incorrect parameter");
this.parameterHandlers = parameterHandlers.stream().sorted(comparing(CommandParameterHandler::getPriority)).collect(Collectors.toList());
}
@Getter
@Builder
public static class CommandParseResult {
private Parameters parameters;
private Command command;
}
@Getter
@Builder
public static class UnParsedCommandResult {
private UnParsedCommandParameter parameter;
private Command command;
}
}

View File

@@ -0,0 +1,118 @@
package dev.sheldan.abstracto.core.command.listener;
import dev.sheldan.abstracto.core.command.CommandReceivedHandler;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.execution.DriedCommandContext;
import dev.sheldan.abstracto.core.command.model.CommandConfirmationPayload;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.config.ListenerPriority;
import dev.sheldan.abstracto.core.interaction.InteractionService;
import dev.sheldan.abstracto.core.listener.ButtonClickedListenerResult;
import dev.sheldan.abstracto.core.listener.async.jda.ButtonClickedListener;
import dev.sheldan.abstracto.core.models.listener.ButtonClickedListenerModel;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.service.management.ComponentPayloadManagementService;
import dev.sheldan.abstracto.core.utils.FutureUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
@Component
@Slf4j
public class ConfirmationButtonClickedListener implements ButtonClickedListener {
@Autowired
private CommandServiceBean commandServiceBean;
@Autowired
private CommandReceivedHandler commandReceivedHandler;
@Autowired
private MessageService messageService;
@Autowired
private ComponentPayloadManagementService componentPayloadManagementService;
@Autowired
private ConfirmationButtonClickedListener self;
@Autowired
private InteractionService interactionService;
@Override
public ButtonClickedListenerResult execute(ButtonClickedListenerModel model) {
CommandConfirmationPayload payload = (CommandConfirmationPayload) model.getDeserializedPayload();
DriedCommandContext commandCtx = payload.getCommandContext();
if(payload.getAction().equals(CommandConfirmationPayload.CommandConfirmationAction.CONFIRM)) {
log.info("Confirming command {} in server {} from message {} in channel {} with event {}.",
commandCtx.getCommandName(), commandCtx.getServerId(), commandCtx.getMessageId(),
commandCtx.getChannelId(), model.getEvent().getInteraction().getId());
commandServiceBean.fillCommandContext(commandCtx)
.thenAccept(context -> self.executeButtonClickedListener(model, payload, context))
.exceptionally(throwable -> {
log.error("Command confirmation failed to execute.", throwable);
return null;
});
} else {
log.info("Denying command {} in server {} from message {} in channel {} with event {}.",
commandCtx.getCommandName(), commandCtx.getServerId(), commandCtx.getMessageId(),
commandCtx.getChannelId(), model.getEvent().getInteraction().getId());
cleanup(model, payload);
}
return ButtonClickedListenerResult.ACKNOWLEDGED;
}
@Transactional
public void executeButtonClickedListener(ButtonClickedListenerModel model, CommandConfirmationPayload payload, CommandServiceBean.RebuiltCommandContext context) {
try {
if(context.getCommand().getConfiguration().isAsync()) {
commandReceivedHandler.executeAsyncCommand(context.getCommand(), context.getContext());
} else {
CommandResult result = commandReceivedHandler.executeCommand(context.getCommand(), context.getContext());
commandReceivedHandler.executePostCommandListener(context.getCommand(), context.getContext(), result);
}
} catch (Exception e) {
commandReceivedHandler.reportException(context.getContext(), context.getCommand(), e, "Confirmation execution of command failed.");
} finally {
cleanup(model, payload);
}
}
private void cleanup(ButtonClickedListenerModel model, CommandConfirmationPayload payload) {
log.debug("Cleaning up component {} and {}.", payload.getOtherButtonComponentId(), model.getEvent().getComponentId());
componentPayloadManagementService.deletePayloads(Arrays.asList(payload.getOtherButtonComponentId(), model.getEvent().getComponentId()));
log.debug("Deleting confirmation message {}.", model.getEvent().getMessageId());
messageService.deleteMessage(model.getEvent().getMessage())
.thenAccept(unused -> self.sendAbortNotification(model))
.exceptionally(throwable -> {
log.warn("Failed to clean up confirmation message {}.", model.getEvent().getMessageId());
return null;
});
}
public CompletableFuture<Void> sendAbortNotification(ButtonClickedListenerModel model) {
log.info("Sending abort notification for message {}", model.getEvent().getMessageId());
return FutureUtils.toSingleFutureGeneric(interactionService.sendMessageToInteraction("command_aborted_notification", new Object(), model.getEvent().getHook()));
}
@Override
public Boolean handlesEvent(ButtonClickedListenerModel model) {
return model.getOrigin().equals(CommandReceivedHandler.COMMAND_CONFIRMATION_ORIGIN);
}
@Override
public FeatureDefinition getFeature() {
return CoreFeatureDefinition.CORE_FEATURE;
}
@Override
public Integer getPriority() {
return ListenerPriority.MEDIUM;
}
}

View File

@@ -0,0 +1,14 @@
package dev.sheldan.abstracto.core.command.model;
import dev.sheldan.abstracto.core.command.execution.DriedCommandContext;
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class CommandConfirmationModel {
private String confirmButtonId;
private String abortButtonId;
private DriedCommandContext driedCommandContext;
private String commandName;
}

View File

@@ -0,0 +1,21 @@
package dev.sheldan.abstracto.core.command.model;
import dev.sheldan.abstracto.core.command.execution.DriedCommandContext;
import dev.sheldan.abstracto.core.models.template.button.ButtonPayload;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Builder
@Setter
public class CommandConfirmationPayload implements ButtonPayload {
private DriedCommandContext commandContext;
private CommandConfirmationAction action;
private String otherButtonComponentId;
public enum CommandConfirmationAction {
CONFIRM, ABORT
}
}

View File

@@ -1,4 +1,4 @@
package dev.sheldan.abstracto.core.command.service;
package dev.sheldan.abstracto.core.command.model;
import com.google.common.collect.Iterables;
import dev.sheldan.abstracto.core.command.Command;
@@ -10,20 +10,29 @@ import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.config.Parameters;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.DriedCommandContext;
import dev.sheldan.abstracto.core.command.execution.UnParsedCommandParameter;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.model.database.ACommandInAServer;
import dev.sheldan.abstracto.core.command.model.database.AModule;
import dev.sheldan.abstracto.core.command.service.CommandRegistry;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.management.CommandInServerManagementService;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.command.service.management.ModuleManagementService;
import dev.sheldan.abstracto.core.config.FeatureDefinition;
import dev.sheldan.abstracto.core.models.context.UserInitiatedServerContext;
import dev.sheldan.abstracto.core.models.database.AFeature;
import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.MemberService;
import dev.sheldan.abstracto.core.service.MessageService;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@@ -59,6 +68,12 @@ public class CommandServiceBean implements CommandService {
@Autowired
private CommandReceivedHandler commandReceivedHandler;
@Autowired
private MemberService memberService;
@Autowired
private MessageService messageService;
@Override
public ACommand createCommand(String name, String moduleName, FeatureDefinition featureDefinition) {
AModule module = moduleManagementService.getOrCreate(moduleName);
@@ -216,5 +231,46 @@ public class CommandServiceBean implements CommandService {
}
}
public CompletableFuture<RebuiltCommandContext> fillCommandContext(DriedCommandContext commandContext) {
CompletableFuture<Member> memberFuture = memberService.getMemberInServerAsync(commandContext.getServerId(), commandContext.getUserId());
CompletableFuture<Message> messageFuture = messageService.loadMessage(commandContext.getServerId(), commandContext.getChannelId(), commandContext.getMessageId());
return CompletableFuture.allOf(memberFuture, messageFuture).thenCompose(unused -> {
Message message = messageFuture.join();
CommandReceivedHandler.UnParsedCommandResult unparsedResult = commandReceivedHandler.getUnparsedCommandResult(message);
CompletableFuture<CommandReceivedHandler.CommandParseResult> getParseResult = commandReceivedHandler.getParametersFromMessage(message, unparsedResult);
return getParseResult.thenApply(commandParseResult -> {
Member author = memberFuture.join();
UserInitiatedServerContext userInitiatedServerContext = UserInitiatedServerContext
.builder()
.messageChannel(message.getChannel())
.message(message)
.guild(message.getGuild())
.build();
CommandContext context = CommandContext
.builder()
.channel(message.getTextChannel())
.author(author)
.guild(message.getGuild())
.jda(message.getJDA())
.parameters(commandParseResult.getParameters())
.userInitiatedContext(userInitiatedServerContext)
.message(message)
.build();
return RebuiltCommandContext
.builder()
.context(context)
.command(unparsedResult.getCommand())
.build();
});
});
}
@Builder
@Getter
public static class RebuiltCommandContext {
private CommandContext context;
private Command command;
}
}

View File

@@ -39,6 +39,7 @@ public class DeleteAlias extends AbstractConditionableCommand {
.module(ConfigModuleDefinition.CONFIG)
.parameters(parameters)
.templated(true)
.requiresConfirmation(true)
.supportsEmbedException(true)
.help(helpInfo)
.causesReaction(true)

View File

@@ -9,7 +9,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.CommandServiceBean;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;

View File

@@ -9,7 +9,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.CommandServiceBean;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;

View File

@@ -9,7 +9,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.CommandServiceBean;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;

View File

@@ -9,7 +9,7 @@ import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.CommandServiceBean;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition;

View File

@@ -0,0 +1,40 @@
package dev.sheldan.abstracto.core.job;
import dev.sheldan.abstracto.core.command.CommandReceivedHandler;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
@Slf4j
@DisallowConcurrentExecution
@Component
@PersistJobDataAfterExecution
@Setter
public class ConfirmationCleanupJob extends QuartzJobBean {
private Long serverId;
private Long channelId;
private Long messageId;
private String confirmationPayloadId;
private String abortPayloadId;
@Autowired
private CommandReceivedHandler commandReceivedHandler;
@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;
});
}
}

View File

@@ -60,6 +60,7 @@ public class ComponentPayloadManagementServiceBean implements ComponentPayloadMa
@Override
public void deletePayloads(List<String> ids) {
ids.forEach(payloadId -> log.info("Deleting payload {}", payloadId));
repository.deleteByIdIn(ids);
}

View File

@@ -13,6 +13,9 @@ abstracto.systemConfigs.prefix.stringValue=!
abstracto.systemConfigs.noCommandFoundReporting.name=noCommandFoundReporting
abstracto.systemConfigs.noCommandFoundReporting.stringValue=true
abstracto.systemConfigs.confirmationTimeout.name=confirmationTimeout
abstracto.systemConfigs.confirmationTimeout.longValue=120
abstracto.systemConfigs.maxMessages.name=maxMessages
abstracto.systemConfigs.maxMessages.longValue=3

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd">
<include file="seedData/data.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -0,0 +1,18 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<changeSet author="Sheldan" id="confirmation-cleanup-job-insert">
<insert tableName="scheduler_job">
<column name="name" value="confirmationCleanupJob"/>
<column name="group_name" value="core"/>
<column name="clazz" value="dev.sheldan.abstracto.core.job.ConfirmationCleanupJob"/>
<column name="active" value="true"/>
<column name="recovery" value="false"/>
</insert>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xmlns:pro="http://www.liquibase.org/xml/ns/pro"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog dbchangelog.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext dbchangelog.xsd
http://www.liquibase.org/xml/ns/pro dbchangelog.xsd" >
<include file="confirmationCleanupJob.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -20,4 +20,5 @@
<include file="1.3.1/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.5/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.6/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.9/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog>

View File

@@ -122,6 +122,9 @@ public class CommandReceivedHandlerTest {
@Mock
private List<Member> members;
@Mock
private Member member;
@Mock
private Bag<Role> roles;
@@ -161,6 +164,8 @@ public class CommandReceivedHandlerTest {
when(commandManager.isCommand(message)).thenReturn(true);
when(event.getGuild()).thenReturn(guild);
when(event.getChannel()).thenReturn(channel);
when(event.getMember()).thenReturn(member);
when(message.getGuild()).thenReturn(guild);
when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getContentRaw()).thenReturn(MESSAGE_CONTENT_COMMAND_ONLY);
when(commandManager.getCommandName(anyString(), eq(SERVER_ID))).thenReturn(COMMAND_NAME);
@@ -286,9 +291,9 @@ public class CommandReceivedHandlerTest {
parameterHandlers.add(parameterHandler);
parameterHandlers.add(secondParameterHandler);
when(event.isFromGuild()).thenReturn(true);
when(message.getGuild()).thenReturn(guild);
when(event.getMessage()).thenReturn(message);
when(commandManager.isCommand(message)).thenReturn(true);
when(event.getGuild()).thenReturn(guild);
when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getContentRaw()).thenReturn(messageContentTwoParameter);
when(commandManager.getCommandName(anyString(), eq(SERVER_ID))).thenReturn(COMMAND_NAME);

View File

@@ -3,6 +3,7 @@ package dev.sheldan.abstracto.core.command.service;
import dev.sheldan.abstracto.core.command.Command;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;

View File

@@ -7,7 +7,6 @@ import dev.sheldan.abstracto.core.command.config.EffectConfig;
import dev.sheldan.abstracto.core.command.execution.CommandContext;
import dev.sheldan.abstracto.core.command.service.management.CommandInServerManagementService;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.RoleImmunity;
import dev.sheldan.abstracto.core.service.RoleImmunityService;
import dev.sheldan.abstracto.core.service.RoleService;
@@ -113,7 +112,6 @@ public class ImmuneUserCondition implements CommandCondition {
return;
}
} else {
resultFuture.completeExceptionally(new AbstractoRunTimeException("No member found for given member in condition."));
return;
}
} else {

View File

@@ -19,6 +19,9 @@ public class CommandConfiguration {
@Builder.Default
private boolean supportsEmbedException = false;
@Builder.Default
private boolean requiresConfirmation = false;
@Builder.Default
private List<Parameter> parameters = new ArrayList<>();

View File

@@ -14,6 +14,7 @@ public class CoreFeatureConfig implements FeatureConfig {
public static final String SUCCESS_REACTION_KEY = "successReaction";
public static final String WARN_REACTION_KEY = "warnReaction";
public static final String MAX_MESSAGES_KEY = "maxMessages";
public static final String CONFIRMATION_TIMEOUT = "confirmationTimeout";
@Override
public FeatureDefinition getFeature() {
@@ -27,6 +28,6 @@ public class CoreFeatureConfig implements FeatureConfig {
@Override
public List<String> getRequiredSystemConfigKeys() {
return Arrays.asList(NO_COMMAND_REPORTING_CONFIG_KEY, MAX_MESSAGES_KEY);
return Arrays.asList(NO_COMMAND_REPORTING_CONFIG_KEY, MAX_MESSAGES_KEY, CONFIRMATION_TIMEOUT);
}
}

View File

@@ -0,0 +1,26 @@
package dev.sheldan.abstracto.core.command.execution;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@Builder
public class DriedCommandContext {
private String commandName;
private Long serverId;
private Long channelId;
private Long messageId;
private Long userId;
public static DriedCommandContext buildFromCommandContext(CommandContext commandContext) {
return DriedCommandContext
.builder()
.channelId(commandContext.getChannel().getIdLong())
.messageId(commandContext.getMessage().getIdLong())
.serverId(commandContext.getGuild().getIdLong())
.userId(commandContext.getMessage().getAuthor().getIdLong())
.build();
}
}

View File

@@ -51,7 +51,7 @@ Change configuration of ARP::
Delete an ARP::
* Usage: `deleteAssignableRolePlace <name>`
* Description: Completely deletes the ARP identified by `name`. This includes any trace in the database and the current message, if any.
* Description: Completely deletes the ARP identified by `name`. This includes any trace in the database and the current message, if any. Requires you to confirm the command.
Change description text of ARP::
* Usage `editAssignableRolePlaceText <name> <newText>`

View File

@@ -2,6 +2,13 @@
The core feature contains necessary commands in order for Abstracto to function and be configured.
==== Relevant system configuration
`noCommandFoundReporting` Whether not found commands should be reported back to the user. Default: true.
`maxMessages` The upper limit of messages created by the template mechanism. Default: 3.
`confirmationTimeout` The duration in seconds after which the confirmation is deleted. Default: 120.
==== Emotes
* `successReaction` reaction emote in case the command completed successfully
* `warnReaction` reaction emote in case the command did not complete successfully
@@ -152,7 +159,7 @@ Create a server specific alias::
* Description: Creates the server specific alias for command `commandName` identified by `alias`. This means that from now on, users can use the command identified by `commandName` by using `alias` in its place, when executing the command or when using the help command. This alias is only available in this server, and it is not allowed to use the names of existing commands or built-in aliases.
Delete a server specific alias::
* Usage: `deleteAlias <alias>`
* Description: Deletes the server specific alias identified by `alias`. It is not possible to delete built-in aliases.
* Description: Deletes the server specific alias identified by `alias`. It is not possible to delete built-in aliases. Requires you to confirm the command.
Creating a profanity group::
* Usage: `createProfanityGroup <profanityGroupName>`
* Description: Creates a profanity group with the given `profanityGroupName`. This name must be unique within the server.

View File

@@ -31,18 +31,18 @@ Setting a role to be awarded at a certain level::
* Usage: `setExpRole <level> <role>`
* Description: Sets `role` to be awarded at the given `level`. If the role was previously assigned,
this will cause to remove this assignment and recalculate the roles for all users previously having this role.
A status image indicating the progress will be shown. It will not award this role to users which qualify for this, a `syncRoles` is necessary for this.
A status image indicating the progress will be shown. It will not award this role to users which qualify for this, a `syncRoles` is necessary for this. Requires you to confirm the command.
* Example: `setExpRole 50 @HighLevel` in order to award the role `HighLevel` at level `50` (the @HighLevel is a role mention)
Syncing the roles of the members with the configuration::
* Usage: `syncRoles`
* Description: Recalculates the appropriate levels for all users on the server and awards the roles appropriate for the level.
There will be a message indicating the current status of the progress, and it is highly advised to not execute this command while another instance is still processing.
There will be a message indicating the current status of the progress, and it is highly advised to not execute this command while another instance is still processing. Requires you to confirm the command.
This command can run for a longer period of time, depending on the amount of members in the guild.
Remove a role from being awarded at a certain level::
* Usage: `unSetExpRole <role>`
* Description: Removes this role from the experience tracking, removes the role from all members previously owning it and recalculates their new role according to the configuration.
* Description: Removes this role from the experience tracking, removes the role from all members previously owning it and recalculates their new role according to the configuration. Requires you to confirm the command.
This will provide a status update message displaying the process.
Disable experience gain for a certain role::

View File

@@ -67,7 +67,7 @@ Showing your warnings::
* Description: Displays the amount of warnings of the user executing on the server. This will show both active and total warnings.
Decaying all warnings regardless of the date::
* Usage: `decayAllWarnings`
* Description: This will cause all warnings of this server which are not decayed yet to be decayed instantly.
* Description: This will cause all warnings of this server which are not decayed yet to be decayed instantly. Requires you to confirm the command.
Deleting a warning::
* Usage: `deleteWarning <warnId>`
* Description: Deletes the warning identified by `warnId` completely from the database.
@@ -91,7 +91,7 @@ Feature key: `warnDecay`
==== Commands
Decaying all warnings if necessary::
* Usage: `decayWarnings`
* Description: Triggers the decay of the warnings instantly, which means, every not decayed warning on this server older than the configured amount of days will be decayed and the decay will be logged.
* Description: Triggers the decay of the warnings instantly, which means, every not decayed warning on this server older than the configured amount of days will be decayed and the decay will be logged. Requires you to confirm the command.
=== Muting
@@ -193,7 +193,7 @@ Showing the tracked filtered invites::
Remove all or individual invites from the tracked filtered invites::
* Usage: `removeTrackedInviteLinks [invite]`
* Description: Removes the stored statistic for the given `invite`. In case `invite` is not given, it will delete all tracked filtered invites from the server.
* Description: Removes the stored statistic for the given `invite`. In case `invite` is not given, it will delete all tracked filtered invites from the server. Requires you to confirm the command.
* Mode Restriction: This command is only available when the feature mode `trackUses` is enabled.
=== Profanity filter

View File

@@ -43,13 +43,13 @@ Synchronize the server emotes with the database::
A message containing the amount of emotes deleted and created is shown. If the feature mode `emoteAutoTrack` is enabled, this should only be necessary in case the bot had an outage.
Delete emote usages::
* Usage: `purgeEmoteStats <emote> [period]`
* Description: This command removes any stored usages of `emote`. The `emote` can either be a valid usage or the ID of an emote. If `period` is given, only usages within this time period will be deleted, if it is not provided, the complete timeline will be deleted.
* Description: This command removes any stored usages of `emote`. The `emote` can either be a valid usage or the ID of an emote. If `period` is given, only usages within this time period will be deleted, if it is not provided, the complete timeline will be deleted. Requires you to confirm the command.
Deleting an individual tracked emote::
* Usage: `deleteTrackedEmote <emote>`
* Description: Deletes the tracked emote from the database including the usages. The `emote` can either be a valid usage or the ID of an emote.
* Description: Deletes the tracked emote from the database including the usages. The `emote` can either be a valid usage or the ID of an emote. Requires you to confirm the command.
Reset emote statistics::
* Usage: `resetEmoteStats`
* Description: This will delete all emote usages and tracked emotes in the database.
* Description: This will delete all emote usages and tracked emotes in the database. Requires you to confirm the command.
Show the image of external tracked emotes::
* Usage: `showExternalTrackedEmote <emote>`
* Description: Shows the ID, name, link to the image and the image directly for `emote` in an embed.

View File

@@ -164,11 +164,11 @@ Feature key: `repostDetection`
==== Commands
Remove stored image posts and reposts of whole server or specific member::
* Usage: `purgeImagePosts [member]`
* Description: If `member` is provided, this will delete all stored image hashes (and their reposts) from the database. If `member` is not provided, this will delete all stored image hashes (and their reposts) from the whole server.
* Description: If `member` is provided, this will delete all stored image hashes (and their reposts) from the database. If `member` is not provided, this will delete all stored image hashes (and their reposts) from the whole server. Requires you to confirm the command.
Remove reposts of whole server or specific member::
* Usage: `purgeReposts [member]`
* Description: If `member` is provided, this will delete all reposts of the given member. If `member` is not provided, this will delete all reposts in the whole server.
* Description: If `member` is provided, this will delete all reposts of the given member. If `member` is not provided, this will delete all reposts in the whole server. Requires you to confirm the command.
Show the leaderboard of reposts::
* Usage: `repostLeaderboard [page]`