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",
|
"cheerio": "^1.0.0",
|
||||||
"discord.js": "^14.17.3",
|
"discord.js": "^14.17.3",
|
||||||
"ky": "^1.7.4",
|
"ky": "^1.7.4",
|
||||||
|
"lru-cache": "^11.0.2",
|
||||||
|
"nanoid": "^5.0.9",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"zod": "^3.24.1",
|
"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=="],
|
"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=="],
|
"magic-bytes.js": ["magic-bytes.js@1.10.0", "", {}, "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ=="],
|
||||||
|
|
||||||
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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",
|
"cheerio": "^1.0.0",
|
||||||
"discord.js": "^14.17.3",
|
"discord.js": "^14.17.3",
|
||||||
"ky": "^1.7.4",
|
"ky": "^1.7.4",
|
||||||
|
"lru-cache": "^11.0.2",
|
||||||
|
"nanoid": "^5.0.9",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import {
|
import { bold, inlineCode, SlashCommandBuilder } from "discord.js";
|
||||||
bold,
|
|
||||||
EmbedBuilder,
|
|
||||||
inlineCode,
|
|
||||||
SlashCommandBuilder,
|
|
||||||
} from "discord.js";
|
|
||||||
import { defineCommand } from "..";
|
import { defineCommand } from "..";
|
||||||
import { getDefinitions } from "../../utils/wiktionary";
|
import { getDefinitions, getSuggestions } from "../../utils/wiktionary";
|
||||||
import { stripHtml } from "../../utils/functions";
|
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({
|
export default defineCommand({
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@ -21,48 +20,26 @@ export default defineCommand({
|
|||||||
),
|
),
|
||||||
|
|
||||||
async autocomplete(interaction) {
|
async autocomplete(interaction) {
|
||||||
let language: string | undefined;
|
const term = interaction.options.getFocused().trim();
|
||||||
let term = interaction.options.getFocused().trim();
|
|
||||||
if (term.length < 3) {
|
if (term.length < 3) {
|
||||||
await interaction.respond([]);
|
await interaction.respond([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = term.split(":");
|
const suggestions = await getSuggestions(term);
|
||||||
if (parsed.length === 2) {
|
if (!suggestions) {
|
||||||
term = parsed[0].trim();
|
|
||||||
language = parsed[1].trim();
|
|
||||||
if (!language) {
|
|
||||||
await interaction.respond([]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const definitions = await getDefinitions(term);
|
|
||||||
if (!definitions) {
|
|
||||||
await interaction.respond([]);
|
await interaction.respond([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (language) {
|
suggestions.forEach((suggestion) => {
|
||||||
const choices = definitions
|
titleCache.set(suggestion.key, suggestion.title);
|
||||||
.filter((definition) =>
|
});
|
||||||
definition.language.toLowerCase().startsWith(language.toLowerCase())
|
|
||||||
)
|
|
||||||
.map((definition) => ({
|
|
||||||
name: `${term} (${definition.language})`,
|
|
||||||
value: `:${term}:${definition.languageCode}:`,
|
|
||||||
}))
|
|
||||||
.slice(0, 25);
|
|
||||||
|
|
||||||
await interaction.respond(choices);
|
const choices = suggestions
|
||||||
return;
|
.map((suggestion) => ({
|
||||||
}
|
name: suggestion.title,
|
||||||
|
value: suggestion.key,
|
||||||
const choices = definitions
|
|
||||||
.map((definition) => ({
|
|
||||||
name: `${term} (${definition.language})`,
|
|
||||||
value: `:${term}:${definition.languageCode}:`,
|
|
||||||
}))
|
}))
|
||||||
.slice(0, 25);
|
.slice(0, 25);
|
||||||
|
|
||||||
@ -70,33 +47,24 @@ export default defineCommand({
|
|||||||
},
|
},
|
||||||
|
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
let term = interaction.options.getString("term", true);
|
const 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 definitions = await getDefinitions(term);
|
const definitions = await getDefinitions(term);
|
||||||
if (!definitions) {
|
if (!definitions?.length) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: "No definitions found",
|
content: "No definitions found",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = languageCode
|
const title = titleCache.get(term) ?? term;
|
||||||
? definitions.find((def) => def.languageCode === languageCode)
|
const msg = new PaginatedMessage();
|
||||||
: definitions[0];
|
msg.setSelectMenuOptions((i) => ({
|
||||||
if (!definition) {
|
label: definitions[i - 1].language,
|
||||||
await interaction.reply({
|
description: `Page ${i}`,
|
||||||
content: "No definitions found",
|
}));
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
definitions.forEach((definition) => {
|
||||||
const description = definition.entries
|
const description = definition.entries
|
||||||
.map((entry) => {
|
.map((entry) => {
|
||||||
const name = entry.partOfSpeech;
|
const name = entry.partOfSpeech;
|
||||||
@ -113,7 +81,8 @@ export default defineCommand({
|
|||||||
})
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
|
|
||||||
const embed = new EmbedBuilder()
|
msg.addPageEmbed((embed) =>
|
||||||
|
embed
|
||||||
.setAuthor({
|
.setAuthor({
|
||||||
name: `Wiktionary - ${definition.language}`,
|
name: `Wiktionary - ${definition.language}`,
|
||||||
iconURL:
|
iconURL:
|
||||||
@ -122,10 +91,12 @@ export default defineCommand({
|
|||||||
definition.languageCode
|
definition.languageCode
|
||||||
}.wiktionary.org/wiki/${encodeURIComponent(term)}`,
|
}.wiktionary.org/wiki/${encodeURIComponent(term)}`,
|
||||||
})
|
})
|
||||||
.setTitle(term)
|
.setTitle(title)
|
||||||
.setColor(0xfefefe)
|
.setColor(0xfefefe)
|
||||||
.setDescription(description);
|
.setDescription(description)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
await interaction.reply({ embeds: [embed] });
|
msg.run(interaction);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@ -9,6 +9,7 @@ const envSchema = z.object({
|
|||||||
.optional()
|
.optional()
|
||||||
.default("development"),
|
.default("development"),
|
||||||
DEV_GUILD_ID: z.string(),
|
DEV_GUILD_ID: z.string(),
|
||||||
|
DEV_CHANNEL_ID: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const env = envSchema.parse(process.env);
|
export const env = envSchema.parse(process.env);
|
||||||
|
|||||||
@ -2,12 +2,14 @@ import {
|
|||||||
AutocompleteInteraction,
|
AutocompleteInteraction,
|
||||||
ChatInputCommandInteraction,
|
ChatInputCommandInteraction,
|
||||||
Events,
|
Events,
|
||||||
|
inlineCode,
|
||||||
MessageFlags,
|
MessageFlags,
|
||||||
} from "discord.js";
|
} from "discord.js";
|
||||||
import { client } from "../client";
|
import { client } from "../client";
|
||||||
import { log } from "../utils/logger";
|
import { log } from "../utils/logger";
|
||||||
import { defineEvent } from ".";
|
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 running = new Map<string, number>();
|
||||||
const getRunning = (command: string) => running.get(command) ?? 0;
|
const getRunning = (command: string) => running.get(command) ?? 0;
|
||||||
@ -52,13 +54,16 @@ async function handleChatInputCommand(
|
|||||||
try {
|
try {
|
||||||
await command.execute(interaction);
|
await command.execute(interaction);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const content = isCommandError(err)
|
let content = isCommandError(err)
|
||||||
? err.message
|
|
||||||
: isError(err)
|
|
||||||
? err.message
|
? err.message
|
||||||
: "An unknown error occurred!";
|
: "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[
|
await interaction[
|
||||||
interaction.replied || interaction.deferred ? "followUp" : "reply"
|
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 class CommandError extends Error {}
|
||||||
|
|
||||||
export function isCommandError(error: any): error is CommandError {
|
export function isCommandError(error: any): error is CommandError {
|
||||||
return error instanceof CommandError;
|
return error instanceof CommandError;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isError(error: any): error is Error {
|
export async function notifyError(trace: string, error: any) {
|
||||||
return error instanceof Error;
|
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 * as cheerio from "cheerio";
|
||||||
|
import { customAlphabet } from "nanoid";
|
||||||
|
|
||||||
|
export const nanoid = customAlphabet("1234567890abcdef");
|
||||||
|
|
||||||
export function noop() {}
|
export function noop() {}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,14 @@
|
|||||||
import ky from "ky";
|
import ky from "ky";
|
||||||
|
|
||||||
|
type Suggestion = {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Suggestions = {
|
||||||
|
pages: Suggestion[];
|
||||||
|
};
|
||||||
|
|
||||||
type ParsedExample = {
|
type ParsedExample = {
|
||||||
example: string;
|
example: string;
|
||||||
};
|
};
|
||||||
@ -19,13 +28,32 @@ type DefinitionsResponse = {
|
|||||||
[key: string]: Entry[];
|
[key: string]: Entry[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = ky.create({
|
const restClient = ky.create({
|
||||||
prefixUrl: "https://en.wiktionary.org/api/rest_v1",
|
prefixUrl: "https://en.wiktionary.org/api/rest_v1",
|
||||||
throwHttpErrors: false,
|
throwHttpErrors: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function getDefinitions(word: string) {
|
const phpClient = ky.create({
|
||||||
const res = await client.get("page/definition/" + encodeURIComponent(word));
|
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>();
|
const data = await res.json<DefinitionsResponse>();
|
||||||
if (!res.ok || !data) return null;
|
if (!res.ok || !data) return null;
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user