Skip to content

Commit a2c1733

Browse files
authored
69 the user should be able to trigger a search query on the mdn via a command (#110)
* refactor: consistency across logger.error calls * feat(commands): add mdn command to trigger a search on MDN docs * style: lint * feat: add autocomplete support for mdn * style: lint * fix(mdn): use character class, toSorted instead of sort * refactor(CommandHandler): reduce cognitive complexity * fix(mdn): remove useless escape * fix: remove entrypoint * fix(mdn): cleanQuery is already a link, no need to encode it * chore: bump minor version * fix(mdn): set correct URL, change footer, add button
1 parent efe18fd commit a2c1733

11 files changed

Lines changed: 286 additions & 88 deletions

File tree

Dockerfile

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN yarn install --frozen-lockfile \
99
&& yarn env-gen \
1010
&& yarn build \
1111
&& yarn install --production \
12-
&& zip -r app.zip ./node_modules ./build ./yarn.lock ./.env ./entrypoint.sh
12+
&& zip -r app.zip ./node_modules ./build ./yarn.lock ./.env
1313

1414
# ------------------------------------------------------------
1515
FROM node:lts-alpine AS app
@@ -22,7 +22,4 @@ COPY --from=build /app/app.zip .
2222
RUN unzip app.zip \
2323
&& rm app.zip \
2424
&& mv ./build/* . \
25-
&& rm -rf ./build \
26-
&& chmod +x ./entrypoint.sh
27-
28-
ENTRYPOINT ["./entrypoint.sh"]
25+
&& rm -rf ./build

docker-compose.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ services:
3131
build:
3232
context: .
3333
dockerfile: ./Dockerfile
34+
command: node ./index.js | tee -a /var/log/datadrop/console.log
3435
volumes:
3536
- bot_logs:/var/log/datadrop/
3637
depends_on:
37-
- database
38+
database:
39+
condition: service_healthy
3840

3941
volumes:
4042
postgres:

entrypoint.sh

Lines changed: 0 additions & 12 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "datadrop",
3-
"version": "2.0.0",
3+
"version": "2.1.0",
44
"type": "module",
55
"main": "./build/index.js",
66
"scripts": {

src/commands/utility/mdn.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
// Copyright https://github.com/discordjs/discord-utils-bot
2+
import {
3+
ActionRowBuilder,
4+
type AutocompleteInteraction,
5+
ButtonBuilder,
6+
ButtonStyle,
7+
type ChatInputCommandInteraction,
8+
Colors,
9+
EmbedBuilder,
10+
SlashCommandBuilder,
11+
bold,
12+
hideLinkEmbed,
13+
hyperlink,
14+
inlineCode,
15+
} from "discord.js";
16+
17+
import type { DatadropClient } from "../../datadrop.js";
18+
import { getErrorMessage } from "../../helpers.js";
19+
import type { Command } from "../../models/Command.js";
20+
21+
type MDNCandidate = {
22+
entry: MDNIndexEntry;
23+
matches: string[];
24+
};
25+
26+
type MDNIndexEntry = {
27+
title: string;
28+
url: string;
29+
};
30+
31+
type APIResult = {
32+
doc: Document;
33+
};
34+
35+
type Document = {
36+
mdn_url: string;
37+
summary: string;
38+
title: string;
39+
};
40+
41+
const MDN_URL = "https://developer.mozilla.org/" as const;
42+
const searchCache = new Map<string, Document>();
43+
const indexCache: MDNIndexEntry[] = [];
44+
45+
async function getMDNIndex() {
46+
const response = await fetch(`${MDN_URL}/en-US/search-index.json`);
47+
if (!response.ok) throw new Error("Failed to fetch MDN index.");
48+
49+
const data = await response.json();
50+
indexCache.push(
51+
...data.map((entry) => ({ title: entry.title, url: entry.url })),
52+
);
53+
}
54+
55+
function sanitize(str: string): string {
56+
return str
57+
.replaceAll("||", "|\u200B|") // avoid spoiler
58+
.replaceAll("*", "\\*") // avoid bold/italic
59+
.replaceAll(/\s+/g, " ") // remove duplicate spaces
60+
.replaceAll(
61+
/\[(.+?)]\((.+?)\)/g,
62+
hyperlink("$1", hideLinkEmbed(`${MDN_URL}$2`)),
63+
) // handle links
64+
.replaceAll(/`\*\*(.*)\*\*`/g, bold(inlineCode("$1"))); // handle code blocks
65+
}
66+
67+
export default {
68+
data: new SlashCommandBuilder()
69+
.setName("mdn")
70+
.setDescription(
71+
"Lance une recherche dans la documentation de Mozila Developer Network!",
72+
)
73+
.addStringOption((option) =>
74+
option
75+
.setName("query")
76+
.setDescription(
77+
"La classe, méthode, propriété ou autre à rechercher.",
78+
)
79+
.setRequired(true)
80+
.setAutocomplete(true),
81+
),
82+
83+
async autocomplete(
84+
client: DatadropClient,
85+
interaction: AutocompleteInteraction,
86+
) {
87+
if (indexCache.length === 0) {
88+
client.logger.verbose("Fetching MDN index...");
89+
try {
90+
await getMDNIndex();
91+
} catch (err) {
92+
client.logger.error(
93+
`Failed to fetch MDN index: ${getErrorMessage(err)}`,
94+
);
95+
}
96+
}
97+
98+
const focusedOption = interaction.options.getFocused(true);
99+
const parts = focusedOption.value
100+
.split(/[.#]/)
101+
.map((part) => part.toLowerCase());
102+
103+
const candidates: MDNCandidate[] = [];
104+
for (const entry of indexCache) {
105+
const lowerTitle = entry.title.toLowerCase();
106+
const matches = parts.filter((phrase) =>
107+
lowerTitle.includes(phrase),
108+
);
109+
if (matches.length) {
110+
candidates.push({ entry, matches });
111+
}
112+
}
113+
114+
await interaction.respond(
115+
candidates
116+
.toSorted((one, other) => {
117+
if (one.matches.length !== other.matches.length) {
118+
return other.matches.length - one.matches.length;
119+
}
120+
121+
const aMatches = one.matches.join("").length;
122+
const bMatches = other.matches.join("").length;
123+
return bMatches - aMatches;
124+
})
125+
.map((candidate) => ({
126+
name: candidate.entry.title,
127+
value: candidate.entry.url,
128+
}))
129+
.slice(0, 24), // 25 is the limit of choices for an autocomplete
130+
);
131+
},
132+
133+
async execute(
134+
client: DatadropClient,
135+
interaction: ChatInputCommandInteraction,
136+
) {
137+
await interaction.deferReply({ ephemeral: false });
138+
139+
const cleanQuery = interaction.options.getString("query", true).trim();
140+
const searchUrl = `${MDN_URL}${cleanQuery}/index.json`;
141+
142+
try {
143+
let hit = searchCache.get(searchUrl);
144+
if (!hit) {
145+
client.logger.debug(`Fetching MDN search results for ${cleanQuery} hitting on ${searchUrl}...`);
146+
const response = await fetch(searchUrl);
147+
if (!response.ok)
148+
throw new Error(
149+
"Erreur lors de la recherche dans la documentation de MDN.",
150+
);
151+
const result = (await response.json()) as APIResult;
152+
hit = result.doc;
153+
searchCache.set(searchUrl, hit);
154+
}
155+
156+
const url = `${MDN_URL}${hit.mdn_url}`;
157+
const embed = new EmbedBuilder()
158+
.setColor(Colors.Purple)
159+
.setURL(url)
160+
.setTitle(sanitize(hit.title))
161+
.setDescription(sanitize(hit.summary))
162+
.setFooter({ text: 'MDN Web Docs', iconURL: 'https://developer.mozilla.org/favicon-48x48.png' })
163+
.setTimestamp();
164+
const button = new ButtonBuilder()
165+
.setStyle(ButtonStyle.Link)
166+
.setLabel("Voir plus")
167+
.setURL(url);
168+
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(button);
169+
await interaction.editReply({
170+
content: "✅ Voici le résultat de votre recherche!",
171+
embeds: [embed],
172+
components: [row]
173+
});
174+
} catch (err) {
175+
client.logger.error(
176+
`Une erreur est survenue lors de la recherche de la documentation MDN : ${getErrorMessage(err)}`,
177+
);
178+
await interaction.editReply({
179+
content:
180+
"❌ **Oups!** - Une erreur est survenue lors de la recherche dans la documentation de MDN... La ressource n'existe probablement pas.",
181+
});
182+
}
183+
},
184+
} as Command;

src/events/guildMemberAdd.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from "discord.js";
99

1010
import type { DatadropClient } from "../datadrop.js";
11+
import { getErrorMessage } from "../helpers.js";
1112
import type { AnnounceConfiguration } from "../models/Configuration.js";
1213
import type { Event } from "../models/Event.js";
1314

@@ -64,8 +65,8 @@ async function guildMemberAdd(client: DatadropClient, member: GuildMember) {
6465
client.logger.info(
6566
`Un DM a été envoyé à <${member.user.tag}> à son entrée dans la guilde`,
6667
);
67-
} catch (err: unknown) {
68-
client.logger.error((<Error>err).message);
68+
} catch (err) {
69+
client.logger.error(getErrorMessage(err));
6970
}
7071
}
7172

src/events/guildMemberRemove.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Events, type GuildMember } from "discord.js";
22

33
import type { DatadropClient } from "../datadrop.js";
4+
import { getErrorMessage } from "../helpers.js";
45
import type { Event } from "../models/Event.js";
56

67
export default {
@@ -17,7 +18,7 @@ async function guildMemberRemove(client: DatadropClient, member: GuildMember) {
1718

1819
try {
1920
await client.database.delete(member.id);
20-
} catch (err: unknown) {
21-
client.logger.error((<Error>err).message);
21+
} catch (err) {
22+
client.logger.error(getErrorMessage(err));
2223
}
2324
}

src/events/interactionCreate.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
ActionRowBuilder,
3+
type AutocompleteInteraction,
34
ButtonBuilder,
45
type ButtonInteraction,
56
ButtonStyle,
@@ -30,7 +31,8 @@ async function interactionCreate(
3031
) {
3132
if (
3233
interaction.isChatInputCommand() ||
33-
interaction.isMessageContextMenuCommand()
34+
interaction.isMessageContextMenuCommand() ||
35+
interaction.isAutocomplete()
3436
) {
3537
await handleCommandInteraction(client, interaction);
3638
} else if (isVerificationButton(interaction)) {
@@ -52,7 +54,8 @@ async function handleCommandInteraction(
5254
client: DatadropClient,
5355
interaction:
5456
| ChatInputCommandInteraction
55-
| MessageContextMenuCommandInteraction,
57+
| MessageContextMenuCommandInteraction
58+
| AutocompleteInteraction,
5659
) {
5760
const commandHandler = new CommandHandler(client);
5861
if (commandHandler.shouldExecute(interaction)) {

src/models/Command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {
2+
AutocompleteInteraction,
23
ChatInputCommandInteraction,
34
ContextMenuCommandBuilder,
45
MessageContextMenuCommandInteraction,
@@ -10,6 +11,10 @@ import type { DatadropClient } from "../datadrop.js";
1011
export interface Command {
1112
data: SlashCommandBuilder | ContextMenuCommandBuilder;
1213
ownerOnly?: boolean;
14+
autocomplete?(
15+
client: DatadropClient,
16+
interaction: AutocompleteInteraction,
17+
): Promise<void>;
1318
execute(
1419
client: DatadropClient,
1520
interaction:

0 commit comments

Comments
 (0)