Autocomplete
Como criar comandos de autocomplete do discord
O que é autocomplete?
Imagine que você queira fornecer escolhas para uma opção de um comando mas de forma dinâmica, por exemplo um comando para pesquisar vídeos no youtube. Você pode usar a opção autocomplete
, assim com base no que o usuário digitar nessa opção, você pode fazer buscas em APIs e retornar uma lista de escolhas.
Criando uma opção autocomplete
É preciso ter um comando de barra para criar uma opção autocomplete
Apenas opções do tipo String, Number e Integer podem ser autocomplete. Para isso basta definir a propriedade autocomplete
como verdadeira no objeto da opção no seu comando:
createCommand({
name: "busca",
description: "Comando de busca",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "termo",
description: "termo",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required,
}
],
// ...
});
Respondendo a opção autocomplete
Quando o usuário digitar algo na opção do comando lá no discord, o evento interactionCreate é emitido com uma interação autocomplete. Você pode responder ela no comando dessa forma:
createCommand({
name: "busca",
description: "Comando de busca",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "termo",
description: "termo",
type: ApplicationCommandOptionType.String,
autocomplete: true,
required,
}
],
async autocomplete(interaction) {
const focused = interaction.options.getFocused();
const results = await searchData(focused);
if (results.length < 1) return;
const choices = results.map(data => ({
name: data.title, value: data.url
}));
return choices;
},
// ...
});
Essa função irá retornar uma lista de escolhas que o usuário poderá escolher na opção! Para obter a opção escolhida quando o comando for enviado, pegue a opção pelo nome dela:
createCommand({
name: "busca",
description: "Comando de busca",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "termo",
autocomplete: true,
// **
}
],
async autocomplete(interaction) {
// **
},
async run(interaction){
const { options } = interaction;
const query = options.getString("termo", true);
interaction.reply({ flags: ["Ephemeral"], content: query });
}
});
Note que você pode apenas retornar as opções, pois o manipulador de comandos autocomplete irá automaticamente limitar os itens do array em 25
// ...
return choices;
// ...
Se você tiver uma grande quantidade de itens, use o autocomplete para tentar encontrá-los
createCommand({
// ...
async autocomplete(interaction) {
const { options, guild } = interaction;
const focused = options.getFocused();
const documents = await db.get(guild.id);
const filtered = documents.filter(
data => data.address.toLowercase().includes(focused.toLowercase())
)
if (filtered.length < 1) return;
return filtered.map(data => ({
name: data.title, value: data.url
}));
},
// ...
})
Se você preferir, pode usar o método respond
da interação como faria normalmente:
return choices;
interaction.respond(choices.slice(0, 25))
Múltiplas opções autocomplete
Se você tiver muitas opções autocomplete no seu comando, será necessário verificar qual é a opção para poder responder ela corretamente. Considere essas opções:
import { createCommand } from "#base";
import { includesIgnoreCase } from "@magicyan/discord";
import { ApplicationCommandOptionType, ApplicationCommandType } from "discord.js";
import { fetchWithCache } from "#functions";
import { env } from "#env";
createCommand({
name: "search",
description: "app command",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "application",
description: "your custom application",
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
},
{
name: "video",
description: "video name, description or tags",
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
},
{
name: "subscriptor",
description: "select the subscriptor",
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
},
],
// ...
});
Cada uma delas recebe escolhas diferentes, então você teria que fazer algo como isso:
createCommand({
name: "search",
description: "app command",
type: ApplicationCommandType.ChatInput,
options: [
// ...
],
async autocomplete({ options }) {
const { name, value } = options.getFocused(true);
switch (name) {
case "application": {
const apps = await fetchWithCache({
url: `${env.API_URL}/apps`
seconds: 30
});
return apps
.filter(app => includesIgnoreCase(app.name, value))
.map(app => ({
name: app.name,
value: app.id
}));
}
case "video": {
const appId = options.getString("application", true);
const app = await fetchWithCache({
url: `${env.API_URL}/apps/${appId}`,
seconds: 50
});
return app.videos
.filter(video =>
includesIgnoreCase(video.title, value) ||
includesIgnoreCase(video.description, value) ||
video.tags.includes(value)
)
.map(video => ({
name: video.title,
value: video.id
}));
}
default: {
const videoId = options.getString("video", true);
const video = await fetchWithCache({
url: `${env.API_URL}/videos/${videoId}`,
seconds: 50
});
return video.subscriptors
.filter(sub => includesIgnoreCase(sub.name, value))
.map(sub => ({
name: sub.title,
value: sub.id
}));
}
}
}
});
Você pode preferir utilizar um recurso exclusivo dessa base para responder as interações autocomplete diretamente no objeto da opção.
Ao invés de definir um boleano verdadeiro, você pode definir diretamente a função que responde aquela opção, assim não é necessário verificar qual é a opção autocomplete, pois o manipulador já fará isso pra você:
import { createCommand } from "#base";
import { includesIgnoreCase } from "@magicyan/discord";
import { ApplicationCommandOptionType, ApplicationCommandType } from "discord.js";
import { fetchWithCache } from "#functions";
import { env } from "#env";
createCommand({
name: "search",
description: "app command",
type: ApplicationCommandType.ChatInput,
options: [
{
name: "application",
description: "your custom application",
type: ApplicationCommandOptionType.String,
required: true,
async autocomplete({ options }){
const apps = await fetchWithCache({
url: `${env.API_URL}/apps`,
seconds: 30
});
return apps
.filter(app => includesIgnoreCase(app.name, options.getFocused()))
.map(app => ({
name: app.name,
value: app.id
}));
}
},
{
name: "video",
description: "video name, description or tags",
type: ApplicationCommandOptionType.String,
required: true,
async autocomplete(){
const appId = options.getString("application", true);
const app = await fetchWithCache({
url: `${env.API_URL}/apps/${appId}`
seconds: 50
});
const query = options.getFocused();
return app.videos
.filter(video =>
includesIgnoreCase(video.title, query) ||
includesIgnoreCase(video.description, query) ||
video.tags.includes(query)
)
.map(video => ({
name: video.title,
value: video.id
}));
}
},
{
name: "subscriptor",
description: "select the subscriptor",
type: ApplicationCommandOptionType.String,
required: true,
async autocomplete(){
const videoId = options.getString("video", true);
const video = await fetchWithCache({
url: `${env.API_URL}/videos/${videoId}`
seconds: 50
});
return video.subscriptors
.filter(sub => includesIgnoreCase(sub.name, options.getFocused()))
.map(sub => ({
name: sub.title,
value: sub.id
}));
}
},
],
async autocomplete({ options }){
// ...
}
});