Skip to content

Commit c12a559

Browse files
Merge pull request #1 from BenjammingKirby/autocomplete
add autocomplete
2 parents 0cebae3 + 8c9a5cf commit c12a559

File tree

4 files changed

+86
-23
lines changed

4 files changed

+86
-23
lines changed

src/bot.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import "dotenv/config";
22
import { Client, Collection, LimitedCollection } from "discord.js";
33
import { MyContext } from "./interfaces";
4-
import { loadCommands, interactionCreateHandler } from "./handlers/CommandHandler";
4+
import { loadCommands, interactionCreateHandler } from "./handlers/InteractionCreateHandler";
55
import { messageHandler } from "./handlers/MessageHandler";
66

77
(async function () {

src/commands/docs/djs.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ const command: Command = {
1616
option
1717
.setName("query")
1818
.setDescription("Enter the phrase you'd like to search for, e.g: client#isready")
19-
.setRequired(true),
19+
.setRequired(true)
20+
.setAutocomplete(true),
2021
)
2122
.addStringOption((option) =>
2223
option
@@ -39,8 +40,12 @@ const command: Command = {
3940
(interaction.options.getString("source") as keyof typeof sources) || "stable";
4041
// Whether to include private elements on the search results, by default false, shows private elements if the search returns an exact result;
4142
const searchPrivate = interaction.options.getBoolean("private") || false;
42-
const doc = await Doc.fetch(source, { force: true });
43+
const doc = await Doc.fetch(source, { force: true }).catch(console.error);
4344

45+
if (!doc) {
46+
await interaction.reply({ content: "Couldn't fetch docs", ephemeral: true }).catch(console.error);
47+
return;
48+
}
4449
// const resultEmbed = doc.resolveEmbed(query, { excludePrivateElements: !searchPrivate });
4550
const result = searchDJSDoc(doc, query, searchPrivate);
4651
// If a result wasn't found

src/commands/docs/mdn.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ const command: Command = {
3131
opt
3232
.setName("query")
3333
.setDescription("Enter the phrase you'd like to search for. Example: Array.filter")
34-
.setRequired(true),
34+
.setRequired(true)
35+
.setAutocomplete(true),
3536
),
3637
async execute(interaction) {
3738
const deleteButtonRow = new MessageActionRow().addComponents([deleteButton(interaction.user.id)]);
@@ -45,10 +46,15 @@ const command: Command = {
4546

4647
if (!search.length) {
4748
embed.setColor(0xff0000).setDescription("No results found...");
48-
await interaction.reply({ embeds: [embed], ephemeral: true });
49+
await interaction.reply({ embeds: [embed], ephemeral: true }).catch(console.error);
4950
return;
5051
} else if (search.length === 1) {
5152
const resultEmbed = await getSingleMDNSearchResults(search[0]);
53+
if (!resultEmbed) {
54+
await interaction.reply({ content: "Couldn't find any results", ephemeral: true }).catch(console.error);
55+
return;
56+
}
57+
5258
await interaction
5359
.reply({
5460
embeds: [resultEmbed],
@@ -59,15 +65,12 @@ const command: Command = {
5965
return;
6066
} else {
6167
// If there are multiple results, send a select menu from which the user can choose which one to send
62-
const results = search.map((path) => `**• [${path.replace(/_|-/g, " ")}](${MDN_BASE_URL}${path})**`);
63-
64-
embed.setDescription(results.join("\n"));
6568
const selectMenuRow = new MessageActionRow().addComponents(
6669
new MessageSelectMenu()
6770
.setCustomId("mdnselect/" + interaction.user.id)
6871
.addOptions(
6972
search.map((val) => {
70-
const parsed = val.length >= 99 ? val.split("/").at(-1) : val;
73+
const parsed = val.length >= 99 ? val.split("/").slice(-2).join("/") : val;
7174
return { label: parsed, value: parsed };
7275
}),
7376
)
@@ -87,8 +90,16 @@ const command: Command = {
8790

8891
// Export to reuse on the select menu handler
8992
export async function getSingleMDNSearchResults(searchQuery: string) {
90-
const res = await fetch(`${MDN_BASE_URL + searchQuery}/index.json`);
91-
const doc: MdnDoc = (await res.json()).doc;
93+
// Search for the match once again
94+
const { index, sitemap } = await getSources();
95+
const secondSearch = index.search(searchQuery, { limit: 10 }).map((id) => sitemap[<number>id].loc)[0];
96+
97+
const res = await fetch(`${MDN_BASE_URL + secondSearch}/index.json`).catch(console.error);
98+
if (!res || !res?.ok) return null;
99+
const resJSON = await res.json?.().catch(console.error);
100+
if (!res.json) return null;
101+
102+
const doc: MdnDoc = resJSON.doc;
92103

93104
return new MessageEmbed()
94105
.setColor(MDN_BLUE_COLOR)
@@ -99,7 +110,7 @@ export async function getSingleMDNSearchResults(searchQuery: string) {
99110
.setThumbnail(MDN_ICON_URL)
100111
.setDescription(doc.summary);
101112
}
102-
async function getSources(): Promise<typeof sources> {
113+
export async function getSources(): Promise<typeof sources> {
103114
if (sources.lastUpdated && Date.now() - sources.lastUpdated < 43200000 /* 12 hours */) return sources;
104115

105116
const res = await fetch("https://developer.mozilla.org/sitemaps/en-us/sitemap.xml.gz");
@@ -109,7 +120,6 @@ async function getSources(): Promise<typeof sources> {
109120
loc: entry.loc.slice(MDN_BASE_URL.length),
110121
lastmod: new Date(entry.lastmod).valueOf(),
111122
}));
112-
113123
const index = new flexsearch.Index();
114124
sitemap.forEach((entry, idx) => index.add(idx, entry.loc));
115125

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
11
import type { Command, MyContext } from "../interfaces";
2-
import type { ButtonInteraction, CommandInteraction, Interaction, Message, SelectMenuInteraction } from "discord.js";
2+
import type {
3+
AutocompleteInteraction,
4+
ButtonInteraction,
5+
CommandInteraction,
6+
Interaction,
7+
Message,
8+
SelectMenuInteraction,
9+
} from "discord.js";
310
import type { APIEmbed } from "discord-api-types";
411

512
import { commandCooldownCheck, commandPermissionCheck, deleteButton } from "../utils/CommandUtils";
6-
import { getSingleMDNSearchResults } from "../commands/docs/mdn";
13+
import { getSingleMDNSearchResults, getSources } from "../commands/docs/mdn";
714
import { searchDJSDoc } from "../commands/docs/djs";
815
import { MessageActionRow } from "discord.js";
916

1017
import glob from "glob";
1118
import Doc from "discord.js-docs";
1219

1320
export async function interactionCreateHandler(context: MyContext, interaction: Interaction<"cached">) {
14-
if (interaction.isCommand()) {
15-
commandInteractionHandler(context, interaction);
16-
} else if (interaction.isButton()) {
17-
buttonInteractionHandler(context, interaction);
18-
} else if (interaction.isSelectMenu()) {
19-
selectMenuInteractionHandler(context, interaction);
21+
try {
22+
if (interaction.isCommand()) {
23+
await commandInteractionHandler(context, interaction);
24+
} else if (interaction.isButton()) {
25+
await buttonInteractionHandler(context, interaction);
26+
} else if (interaction.isSelectMenu()) {
27+
await selectMenuInteractionHandler(context, interaction);
28+
} else if (interaction.isAutocomplete()) {
29+
await autocompleteInteractionHandler(context, interaction);
30+
}
31+
} catch (e) {
32+
console.error(e);
2033
}
2134
}
2235
/**
@@ -119,5 +132,40 @@ async function buttonInteractionHandler(context: MyContext, interaction: ButtonI
119132
.catch(console.error);
120133
}
121134
}
122-
// TODO add autocomplete
123-
// async function autocompleteInteractionHandler(context: MyContext, interaction: AutocompleteInteraction) {}s
135+
136+
async function autocompleteInteractionHandler(context: MyContext, interaction: AutocompleteInteraction) {
137+
switch (interaction.commandName) {
138+
case "djs": {
139+
// Check the cache, the command will force fetch anyway
140+
const doc = await Doc.fetch("stable", { force: false });
141+
const query = interaction.options.getFocused() as string;
142+
const singleElement = doc.get(...query.split(/\.|#/));
143+
if (singleElement) {
144+
await interaction
145+
.respond([{ name: singleElement.formattedName, value: singleElement.formattedName }])
146+
.catch(console.error);
147+
return;
148+
}
149+
const searchResults = doc.search(query, { excludePrivateElements: false });
150+
if (!searchResults) {
151+
await interaction.respond([]).catch(console.error);
152+
return;
153+
}
154+
await interaction
155+
.respond(searchResults.map((elem) => ({ name: elem.formattedName, value: elem.formattedName })))
156+
.catch(console.error);
157+
break;
158+
}
159+
case "mdn": {
160+
const query = interaction.options.getFocused() as string;
161+
162+
const { index, sitemap } = await getSources();
163+
const search = index.search(query, { limit: 10 }).map((id) => {
164+
const val = sitemap[<number>id].loc;
165+
const parsed = val.length >= 99 ? val.split("/").slice(-2).join("/") : val;
166+
return { name: parsed, value: parsed };
167+
});
168+
await interaction.respond(search).catch(console.error);
169+
}
170+
}
171+
}

0 commit comments

Comments
 (0)