diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/CommandReceivedHandler.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/CommandReceivedHandler.java index f22f06c78..c0accc609 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/CommandReceivedHandler.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/CommandReceivedHandler.java @@ -149,38 +149,44 @@ public class CommandReceivedHandler extends ListenerAdapter { .userInitiatedContext(userInitiatedContext); validateCommandParameters(parsedParameters, foundCommand); CommandContext commandContext = commandContextBuilder.parameters(parsedParameters).build(); - ConditionResult conditionResult = commandService.isCommandExecutable(foundCommand, commandContext); - CommandResult commandResult = null; - if(conditionResult.isResult()) { - if(foundCommand.getConfiguration().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()); + CompletableFuture conditionResultFuture = commandService.isCommandExecutable(foundCommand, commandContext); + conditionResultFuture.thenAccept(conditionResult -> { + CommandResult commandResult = null; + if(conditionResult.isResult()) { + if(foundCommand.getConfiguration().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()); - self.executeAsyncCommand(foundCommand, commandContext).exceptionally(throwable -> { - log.error("Asynchronous command {} failed.", foundCommand.getConfiguration().getName(), throwable); - UserInitiatedServerContext rebuildUserContext = buildTemplateParameter(event); - CommandContext rebuildContext = CommandContext.builder() - .author(event.getMember()) - .guild(event.getGuild()) - .channel(event.getTextChannel()) - .message(event.getMessage()) - .jda(event.getJDA()) - .undoActions(commandContext.getUndoActions()) // TODO really do this? it would need to guarantee that its available and usable - .userInitiatedContext(rebuildUserContext) - .parameters(parsedParameters).build(); - CommandResult failedResult = CommandResult.fromError(throwable.getMessage(), throwable); - self.executePostCommandListener(foundCommand, rebuildContext, failedResult); - return null; - }); + self.executeAsyncCommand(foundCommand, commandContext) + .exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable)); + } else { + commandResult = self.executeCommand(foundCommand, commandContext); + } } else { - commandResult = self.executeCommand(foundCommand, commandContext); + commandResult = CommandResult.fromCondition(conditionResult); } - } else { - commandResult = CommandResult.fromCondition(conditionResult); - } - if(commandResult != null) { - self.executePostCommandListener(foundCommand, commandContext, commandResult); - } + if(commandResult != null) { + self.executePostCommandListener(foundCommand, commandContext, commandResult); + } + }).exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable)); + + } + + 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); + CommandContext rebuildContext = CommandContext.builder() + .author(event.getMember()) + .guild(event.getGuild()) + .channel(event.getTextChannel()) + .message(event.getMessage()) + .jda(event.getJDA()) + .undoActions(commandContext.getUndoActions()) // TODO really do this? it would need to guarantee that its available and usable + .userInitiatedContext(rebuildUserContext) + .parameters(parsedParameters).build(); + CommandResult failedResult = CommandResult.fromError(throwable.getMessage(), throwable); + self.executePostCommandListener(foundCommand, rebuildContext, failedResult); + return null; } @Transactional(isolation = Isolation.SERIALIZABLE) diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/service/CommandServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/service/CommandServiceBean.java index b773a8143..690154e84 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/service/CommandServiceBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/command/service/CommandServiceBean.java @@ -22,12 +22,14 @@ import dev.sheldan.abstracto.core.config.FeatureDefinition; 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.utils.CompletableFutureList; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Message; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; +import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; @@ -142,12 +144,12 @@ public class CommandServiceBean implements CommandService { log.info("Disallowing feature {} for role {} in server {}.", feature.getKey(), role.getId(), role.getServer().getId()); } - public ConditionResult isCommandExecutable(Command command, CommandContext commandContext) { + public CompletableFuture isCommandExecutable(Command command, CommandContext commandContext) { if(command instanceof ConditionalCommand) { ConditionalCommand castedCommand = (ConditionalCommand) command; return checkConditions(commandContext, command, castedCommand.getConditions()); } else { - return ConditionResult.builder().result(true).build(); + return ConditionResult.fromAsyncSuccess(); } } @@ -179,16 +181,39 @@ public class CommandServiceBean implements CommandService { .build(); } - private ConditionResult checkConditions(CommandContext commandContext, Command command, List conditions) { - if(conditions != null) { + private CompletableFuture checkConditions(CommandContext commandContext, Command command, List conditions) { + if(conditions != null && !conditions.isEmpty()) { + List> futures = new ArrayList<>(); for (CommandCondition condition : conditions) { - ConditionResult conditionResult = condition.shouldExecute(commandContext, command); - if(!conditionResult.isResult()) { - return conditionResult; + if(condition.isAsync()) { + futures.add(condition.shouldExecuteAsync(commandContext, command)); + } else { + futures.add(CompletableFuture.completedFuture(condition.shouldExecute(commandContext, command))); } } + CompletableFuture resultFuture = new CompletableFuture<>(); + CompletableFutureList futureList = new CompletableFutureList<>(futures); + futureList.getMainFuture().whenComplete((unused, throwable) -> { + List results = futureList.getObjects(); + boolean foundResult = false; + for (ConditionResult conditionResult : results) { + if (!conditionResult.isResult()) { + foundResult = true; + resultFuture.complete(conditionResult); + break; + } + } + if(!foundResult) { + resultFuture.complete(ConditionResult.fromSuccess()); + } + }).exceptionally(throwable -> { + resultFuture.completeExceptionally(throwable); + return null; + }); + return resultFuture; + } else { + return ConditionResult.fromAsyncSuccess(); } - return ConditionResult.builder().result(true).build(); } diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/help/Help.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/help/Help.java index 9758ab301..bd8bf1658 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/help/Help.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/help/Help.java @@ -1,6 +1,7 @@ package dev.sheldan.abstracto.core.commands.help; import dev.sheldan.abstracto.core.command.Command; +import dev.sheldan.abstracto.core.command.condition.ConditionResult; import dev.sheldan.abstracto.core.command.config.*; import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition; import dev.sheldan.abstracto.core.command.execution.CommandContext; @@ -21,6 +22,7 @@ import dev.sheldan.abstracto.core.models.template.commands.help.HelpModuleDetail import dev.sheldan.abstracto.core.models.template.commands.help.HelpModuleOverviewModel; import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.RoleService; +import dev.sheldan.abstracto.core.utils.CompletableFutureList; import dev.sheldan.abstracto.core.utils.FutureUtils; import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.core.templating.service.TemplateService; @@ -114,14 +116,25 @@ public class Help implements Command { ModuleDefinition moduleDefinition = moduleService.getModuleByName(parameter); log.debug("Displaying help for module {}.", moduleDefinition.getInfo().getName()); SingleLevelPackedModule module = moduleService.getPackedModule(moduleDefinition); - List commands = module.getCommands(); - List filteredCommands = new ArrayList<>(); - commands.forEach(command -> { - if(commandService.isCommandExecutable(command, commandContext).isResult()) { - filteredCommands.add(command); - } + List filteredCommand = new ArrayList<>(); + List> conditionFutures = new ArrayList<>(); + Map, Command> futureCommandMap = new HashMap<>(); + module.getCommands().forEach(command -> { + // TODO dont provide the parameters, else the condition uses the wrong parameters, as we are not actually executing the command + CompletableFuture future = commandService.isCommandExecutable(command, commandContext); + conditionFutures.add(future); + futureCommandMap.put(future, command); }); - module.setCommands(filteredCommands); + CompletableFutureList conditionFuturesList = new CompletableFutureList<>(conditionFutures); + conditionFuturesList.getMainFuture().thenAccept(unused -> conditionFutures.forEach(conditionResultCompletableFuture -> { + if(!conditionResultCompletableFuture.isCompletedExceptionally()) { + ConditionResult result = conditionResultCompletableFuture.join(); + if(result.isResult()) { + filteredCommand.add(futureCommandMap.get(conditionResultCompletableFuture)); + } + } + })); + module.setCommands(filteredCommand); List subModules = moduleService.getSubModules(moduleDefinition); HelpModuleDetailsModel model = (HelpModuleDetailsModel) ContextConverter.fromCommandContext(commandContext, HelpModuleDetailsModel.class); model.setModule(module); diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/RoleImmunityServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/RoleImmunityServiceBean.java index acfd1c93f..1c496feac 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/RoleImmunityServiceBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/RoleImmunityServiceBean.java @@ -62,7 +62,7 @@ public class RoleImmunityServiceBean implements RoleImmunityService { if(immuneRoles.isEmpty()) { return Optional.empty(); } - return immuneRoles + return immuneRoles .stream() .filter(role -> member .getRoles() diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/CommandCondition.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/CommandCondition.java index c1abebb58..6d76d39c6 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/CommandCondition.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/CommandCondition.java @@ -4,6 +4,16 @@ package dev.sheldan.abstracto.core.command.condition; import dev.sheldan.abstracto.core.command.Command; import dev.sheldan.abstracto.core.command.execution.CommandContext; +import java.util.concurrent.CompletableFuture; + public interface CommandCondition { - ConditionResult shouldExecute(CommandContext commandContext, Command command); + default ConditionResult shouldExecute(CommandContext commandContext, Command command) { + return ConditionResult.fromSuccess(); + } + default boolean isAsync() { + return false; + } + default CompletableFuture shouldExecuteAsync(CommandContext commandContext, Command command) { + return CompletableFuture.completedFuture(ConditionResult.fromSuccess()); + } } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ConditionResult.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ConditionResult.java index 5f223a57d..a5a4ea7e5 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ConditionResult.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ConditionResult.java @@ -4,6 +4,8 @@ import lombok.Builder; import lombok.Getter; import lombok.Setter; +import java.util.concurrent.CompletableFuture; + @Getter @Setter @Builder @@ -12,10 +14,16 @@ public class ConditionResult { private String reason; private ConditionDetail conditionDetail; + public static final ConditionResult SUCCESS = ConditionResult.builder().result(true).build(); + public static ConditionResult fromSuccess() { return ConditionResult.builder().result(true).build(); } + public static CompletableFuture fromAsyncSuccess() { + return CompletableFuture.completedFuture(fromSuccess()); + } + public static ConditionResult fromFailure(ConditionDetail detail) { return ConditionResult.builder().result(false).conditionDetail(detail).build(); } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ImmuneUserCondition.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ImmuneUserCondition.java index c78cfd3d2..4cd7af071 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ImmuneUserCondition.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/condition/ImmuneUserCondition.java @@ -7,16 +7,22 @@ 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; +import dev.sheldan.abstracto.core.utils.CompletableFutureList; import lombok.extern.slf4j.Slf4j; import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.entities.User; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; -import java.util.List; -import java.util.Optional; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; @Component @Slf4j @@ -34,32 +40,92 @@ public class ImmuneUserCondition implements CommandCondition { @Autowired private RoleImmunityService roleImmunityService; + @Autowired + private ImmuneUserCondition self; + @Override - public ConditionResult shouldExecute(CommandContext context, Command command) { + public CompletableFuture shouldExecuteAsync(CommandContext commandContext, Command command) { CommandConfiguration commandConfig = command.getConfiguration(); if(commandConfig.getEffects().isEmpty()) { - return ConditionResult.fromSuccess(); + return ConditionResult.fromAsyncSuccess(); } - List parameters = context.getParameters().getParameters(); + List> futures = new ArrayList<>(); + List parameters = commandContext.getParameters().getParameters(); for (EffectConfig effectConfig : commandConfig.getEffects()) { Integer position = effectConfig.getPosition(); if (position < parameters.size()) { Object parameter = parameters.get(position); if (parameter instanceof Member) { Member member = (Member) parameter; - Optional immunityOptional = roleImmunityService.getRoleImmunity(member, effectConfig.getEffectKey()); - if (immunityOptional.isPresent()) { - RoleImmunity immunity = immunityOptional.get(); - ImmuneUserConditionDetail conditionDetail = new ImmuneUserConditionDetail(roleService.getRoleFromGuild(immunity.getRole()), - effectConfig.getEffectKey()); - return ConditionResult.fromFailure(conditionDetail); - } + futures.add(CompletableFuture.completedFuture(member)); + } else if (parameter instanceof User) { + User user = (User) parameter; + futures.add(commandContext.getGuild().retrieveMember(user).submit()); } } else { log.info("Not enough parameters ({}) in command {} to retrieve position {} to check for immunity.", parameters.size(), commandConfig.getName(), position); } } - return ConditionResult.fromSuccess(); + if(!futures.isEmpty()) { + CompletableFuture resultFuture = new CompletableFuture<>(); + CompletableFutureList futureList = new CompletableFutureList<>(futures); + futureList.getMainFuture().whenComplete((unused, throwable) -> { + if(throwable != null) { + log.warn("Future for user immune condition failed. Continuing processing.", throwable); + } + Map memberMap = futureList + .getObjects() + .stream() + .collect(Collectors.toMap(Member::getIdLong, Function.identity())); + self.checkConditions(commandConfig, parameters, resultFuture, memberMap); + }).exceptionally(throwable -> { + resultFuture.completeExceptionally(throwable); + return null; + }); + + return resultFuture; + } else { + return ConditionResult.fromAsyncSuccess(); + } + } + + @Transactional + public void checkConditions(CommandConfiguration commandConfig, List parameters, CompletableFuture resultFuture, Map memberMap) { + for (EffectConfig effectConfig : commandConfig.getEffects()) { + Integer position = effectConfig.getPosition(); + if (position < parameters.size()) { + Object parameter = parameters.get(position); + Member member = null; + if (parameter instanceof Member) { + member = (Member) parameter; + } else if (parameter instanceof User) { + User user = (User) parameter; + member = memberMap.get(user.getIdLong()); + } + if(member != null) { + Optional immunityOptional = roleImmunityService.getRoleImmunity(member, effectConfig.getEffectKey()); + if (immunityOptional.isPresent()) { + RoleImmunity immunity = immunityOptional.get(); + ImmuneUserConditionDetail conditionDetail = new ImmuneUserConditionDetail(roleService.getRoleFromGuild(immunity.getRole()), + effectConfig.getEffectKey()); + resultFuture.complete(ConditionResult.fromFailure(conditionDetail)); + return; + } + } else { + resultFuture.completeExceptionally(new AbstractoRunTimeException("No member found for given member in condition.")); + return; + } + } else { + log.info("Not enough parameters ({}) in command {} to retrieve position {} to check for immunity.", + parameters.size(), commandConfig.getName(), position); + } + } + resultFuture.complete(ConditionResult.fromSuccess()); + } + + @Override + public boolean isAsync() { + return true; } } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/service/CommandService.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/service/CommandService.java index 5b3b32560..7a847eaad 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/service/CommandService.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/service/CommandService.java @@ -24,7 +24,7 @@ public interface CommandService { void unRestrictCommand(ACommand aCommand, AServer server); void disAllowCommandForRole(ACommand aCommand, ARole role); void disAllowFeatureForRole(FeatureDefinition featureDefinition, ARole role); - ConditionResult isCommandExecutable(Command command, CommandContext commandContext); + CompletableFuture isCommandExecutable(Command command, CommandContext commandContext); UnParsedCommandParameter getUnParsedCommandParameter(String messageContent, Message message); CompletableFuture getParametersForCommand(String commandName, Message messageContainingContent); Parameter cloneParameter(Parameter parameter);