Skip to content

Commit 19e7f3c

Browse files
authored
Merge pull request #18 from BenjammingKirby/master
Add autocomplete
2 parents 1bb5dc0 + f933de3 commit 19e7f3c

File tree

4 files changed

+108
-41
lines changed

4 files changed

+108
-41
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: 15 additions & 7 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.editReply({ content: "Couldn't fetch docs" }).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
@@ -52,7 +57,7 @@ const command: Command = {
5257
// Satisfies the method's MessageEmbedOption type
5358
const embedObj = { ...notFoundEmbed, timestamp: timeStampDate };
5459

55-
await interaction.reply({ embeds: [embedObj], ephemeral: true }).catch(console.error);
60+
await interaction.editReply({ embeds: [embedObj] }).catch(console.error);
5661
return;
5762
} else if (Array.isArray(result)) {
5863
// If there are multiple results, send a select menu from which the user can choose which one to send
@@ -64,9 +69,8 @@ const command: Command = {
6469
.setPlaceholder("Select documentation to send"),
6570
);
6671
await interaction
67-
.reply({
72+
.editReply({
6873
content: "Didn't find an exact match, please select one from below",
69-
ephemeral: true,
7074
components: [selectMenuRow],
7175
})
7276
.catch(console.error);
@@ -82,7 +86,10 @@ const command: Command = {
8286
// The final field should be the View Source button
8387
embedObj.fields = [embedObj.fields?.at(-1)];
8488
}
85-
await interaction.reply({ embeds: [embedObj], components: [deleteButtonRow] }).catch(console.error);
89+
await interaction.editReply({
90+
content: "Sent documentations for " + (query.length >= 100 ? query.slice(0, 100) + "..." : query),
91+
});
92+
await interaction.followUp({ embeds: [embedObj], components: [deleteButtonRow] }).catch(console.error);
8693
return;
8794
},
8895
};
@@ -105,8 +112,9 @@ export function searchDJSDoc(doc: Doc, query: string, searchPrivate?: boolean) {
105112
const searchResults = doc.search(query, options);
106113
if (!searchResults) return null;
107114
return searchResults.map((res) => {
115+
const parsedDescription = res.description?.trim?.() ?? "No description provided";
108116
// Labels and values have a limit of 100 characters
109-
const description = res.description.length >= 99 ? res.description.slice(0, 96) + "..." : res.description;
117+
const description = parsedDescription.length >= 99 ? parsedDescription.slice(0, 96) + "..." : parsedDescription;
110118
return {
111119
label: res.formattedName,
112120
description,

src/commands/docs/mdn.ts

Lines changed: 24 additions & 13 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,12 +46,19 @@ 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.editReply({ embeds: [embed] }).catch(console.error);
4950
return;
5051
} else if (search.length === 1) {
5152
const resultEmbed = await getSingleMDNSearchResults(search[0]);
53+
if (!resultEmbed) {
54+
await interaction.editReply({ content: "Couldn't find any results" }).catch(console.error);
55+
return;
56+
}
5257
await interaction
53-
.reply({
58+
.editReply("Sent documentation for " + (query.length >= 100 ? query.slice(0, 100) + "..." : query))
59+
.catch(console.error);
60+
await interaction
61+
.followUp({
5462
embeds: [resultEmbed],
5563
components: [deleteButtonRow],
5664
})
@@ -59,24 +67,20 @@ const command: Command = {
5967
return;
6068
} else {
6169
// 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"));
6570
const selectMenuRow = new MessageActionRow().addComponents(
6671
new MessageSelectMenu()
6772
.setCustomId("mdnselect/" + interaction.user.id)
6873
.addOptions(
6974
search.map((val) => {
70-
const parsed = val.length >= 99 ? val.split("/").at(-1) : val;
75+
const parsed = val.length >= 99 ? val.split("/").slice(-2).join("/") : val;
7176
return { label: parsed, value: parsed };
7277
}),
7378
)
7479
.setPlaceholder("Select documentation to send"),
7580
);
7681
await interaction
77-
.reply({
82+
.editReply({
7883
content: "Didn't find an exact match, please select one from below",
79-
ephemeral: true,
8084
components: [selectMenuRow],
8185
})
8286
.catch(console.error);
@@ -87,8 +91,16 @@ const command: Command = {
8791

8892
// Export to reuse on the select menu handler
8993
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;
94+
// Search for the match once again
95+
const { index, sitemap } = await getSources();
96+
const secondSearch = index.search(searchQuery, { limit: 10 }).map((id) => sitemap[<number>id].loc)[0];
97+
98+
const res = await fetch(`${MDN_BASE_URL + secondSearch}/index.json`).catch(console.error);
99+
if (!res || !res?.ok) return null;
100+
const resJSON = await res.json?.().catch(console.error);
101+
if (!res.json) return null;
102+
103+
const doc: MdnDoc = resJSON.doc;
92104

93105
return new MessageEmbed()
94106
.setColor(MDN_BLUE_COLOR)
@@ -99,7 +111,7 @@ export async function getSingleMDNSearchResults(searchQuery: string) {
99111
.setThumbnail(MDN_ICON_URL)
100112
.setDescription(doc.summary);
101113
}
102-
async function getSources(): Promise<typeof sources> {
114+
export async function getSources(): Promise<typeof sources> {
103115
if (sources.lastUpdated && Date.now() - sources.lastUpdated < 43200000 /* 12 hours */) return sources;
104116

105117
const res = await fetch("https://developer.mozilla.org/sitemaps/en-us/sitemap.xml.gz");
@@ -109,7 +121,6 @@ async function getSources(): Promise<typeof sources> {
109121
loc: entry.loc.slice(MDN_BASE_URL.length),
110122
lastmod: new Date(entry.lastmod).valueOf(),
111123
}));
112-
113124
const index = new flexsearch.Index();
114125
sitemap.forEach((entry, idx) => index.add(idx, entry.loc));
115126

Lines changed: 68 additions & 20 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
/**
@@ -45,8 +58,10 @@ export function loadCommands(context: MyContext) {
4558
});
4659
}
4760
async function commandInteractionHandler(context: MyContext, interaction: CommandInteraction) {
61+
await interaction.deferReply({ ephemeral: true }).catch(console.error);
62+
4863
const command = context.commands.get(interaction.commandName);
49-
if (!command) return interaction.reply({ content: "Command not found", ephemeral: true });
64+
if (!command) return interaction.editReply({ content: "Command not found" }).catch(console.error);
5065

5166
if (commandPermissionCheck(interaction, command)) return;
5267
if (commandCooldownCheck(interaction, command, context)) return;
@@ -55,15 +70,13 @@ async function commandInteractionHandler(context: MyContext, interaction: Comman
5570
} catch (e) {
5671
console.error(e);
5772
const errorMessage = "An error has occurred";
58-
interaction
59-
.reply({
60-
content: errorMessage,
61-
ephemeral: true,
62-
})
63-
.catch(console.error);
73+
await interaction[interaction.replied ? "editReply" : "reply"]?.({
74+
content: errorMessage,
75+
}).catch(console.error);
6476
}
6577
}
6678
async function selectMenuInteractionHandler(context: MyContext, interaction: SelectMenuInteraction) {
79+
await interaction.deferUpdate().catch(console.error);
6780
const CommandName = interaction.customId.split("/")[0];
6881
switch (CommandName) {
6982
case "mdnselect": {
@@ -74,7 +87,7 @@ async function selectMenuInteractionHandler(context: MyContext, interaction: Sel
7487

7588
// Remove the menu and update the ephemeral message
7689
await interaction
77-
.update({ content: "Sent documentations for " + selectedValue, components: [] })
90+
.editReply({ content: "Sent documentations for " + selectedValue, components: [] })
7891
.catch(console.error);
7992
// Send documentation
8093
await interaction.followUp({ embeds: [resultEmbed], components: [deleteButtonRow] }).catch(console.error);
@@ -92,14 +105,14 @@ async function selectMenuInteractionHandler(context: MyContext, interaction: Sel
92105

93106
// Remove the menu and update the ephemeral message
94107
await interaction
95-
.update({ content: "Sent documentations for " + selectedValue, components: [] })
108+
.editReply({ content: "Sent documentations for " + selectedValue, components: [] })
96109
.catch(console.error);
97110
// Send documentation
98111
await interaction.followUp({ embeds: [resultEmbed], components: [deleteButtonRow] }).catch(console.error);
99112
break;
100113
}
101114
default: {
102-
interaction.reply({ content: "Unknown menu", ephemeral: true }).catch(console.error);
115+
interaction.editReply({ content: "Unknown menu" }).catch(console.error);
103116
}
104117
}
105118
}
@@ -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)