enabling auto complete for youtube command

This commit is contained in:
Sheldan
2022-07-27 22:13:48 +02:00
parent 51edb50eee
commit f9b85feeaf
17 changed files with 238 additions and 7 deletions

View File

@@ -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<String> 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<String> 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<String> getYoutubeSuggestionsForQuery(String query) {
if(query == null || "".equals(query)) {
return new ArrayList<>();
}
return self.getSuggestionsForQuery(query, "yt");
}
}

View File

@@ -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()) {

View File

@@ -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<CommandResult> 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<String> 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<Parameter> 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);

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,8 @@
package dev.sheldan.abstracto.webservices.common.service;
import java.util.List;
public interface SuggestQueriesService {
List<String> getSuggestionsForQuery(String query, String service);
List<String> getYoutubeSuggestionsForQuery(String query);
}

View File

@@ -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");

View File

@@ -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<Command> potentialCommand = findCommand(event);
potentialCommand.ifPresent(command -> {
try {
List<String> 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<CommandResult> commandOutput;
@@ -136,6 +164,14 @@ public class SlashCommandListenerBean extends ListenerAdapter {
.findAny();
}
private Optional<Command> 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");

View File

@@ -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()));
}
}
});

View File

@@ -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);
}
}

View File

@@ -15,6 +15,16 @@
<heap unit="entries">2000</heap>
</resources>
</cache>
<!-- TODO no nice way yet to have this configuration split up -->
<cache uses-template="default" alias="general-use-cache">
<expiry>
<ttl unit="seconds">7200</ttl>
</expiry>
<resources>
<heap unit="entries">2000</heap>
</resources>
</cache>
<cache-template name="default">
<expiry>
<ttl unit="seconds">600</ttl>

View File

@@ -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<CommandResult> executeAsync(CommandContext commandContext) {return CompletableFuture.completedFuture(CommandResult.fromSuccess());}
default CompletableFuture<CommandResult> executeSlash(SlashCommandInteractionEvent event) { return CompletableFuture.completedFuture(CommandResult.fromSuccess());}
default List<String> performAutoComplete(CommandAutoCompleteInteractionEvent event) { return new ArrayList<>(); }
CommandConfiguration getConfiguration();
}

View File

@@ -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<ParameterValidator> validators = new ArrayList<>();
@Builder.Default
private Map<String, Object> additionalInfo = new HashMap<>();

View File

@@ -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);
}

View File

@@ -58,7 +58,6 @@
<properties>
<maven.build.timestamp.format>yyyy/MM/dd HH:mm</maven.build.timestamp.format>
<jda.version>5.0.0-alpha.12</jda.version>
<jda.utilities.version>3.0.4</jda.utilities.version>
<asciidoctor.maven.plugin.version>2.0.0-RC.1</asciidoctor.maven.plugin.version>
<asciidoctorj.pdf.version>1.5.3</asciidoctorj.pdf.version>
<asciidoctorj.version>2.3.0</asciidoctorj.version>