[AB-325] adding async command conditions - as this is required for some conditions

adding user parameter to immune user condition evaluation
This commit is contained in:
Sheldan
2021-09-04 13:47:03 +02:00
parent 69abff77fb
commit d7f889971d
8 changed files with 188 additions and 60 deletions

View File

@@ -149,38 +149,44 @@ public class CommandReceivedHandler extends ListenerAdapter {
.userInitiatedContext(userInitiatedContext); .userInitiatedContext(userInitiatedContext);
validateCommandParameters(parsedParameters, foundCommand); validateCommandParameters(parsedParameters, foundCommand);
CommandContext commandContext = commandContextBuilder.parameters(parsedParameters).build(); CommandContext commandContext = commandContextBuilder.parameters(parsedParameters).build();
ConditionResult conditionResult = commandService.isCommandExecutable(foundCommand, commandContext); CompletableFuture<ConditionResult> conditionResultFuture = commandService.isCommandExecutable(foundCommand, commandContext);
CommandResult commandResult = null; conditionResultFuture.thenAccept(conditionResult -> {
if(conditionResult.isResult()) { CommandResult commandResult = null;
if(foundCommand.getConfiguration().isAsync()) { if(conditionResult.isResult()) {
log.info("Executing async command {} for server {} in channel {} based on message {} by user {}.", if(foundCommand.getConfiguration().isAsync()) {
foundCommand.getConfiguration().getName(), commandContext.getGuild().getId(), commandContext.getChannel().getId(), commandContext.getMessage().getId(), commandContext.getAuthor().getId()); 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 -> { self.executeAsyncCommand(foundCommand, commandContext)
log.error("Asynchronous command {} failed.", foundCommand.getConfiguration().getName(), throwable); .exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable));
UserInitiatedServerContext rebuildUserContext = buildTemplateParameter(event); } else {
CommandContext rebuildContext = CommandContext.builder() commandResult = self.executeCommand(foundCommand, commandContext);
.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;
});
} else { } else {
commandResult = self.executeCommand(foundCommand, commandContext); commandResult = CommandResult.fromCondition(conditionResult);
} }
} else { if(commandResult != null) {
commandResult = CommandResult.fromCondition(conditionResult); self.executePostCommandListener(foundCommand, commandContext, commandResult);
} }
if(commandResult != null) { }).exceptionally(throwable -> failedCommandHandling(event, foundCommand, parsedParameters, commandContext, throwable));
self.executePostCommandListener(foundCommand, commandContext, commandResult);
} }
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) @Transactional(isolation = Isolation.SERIALIZABLE)

View File

