From f2aa7035aa601ecf1213b4626aebb45c2da1fbdc Mon Sep 17 00:00:00 2001 From: Sheldan <5037282+Sheldan@users.noreply.github.com> Date: Sun, 28 Nov 2021 13:19:17 +0100 Subject: [PATCH] [AB-307] enabling dependent features and including them into the setup wizard this comes with the usual limitation, that dependent features must be referenced via importing --- .../config/features/DisableFeature.java | 44 +++++++++++++----- .../config/features/EnableFeature.java | 44 +++++++++++++----- .../config/{ => features}/SetupFeature.java | 3 +- .../core/service/FeatureSetupServiceBean.java | 45 +++++++++++++------ ...ableModel.java => FeatureSwitchModel.java} | 2 +- 5 files changed, 103 insertions(+), 35 deletions(-) rename abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/{ => features}/SetupFeature.java (96%) rename abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/{EnableModel.java => FeatureSwitchModel.java} (82%) diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/DisableFeature.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/DisableFeature.java index 8cb05d2a3..014608156 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/DisableFeature.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/DisableFeature.java @@ -13,18 +13,22 @@ import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.models.database.AServer; -import dev.sheldan.abstracto.core.models.template.commands.EnableModel; +import dev.sheldan.abstracto.core.models.template.commands.FeatureSwitchModel; import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.FeatureConfigService; import dev.sheldan.abstracto.core.service.FeatureFlagService; import dev.sheldan.abstracto.core.service.management.ServerManagementService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.core.templating.service.TemplateService; +import dev.sheldan.abstracto.core.utils.FutureUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; @Component public class DisableFeature extends AbstractConditionableCommand { @@ -43,26 +47,46 @@ public class DisableFeature extends AbstractConditionableCommand { @Autowired private ServerManagementService serverManagementService; + private static final String DISABLE_FEATURE_ALL_FEATURES_RESPONSE_TEMPLATE_KEY = "disableFeature_all_features_response"; + private static final String DISABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY = "disableFeature_feature_dependencies_response"; @Override public CompletableFuture executeAsync(CommandContext commandContext) { - if(commandContext.getParameters().getParameters().isEmpty()) { - EnableModel model = (EnableModel) ContextConverter.fromCommandContext(commandContext, EnableModel.class); + if (commandContext.getParameters().getParameters().isEmpty()) { + FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class); model.setFeatures(featureConfigService.getAllFeatures()); - String response = templateService.renderTemplate("disable_features_response", model, commandContext.getGuild().getIdLong()); - return channelService.sendTextToChannel(response, commandContext.getChannel()) - .thenApply(aVoid -> CommandResult.fromSuccess()); + MessageToSend messageToSend = templateService.renderEmbedTemplate(DISABLE_FEATURE_ALL_FEATURES_RESPONSE_TEMPLATE_KEY, model, commandContext.getGuild().getIdLong()); + return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel())) + .thenApply(message -> CommandResult.fromIgnored()); } else { String flagKey = (String) commandContext.getParameters().getParameters().get(0); FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(flagKey); featureFlagService.disableFeature(feature, commandContext.getGuild().getIdLong()); - if(feature.getDependantFeatures() != null) { + List featureDependencies = new ArrayList<>(); + if (feature.getDependantFeatures() != null) { AServer server = serverManagementService.loadServer(commandContext.getGuild()); - feature.getDependantFeatures().forEach(featureDisplay -> - featureFlagService.disableFeature(featureDisplay, server) + feature.getDependantFeatures().forEach(featureDisplay -> { + if (featureFlagService.isFeatureEnabled(featureDisplay, server)) { + featureFlagService.disableFeature(featureDisplay, server); + featureDependencies.add(featureDisplay); + } + } ); } - return CompletableFuture.completedFuture(CommandResult.fromSuccess()); + if (featureDependencies.isEmpty()) { + return CompletableFuture.completedFuture(CommandResult.fromSuccess()); + } else { + List additionalFeatures = featureDependencies + .stream() + .map(featureDef -> featureDef.getFeature().getKey()). + collect(Collectors.toList()); + FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class); + model.setFeatures(additionalFeatures); + MessageToSend messageToSend = templateService.renderEmbedTemplate(DISABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY, + model, commandContext.getGuild().getIdLong()); + return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel())) + .thenApply(message -> CommandResult.fromIgnored()); + } } } diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/EnableFeature.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/EnableFeature.java index c3d556b15..036f57f0e 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/EnableFeature.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/EnableFeature.java @@ -14,19 +14,23 @@ import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.models.FeatureValidationResult; import dev.sheldan.abstracto.core.models.database.AServer; -import dev.sheldan.abstracto.core.models.template.commands.EnableModel; +import dev.sheldan.abstracto.core.models.template.commands.FeatureSwitchModel; import dev.sheldan.abstracto.core.service.ChannelService; import dev.sheldan.abstracto.core.service.FeatureConfigService; import dev.sheldan.abstracto.core.service.FeatureFlagService; import dev.sheldan.abstracto.core.service.management.ServerManagementService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; import dev.sheldan.abstracto.core.templating.service.TemplateService; +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 java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; @Component @Slf4j @@ -47,32 +51,52 @@ public class EnableFeature extends AbstractConditionableCommand { @Autowired private ServerManagementService serverManagementService; + private static final String ENABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY = "enableFeature_feature_dependencies_response"; + private static final String ENABLE_FEATURE_ALL_FEATURES_RESPONSE = "enableFeature_all_features_response"; + @Override public CompletableFuture executeAsync(CommandContext commandContext) { if(commandContext.getParameters().getParameters().isEmpty()) { - EnableModel model = (EnableModel) ContextConverter.fromCommandContext(commandContext, EnableModel.class); + FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class); model.setFeatures(featureConfigService.getAllFeatures()); - String response = templateService.renderTemplate("enable_features_response", model, commandContext.getGuild().getIdLong()); - return channelService.sendTextToChannel(response, commandContext.getChannel()) - .thenApply(message -> CommandResult.fromSuccess()); + MessageToSend messageToSend = templateService.renderEmbedTemplate(ENABLE_FEATURE_ALL_FEATURES_RESPONSE, model, commandContext.getGuild().getIdLong()); + return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel())) + .thenApply(message -> CommandResult.fromIgnored()); } else { - String flagKey = (String) commandContext.getParameters().getParameters().get(0); - FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(flagKey); + String featureKey = (String) commandContext.getParameters().getParameters().get(0); + FeatureConfig feature = featureConfigService.getFeatureDisplayForFeature(featureKey); AServer server = serverManagementService.loadServer(commandContext.getUserInitiatedContext().getGuild().getIdLong()); FeatureValidationResult featureSetup = featureConfigService.validateFeatureSetup(feature, server); if(Boolean.FALSE.equals(featureSetup.getValidationResult())) { - log.info("Feature {} has failed the setup validation. Notifying user.", flagKey); + log.info("Feature {} has failed the setup validation. Notifying user.", featureKey); channelService.sendTextToChannelNotAsync(templateService.renderTemplatable(featureSetup, commandContext.getGuild().getIdLong()), commandContext.getChannel()); } featureFlagService.enableFeature(feature, server); + List featureDependencies = new ArrayList<>(); if(feature.getRequiredFeatures() != null) { feature.getRequiredFeatures().forEach(featureDisplay -> { log.info("Also enabling required feature {}.", featureDisplay.getFeature().getKey()); - featureFlagService.enableFeature(featureDisplay, server); + if(!featureFlagService.isFeatureEnabled(featureDisplay, server)) { + featureFlagService.enableFeature(featureDisplay, server); + featureDependencies.add(featureDisplay); + } }); } - return CompletableFuture.completedFuture(CommandResult.fromSuccess()); + if(featureDependencies.isEmpty()) { + return CompletableFuture.completedFuture(CommandResult.fromSuccess()); + } else { + List additionalFeatures = featureDependencies + .stream() + .map(featureDef -> featureDef.getFeature().getKey()). + collect(Collectors.toList()); + FeatureSwitchModel model = (FeatureSwitchModel) ContextConverter.fromCommandContext(commandContext, FeatureSwitchModel.class); + model.setFeatures(additionalFeatures); + MessageToSend messageToSend = templateService.renderEmbedTemplate(ENABLE_FEATURE_DEPENDENCIES_RESPONSE_TEMPLATE_KEY, + model, commandContext.getGuild().getIdLong()); + return FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, commandContext.getChannel())) + .thenApply(message -> CommandResult.fromIgnored()); + } } } diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/SetupFeature.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/SetupFeature.java similarity index 96% rename from abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/SetupFeature.java rename to abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/SetupFeature.java index ec41787a6..e0d45799e 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/SetupFeature.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/config/features/SetupFeature.java @@ -1,4 +1,4 @@ -package dev.sheldan.abstracto.core.commands.config; +package dev.sheldan.abstracto.core.commands.config.features; import dev.sheldan.abstracto.core.command.condition.AbstractConditionableCommand; import dev.sheldan.abstracto.core.command.config.CommandConfiguration; @@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition; import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; import dev.sheldan.abstracto.core.command.service.management.FeatureManagementService; +import dev.sheldan.abstracto.core.commands.config.ConfigModuleDefinition; import dev.sheldan.abstracto.core.config.FeatureConfig; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.exception.FeatureNotFoundException; diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/FeatureSetupServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/FeatureSetupServiceBean.java index e6a58bfc2..3a3b90749 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/FeatureSetupServiceBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/service/FeatureSetupServiceBean.java @@ -2,6 +2,7 @@ package dev.sheldan.abstracto.core.service; import dev.sheldan.abstracto.core.command.service.ExceptionService; import dev.sheldan.abstracto.core.config.FeatureConfig; +import dev.sheldan.abstracto.core.config.PostTargetEnum; import dev.sheldan.abstracto.core.exception.ChannelNotInGuildException; import dev.sheldan.abstracto.core.interactive.*; import dev.sheldan.abstracto.core.models.AServerChannelUserId; @@ -14,9 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; +import java.util.*; import java.util.concurrent.CompletableFuture; @Component @@ -61,8 +60,13 @@ public class FeatureSetupServiceBean implements FeatureSetupService { log.info("Performing setup of feature {} for user {} in channel {} in server {}.", featureConfig.getFeature().getKey(), user.getUserId(), user.getChannelId(), user.getGuildId()); Optional textChannelInGuild = channelService.getTextChannelFromServerOptional(user.getGuildId(), user.getChannelId()); - if(textChannelInGuild.isPresent()) { - List requiredSystemConfigKeys = featureConfig.getRequiredSystemConfigKeys(); + if (textChannelInGuild.isPresent()) { + Set requiredSystemConfigKeys = new HashSet<>(); + Set requiredPostTargets = new HashSet<>(); + Set customSetupSteps = new HashSet<>(); + + collectRequiredFeatureSteps(featureConfig, requiredSystemConfigKeys, requiredPostTargets, customSetupSteps, new HashSet<>()); + List steps = new ArrayList<>(); requiredSystemConfigKeys.forEach(s -> { log.debug("Feature requires system config key {}.", s); @@ -73,7 +77,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { .build(); steps.add(execution); }); - featureConfig.getRequiredPostTargets().forEach(postTargetEnum -> { + requiredPostTargets.forEach(postTargetEnum -> { log.debug("Feature requires post target {}.", postTargetEnum.getKey()); SetupExecution execution = SetupExecution .builder() @@ -82,7 +86,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { .build(); steps.add(execution); }); - featureConfig.getCustomSetupSteps().forEach(setupStep -> { + customSetupSteps.forEach(setupStep -> { log.debug("Feature requires custom setup step {}.", setupStep.getClass().getName()); SetupExecution execution = SetupExecution .builder() @@ -94,7 +98,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { for (int i = 0; i < steps.size(); i++) { SetupExecution setupExecution = steps.get(i); setupExecution.getParameter().setPreviousMessageId(initialMessageId); - if(i < steps.size() - 1) { + if (i < steps.size() - 1) { setupExecution.setNextStep(steps.get(i + 1)); } } @@ -117,7 +121,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { @Override public CompletableFuture executeFeatureSetup(FeatureConfig featureConfig, List steps, AServerChannelUserId user, List delayedActionConfigs) { - if(!steps.isEmpty()) { + if (!steps.isEmpty()) { SetupExecution nextStep = steps.get(0); return executeStep(user, nextStep, delayedActionConfigs, featureConfig); } else { @@ -130,10 +134,10 @@ public class FeatureSetupServiceBean implements FeatureSetupService { private CompletableFuture executeStep(AServerChannelUserId aUserInAServer, SetupExecution execution, List delayedActionConfigs, FeatureConfig featureConfig) { log.debug("Executing step {} in server {} in channel {} for user {}.", execution.getStep().getClass(), aUserInAServer.getGuildId(), aUserInAServer.getChannelId(), aUserInAServer.getUserId()); return execution.getStep().execute(aUserInAServer, execution.getParameter()).thenAccept(setpResult -> { - if(setpResult.getResult().equals(SetupStepResultType.SUCCESS)) { + if (setpResult.getResult().equals(SetupStepResultType.SUCCESS)) { log.info("Step {} in server {} has been executed successfully. Proceeding.", execution.getStep().getClass(), aUserInAServer.getGuildId()); delayedActionConfigs.addAll(setpResult.getDelayedActionConfigList()); - if(execution.getNextStep() != null) { + if (execution.getNextStep() != null) { log.debug("Executing next step {}.", execution.getNextStep().getStep().getClass()); executeStep(aUserInAServer, execution.getNextStep(), delayedActionConfigs, featureConfig); } else { @@ -156,7 +160,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { public void showExceptionMessage(Throwable throwable, AServerChannelUserId aServerChannelUserId) { Optional channelOptional = channelService.getTextChannelFromServerOptional(aServerChannelUserId.getGuildId(), aServerChannelUserId.getChannelId()); memberService.getMemberInServerAsync(aServerChannelUserId.getGuildId(), aServerChannelUserId.getUserId()).thenAccept(member -> - channelOptional.ifPresent(textChannel -> exceptionService.reportExceptionToChannel(throwable, textChannel, member)) + channelOptional.ifPresent(textChannel -> exceptionService.reportExceptionToChannel(throwable, textChannel, member)) ).exceptionally(innserThrowable -> { log.error("Failed to report exception message for exception {} for user {} in channel {} in server {}.", throwable, aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), innserThrowable); return null; @@ -178,7 +182,7 @@ public class FeatureSetupServiceBean implements FeatureSetupService { log.debug("Notifying user {} in channel {} in server {} about completion of setup for feature {}.", aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey()); String templateKey; - if(result.getResult().equals(SetupStepResultType.CANCELLED)) { + if (result.getResult().equals(SetupStepResultType.CANCELLED)) { templateKey = FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE; } else { templateKey = FEATURE_SETUP_COMPLETION_NOTIFICATION_TEMPLATE; @@ -202,4 +206,19 @@ public class FeatureSetupServiceBean implements FeatureSetupService { aServerChannelUserId.getUserId(), aServerChannelUserId.getChannelId(), aServerChannelUserId.getGuildId(), featureConfig.getFeature().getKey()); notifyUserWithTemplate(aServerChannelUserId, featureConfig, FEATURE_SETUP_CANCELLATION_NOTIFICATION_TEMPLATE); } + + private void collectRequiredFeatureSteps(FeatureConfig featureConfig, Set requiredSystemConfigKeys, + Set requiredPostTargets, Set customSetupSteps, + Set coveredFeatures) { + if (coveredFeatures.contains(featureConfig.getFeature().getKey())) { + return; + } + coveredFeatures.add(featureConfig.getFeature().getKey()); + requiredSystemConfigKeys.addAll(featureConfig.getRequiredSystemConfigKeys()); + requiredPostTargets.addAll(featureConfig.getRequiredPostTargets()); + customSetupSteps.addAll(featureConfig.getCustomSetupSteps()); + featureConfig.getRequiredFeatures() + .forEach(requiredFeature -> collectRequiredFeatureSteps(requiredFeature, requiredSystemConfigKeys, requiredPostTargets, customSetupSteps, coveredFeatures)); + } + } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/EnableModel.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/FeatureSwitchModel.java similarity index 82% rename from abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/EnableModel.java rename to abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/FeatureSwitchModel.java index 3928a7de6..30526413c 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/EnableModel.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/FeatureSwitchModel.java @@ -10,6 +10,6 @@ import java.util.List; @Getter @Setter @SuperBuilder -public class EnableModel extends UserInitiatedServerContext { +public class FeatureSwitchModel extends UserInitiatedServerContext { private List features; }