diff --git a/src/commands/docs.tsx b/src/commands/docs.tsx index 2b99114..d723742 100644 --- a/src/commands/docs.tsx +++ b/src/commands/docs.tsx @@ -5,6 +5,7 @@ import { } from "@lilybird/jsx"; import { ApplicationCommand } from "@lilybird/handlers"; import algoliasearch from "algoliasearch"; +import { safeSlice } from "src/util.ts"; // @ts-expect-error It is callable, but algolia for some reason has a namespace with the same name const algoliaClient = algoliasearch( @@ -37,8 +38,8 @@ export default { const name = getHitName(hit); return { - name: name.full.length > 100 ? name.full.slice(0, 100) : name.full, - value: name.name.length > 100 ? name.name.slice(0, 100) : name.name, + name: safeSlice(name.full, 100), + value: safeSlice(name.name, 100), }; }) ); diff --git a/src/commands/mdn.tsx b/src/commands/mdn.tsx new file mode 100644 index 0000000..d2777a9 --- /dev/null +++ b/src/commands/mdn.tsx @@ -0,0 +1,154 @@ +// https://github.com/discordjs/discord-utils-bot/blob/main/src/functions/autocomplete/mdnAutoComplete.ts#L23-L47 thanks +// https://github.com/discordjs/discord-utils-bot/blob/main/src/functions/mdn.ts#L59C1-L78C3 thanks + +import { + BooleanOption, + ApplicationCommand as JSXApplicationCommand, + StringOption, + UserOption, +} from "@lilybird/jsx"; +import { ApplicationCommand } from "@lilybird/handlers"; +import { MDN_API, MDN_DISCORD_EMOJI } from "src/constants.ts"; +import { safeSlice, silently } from "src/util.ts"; + +type MDNIndexEntry = { + title: string; + url: string; +}; + +type APIResult = { + doc: Document; +}; + +type Document = { + archived: boolean; + highlight: Highlight; + locale: string; + mdn_url: string; + popularity: number; + score: number; + slug: string; + summary: string; + title: string; +}; + +type Highlight = { + body: string[]; + title: string[]; +}; + +const MDN_DATA = (await ( + await fetch(`${MDN_API}/en-US/search-index.json`) +).json()) as MDNIndexEntry[]; + +const cache = new Map(); + +export default { + post: "GLOBAL", + data: ( + + + + + + ), + run: async (interaction) => { + const hide = interaction.data.getBoolean("hide") ?? false; + + await interaction.deferReply(hide); + + const query = interaction.data.getString("query", true).trim(); + const target = interaction.data.getUser("target"); + + const key = `${MDN_API}/${query}/index.json`; + + let hit = cache.get(key); + if (!hit) { + try { + const result = (await fetch(key).then(async (response) => + response.json() + )) as APIResult; + hit = result.doc; + } catch { + interaction.editReply({ + content: `❌ Invalid result. Make sure to select an entry from the autocomplete.`, + }); + return; + } + } + + const url = MDN_API + hit.mdn_url; + + const linkReplaceRegex = /\[(.+?)]\((.+?)\)/g; + const boldCodeBlockRegex = /`\*\*(.*)\*\*`/g; + + const intro = escape(hit.summary) + .replaceAll(/\s+/g, " ") + .replaceAll(linkReplaceRegex, `[$1](<${MDN_API}$2>)`) + .replaceAll(boldCodeBlockRegex, `**\`$1\``); + + const parts = [ + `<:${MDN_DISCORD_EMOJI}:> __**[${escape(hit.title)}](<${url}>)**__`, + intro, + ]; + + await interaction.editReply({ + content: [ + target ? `*Suggestion for <@${target}>:*\n` : "", + parts.join("\n"), + ].join("\n"), + }); + }, + autocomplete: async (interaction) => { + const query = interaction.data.getFocused().value; + + const parts = query.split(/\.|#/).map((part) => part.toLowerCase()); + const candidates = []; + + for (const entry of MDN_DATA) { + const lowerTitle = entry.title.toLowerCase(); + const matches = parts.filter((phrase) => lowerTitle.includes(phrase)); + if (matches.length) { + candidates.push({ + entry, + matches, + }); + } + } + + const sortedCandidates = candidates.sort((one, other) => { + if (one.matches.length !== other.matches.length) { + return other.matches.length - one.matches.length; + } + + const aMatches = one.matches.join("").length; + const bMatches = other.matches.join("").length; + return bMatches - aMatches; + }); + + await silently( + interaction.showChoices( + safeSlice( + sortedCandidates.map((candidate) => ({ + name: candidate.entry.title, + value: candidate.entry.url, + })), + 25 + ) + ) + ); + }, +} satisfies ApplicationCommand; + +function escape(text: string) { + return text.replaceAll("||", "|\u200B|").replaceAll("*", "\\*"); +} diff --git a/src/constants.ts b/src/constants.ts index 29b60fc..b71eed9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -47,3 +47,6 @@ export const BUN_EMOJIS = [ { name: "bunpet", id: "1172808445737574591", animated: true }, { name: "bunsegfault", id: "1175208306533486632", animated: true }, ]; + +export const MDN_API = "https://developer.mozilla.org"; +export const MDN_DISCORD_EMOJI = "mdn:1236028636826566758";