Skip to content

Commit 2914a20

Browse files
add all the handlers to command file
1 parent b7d3f13 commit 2914a20

File tree

8 files changed

+420
-359
lines changed

8 files changed

+420
-359
lines changed

.eslintrc.json

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,26 @@
44
"es2021": true,
55
"node": true
66
},
7-
"extends": [
8-
"eslint:recommended",
9-
"plugin:@typescript-eslint/recommended"
10-
],
7+
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
118
"parser": "@typescript-eslint/parser",
129
"parserOptions": {
1310
"ecmaVersion": 12
1411
},
15-
"plugins": [
16-
"@typescript-eslint"
17-
],
12+
"plugins": ["@typescript-eslint"],
1813
"rules": {
19-
"indent": [
20-
"error",
21-
4, { "SwitchCase": 1 }
22-
],
23-
"linebreak-style": [
24-
"error",
25-
"unix"
26-
],
27-
"quotes": [
28-
"error",
29-
"double"
30-
],
31-
"semi": [
32-
"error",
33-
"always"
34-
],
35-
"strict" :"error",
36-
"no-var":"error",
37-
"no-console": ["warn", {"allow": ["info", "error", "warn"]}],
38-
"array-callback-return":"error",
39-
"yoda":"error",
40-
"@typescript-eslint/no-unused-vars": ["error", {"varsIgnorePattern": "^_", "argsIgnorePattern": "^_"}],
14+
"indent": ["off"],
15+
"@typescript-eslint/indent": ["error", 4, { "SwitchCase": 1 }],
16+
"linebreak-style": ["error", "unix"],
17+
"quotes": ["error", "double"],
18+
"semi": ["error", "always"],
19+
"strict": "error",
20+
"no-var": "error",
21+
"no-console": ["warn", { "allow": ["info", "error", "warn"] }],
22+
"array-callback-return": "error",
23+
"yoda": "error",
24+
"@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],
4125
"@typescript-eslint/ban-ts-comment": "off",
42-
"@typescript-eslint/no-non-null-assertion": "off"
26+
"@typescript-eslint/no-non-null-assertion": "off",
27+
"prefer-const": ["error", { "destructuring": "all" }]
4328
}
4429
}

src/bot.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@ import { messageHandler } from "./handlers/MessageHandler";
2828
return new LimitedCollection({ maxSize: 0 });
2929
},
3030
}),
31-
commands: new Collection(),
31+
commands: {
32+
autocompletes: new Collection(),
33+
buttons: new Collection(),
34+
selectMenus: new Collection(),
35+
slashCommands: new Collection(),
36+
},
3237
cooldownCounter: new Collection(),
3338
};
3439
const docsBot = context.client;

src/commands/docs/djs.ts

Lines changed: 155 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -2,96 +2,167 @@ import { Command } from "../../interfaces";
22
import { SlashCommandBuilder } from "@discordjs/builders";
33
import Doc, { sources } from "discord.js-docs";
44
import { checkEmbedLimits } from "../../utils/EmbedUtils";
5-
import { deleteButton } from "../../utils/CommandUtils";
5+
import { deleteButton, deleteButtonHandler } from "../../utils/CommandUtils";
66
import { MessageActionRow, MessageSelectMenu } from "discord.js";
77
import type { APIEmbed } from "discord-api-types";
88

99
const supportedBranches = Object.keys(sources).map((branch) => [capitalize(branch), branch] as [string, string]);
1010

