mirror of
https://github.com/artiemis/artemis.js.git
synced 2026-02-14 10:21:54 +00:00
wiktionary + error handling improvements
This commit is contained in:
parent
5fa93ca95e
commit
3e2d9634a9
6
bun.lock
6
bun.lock
@ -9,6 +9,8 @@
|
||||
"cheerio": "^1.0.0",
|
||||
"discord.js": "^14.17.3",
|
||||
"ky": "^1.7.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.1",
|
||||
},
|
||||
@ -286,6 +288,8 @@
|
||||
|
||||
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
|
||||
|
||||
"lru-cache": ["lru-cache@11.0.2", "", {}, "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA=="],
|
||||
|
||||
"magic-bytes.js": ["magic-bytes.js@1.10.0", "", {}, "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
||||
@ -296,6 +300,8 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"nanoid": ["nanoid@5.0.9", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="],
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
"cheerio": "^1.0.0",
|
||||
"discord.js": "^14.17.3",
|
||||
"ky": "^1.7.4",
|
||||
"lru-cache": "^11.0.2",
|
||||
"nanoid": "^5.0.9",
|
||||
"winston": "^3.17.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import {
|
||||
bold,
|
||||
EmbedBuilder,
|
||||
inlineCode,
|
||||
SlashCommandBuilder,
|
||||
} from "discord.js";
|
||||
import { bold, inlineCode, SlashCommandBuilder } from "discord.js";
|
||||
import { defineCommand } from "..";
|
||||
import { getDefinitions } from "../../utils/wiktionary";
|
||||
import { getDefinitions, getSuggestions } from "../../utils/wiktionary";
|
||||
import { stripHtml } from "../../utils/functions";
|
||||
import { PaginatedMessage } from "@sapphire/discord.js-utilities";
|
||||
import { LRUCache } from "lru-cache";
|
||||
|
||||
const titleCache = new LRUCache<string, string>({ max: 100 });
|
||||
|
||||
export default defineCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
@ -21,48 +20,26 @@ export default defineCommand({
|
||||
),
|
||||
|
||||
async autocomplete(interaction) {
|
||||
let language: string | undefined;
|
||||
let term = interaction.options.getFocused().trim();
|
||||
const term = interaction.options.getFocused().trim();
|
||||
if (term.length < 3) {
|
||||
await interaction.respond([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = term.split(":");
|
||||
if (parsed.length === 2) {
|
||||
term = parsed[0].trim();
|
||||
language = parsed[1].trim();
|
||||
if (!language) {
|
||||
await interaction.respond([]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const definitions = await getDefinitions(term);
|
||||
if (!definitions) {
|
||||
const suggestions = await getSuggestions(term);
|
||||
if (!suggestions) {
|
||||
await interaction.respond([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (language) {
|
||||
const choices = definitions
|
||||
.filter((definition) =>
|
||||
definition.language.toLowerCase().startsWith(language.toLowerCase())
|
||||
)
|
||||
.map((definition) => ({
|
||||
name: `${term} (${definition.language})`,
|
||||
value: `:${term}:${definition.languageCode}:`,
|
||||
}))
|
||||
.slice(0, 25);
|
||||
suggestions.forEach((suggestion) => {
|
||||
titleCache.set(suggestion.key, suggestion.title);
|
||||
});
|
||||
|
||||
await interaction.respond(choices);
|
||||
return;
|
||||
}
|
||||
|
||||
const choices = definitions
|
||||
.map((definition) => ({
|
||||
name: `${term} (${definition.language})`,
|
||||
value: `:${term}:${definition.languageCode}:`,
|
||||
const choices = suggestions
|
||||
.map((suggestion) => ({
|
||||
name: suggestion.title,
|
||||
value: suggestion.key,
|
||||
}))
|
||||
.slice(0, 25);
|
||||
|
||||
@ -70,62 +47,56 @@ export default defineCommand({
|
||||
},
|
||||
|
||||
async execute(interaction) {
|
||||
let term = interaction.options.getString("term", true);
|
||||
let languageCode: string | undefined;
|
||||
|
||||
const parsed = term.match(/^:(?<term>.+):(?<languageCode>.+):$/);
|
||||
if (parsed?.groups) {
|
||||
term = parsed.groups.term;
|
||||
languageCode = parsed.groups.languageCode;
|
||||
}
|
||||
const term = interaction.options.getString("term", true);
|
||||
|
||||
const definitions = await getDefinitions(term);
|
||||
if (!definitions) {
|
||||
if (!definitions?.length) {
|
||||
await interaction.reply({
|
||||
content: "No definitions found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const definition = languageCode
|
||||
? definitions.find((def) => def.languageCode === languageCode)
|
||||
: definitions[0];
|
||||
if (!definition) {
|
||||
await interaction.reply({
|
||||
content: "No definitions found",
|
||||
});
|
||||
return;
|
||||
}
|
||||
const title = titleCache.get(term) ?? term;
|
||||
const msg = new PaginatedMessage();
|
||||
msg.setSelectMenuOptions((i) => ({
|
||||
label: definitions[i - 1].language,
|
||||
description: `Page ${i}`,
|
||||
}));
|
||||
|
||||
const description = definition.entries
|
||||
.map((entry) => {
|
||||
const name = entry.partOfSpeech;
|
||||
const definitions = entry.definitions
|
||||
.filter((def) => def.definition)
|
||||
.map((def, i) => {
|
||||
const prefix = inlineCode(`${i + 1}.`);
|
||||
const definition = stripHtml(def.definition);
|
||||
return `${prefix} ${definition.trim()}`;
|
||||
definitions.forEach((definition) => {
|
||||
const description = definition.entries
|
||||
.map((entry) => {
|
||||
const name = entry.partOfSpeech;
|
||||
const definitions = entry.definitions
|
||||
.filter((def) => def.definition)
|
||||
.map((def, i) => {
|
||||
const prefix = inlineCode(`${i + 1}.`);
|
||||
const definition = stripHtml(def.definition);
|
||||
return `${prefix} ${definition.trim()}`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return `${bold(name)}\n${definitions}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
msg.addPageEmbed((embed) =>
|
||||
embed
|
||||
.setAuthor({
|
||||
name: `Wiktionary - ${definition.language}`,
|
||||
iconURL:
|
||||
"https://en.wiktionary.org/static/apple-touch/wiktionary/en.png",
|
||||
url: `https://${
|
||||
definition.languageCode
|
||||
}.wiktionary.org/wiki/${encodeURIComponent(term)}`,
|
||||
})
|
||||
.join("\n");
|
||||
.setTitle(title)
|
||||
.setColor(0xfefefe)
|
||||
.setDescription(description)
|
||||
);
|
||||
});
|
||||
|
||||
return `${bold(name)}\n${definitions}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setAuthor({
|
||||
name: `Wiktionary - ${definition.language}`,
|
||||
iconURL:
|
||||
"https://en.wiktionary.org/static/apple-touch/wiktionary/en.png",
|
||||
url: `https://${
|
||||
definition.languageCode
|
||||
}.wiktionary.org/wiki/${encodeURIComponent(term)}`,
|
||||
})
|
||||
.setTitle(term)
|
||||
.setColor(0xfefefe)
|
||||
.setDescription(description);
|
||||
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
msg.run(interaction);
|
||||
},
|
||||
});
|
||||
|
||||
@ -9,6 +9,7 @@ const envSchema = z.object({
|
||||
.optional()
|
||||
.default("development"),
|
||||
DEV_GUILD_ID: z.string(),
|
||||
DEV_CHANNEL_ID: z.string(),
|
||||
});
|
||||
|
||||
export const env = envSchema.parse(process.env);
|
||||
|
||||
@ -2,12 +2,14 @@ import {
|
||||
AutocompleteInteraction,
|
||||
ChatInputCommandInteraction,
|
||||
Events,
|
||||
inlineCode,
|
||||
MessageFlags,
|
||||
} from "discord.js";
|
||||
import { client } from "../client";
|
||||
import { log } from "../utils/logger";
|
||||
import { defineEvent } from ".";
|
||||
import { isCommandError, isError } from "../utils/error";
|
||||
import { isCommandError, notifyError } from "../utils/error";
|
||||
import { nanoid } from "../utils/functions";
|
||||
|
||||
const running = new Map<string, number>();
|
||||
const getRunning = (command: string) => running.get(command) ?? 0;
|
||||
@ -52,13 +54,16 @@ async function handleChatInputCommand(
|
||||
try {
|
||||
await command.execute(interaction);
|
||||
} catch (err) {
|
||||
const content = isCommandError(err)
|
||||
? err.message
|
||||
: isError(err)
|
||||
let content = isCommandError(err)
|
||||
? err.message
|
||||
: "An unknown error occurred!";
|
||||
|
||||
if (!isCommandError(err)) log.error("Unhandled Command Error", err);
|
||||
if (!isCommandError(err)) {
|
||||
const trace = nanoid();
|
||||
content += `\ntrace: ${inlineCode(trace)}`;
|
||||
log.error("Unhandled Command Error", { trace, err });
|
||||
notifyError(trace, err);
|
||||
}
|
||||
|
||||
await interaction[
|
||||
interaction.replied || interaction.deferred ? "followUp" : "reply"
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
import { codeBlock, type TextChannel } from "discord.js";
|
||||
import { client } from "../client";
|
||||
import { env } from "../env";
|
||||
|
||||
export class CommandError extends Error {}
|
||||
|
||||
export function isCommandError(error: any): error is CommandError {
|
||||
return error instanceof CommandError;
|
||||
}
|
||||
|
||||
export function isError(error: any): error is Error {
|
||||
return error instanceof Error;
|
||||
export async function notifyError(trace: string, error: any) {
|
||||
return (client.channels.cache.get(env.DEV_CHANNEL_ID) as TextChannel).send({
|
||||
content: trace,
|
||||
embeds: [
|
||||
{
|
||||
title: "Unhandled Error",
|
||||
description: codeBlock("js", error.stack ?? error.message),
|
||||
color: 0xff0000,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
import * as cheerio from "cheerio";
|
||||
import { customAlphabet } from "nanoid";
|
||||
|
||||
export const nanoid = customAlphabet("1234567890abcdef");
|
||||
|
||||
export function noop() {}
|
||||
|
||||
|
||||
@ -1,5 +1,14 @@
|
||||
import ky from "ky";
|
||||
|
||||
type Suggestion = {
|
||||
key: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type Suggestions = {
|
||||
pages: Suggestion[];
|
||||
};
|
||||
|
||||
type ParsedExample = {
|
||||
example: string;
|
||||
};
|
||||
@ -19,13 +28,32 @@ type DefinitionsResponse = {
|
||||
[key: string]: Entry[];
|
||||
};
|
||||
|
||||
const client = ky.create({
|
||||
const restClient = ky.create({
|
||||
prefixUrl: "https://en.wiktionary.org/api/rest_v1",
|
||||
throwHttpErrors: false,
|
||||
});
|
||||
|
||||
export async function getDefinitions(word: string) {
|
||||
const res = await client.get("page/definition/" + encodeURIComponent(word));
|
||||
const phpClient = ky.create({
|
||||
prefixUrl: "https://en.wiktionary.org/w/rest.php/v1",
|
||||
throwHttpErrors: false,
|
||||
});
|
||||
|
||||
export async function getSuggestions(term: string) {
|
||||
const res = await phpClient.get("search/title", {
|
||||
searchParams: {
|
||||
q: term,
|
||||
limit: 25,
|
||||
},
|
||||
});
|
||||
const data = await res.json<Suggestions>();
|
||||
if (!res.ok || !data.pages.length) return null;
|
||||
return data.pages;
|
||||
}
|
||||
|
||||
export async function getDefinitions(term: string) {
|
||||
const res = await restClient.get(
|
||||
"page/definition/" + encodeURIComponent(term)
|
||||
);
|
||||
const data = await res.json<DefinitionsResponse>();
|
||||
if (!res.ok || !data) return null;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user