|
| 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; |
0 commit comments