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 081b3cf6b..b03b877a2 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 @@ -182,10 +182,8 @@ public class CommandReceivedHandler extends ListenerAdapter { } public UnParsedCommandResult getUnparsedCommandResult(Message message) { - String contentStripped = message.getContentRaw(); - List parameters = Arrays.asList(contentStripped.split(" ")); - UnParsedCommandParameter unParsedParameter = new UnParsedCommandParameter(contentStripped, message); - String commandName = commandManager.getCommandName(parameters.get(0), message.getGuild().getIdLong()); + String commandName = getCommandName(message); + UnParsedCommandParameter unParsedParameter = new UnParsedCommandParameter(message.getContentRaw(), message); Command foundCommand = commandManager.findCommandByParameters(commandName, unParsedParameter, message.getGuild().getIdLong()).orElse(null); return UnParsedCommandResult .builder() @@ -194,13 +192,9 @@ public class CommandReceivedHandler extends ListenerAdapter { .build(); } - public CompletableFuture 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 String getCommandName(Message message) { + List parameters = Arrays.asList( message.getContentRaw().split(" ")); + return commandManager.getCommandName(parameters.get(0), message.getGuild().getIdLong()); } public CompletableFuture getParametersFromMessage(Message message, UnParsedCommandResult result) { diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestor.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestor.java new file mode 100644 index 000000000..34a02ae78 --- /dev/null +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestor.java @@ -0,0 +1,110 @@ +package dev.sheldan.abstracto.core.commands.utility; + +import dev.sheldan.abstracto.core.command.Command; +import dev.sheldan.abstracto.core.command.CommandAlternative; +import dev.sheldan.abstracto.core.command.CommandReceivedHandler; +import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition; +import dev.sheldan.abstracto.core.command.config.features.CoreFeatureMode; +import dev.sheldan.abstracto.core.command.execution.UnParsedCommandParameter; +import dev.sheldan.abstracto.core.command.service.CommandManager; +import dev.sheldan.abstracto.core.config.FeatureMode; +import dev.sheldan.abstracto.core.config.ListenerPriority; +import dev.sheldan.abstracto.core.models.template.commands.SlashCommandSuggestionModel; +import dev.sheldan.abstracto.core.service.ChannelService; +import dev.sheldan.abstracto.core.service.FeatureFlagService; +import dev.sheldan.abstracto.core.service.FeatureModeService; +import dev.sheldan.abstracto.core.templating.model.MessageToSend; +import dev.sheldan.abstracto.core.templating.service.TemplateService; +import dev.sheldan.abstracto.core.utils.FutureUtils; +import java.util.Optional; +import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class SlashCommandSuggestor implements CommandAlternative { + + @Autowired + private FeatureModeService featureModeService; + + @Autowired + private CommandManager commandManager; + + @Autowired + private CommandReceivedHandler commandReceivedHandler; + + @Autowired + private FeatureFlagService featureFlagService; + + @Autowired + private TemplateService templateService; + + @Autowired + private ChannelService channelService; + + public static final String SUGGESTION_TEMPLATE_KEY = "slash_command_suggestion"; + + @Override + public Integer getPriority() { + return ListenerPriority.MEDIUM; + } + + @Override + public boolean shouldExecute(UnParsedCommandParameter parameter, Guild guild, Message message) { + boolean featureModeActive = featureModeService.featureModeActive(CoreFeatureDefinition.CORE_FEATURE, guild.getIdLong(), CoreFeatureMode.SUGGEST_SLASH_COMMANDS); + if(!featureModeActive) { + return false; + } + String commandName = commandReceivedHandler.getCommandName(message); + Long guildId = message.getGuildIdLong(); + Optional potentialCommand = commandManager.getCommandByNameOptional(commandName, true, guildId); + return potentialCommand.isPresent() && potentialCommand.get().getConfiguration().isSlashCommandOnly(); + } + + @Override + public void execute(UnParsedCommandParameter parameter, Message message) { + String commandName = commandReceivedHandler.getCommandName(message); + Long guildId = message.getGuildIdLong(); + Optional potentialCommand = commandManager.getCommandByNameOptional(commandName, true, guildId); + // limitation to not check conditions if command is executable, I dont want to completely built the entire command context, as that would require + // to parse the parameters, therefore the major checks should suffice + if(potentialCommand.isPresent()) { + Command command = potentialCommand.get(); + if(command.getConfiguration().isSlashCommandOnly()) { + boolean featureAvailable = featureFlagService.getFeatureFlagValue(command.getFeature(), guildId); + if(featureAvailable) { + boolean shouldNotifyUser = command.getFeatureModeLimitations().isEmpty(); + for (FeatureMode featureModeLimitation : command.getFeatureModeLimitations()) { + if(featureModeService.featureModeActive(command.getFeature(), guildId, featureModeLimitation)) { + shouldNotifyUser = true; + } + } + if(shouldNotifyUser) { + notifyUser(message, command, commandName, guildId); + } + } + } + } + } + + private void notifyUser(Message message, Command command, String commandName, Long guildId) { + String path = command.getConfiguration().getSlashCommandConfig().getSlashCommandPath(); + SlashCommandSuggestionModel model = SlashCommandSuggestionModel + .builder() + .slashCommandPath(path) + .build(); + Long userId = message.getAuthor().getIdLong(); + log.info("Suggesting slash command for command {} to user {}.", commandName, userId); + MessageToSend messageToSend = templateService.renderEmbedTemplate(SUGGESTION_TEMPLATE_KEY, model, guildId); + FutureUtils.toSingleFutureGeneric(channelService.sendMessageToSendToChannel(messageToSend, message.getChannel())) + .thenAccept(unused -> { + log.debug("Successfully suggested command."); + }).exceptionally(throwable -> { + log.warn("Failed to suggest slash command for command {} to user {}", commandName, userId); + return null; + }); + } +} diff --git a/abstracto-application/core/core-impl/src/main/resources/abstracto.properties b/abstracto-application/core/core-impl/src/main/resources/abstracto.properties index e03908e17..bd1ca465f 100644 --- a/abstracto-application/core/core-impl/src/main/resources/abstracto.properties +++ b/abstracto-application/core/core-impl/src/main/resources/abstracto.properties @@ -21,6 +21,10 @@ abstracto.systemConfigs.confirmationTimeout.longValue=120 abstracto.systemConfigs.maxMessages.name=maxMessages abstracto.systemConfigs.maxMessages.longValue=3 +abstracto.featureModes.suggestSlashCommands.featureName=core +abstracto.featureModes.suggestSlashCommands.mode=suggestSlashCommands +abstracto.featureModes.suggestSlashCommands.enabled=true + abstracto.featureFlags.core.featureName=core abstracto.featureFlags.core.enabled=true diff --git a/abstracto-application/core/core-impl/src/test/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestorTest.java b/abstracto-application/core/core-impl/src/test/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestorTest.java new file mode 100644 index 000000000..b4c55c8bd --- /dev/null +++ b/abstracto-application/core/core-impl/src/test/java/dev/sheldan/abstracto/core/commands/utility/SlashCommandSuggestorTest.java @@ -0,0 +1,169 @@ +package dev.sheldan.abstracto.core.commands.utility; + +import static dev.sheldan.abstracto.core.commands.utility.SlashCommandSuggestor.SUGGESTION_TEMPLATE_KEY; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import dev.sheldan.abstracto.core.command.Command; +import dev.sheldan.abstracto.core.command.CommandReceivedHandler; +import dev.sheldan.abstracto.core.command.config.CommandConfiguration; +import dev.sheldan.abstracto.core.command.config.features.CoreFeatureDefinition; +import dev.sheldan.abstracto.core.command.config.features.CoreFeatureMode; +import dev.sheldan.abstracto.core.command.service.CommandManager; +import dev.sheldan.abstracto.core.config.FeatureMode; +import dev.sheldan.abstracto.core.service.ChannelService; +import dev.sheldan.abstracto.core.service.FeatureFlagService; +import dev.sheldan.abstracto.core.service.FeatureModeService; +import dev.sheldan.abstracto.core.templating.service.TemplateService; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Optional; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.Message; +import net.dv8tion.jda.api.entities.User; +import net.dv8tion.jda.api.entities.channel.unions.MessageChannelUnion; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class SlashCommandSuggestorTest { + + @InjectMocks + private SlashCommandSuggestor unitUnderTest; + + @Mock + private FeatureModeService featureModeService; + + @Mock + private CommandReceivedHandler commandReceivedHandler; + + @Mock + private CommandManager commandManager; + + @Mock + private FeatureFlagService featureFlagService; + + @Mock + private TemplateService templateService; + + @Mock + private ChannelService channelService; + + @Mock + private Guild guild; + + @Mock + private Message message; + + @Mock + private Command command; + + @Mock + private FeatureMode featureMode; + + private static final Long SERVER_ID = 1L; + private static final String COMMAND_NAME = "commandName"; + + @Before + public void setup() { + when(guild.getIdLong()).thenReturn(SERVER_ID); + when(message.getGuildIdLong()).thenReturn(SERVER_ID); + when(message.getAuthor()).thenReturn(Mockito.mock(User.class)); + when(message.getChannel()).thenReturn(Mockito.mock(MessageChannelUnion.class)); + } + + @Test + public void shouldNotExecute_DueToFeatureMode() { + when(featureModeService.featureModeActive(CoreFeatureDefinition.CORE_FEATURE, SERVER_ID, CoreFeatureMode.SUGGEST_SLASH_COMMANDS)).thenReturn(false); + boolean shouldExecute = unitUnderTest.shouldExecute(null, guild, message); + assertThat(shouldExecute).isFalse(); + } + + @Test + public void shouldNotExecute_DueToNotFoundCommand() { + when(featureModeService.featureModeActive(CoreFeatureDefinition.CORE_FEATURE, SERVER_ID, CoreFeatureMode.SUGGEST_SLASH_COMMANDS)).thenReturn(true); + commandFound(null); + boolean shouldExecute = unitUnderTest.shouldExecute(null, guild, message); + assertThat(shouldExecute).isFalse(); + } + + @Test + public void shouldNotExecute_DueToFoundCommandWhichIsNotSlashCommandOnly() { + when(featureModeService.featureModeActive(CoreFeatureDefinition.CORE_FEATURE, SERVER_ID, CoreFeatureMode.SUGGEST_SLASH_COMMANDS)).thenReturn(true); + commandSetup(false); + boolean shouldExecute = unitUnderTest.shouldExecute(null, guild, message); + assertThat(shouldExecute).isFalse(); + } + + @Test + public void shouldExecute_DueToFoundCommandWhichIsSlashCommandOnly() { + when(featureModeService.featureModeActive(CoreFeatureDefinition.CORE_FEATURE, SERVER_ID, CoreFeatureMode.SUGGEST_SLASH_COMMANDS)).thenReturn(true); + commandSetup(true); + boolean shouldExecute = unitUnderTest.shouldExecute(null, guild, message); + assertThat(shouldExecute).isTrue(); + } + + @Test + public void shouldNotFindCommand() { + commandFound(null); + unitUnderTest.execute(null, message); + verify(templateService, times(0)).renderEmbedTemplate(eq(SUGGESTION_TEMPLATE_KEY), any(), any()); + } + + @Test + public void foundCommandIsNotSlashCommandOnly() { + commandSetup(false); + unitUnderTest.execute(null, message); + verify(templateService, times(0)).renderEmbedTemplate(eq(SUGGESTION_TEMPLATE_KEY), any(), any()); + } + + @Test + public void featureNotEnabled() { + commandSetup(true); + when(featureFlagService.getFeatureFlagValue(any(), eq(SERVER_ID))).thenReturn(false); + unitUnderTest.execute(null, message); + verify(templateService, times(0)).renderEmbedTemplate(eq(SUGGESTION_TEMPLATE_KEY), any(), any()); + } + + @Test + public void noFeatureModesAvailable() { + commandSetup(true); + when(command.getFeatureModeLimitations()).thenReturn(new ArrayList<>()); + when(featureFlagService.getFeatureFlagValue(any(), eq(SERVER_ID))).thenReturn(true); + unitUnderTest.execute(null, message); + verify(templateService).renderEmbedTemplate(eq(SUGGESTION_TEMPLATE_KEY), any(), any()); + } + + @Test + public void featureModesAvailable() { + commandSetup(true); + when(command.getFeatureModeLimitations()).thenReturn(Collections.singletonList(featureMode)); + when(featureFlagService.getFeatureFlagValue(any(), eq(SERVER_ID))).thenReturn(true); + when(featureModeService.featureModeActive(any(), eq(SERVER_ID), any())).thenReturn(true); + unitUnderTest.execute(null, message); + verify(templateService).renderEmbedTemplate(eq(SUGGESTION_TEMPLATE_KEY), any(), any()); + } + + private void commandSetup(boolean slashCommandOnly) { + commandFound(command); + CommandConfiguration commandConfiguration = CommandConfiguration + .builder() + .slashCommandOnly(slashCommandOnly) + .build(); + when(command.getConfiguration()).thenReturn(commandConfiguration); + } + + private void commandFound(Command command) { + when(commandReceivedHandler.getCommandName(message)).thenReturn(COMMAND_NAME); + when(commandManager.getCommandByNameOptional(COMMAND_NAME, true, SERVER_ID)).thenReturn(Optional.ofNullable(command)); + } +} diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/features/CoreFeatureMode.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/features/CoreFeatureMode.java new file mode 100644 index 000000000..a9489222f --- /dev/null +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/features/CoreFeatureMode.java @@ -0,0 +1,15 @@ +package dev.sheldan.abstracto.core.command.config.features; + +import dev.sheldan.abstracto.core.config.FeatureMode; +import lombok.Getter; + +@Getter +public enum CoreFeatureMode implements FeatureMode { + SUGGEST_SLASH_COMMANDS("suggestSlashCommands"); + + private final String key; + + CoreFeatureMode(String key) { + this.key = key; + } +} diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/execution/UnParsedCommandParameter.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/execution/UnParsedCommandParameter.java index a5acebdc3..338aa0f89 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/execution/UnParsedCommandParameter.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/execution/UnParsedCommandParameter.java @@ -32,12 +32,12 @@ public class UnParsedCommandParameter { } if (m.group(1) != null) { String group = m.group(1); - if(!group.equals("")) { + if(!group.isEmpty()) { this.parameters.add(UnparsedCommandParameterPiece.builder().value(group).build()); } } else { String group = m.group(2); - if(!group.equals("")) { + if(!group.isEmpty()) { this.parameters.add(UnparsedCommandParameterPiece.builder().value(group).build()); } } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandConfig.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandConfig.java index a8068116e..b3c3c1e6a 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandConfig.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandConfig.java @@ -2,6 +2,10 @@ package dev.sheldan.abstracto.core.interaction.slash; import dev.sheldan.abstracto.core.command.config.UserCommandConfig; import dev.sheldan.abstracto.core.utils.ContextUtils; +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; @@ -67,4 +71,13 @@ public class SlashCommandConfig { public String getUserSlashCompatibleCommandName() { return userCommandName != null ? userCommandName.toLowerCase(Locale.ROOT) : null; } + + public String getSlashCommandPath() { + String root = getSlashCompatibleRootName(); + String group = getSlashCompatibleGroupName(); + String command = getSlashCompatibleCommandName(); + return Stream.of(root, group, command) + .filter(Objects::nonNull) + .collect(Collectors.joining(" ")); + } } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/SlashCommandSuggestionModel.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/SlashCommandSuggestionModel.java new file mode 100644 index 000000000..eadef5a72 --- /dev/null +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/models/template/commands/SlashCommandSuggestionModel.java @@ -0,0 +1,10 @@ +package dev.sheldan.abstracto.core.models.template.commands; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class SlashCommandSuggestionModel { + private String slashCommandPath; +}