diff --git a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesServiceBean.java b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesServiceBean.java new file mode 100644 index 000000000..e679fd570 --- /dev/null +++ b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesServiceBean.java @@ -0,0 +1,84 @@ +package dev.sheldan.abstracto.webservices.common.service; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; +import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException; +import dev.sheldan.abstracto.webservices.common.exception.SuggestQueriesException; +import lombok.extern.slf4j.Slf4j; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +@Component +@Slf4j +@CacheConfig(cacheNames = "general-use-cache") +public class SuggestQueriesServiceBean implements SuggestQueriesService { + + @Autowired + private OkHttpClient okHttpClient; + + @Value("${abstracto.feature.webservices.common.suggestionsURL}") + private String suggestionUrl; + + @Autowired + private Gson gson; + + @Autowired + private SuggestQueriesServiceBean self; + + private List getSuggestionsFromResponse(String response) { + JsonElement rootJson = JsonParser.parseString(response); + if(!rootJson.isJsonArray()) { + return new ArrayList<>(); + } + JsonArray mainArray = rootJson.getAsJsonArray(); + if(mainArray.size() < 2 || !mainArray.get(1).isJsonArray() || mainArray.get(1).getAsJsonArray().size() == 0) { + return new ArrayList<>(); + } + JsonArray suggestionArray = mainArray.get(1).getAsJsonArray(); + return Arrays.asList(gson.fromJson(suggestionArray, String[].class)); + } + + @Override + @Cacheable(key = "{#query, #service}") + public List getSuggestionsForQuery(String query, String service) { + Request request = new Request.Builder() + .url(String.format(suggestionUrl, service, query)) + .get() + .build(); + Response response; + try { + response = okHttpClient.newCall(request).execute(); + if(!response.isSuccessful()) { + if(log.isDebugEnabled()) { + log.error("Failed to retrieve suggestions. Response had code {} with body {}.", + response.code(), response.body()); + } + throw new SuggestQueriesException(response.code()); + } + return getSuggestionsFromResponse(response.body().string()); + } catch (IOException e) { + throw new AbstractoRunTimeException(e); + } + } + + @Override + public List getYoutubeSuggestionsForQuery(String query) { + if(query == null || "".equals(query)) { + return new ArrayList<>(); + } + return self.getSuggestionsForQuery(query, "yt"); + } +} diff --git a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/urban/service/UrbanServiceBean.java b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/urban/service/UrbanServiceBean.java index e4656da57..1ec9255ca 100644 --- a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/urban/service/UrbanServiceBean.java +++ b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/urban/service/UrbanServiceBean.java @@ -31,7 +31,10 @@ public class UrbanServiceBean implements UrbanService { @Override public UrbanDefinition getUrbanDefinition(String query) throws IOException { - Request request = new Request.Builder().url(String.format(requestUrl, query)).get().build(); + Request request = new Request.Builder() + .url(String.format(requestUrl, query)) + .get() + .build(); Response response = okHttpClient.newCall(request).execute(); if(!response.isSuccessful()) { if(log.isDebugEnabled()) { diff --git a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/command/YoutubeVideoSearch.java b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/command/YoutubeVideoSearch.java index 776b82a21..f038b8ea2 100644 --- a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/command/YoutubeVideoSearch.java +++ b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/command/YoutubeVideoSearch.java @@ -8,6 +8,7 @@ import dev.sheldan.abstracto.core.command.config.Parameter; import dev.sheldan.abstracto.core.interaction.slash.SlashCommandConfig; import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; +import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandAutoCompleteService; import dev.sheldan.abstracto.core.interaction.slash.parameter.SlashCommandParameterService; import dev.sheldan.abstracto.core.config.FeatureDefinition; import dev.sheldan.abstracto.core.interaction.InteractionService; @@ -16,12 +17,14 @@ 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 dev.sheldan.abstracto.webservices.common.service.SuggestQueriesService; import dev.sheldan.abstracto.webservices.config.WebServicesSlashCommandNames; import dev.sheldan.abstracto.webservices.config.WebserviceFeatureDefinition; import dev.sheldan.abstracto.webservices.youtube.config.YoutubeWebServiceFeatureMode; import dev.sheldan.abstracto.webservices.youtube.model.YoutubeVideo; import dev.sheldan.abstracto.webservices.youtube.model.command.YoutubeVideoSearchCommandModel; import dev.sheldan.abstracto.webservices.youtube.service.YoutubeSearchService; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -57,6 +60,12 @@ public class YoutubeVideoSearch extends AbstractConditionableCommand { @Autowired private InteractionService interactionService; + @Autowired + private SlashCommandAutoCompleteService slashCommandAutoCompleteService; + + @Autowired + private SuggestQueriesService suggestQueriesService; + @Override public CompletableFuture executeAsync(CommandContext commandContext) { String query = (String) commandContext.getParameters().getParameters().get(0); @@ -100,6 +109,15 @@ public class YoutubeVideoSearch extends AbstractConditionableCommand { .thenApply(o -> CommandResult.fromSuccess()); } + @Override + public List performAutoComplete(CommandAutoCompleteInteractionEvent event) { + if(slashCommandAutoCompleteService.matchesParameter(event.getFocusedOption(), SEARCH_QUERY_PARAMETER)) { + return suggestQueriesService.getYoutubeSuggestionsForQuery(event.getFocusedOption().getValue()); + } else { + return new ArrayList<>(); + } + } + @Override public CommandConfiguration getConfiguration() { List parameters = new ArrayList<>(); @@ -108,6 +126,7 @@ public class YoutubeVideoSearch extends AbstractConditionableCommand { .name(SEARCH_QUERY_PARAMETER) .type(String.class) .remainder(true) + .supportsAutoComplete(true) .templated(true) .build(); parameters.add(queryParameter); diff --git a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/service/YoutubeSearchServiceBean.java b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/service/YoutubeSearchServiceBean.java index c26075683..bac980bf9 100644 --- a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/service/YoutubeSearchServiceBean.java +++ b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/java/dev/sheldan/abstracto/webservices/youtube/service/YoutubeSearchServiceBean.java @@ -6,6 +6,7 @@ import com.google.api.services.youtube.model.SearchResult; import dev.sheldan.abstracto.webservices.youtube.exception.YoutubeAPIException; import dev.sheldan.abstracto.webservices.youtube.exception.YoutubeVideoNotFoundException; import dev.sheldan.abstracto.webservices.youtube.model.YoutubeVideo; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -13,6 +14,7 @@ import java.io.IOException; import java.util.List; @Component +@Slf4j public class YoutubeSearchServiceBean implements YoutubeSearchService { @Autowired diff --git a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/resources/webservices-config.properties b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/resources/webservices-config.properties index 5cc16fe5b..287d4604f 100644 --- a/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/resources/webservices-config.properties +++ b/abstracto-application/abstracto-modules/webservices/webservices-impl/src/main/resources/webservices-config.properties @@ -1,3 +1,5 @@ +abstracto.feature.webservices.common.suggestionsURL=http://suggestqueries.google.com/complete/search?client=firefox&ds=%s&q=%s + abstracto.featureFlags.youtube.featureName=youtube abstracto.featureFlags.youtube.enabled=false diff --git a/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/exception/SuggestQueriesException.java b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/exception/SuggestQueriesException.java new file mode 100644 index 000000000..cb40b1f30 --- /dev/null +++ b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/exception/SuggestQueriesException.java @@ -0,0 +1,28 @@ +package dev.sheldan.abstracto.webservices.common.exception; + +import dev.sheldan.abstracto.core.exception.AbstractoRunTimeException; +import dev.sheldan.abstracto.core.templating.Templatable; +import dev.sheldan.abstracto.webservices.common.model.exception.SuggestQueriesExceptionModel; + +public class SuggestQueriesException extends AbstractoRunTimeException implements Templatable { + + private final SuggestQueriesExceptionModel model; + + public SuggestQueriesException(Integer responseCode) { + super(String.format("Request failure towards suggest queries %s.", responseCode)); + this.model = SuggestQueriesExceptionModel + .builder() + .responseCode(responseCode) + .build(); + } + + @Override + public String getTemplateName() { + return "suggest_queries_request_exception"; + } + + @Override + public Object getTemplateModel() { + return model; + } +} diff --git a/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/model/exception/SuggestQueriesExceptionModel.java b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/model/exception/SuggestQueriesExceptionModel.java new file mode 100644 index 000000000..dec527367 --- /dev/null +++ b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/model/exception/SuggestQueriesExceptionModel.java @@ -0,0 +1,14 @@ +package dev.sheldan.abstracto.webservices.common.model.exception; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +@Getter +@Setter +@Builder +public class SuggestQueriesExceptionModel implements Serializable { + private Integer responseCode; +} diff --git a/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesService.java b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesService.java new file mode 100644 index 000000000..03fe39e56 --- /dev/null +++ b/abstracto-application/abstracto-modules/webservices/webservices-int/src/main/java/dev/sheldan/abstracto/webservices/common/service/SuggestQueriesService.java @@ -0,0 +1,8 @@ +package dev.sheldan.abstracto.webservices.common.service; + +import java.util.List; + +public interface SuggestQueriesService { + List getSuggestionsForQuery(String query, String service); + List getYoutubeSuggestionsForQuery(String query); +} diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/config/ListenerExecutorConfig.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/config/ListenerExecutorConfig.java index aff168a00..0840c3195 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/config/ListenerExecutorConfig.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/config/ListenerExecutorConfig.java @@ -82,6 +82,11 @@ public class ListenerExecutorConfig { return executorService.setupExecutorFor("slashCommandListener"); } + @Bean(name = "slashCommandAutoCompleteExecutor") + public TaskExecutor slashCommandAutoCompleteExecutor() { + return executorService.setupExecutorFor("slashCommandAutoCompleteListener"); + } + @Bean(name = "emoteDeletedExecutor") public TaskExecutor emoteDeletedExecutor() { return executorService.setupExecutorFor("emoteDeletedListener"); diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandListenerBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandListenerBean.java index bb5e614ab..ec0352efc 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandListenerBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandListenerBean.java @@ -12,6 +12,7 @@ import dev.sheldan.abstracto.core.metric.service.MetricService; import dev.sheldan.abstracto.core.metric.service.MetricTag; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; import net.dv8tion.jda.api.hooks.ListenerAdapter; import org.jetbrains.annotations.NotNull; @@ -41,6 +42,10 @@ public class SlashCommandListenerBean extends ListenerAdapter { @Qualifier("slashCommandExecutor") private TaskExecutor slashCommandExecutor; + @Autowired + @Qualifier("slashCommandAutoCompleteExecutor") + private TaskExecutor slashCommandAutoCompleteExecutor; + @Autowired private SlashCommandListenerBean self; @@ -100,6 +105,29 @@ public class SlashCommandListenerBean extends ListenerAdapter { }); } + @Override + public void onCommandAutoCompleteInteraction(@NotNull CommandAutoCompleteInteractionEvent event) { + if(commands == null || commands.isEmpty()) return; + CompletableFuture.runAsync(() -> self.executeAutCompleteListenerLogic(event), slashCommandAutoCompleteExecutor).exceptionally(throwable -> { + log.error("Failed to execute listener logic in async auto complete interaction event.", throwable); + return null; + }); + } + + @Transactional + public void executeAutCompleteListenerLogic(CommandAutoCompleteInteractionEvent event) { + Optional potentialCommand = findCommand(event); + potentialCommand.ifPresent(command -> { + try { + List replies = command.performAutoComplete(event); + event.replyChoiceStrings(replies).queue(unused -> {}, + throwable -> log.error("Failed to response to complete of command {} in guild {}.", command.getConfiguration().getName(), event.getGuild().getIdLong())); + } catch (Exception exception) { + log.error("Error while executing autocomplete of command {}.", command.getConfiguration().getName(), exception); + } + }); + } + @Transactional(rollbackFor = AbstractoRunTimeException.class) public void executeCommand(SlashCommandInteractionEvent event, Command command, ConditionResult conditionResult) { CompletableFuture commandOutput; @@ -136,6 +164,14 @@ public class SlashCommandListenerBean extends ListenerAdapter { .findAny(); } + private Optional findCommand(CommandAutoCompleteInteractionEvent event) { + return commands + .stream() + .filter(command -> command.getConfiguration().getSlashCommandConfig().isEnabled()) + .filter(command -> command.getConfiguration().getSlashCommandConfig().matchesInteraction(event.getInteraction())) + .findAny(); + } + @PostConstruct public void filterPostProcessors() { metricService.registerCounter(SLASH_COMMANDS_PROCESSED_COUNTER, "Slash Commands processed"); diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandServiceBean.java index 89601ec57..074a0cf34 100644 --- a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandServiceBean.java +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/SlashCommandServiceBean.java @@ -124,11 +124,7 @@ public class SlashCommandServiceBean implements SlashCommandService { optionalParameters.add(new OptionData(type, parameter.getSlashCompatibleName() + "_" + i, parameterDescription, false)); } } else { - if(!parameter.isOptional()) { - requiredParameters.add(new OptionData(type, parameter.getSlashCompatibleName(), parameterDescription, true)); - } else { - optionalParameters.add(new OptionData(type, parameter.getSlashCompatibleName(), parameterDescription, false)); - } + requiredParameters.add(new OptionData(type, parameter.getSlashCompatibleName(), parameterDescription, !parameter.isOptional(), parameter.getSupportsAutoComplete())); } } }); diff --git a/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteServiceBean.java b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteServiceBean.java new file mode 100644 index 000000000..c1ee1d61b --- /dev/null +++ b/abstracto-application/core/core-impl/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteServiceBean.java @@ -0,0 +1,12 @@ +package dev.sheldan.abstracto.core.interaction.slash.parameter; + +import net.dv8tion.jda.api.interactions.AutoCompleteQuery; +import org.springframework.stereotype.Component; + +@Component +public class SlashCommandAutoCompleteServiceBean implements SlashCommandAutoCompleteService{ + @Override + public boolean matchesParameter(AutoCompleteQuery query, String parameterName) { + return query.getName().equalsIgnoreCase(parameterName); + } +} diff --git a/abstracto-application/core/core-impl/src/main/resources/ehcache.xml b/abstracto-application/core/core-impl/src/main/resources/ehcache.xml index ea178cd08..f44cd2f84 100644 --- a/abstracto-application/core/core-impl/src/main/resources/ehcache.xml +++ b/abstracto-application/core/core-impl/src/main/resources/ehcache.xml @@ -15,6 +15,16 @@ 2000 + + + + 7200 + + + + 2000 + + 600 diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/Command.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/Command.java index 19c8855e8..ea0ca4e45 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/Command.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/Command.java @@ -4,8 +4,11 @@ import dev.sheldan.abstracto.core.FeatureAware; import dev.sheldan.abstracto.core.command.config.CommandConfiguration; import dev.sheldan.abstracto.core.command.execution.CommandContext; import dev.sheldan.abstracto.core.command.execution.CommandResult; +import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent; import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.CompletableFuture; public interface Command extends FeatureAware { @@ -13,5 +16,6 @@ public interface Command extends FeatureAware { default CommandResult execute(CommandContext commandContext) {return CommandResult.fromSuccess();} default CompletableFuture executeAsync(CommandContext commandContext) {return CompletableFuture.completedFuture(CommandResult.fromSuccess());} default CompletableFuture executeSlash(SlashCommandInteractionEvent event) { return CompletableFuture.completedFuture(CommandResult.fromSuccess());} + default List performAutoComplete(CommandAutoCompleteInteractionEvent event) { return new ArrayList<>(); } CommandConfiguration getConfiguration(); } diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/Parameter.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/Parameter.java index 9cbd5c8c2..f88a3fe6e 100644 --- a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/Parameter.java +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/command/config/Parameter.java @@ -29,6 +29,8 @@ public class Parameter implements Serializable { @Builder.Default private Integer listSize = 0; @Builder.Default + private Boolean supportsAutoComplete = false; + @Builder.Default private List validators = new ArrayList<>(); @Builder.Default private Map additionalInfo = new HashMap<>(); diff --git a/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteService.java b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteService.java new file mode 100644 index 000000000..7c32395be --- /dev/null +++ b/abstracto-application/core/core-int/src/main/java/dev/sheldan/abstracto/core/interaction/slash/parameter/SlashCommandAutoCompleteService.java @@ -0,0 +1,7 @@ +package dev.sheldan.abstracto.core.interaction.slash.parameter; + +import net.dv8tion.jda.api.interactions.AutoCompleteQuery; + +public interface SlashCommandAutoCompleteService { + boolean matchesParameter(AutoCompleteQuery query, String parameterName); +} diff --git a/abstracto-application/pom.xml b/abstracto-application/pom.xml index 5dd4c5871..a2e63a6fc 100644 --- a/abstracto-application/pom.xml +++ b/abstracto-application/pom.xml @@ -58,7 +58,6 @@ yyyy/MM/dd HH:mm 5.0.0-alpha.12 - 3.0.4 2.0.0-RC.1 1.5.3 2.3.0