1111
const command: Command = {
12-
data: new SlashCommandBuilder()
13-
.setName("djs")
14-
.setDescription("Search discord.js documentations, supports builders, voice, collection and rpc documentations")
15-
.addStringOption((option) =>
16-
option
17-
.setName("query")
18-
.setDescription("Enter the phrase you'd like to search for, e.g: client#isready")
19-
.setRequired(true)
20-
.setAutocomplete(true),
21-
)
22-
.addStringOption((option) =>
23-
option
24-
.setName("source")
25-
.setDescription("Select which branch/repository to get documentation off of (Default: stable)")
26-
.addChoices(supportedBranches)
27-
.setRequired(false),
28-
)
29-
.addBooleanOption((option) =>
30-
option
31-
.setName("private")
32-
.setDescription("Whether or not to search private elements (default false)")
33-
.setRequired(false),
34-
),
35-
async execute(interaction) {
36-
const deleteButtonRow = new MessageActionRow().addComponents([deleteButton(interaction.user.id)]);
37-
const query = interaction.options.getString("query");
38-
// The Default source should be stable
39-
const source: keyof typeof sources =
40-
(interaction.options.getString("source") as keyof typeof sources) || "stable";
41-
// Whether to include private elements on the search results, by default false, shows private elements if the search returns an exact result;
42-
const searchPrivate = interaction.options.getBoolean("private") || false;
43-
const doc = await Doc.fetch(source, { force: true }).catch(console.error);
44-
45-
if (!doc) {
46-
await interaction.editReply({ content: "Couldn't fetch docs" }).catch(console.error);
47-
return;
48-
}
49-
// const resultEmbed = doc.resolveEmbed(query, { excludePrivateElements: !searchPrivate });
50-
const result = searchDJSDoc(doc, query, searchPrivate);
51-
// If a result wasn't found
52-
if (!result || (result as APIEmbed).description === "") {
53-
const notFoundEmbed = doc.baseEmbed();
54-
notFoundEmbed.description = "Didn't find any results for that query";
55-
56-
const timeStampDate = new Date(notFoundEmbed.timestamp);
57-
// Satisfies the method's MessageEmbedOption type
58-
const embedObj = { ...notFoundEmbed, timestamp: timeStampDate };
59-
60-
await interaction.editReply({ embeds: [embedObj] }).catch(console.error);
61-
return;
62-
} else if (Array.isArray(result)) {
63-
// If there are multiple results, send a select menu from which the user can choose which one to send
64-
65-
const selectMenuRow = new MessageActionRow().addComponents(
66-
new MessageSelectMenu()
67-
.setCustomId(`djsselect/${source}/${searchPrivate}/${interaction.user.id}`)
68-
.addOptions(result)
69-
.setPlaceholder("Select documentation to send"),
70-
);
71-
await interaction
72-
.editReply({
73-
content: "Didn't find an exact match, please select one from below",
74-
components: [selectMenuRow],
75-
})
76-
.catch(console.error);
12+
slashCommand: {
13+
data: new SlashCommandBuilder()
14+
.setName("djs")
15+
.setDescription(
16+
"Search discord.js documentations, supports builders, voice, collection and rpc documentations",
17+
)
18+
.addStringOption((option) =>
19+
option
20+
.setName("query")
21+
.setDescription("Enter the phrase you'd like to search for, e.g: client#isready")
22+
.setRequired(true)
23+
.setAutocomplete(true),
24+
)
25+
.addStringOption((option) =>
26+
option
27+
.setName("source")
28+
.setDescription("Select which branch/repository to get documentation off of (Default: stable)")
29+
.addChoices(supportedBranches)
30+
.setRequired(false),
31+
)
32+
.addBooleanOption((option) =>
33+
option
34+
.setName("private")
35+
.setDescription("Whether or not to search private elements (default false)")
36+
.setRequired(false),
37+
),
38+
async run(interaction) {
39+
const deleteButtonRow = new MessageActionRow().addComponents([deleteButton(interaction.user.id)]);
40+
const queryOption = interaction.options.getString("query");
41+
const sourceOption = interaction.options.getString("source") as keyof typeof sources;
42+
43+
let { Source: source = "stable", Query: query } =
44+
queryOption.match(/(?:(?<Source>[^/]*)\/)?(?<Query>(?:.|\s)*)/i)?.groups ?? {};
45+
// The Default source should be stable
46+
if (!sources[source]) source = "stable";
47+
48+
if (sourceOption) source = sourceOption;
49+
// Whether to include private elements on the search results, by default false, shows private elements if the search returns an exact result;
50+
const searchPrivate = interaction.options.getBoolean("private") || false;
51+
const doc = await Doc.fetch(source, { force: true }).catch(console.error);
52+
53+
if (!doc) {
54+
await interaction.editReply({ content: "Couldn't fetch docs" }).catch(console.error);
55+
return;
56+
}
57+
// const resultEmbed = doc.resolveEmbed(query, { excludePrivateElements: !searchPrivate });
58+
const result = searchDJSDoc(doc, query, searchPrivate);
59+
// If a result wasn't found
60+
if (!result || (result as APIEmbed).description === "") {
61+
const notFoundEmbed = doc.baseEmbed();
62+
notFoundEmbed.description = "Didn't find any results for that query";
63+
64+
const timeStampDate = new Date(notFoundEmbed.timestamp);
65+
// Satisfies the method's MessageEmbedOption type
66+
const embedObj = { ...notFoundEmbed, timestamp: timeStampDate };
67+
68+
await interaction.editReply({ embeds: [embedObj] }).catch(console.error);
69+
return;
70+
} else if (Array.isArray(result)) {
71+
// If there are multiple results, send a select menu from which the user can choose which one to send
72+
73+
const selectMenuRow = new MessageActionRow().addComponents(
74+
new MessageSelectMenu()
75+
.setCustomId(`djsselect/${source}/${searchPrivate}/${interaction.user.id}`)
76+
.addOptions(result)
77+
.setPlaceholder("Select documentation to send"),
78+
);
79+
await interaction
80+
.editReply({
81+
content: "Didn't find an exact match, please select one from below",
82+
components: [selectMenuRow],
83+
})
84+
.catch(console.error);
85+
return;
86+
}
87+
const resultEmbed = result;
88+
const timeStampDate = new Date(resultEmbed.timestamp);
89+
const embedObj = { ...resultEmbed, timestamp: timeStampDate };
90+
91+
//! "checkEmbedLimits" does not support MessageEmbed objects due to the properties being null by default, use a raw embed object for this method
92+
// Check if the embed exceeds any of the limits
93+
if (!checkEmbedLimits([resultEmbed])) {
94+
// The final field should be the View Source button
95+
embedObj.fields = [embedObj.fields?.at(-1)];
96+
}
97+
await interaction.editReply({
98+
content: "Sent documentations for " + (query.length >= 100 ? query.slice(0, 100) + "..." : query),
99+
});
100+
await interaction.followUp({ embeds: [embedObj], components: [deleteButtonRow] }).catch(console.error);
77101
return;
78-
}
79-
const resultEmbed = result;
80-
const timeStampDate = new Date(resultEmbed.timestamp);
81-
const embedObj = { ...resultEmbed, timestamp: timeStampDate };
82-
83-
//! "checkEmbedLimits" does not support MessageEmbed objects due to the properties being null by default, use a raw embed object for this method
84-
// Check if the embed exceeds any of the limits
85-
if (!checkEmbedLimits([resultEmbed])) {
86-
// The final field should be the View Source button
87-
embedObj.fields = [embedObj.fields?.at(-1)];
88-
}
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);
93-
return;
102+
},
94103
},
104+
buttons: [{ custom_id: "deletebtn", run: deleteButtonHandler }],
105+
selectMenus: [
106+
{
107+
custom_id: "djsselect",
108+
async run(interaction) {
109+
const selectedValue = interaction.values[0];
110+
const [, source, searchPrivate, Initiator] = interaction.customId.split("/");
111+
const deleteButtonRow = new MessageActionRow().addComponents([deleteButton(Initiator)]);
112+
113+
const doc = await Doc.fetch(source, { force: true });
114+
115+
const resultEmbed = searchDJSDoc(doc, selectedValue, searchPrivate === "true") as APIEmbed;
116+
117+
// Remove the menu and update the ephemeral message
118+
await interaction
119+
.editReply({ content: "Sent documentations for " + selectedValue, components: [] })
120+
.catch(console.error);
121+
// Send documentation
122+
await interaction
123+
.followUp({ embeds: [resultEmbed], components: [deleteButtonRow] })
124+
.catch(console.error);
125+
},
126+
},
127+
],
128+
autocomplete: [
129+
{
130+
focusedOption: "query",
131+
async run(interaction, focusedOption) {
132+
const focusedOptionValue = focusedOption.value as string;
133+
let { Branch: branchOrProject = "stable", Query: query } =
134+
focusedOptionValue.match(/(?:(?<Branch>[^/]*)\/)?(?<Query>(?:.|\s)*)/i)?.groups ?? {};
135+
if (!sources[branchOrProject]) branchOrProject = "stable";
136+
137+
const doc = await Doc.fetch(branchOrProject, { force: false });
138+
const singleElement = doc.get(...query.split(/\.|#/));
139+
if (singleElement) {
140+
await interaction
141+
.respond([
142+
{
143+
name: singleElement.formattedName,
144+
value: branchOrProject + "/" + singleElement.formattedName,
145+
},
146+
])
147+
.catch(console.error);
148+
return;
149+
}
150+
const searchResults = doc.search(query, { excludePrivateElements: false });
151+
if (!searchResults) {
152+
await interaction.respond([]).catch(console.error);
153+
return;
154+
}
155+
await interaction
156+
.respond(
157+
searchResults.map((elem) => ({
158+
name: elem.formattedName,
159+
value: branchOrProject + "/" + elem.formattedName,
160+
})),
161+
)
162+
.catch(console.error);
163+
},
164+
},
165+
],
95166
};
96167

97168
function capitalize(str: string) {
@@ -112,8 +183,9 @@ export function searchDJSDoc(doc: Doc, query: string, searchPrivate?: boolean) {
112183
const searchResults = doc.search(query, options);
113184
if (!searchResults) return null;
114185
return searchResults.map((res) => {
186+
const parsedDescription = res.description?.trim?.() ?? "No description provided";
115187
// Labels and values have a limit of 100 characters
116-
const description = res.description.length >= 99 ? res.description.slice(0, 96) + "..." : res.description;
188+
const description = parsedDescription.length >= 99 ? parsedDescription.slice(0, 96) + "..." : parsedDescription;
117189
return {
118190
label: res.formattedName,
119191
description,

0 commit comments

Comments
 (0)