[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) .templated(true)
.causesReaction(true) .causesReaction(true)
.async(true) .async(true)
.requiresConfirmation(true)
.supportsEmbedException(true) .supportsEmbedException(true)
.parameters(parameters) .parameters(parameters)
.help(helpInfo) .help(helpInfo)

View File

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

View File

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

View File

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

View File

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

View File

@@ -65,7 +65,10 @@ public class MessageDeleteLogListener implements AsyncMessageDeletedListener {
CachedMessage message = model.getCachedMessage(); CachedMessage message = model.getCachedMessage();
memberService.getMemberInServerAsync(model.getServerId(), message.getAuthor().getAuthorId()).thenAccept(member -> memberService.getMemberInServerAsync(model.getServerId(), message.getAuthor().getAuthorId()).thenAccept(member ->
self.executeListener(message, 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; return DefaultListenerResult.PROCESSED;
} }

View File

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

View File

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

View File

@@ -54,7 +54,7 @@ public class AnonReply extends AbstractConditionableCommand {
ModMailThread thread = modMailThreadManagementService.getByChannel(channel); ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
Long threadId = thread.getId(); Long threadId = thread.getId();
return memberService.getMemberInServerAsync(thread.getUser()).thenCompose(member -> 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()); ).thenApply(aVoid -> CommandResult.fromSuccess());
} }

View File

@@ -52,7 +52,7 @@ public class Reply extends AbstractConditionableCommand {
ModMailThread thread = modMailThreadManagementService.getByChannel(channel); ModMailThread thread = modMailThreadManagementService.getByChannel(channel);
Long threadId = thread.getId(); Long threadId = thread.getId();
return memberService.getMemberInServerAsync(thread.getUser()).thenCompose(member -> 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()); ).thenApply(aVoid -> CommandResult.fromSuccess());
} }

View File

