feat: mdn command

This commit is contained in:
Jozef Steinhübl 2024-05-03 20:58:17 +02:00
parent 97ecf6fa0f
commit f051c142b0
No known key found for this signature in database
GPG key ID: E6BC90C91973B08F
3 changed files with 160 additions and 2 deletions

View file

@ -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),
};
})
);

154
src/commands/mdn.tsx Normal file
View file

@ -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<string, Document>();
export default {
post: "GLOBAL",
data: (
<JSXApplicationCommand
name="mdn"
description="Search the Mozilla Developer Network documentation"
>
<StringOption
name="query"
description="Class or method to search for"
required
autocomplete
max_length={100}
/>
<UserOption name="target" description="User to mention" />
<BooleanOption name="hide" description="Show this message only for you" />
</JSXApplicationCommand>
),
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<string>().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("*", "\\*");
}

View file

@ -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";