@@ -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.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.utils.CompletableFutureList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
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;
import java.io.File; import java.io.File;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.CompletableFuture; 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()); log.info("Disallowing feature {} for role {} in server {}.", feature.getKey(), role.getId(), role.getServer().getId());
} }
public ConditionResult isCommandExecutable(Command command, CommandContext commandContext) { public CompletableFuture<ConditionResult> isCommandExecutable(Command command, CommandContext commandContext) {
if(command instanceof ConditionalCommand) { if(command instanceof ConditionalCommand) {
ConditionalCommand castedCommand = (ConditionalCommand) command; ConditionalCommand castedCommand = (ConditionalCommand) command;
return checkConditions(commandContext, command, castedCommand.getConditions()); return checkConditions(commandContext, command, castedCommand.getConditions());
} else { } else {
return ConditionResult.builder().result(true).build(); return ConditionResult.fromAsyncSuccess();
} }
} }
@@ -179,16 +181,39 @@ public class CommandServiceBean implements CommandService {
.build(); .build();
} }
private ConditionResult checkConditions(CommandContext commandContext, Command command, List<CommandCondition> conditions) { private CompletableFuture<ConditionResult> checkConditions(CommandContext commandContext, Command command, List<CommandCondition> conditions) {
if(conditions != null) { if(conditions != null && !conditions.isEmpty()) {
List<CompletableFuture<ConditionResult>> futures = new ArrayList<>();
for (CommandCondition condition : conditions) { for (CommandCondition condition : conditions) {
ConditionResult conditionResult = condition.shouldExecute(commandContext, command); if(condition.isAsync()) {
if(!conditionResult.isResult()) { futures.add(condition.shouldExecuteAsync(commandContext, command));
return conditionResult; } else {
futures.add(CompletableFuture.completedFuture(condition.shouldExecute(commandContext, command)));
} }
} }
CompletableFuture<ConditionResult> resultFuture = new CompletableFuture<>();
CompletableFutureList<ConditionResult> futureList = new CompletableFutureList<>(futures);
futureList.getMainFuture().whenComplete((unused, throwable) -> {
List<ConditionResult> 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();
} }

View File

@@ -1,6 +1,7 @@
package dev.sheldan.abstracto.core.commands.help; package dev.sheldan.abstracto.core.commands.help;
import dev.sheldan.abstracto.core.command.Command; 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.*;
import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition; import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition;
import dev.sheldan.abstracto.core.command.execution.CommandContext; 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.models.template.commands.help.HelpModuleOverviewModel;
import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.ChannelService;
import dev.sheldan.abstracto.core.service.RoleService; 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.utils.FutureUtils;
import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.core.templating.model.MessageToSend;
import dev.sheldan.abstracto.core.templating.service.TemplateService; import dev.sheldan.abstracto.core.templating.service.TemplateService;
@@ -114,14 +116,25 @@ public class Help implements Command {
ModuleDefinition moduleDefinition = moduleService.getModuleByName(parameter); ModuleDefinition moduleDefinition = moduleService.getModuleByName(parameter);
log.debug("Displaying help for module {}.", moduleDefinition.getInfo().getName()); log.debug("Displaying help for module {}.", moduleDefinition.getInfo().getName());
SingleLevelPackedModule module = moduleService.getPackedModule(moduleDefinition); SingleLevelPackedModule module = moduleService.getPackedModule(moduleDefinition);
List<Command> commands = module.getCommands(); List<Command> filteredCommand = new ArrayList<>();
List<Command> filteredCommands = new ArrayList<>(); List<CompletableFuture<ConditionResult>> conditionFutures = new ArrayList<>();
commands.forEach(command -> { Map<CompletableFuture<ConditionResult>, Command> futureCommandMap = new HashMap<>();
if(commandService.isCommandExecutable(command, commandContext).isResult()) { module.getCommands().forEach(command -> {
filteredCommands.add(command); // TODO dont provide the parameters, else the condition uses the wrong parameters, as we are not actually executing the command
} CompletableFuture<ConditionResult> future = commandService.isCommandExecutable(command, commandContext);
conditionFutures.add(future);
futureCommandMap.put(future, command);
}); });
module.setCommands(filteredCommands); CompletableFutureList<ConditionResult> 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<ModuleDefinition> subModules = moduleService.getSubModules(moduleDefinition); List<ModuleDefinition> subModules = moduleService.getSubModules(moduleDefinition);
HelpModuleDetailsModel model = (HelpModuleDetailsModel) ContextConverter.fromCommandContext(commandContext, HelpModuleDetailsModel.class); HelpModuleDetailsModel model = (HelpModuleDetailsModel) ContextConverter.fromCommandContext(commandContext, HelpModuleDetailsModel.class);
model.setModule(module); model.setModule(module);

View File

@@ -62,7 +62,7 @@ public class RoleImmunityServiceBean implements RoleImmunityService {
if(immuneRoles.isEmpty()) { if(immuneRoles.isEmpty()) {
return Optional.empty(); return Optional.empty();
} }
return immuneRoles return immuneRoles
.stream() .stream()
.filter(role -> member .filter(role -> member
.getRoles() .getRoles()

View File

@@ -4,6 +4,16 @@ package dev.sheldan.abstracto.core.command.condition;
import dev.sheldan.abstracto.core.command.Command; import dev.sheldan.abstracto.core.command.Command;
import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandContext;
import java.util.concurrent.CompletableFuture;
public interface CommandCondition { 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<ConditionResult> shouldExecuteAsync(CommandContext commandContext, Command command) {
return CompletableFuture.completedFuture(ConditionResult.fromSuccess());
}
} }

View File

@@ -4,6 +4,8 @@ import lombok.Builder;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
import java.util.concurrent.CompletableFuture;
@Getter @Getter
@Setter @Setter
@Builder @Builder
@@ -12,10 +14,16 @@ public class ConditionResult {
private String reason; private String reason;
private ConditionDetail conditionDetail; private ConditionDetail conditionDetail;
public static final ConditionResult SUCCESS = ConditionResult.builder().result(true).build();
public static ConditionResult fromSuccess() { public static ConditionResult fromSuccess() {
return ConditionResult.builder().result(true).build(); return ConditionResult.builder().result(true).build();
} }
public static CompletableFuture<ConditionResult> fromAsyncSuccess() {
return CompletableFuture.completedFuture(fromSuccess());
}
public static ConditionResult fromFailure(ConditionDetail detail) { public static ConditionResult fromFailure(ConditionDetail detail) {
return ConditionResult.builder().result(false).conditionDetail(detail).build(); return ConditionResult.builder().result(false).conditionDetail(detail).build();
} }

View File

@@ -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.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;
import dev.sheldan.abstracto.core.utils.CompletableFutureList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.dv8tion.jda.api.entities.Member; import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.User;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.util.List; import java.util.*;
import java.util.Optional; import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component @Component
@Slf4j @Slf4j
@@ -34,32 +40,92 @@ public class ImmuneUserCondition implements CommandCondition {
@Autowired @Autowired
private RoleImmunityService roleImmunityService; private RoleImmunityService roleImmunityService;
@Autowired
private ImmuneUserCondition self;
@Override @Override
public ConditionResult shouldExecute(CommandContext context, Command command) { public CompletableFuture<ConditionResult> shouldExecuteAsync(CommandContext commandContext, Command command) {
CommandConfiguration commandConfig = command.getConfiguration(); CommandConfiguration commandConfig = command.getConfiguration();
if(commandConfig.getEffects().isEmpty()) { if(commandConfig.getEffects().isEmpty()) {
return ConditionResult.fromSuccess(); return ConditionResult.fromAsyncSuccess();
} }
List<Object> parameters = context.getParameters().getParameters(); List<CompletableFuture<Member>> futures = new ArrayList<>();
List<Object> parameters = commandContext.getParameters().getParameters();
for (EffectConfig effectConfig : commandConfig.getEffects()) { for (EffectConfig effectConfig : commandConfig.getEffects()) {
Integer position = effectConfig.getPosition(); Integer position = effectConfig.getPosition();
if (position < parameters.size()) { if (position < parameters.size()) {
Object parameter = parameters.get(position); Object parameter = parameters.get(position);
if (parameter instanceof Member) { if (parameter instanceof Member) {
Member member = (Member) parameter; Member member = (Member) parameter;
Optional<RoleImmunity> immunityOptional = roleImmunityService.getRoleImmunity(member, effectConfig.getEffectKey()); futures.add(CompletableFuture.completedFuture(member));
if (immunityOptional.isPresent()) { } else if (parameter instanceof User) {
RoleImmunity immunity = immunityOptional.get(); User user = (User) parameter;
ImmuneUserConditionDetail conditionDetail = new ImmuneUserConditionDetail(roleService.getRoleFromGuild(immunity.getRole()), futures.add(commandContext.getGuild().retrieveMember(user).submit());
effectConfig.getEffectKey());
return ConditionResult.fromFailure(conditionDetail);
}
} }
} else { } else {
log.info("Not enough parameters ({}) in command {} to retrieve position {} to check for immunity.", log.info("Not enough parameters ({}) in command {} to retrieve position {} to check for immunity.",
parameters.size(), commandConfig.getName(), position); parameters.size(), commandConfig.getName(), position);
} }
} }
return ConditionResult.fromSuccess(); if(!futures.isEmpty()) {
CompletableFuture<ConditionResult> resultFuture = new CompletableFuture<>();
CompletableFutureList<Member> futureList = new CompletableFutureList<>(futures);
futureList.getMainFuture().whenComplete((unused, throwable) -> {
if(throwable != null) {
log.warn("Future for user immune condition failed. Continuing processing.", throwable);
}
Map<Long, Member> 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<Object> parameters, CompletableFuture<ConditionResult> resultFuture, Map<Long, Member> 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<RoleImmunity> 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;
} }
} }

View File

@@ -24,7 +24,7 @@ public interface CommandService {
void unRestrictCommand(ACommand aCommand, AServer server); void unRestrictCommand(ACommand aCommand, AServer server);
void disAllowCommandForRole(ACommand aCommand, ARole role); void disAllowCommandForRole(ACommand aCommand, ARole role);
void disAllowFeatureForRole(FeatureDefinition featureDefinition, ARole role); void disAllowFeatureForRole(FeatureDefinition featureDefinition, ARole role);
ConditionResult isCommandExecutable(Command command, CommandContext commandContext); CompletableFuture<ConditionResult> isCommandExecutable(Command command, CommandContext commandContext);
UnParsedCommandParameter getUnParsedCommandParameter(String messageContent, Message message); UnParsedCommandParameter getUnParsedCommandParameter(String messageContent, Message message);
CompletableFuture<Parameters> getParametersForCommand(String commandName, Message messageContainingContent); CompletableFuture<Parameters> getParametersForCommand(String commandName, Message messageContainingContent);
Parameter cloneParameter(Parameter parameter); Parameter cloneParameter(Parameter parameter);