@@ -553,8 +553,14 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
@Override @Override
@Transactional @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()); 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); metricService.incrementCounter(MDOMAIL_THREAD_MESSAGE_SENT);
ModMailThread modMailThread = modMailThreadManagementService.getById(modmailThreadId); ModMailThread modMailThread = modMailThreadManagementService.getById(modmailThreadId);
FullUserInServer fullThreadUser = FullUserInServer FullUserInServer fullThreadUser = FullUserInServer
@@ -573,7 +579,7 @@ public class ModMailThreadServiceBean implements ModMailThreadService {
log.debug("Message is sent anonymous."); log.debug("Message is sent anonymous.");
modMailModeratorReplyModelBuilder.moderator(memberService.getBotInGuild(modMailThread.getServer())); modMailModeratorReplyModelBuilder.moderator(memberService.getBotInGuild(modMailThread.getServer()));
} else { } else {
modMailModeratorReplyModelBuilder.moderator(replyCommandMessage.getMember()); modMailModeratorReplyModelBuilder.moderator(executingMember);
} }
ModMailModeratorReplyModel modMailUserReplyModel = modMailModeratorReplyModelBuilder.build(); ModMailModeratorReplyModel modMailUserReplyModel = modMailModeratorReplyModelBuilder.build();
MessageToSend messageToSend = templateService.renderEmbedTemplate(MODMAIL_STAFF_MESSAGE_TEMPLATE_KEY, modMailUserReplyModel, modMailThread.getServer().getId()); 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 text The parsed text of the reply
* @param message The pure {@link Message} containing the command which caused 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 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. * @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 * @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, * 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) .module(UtilityModuleDefinition.UTILITY)
.templated(true) .templated(true)
.supportsEmbedException(true) .supportsEmbedException(true)
.causesReaction(true) .causesReaction(false)
.parameters(parameters) .parameters(parameters)
.help(helpInfo) .help(helpInfo)
.build(); .build();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,7 @@
package dev.sheldan.abstracto.suggestion.service; 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.AChannel;
import dev.sheldan.abstracto.core.models.database.AServer; 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.models.database.AUserInAServer;
import dev.sheldan.abstracto.core.service.*; import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.ServerManagementService; 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.SuggestionFeatureMode;
import dev.sheldan.abstracto.suggestion.config.SuggestionPostTarget; import dev.sheldan.abstracto.suggestion.config.SuggestionPostTarget;
import dev.sheldan.abstracto.suggestion.exception.SuggestionNotFoundException; 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.database.SuggestionState;
import dev.sheldan.abstracto.suggestion.model.template.SuggestionLog; 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 dev.sheldan.abstracto.suggestion.service.management.SuggestionManagementService;
import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.*;
import org.junit.Test; import org.junit.Test;
@@ -113,17 +107,12 @@ public class SuggestionServiceBeanTest {
public void testCreateSuggestionMessage() { public void testCreateSuggestionMessage() {
String suggestionText = "text"; String suggestionText = "text";
when(guild.getIdLong()).thenReturn(SERVER_ID); when(guild.getIdLong()).thenReturn(SERVER_ID);
when(serverManagementService.loadServer(SERVER_ID)).thenReturn(server); when(message.getAuthor()).thenReturn(suggesterUser);
MessageToSend messageToSend = Mockito.mock(MessageToSend.class); when(message.getGuild()).thenReturn(guild);
when(templateService.renderEmbedTemplate(eq(SuggestionServiceBean.SUGGESTION_CREATION_TEMPLATE), any(SuggestionLog.class), eq(SERVER_ID))).thenReturn(messageToSend); when(suggesterUser.getIdLong()).thenReturn(SUGGESTER_ID);
Message suggestionMessage = Mockito.mock(Message.class); when(memberService.getMemberInServerAsync(SERVER_ID, SUGGESTER_ID)).thenReturn(CompletableFuture.completedFuture(member));
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);
testUnit.createSuggestionMessage(message, suggestionText); testUnit.createSuggestionMessage(message, suggestionText);
verify(self).createMessageWithSuggester(message, suggestionText, member);
} }
@Test @Test
@@ -139,20 +128,26 @@ public class SuggestionServiceBeanTest {
} }
@Test(expected = SuggestionNotFoundException.class) @Test
public void testAcceptNotExistingSuggestion() { public void testAcceptNotExistingSuggestion() {
when(suggestionManagementService.getSuggestion(SERVER_ID, SUGGESTION_ID)).thenThrow(new SuggestionNotFoundException(SUGGESTION_ID));
when(guild.getIdLong()).thenReturn(SERVER_ID); when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getGuild()).thenReturn(guild); 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); 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() { public void testRejectNotExistingSuggestion() {
when(suggestionManagementService.getSuggestion(SERVER_ID, SUGGESTION_ID)).thenThrow(new SuggestionNotFoundException(SUGGESTION_ID));
when(guild.getIdLong()).thenReturn(SERVER_ID); when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getGuild()).thenReturn(guild); 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); 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.condition.ConditionResult;
import dev.sheldan.abstracto.core.command.config.*; 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.CommandParameterValidationException;
import dev.sheldan.abstracto.core.command.exception.IncorrectParameterException; import dev.sheldan.abstracto.core.command.exception.IncorrectParameterException;
import dev.sheldan.abstracto.core.command.exception.InsufficientParametersException; import dev.sheldan.abstracto.core.command.exception.InsufficientParametersException;
import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.*;
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.handler.CommandParameterHandler; import dev.sheldan.abstracto.core.command.handler.CommandParameterHandler;
import dev.sheldan.abstracto.core.command.handler.CommandParameterIterators; 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.CommandManager;
import dev.sheldan.abstracto.core.command.service.CommandService; import dev.sheldan.abstracto.core.command.service.CommandService;
import dev.sheldan.abstracto.core.command.service.ExceptionService; 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.MetricService;
import dev.sheldan.abstracto.core.metric.service.MetricTag; import dev.sheldan.abstracto.core.metric.service.MetricTag;
import dev.sheldan.abstracto.core.models.context.UserInitiatedServerContext; import dev.sheldan.abstracto.core.models.context.UserInitiatedServerContext;
import dev.sheldan.abstracto.core.service.EmoteService; import dev.sheldan.abstracto.core.models.database.AServer;
import dev.sheldan.abstracto.core.service.RoleService; import dev.sheldan.abstracto.core.service.*;
import dev.sheldan.abstracto.core.service.management.*; 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.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 lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.*; import net.dv8tion.jda.api.entities.*;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent; 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 org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -88,6 +96,32 @@ public class CommandReceivedHandler extends ListenerAdapter {
@Autowired @Autowired
private MetricService metricService; 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 COMMAND_PROCESSED = "command.processed";
public static final String STATUS_TAG = "status"; public static final String STATUS_TAG = "status";
public static final CounterMetric COMMANDS_PROCESSED_COUNTER = CounterMetric public static final CounterMetric COMMANDS_PROCESSED_COUNTER = CounterMetric
@@ -107,38 +141,105 @@ public class CommandReceivedHandler extends ListenerAdapter {
if(!event.isFromGuild()) { if(!event.isFromGuild()) {
return; return;
} }
if(!commandManager.isCommand(event.getMessage())) { Message message = event.getMessage();
if(!commandManager.isCommand(message)) {
return; return;
} }
metricService.incrementCounter(COMMANDS_PROCESSED_COUNTER); metricService.incrementCounter(COMMANDS_PROCESSED_COUNTER);
final Command foundCommand;
try { try {
String contentStripped = event.getMessage().getContentRaw(); UnParsedCommandResult result = getUnparsedCommandResult(message);
List<String> parameters = Arrays.asList(contentStripped.split(" ")); CompletableFuture<CommandParseResult> parsingFuture = getParametersFromMessage(message, result);
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);
} 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()));
}
}
private void tryToExecuteFoundCommand(MessageReceivedEvent event, Command foundCommand, UnParsedCommandParameter unParsedParameter) {
CompletableFuture<Parameters> parsingFuture = getParsedParameters(unParsedParameter, foundCommand, event.getMessage());
parsingFuture.thenAccept(parsedParameters -> parsingFuture.thenAccept(parsedParameters ->
self.executeCommand(event, foundCommand, parsedParameters) self.executeCommand(event, parsedParameters.getCommand(), parsedParameters.getParameters())
).exceptionally(throwable -> { ).exceptionally(throwable -> {
self.reportException(event, foundCommand, throwable, "Exception when executing or parsing command."); self.reportException(event, result.getCommand(), throwable, "Exception when executing or parsing command.");
return null; return null;
}); });
} catch (Exception e) {
reportException(event, null, e, String.format("Exception when executing command from message %d in message %d in guild %d."
, message.getIdLong(), event.getChannel().getIdLong(), event.getGuild().getIdLong()));
}
}
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 @Transactional
public void executeCommand(MessageReceivedEvent event, Command foundCommand, Parameters parsedParameters) { public void executeCommand(MessageReceivedEvent event, Command foundCommand, Parameters parsedParameters) {
UserInitiatedServerContext userInitiatedContext = buildTemplateParameter(event); UserInitiatedServerContext userInitiatedContext = buildUserInitiatedServerContext(event);
CommandContext.CommandContextBuilder commandContextBuilder = CommandContext.builder() CommandContext.CommandContextBuilder commandContextBuilder = CommandContext.builder()
.author(event.getMember()) .author(event.getMember())
.guild(event.getGuild()) .guild(event.getGuild())
@@ -153,9 +254,27 @@ public class CommandReceivedHandler extends ListenerAdapter {
conditionResultFuture.thenAccept(conditionResult -> { conditionResultFuture.thenAccept(conditionResult -> {
CommandResult commandResult = null; CommandResult commandResult = null;
if(conditionResult.isResult()) { 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 {}.", 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) self.executeAsyncCommand(foundCommand, commandContext)
.exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable)); .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) { private Void failedCommandHandling(MessageReceivedEvent event, Command foundCommand, Parameters parsedParameters, CommandContext commandContext, Throwable throwable) {
log.error("Asynchronous command {} failed.", foundCommand.getConfiguration().getName(), throwable); log.error("Asynchronous command {} failed.", foundCommand.getConfiguration().getName(), throwable);
UserInitiatedServerContext rebuildUserContext = buildTemplateParameter(event); UserInitiatedServerContext rebuildUserContext = buildUserInitiatedServerContext(commandContext);
CommandContext rebuildContext = CommandContext.builder() CommandContext rebuildContext = CommandContext.builder()
.author(event.getMember()) .author(event.getMember())
.guild(event.getGuild()) .guild(event.getGuild())
@@ -197,15 +316,20 @@ public class CommandReceivedHandler extends ListenerAdapter {
} }
@Transactional @Transactional
public void reportException(MessageReceivedEvent event, Command foundCommand, Throwable throwable, String s) { public void reportException(CommandContext context, Command foundCommand, Throwable throwable, String s) {
UserInitiatedServerContext userInitiatedContext = buildTemplateParameter(event); 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() CommandContext.CommandContextBuilder commandContextBuilder = CommandContext.builder()
.author(event.getMember()) .author(member)
.guild(event.getGuild()) .guild(message.getGuild())
.undoActions(new ArrayList<>()) .undoActions(new ArrayList<>())
.channel(event.getTextChannel()) .channel(message.getTextChannel())
.message(event.getMessage()) .message(message)
.jda(event.getJDA()) .jda(message.getJDA())
.userInitiatedContext(userInitiatedContext); .userInitiatedContext(userInitiatedContext);
log.error(s, throwable); log.error(s, throwable);
CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable); CommandResult commandResult = CommandResult.fromError(throwable.getMessage(), throwable);
@@ -213,6 +337,11 @@ public class CommandReceivedHandler extends ListenerAdapter {
self.executePostCommandListener(foundCommand, commandContext, commandResult); 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) { private void validateCommandParameters(Parameters parameters, Command foundCommand) {
CommandConfiguration commandConfiguration = foundCommand.getConfiguration(); CommandConfiguration commandConfiguration = foundCommand.getConfiguration();
List<Parameter> parameterList = commandConfiguration.getParameters(); List<Parameter> parameterList = commandConfiguration.getParameters();
@@ -249,15 +378,23 @@ public class CommandReceivedHandler extends ListenerAdapter {
return foundCommand.execute(commandContext); return foundCommand.execute(commandContext);
} }
private UserInitiatedServerContext buildTemplateParameter(MessageReceivedEvent event) { private UserInitiatedServerContext buildUserInitiatedServerContext(Member member, TextChannel textChannel, Guild guild) {
return UserInitiatedServerContext return UserInitiatedServerContext
.builder() .builder()
.member(event.getMember()) .member(member)
.messageChannel(event.getTextChannel()) .messageChannel(textChannel)
.guild(event.getGuild()) .guild(guild)
.build(); .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){ public CompletableFuture<Parameters> getParsedParameters(UnParsedCommandParameter unParsedCommandParameter, Command command, Message message){
List<ParseResult> parsedParameters = new ArrayList<>(); List<ParseResult> parsedParameters = new ArrayList<>();
List<Parameter> parameters = command.getConfiguration().getParameters(); 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"); metricService.registerCounter(COMMANDS_WRONG_PARAMETER_COUNTER, "Commands with incorrect parameter");
this.parameterHandlers = parameterHandlers.stream().sorted(comparing(CommandParameterHandler::getPriority)).collect(Collectors.toList()); 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 com.google.common.collect.Iterables;
import dev.sheldan.abstracto.core.command.Command; 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.Parameter;
import dev.sheldan.abstracto.core.command.config.Parameters; import dev.sheldan.abstracto.core.command.config.Parameters;
import dev.sheldan.abstracto.core.command.execution.CommandContext; 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.execution.UnParsedCommandParameter;
import dev.sheldan.abstracto.core.command.model.database.ACommand; 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.ACommandInAServer;
import dev.sheldan.abstracto.core.command.model.database.AModule; 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.CommandInServerManagementService;
import dev.sheldan.abstracto.core.command.service.management.CommandManagementService; 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.FeatureManagementService;
import dev.sheldan.abstracto.core.command.service.management.ModuleManagementService; import dev.sheldan.abstracto.core.command.service.management.ModuleManagementService;
import dev.sheldan.abstracto.core.config.FeatureDefinition; 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.AFeature;
import dev.sheldan.abstracto.core.models.database.ARole; import dev.sheldan.abstracto.core.models.database.ARole;
import dev.sheldan.abstracto.core.models.database.AServer; 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 dev.sheldan.abstracto.core.utils.CompletableFutureList;
import lombok.Builder;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message; import net.dv8tion.jda.api.entities.Message;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -59,6 +68,12 @@ public class CommandServiceBean implements CommandService {
@Autowired @Autowired
private CommandReceivedHandler commandReceivedHandler; private CommandReceivedHandler commandReceivedHandler;
@Autowired
private MemberService memberService;
@Autowired
private MessageService messageService;
@Override @Override
public ACommand createCommand(String name, String moduleName, FeatureDefinition featureDefinition) { public ACommand createCommand(String name, String moduleName, FeatureDefinition featureDefinition) {
AModule module = moduleManagementService.getOrCreate(moduleName); 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) .module(ConfigModuleDefinition.CONFIG)
.parameters(parameters) .parameters(parameters)
.templated(true) .templated(true)
.requiresConfirmation(true)
.supportsEmbedException(true) .supportsEmbedException(true)
.help(helpInfo) .help(helpInfo)
.causesReaction(true) .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.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand; import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService; 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.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; 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.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand; import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService; 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.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; 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.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand; import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService; 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.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; 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.execution.CommandResult;
import dev.sheldan.abstracto.core.command.model.database.ACommand; import dev.sheldan.abstracto.core.command.model.database.ACommand;
import dev.sheldan.abstracto.core.command.service.CommandService; 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.CommandManagementService;
import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService;
import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; 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 @Override
public void deletePayloads(List<String> ids) { public void deletePayloads(List<String> ids) {
ids.forEach(payloadId -> log.info("Deleting payload {}", payloadId));
repository.deleteByIdIn(ids); repository.deleteByIdIn(ids);
} }

View File

@@ -13,6 +13,9 @@ abstracto.systemConfigs.prefix.stringValue=!
abstracto.systemConfigs.noCommandFoundReporting.name=noCommandFoundReporting abstracto.systemConfigs.noCommandFoundReporting.name=noCommandFoundReporting
abstracto.systemConfigs.noCommandFoundReporting.stringValue=true abstracto.systemConfigs.noCommandFoundReporting.stringValue=true
abstracto.systemConfigs.confirmationTimeout.name=confirmationTimeout
abstracto.systemConfigs.confirmationTimeout.longValue=120
abstracto.systemConfigs.maxMessages.name=maxMessages abstracto.systemConfigs.maxMessages.name=maxMessages
abstracto.systemConfigs.maxMessages.longValue=3 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.1/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.5/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.6/collection.xml" relativeToChangelogFile="true"/>
<include file="1.3.9/collection.xml" relativeToChangelogFile="true"/>
</databaseChangeLog> </databaseChangeLog>

View File

@@ -122,6 +122,9 @@ public class CommandReceivedHandlerTest {
@Mock @Mock
private List<Member> members; private List<Member> members;
@Mock
private Member member;
@Mock @Mock
private Bag<Role> roles; private Bag<Role> roles;
@@ -161,6 +164,8 @@ public class CommandReceivedHandlerTest {
when(commandManager.isCommand(message)).thenReturn(true); when(commandManager.isCommand(message)).thenReturn(true);
when(event.getGuild()).thenReturn(guild); when(event.getGuild()).thenReturn(guild);
when(event.getChannel()).thenReturn(channel); when(event.getChannel()).thenReturn(channel);
when(event.getMember()).thenReturn(member);
when(message.getGuild()).thenReturn(guild);
when(guild.getIdLong()).thenReturn(SERVER_ID); when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getContentRaw()).thenReturn(MESSAGE_CONTENT_COMMAND_ONLY); when(message.getContentRaw()).thenReturn(MESSAGE_CONTENT_COMMAND_ONLY);
when(commandManager.getCommandName(anyString(), eq(SERVER_ID))).thenReturn(COMMAND_NAME); when(commandManager.getCommandName(anyString(), eq(SERVER_ID))).thenReturn(COMMAND_NAME);
@@ -286,9 +291,9 @@ public class CommandReceivedHandlerTest {
parameterHandlers.add(parameterHandler); parameterHandlers.add(parameterHandler);
parameterHandlers.add(secondParameterHandler); parameterHandlers.add(secondParameterHandler);
when(event.isFromGuild()).thenReturn(true); when(event.isFromGuild()).thenReturn(true);
when(message.getGuild()).thenReturn(guild);
when(event.getMessage()).thenReturn(message); when(event.getMessage()).thenReturn(message);
when(commandManager.isCommand(message)).thenReturn(true); when(commandManager.isCommand(message)).thenReturn(true);
when(event.getGuild()).thenReturn(guild);
when(guild.getIdLong()).thenReturn(SERVER_ID); when(guild.getIdLong()).thenReturn(SERVER_ID);
when(message.getContentRaw()).thenReturn(messageContentTwoParameter); when(message.getContentRaw()).thenReturn(messageContentTwoParameter);
when(commandManager.getCommandName(anyString(), eq(SERVER_ID))).thenReturn(COMMAND_NAME); 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.Command;
import dev.sheldan.abstracto.core.command.config.CommandConfiguration; import dev.sheldan.abstracto.core.command.config.CommandConfiguration;
import dev.sheldan.abstracto.core.command.config.Parameter; import dev.sheldan.abstracto.core.command.config.Parameter;
import dev.sheldan.abstracto.core.command.model.CommandServiceBean;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; 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.execution.CommandContext;
import dev.sheldan.abstracto.core.command.service.management.CommandInServerManagementService; 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.CommandManagementService;
import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException;
import dev.sheldan.abstracto.core.models.database.RoleImmunity; import dev.sheldan.abstracto.core.models.database.RoleImmunity;
import dev.sheldan.abstracto.core.service.RoleImmunityService; import dev.sheldan.abstracto.core.service.RoleImmunityService;
import dev.sheldan.abstracto.core.service.RoleService; import dev.sheldan.abstracto.core.service.RoleService;
@@ -113,7 +112,6 @@ public class ImmuneUserCondition implements CommandCondition {
return; return;
} }
} else { } else {
resultFuture.completeExceptionally(new AbstractoRunTimeException("No member found for given member in condition."));
return; return;
} }
} else { } else {

View File

@@ -19,6 +19,9 @@ public class CommandConfiguration {
@Builder.Default @Builder.Default
private boolean supportsEmbedException = false; private boolean supportsEmbedException = false;
@Builder.Default
private boolean requiresConfirmation = false;
@Builder.Default @Builder.Default
private List<Parameter> parameters = new ArrayList<>(); 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 SUCCESS_REACTION_KEY = "successReaction";
public static final String WARN_REACTION_KEY = "warnReaction"; public static final String WARN_REACTION_KEY = "warnReaction";
public static final String MAX_MESSAGES_KEY = "maxMessages"; public static final String MAX_MESSAGES_KEY = "maxMessages";
public static final String CONFIRMATION_TIMEOUT = "confirmationTimeout";
@Override @Override
public FeatureDefinition getFeature() { public FeatureDefinition getFeature() {
@@ -27,6 +28,6 @@ public class CoreFeatureConfig implements FeatureConfig {
@Override @Override
public List<String> getRequiredSystemConfigKeys() { 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:: Delete an ARP::
* Usage: `deleteAssignableRolePlace <name>` * 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:: Change description text of ARP::
* Usage `editAssignableRolePlaceText <name> <newText>` * 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. 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 ==== Emotes
* `successReaction` reaction emote in case the command completed successfully * `successReaction` reaction emote in case the command completed successfully
* `warnReaction` reaction emote in case the command did not complete 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. * 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:: Delete a server specific alias::
* Usage: `deleteAlias <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:: Creating a profanity group::
* Usage: `createProfanityGroup <profanityGroupName>` * Usage: `createProfanityGroup <profanityGroupName>`
* Description: Creates a profanity group with the given `profanityGroupName`. This name must be unique within the server. * 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>` * Usage: `setExpRole <level> <role>`
* Description: Sets `role` to be awarded at the given `level`. If the role was previously assigned, * 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. 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) * 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:: Syncing the roles of the members with the configuration::
* Usage: `syncRoles` * Usage: `syncRoles`
* Description: Recalculates the appropriate levels for all users on the server and awards the roles appropriate for the level. * 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. 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:: Remove a role from being awarded at a certain level::
* Usage: `unSetExpRole <role>` * 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. This will provide a status update message displaying the process.
Disable experience gain for a certain role:: 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. * 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:: Decaying all warnings regardless of the date::
* Usage: `decayAllWarnings` * 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:: Deleting a warning::
* Usage: `deleteWarning <warnId>` * Usage: `deleteWarning <warnId>`
* Description: Deletes the warning identified by `warnId` completely from the database. * Description: Deletes the warning identified by `warnId` completely from the database.
@@ -91,7 +91,7 @@ Feature key: `warnDecay`
==== Commands ==== Commands
Decaying all warnings if necessary:: Decaying all warnings if necessary::
* Usage: `decayWarnings` * 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 === Muting
@@ -193,7 +193,7 @@ Showing the tracked filtered invites::
Remove all or individual invites from the tracked filtered invites:: Remove all or individual invites from the tracked filtered invites::
* Usage: `removeTrackedInviteLinks [invite]` * 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. * Mode Restriction: This command is only available when the feature mode `trackUses` is enabled.
=== Profanity filter === 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. 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:: Delete emote usages::
* Usage: `purgeEmoteStats <emote> [period]` * 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:: Deleting an individual tracked emote::
* Usage: `deleteTrackedEmote <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:: Reset emote statistics::
* Usage: `resetEmoteStats` * 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:: Show the image of external tracked emotes::
* Usage: `showExternalTrackedEmote <emote>` * Usage: `showExternalTrackedEmote <emote>`
* Description: Shows the ID, name, link to the image and the image directly for `emote` in an embed. * 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 ==== Commands
Remove stored image posts and reposts of whole server or specific member:: Remove stored image posts and reposts of whole server or specific member::
* Usage: `purgeImagePosts [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:: Remove reposts of whole server or specific member::
* Usage: `purgeReposts [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:: Show the leaderboard of reposts::
* Usage: `repostLeaderboard [page]` * Usage: `repostLeaderboard [page]`