From 1357417efec5abcc7c46f6ff287b1a13f92404c2 Mon Sep 17 00:00:00 2001 From: artie Date: Sun, 9 Feb 2025 11:33:47 +0100 Subject: [PATCH] yandex ocr + error handling fixes --- bun.lock | 19 +++++++++ package.json | 1 + src/api.ts | 6 --- src/client.ts | 6 +-- src/commands/language/wiktionary.ts | 6 +-- src/commands/owner/sync.ts | 6 +-- src/commands/utility/ocr.ts | 61 +++++++++++++++++++++++++++++ src/commands/utility/ping.ts | 4 +- src/env.ts | 2 + src/events/interactionCreate.ts | 6 +-- src/utils/api.ts | 26 ++++++++++++ src/utils/error.ts | 12 ++++-- src/utils/http.ts | 8 ++++ 13 files changed, 138 insertions(+), 25 deletions(-) delete mode 100644 src/api.ts create mode 100644 src/commands/utility/ocr.ts create mode 100644 src/utils/api.ts create mode 100644 src/utils/http.ts diff --git a/bun.lock b/bun.lock index 207dada..26f0904 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@sapphire/discord.js-utilities": "^7.3.2", "cheerio": "^1.0.0", "discord.js": "^14.17.3", + "file-type": "^20.1.0", "ky": "^1.7.4", "lru-cache": "^11.0.2", "nanoid": "^5.0.9", @@ -88,6 +89,10 @@ "@sapphire/utilities": ["@sapphire/utilities@3.18.2", "", {}, "sha512-QGLdC9+pT74Zd7aaObqn0EUfq40c4dyTL65pFnkM6WO1QYN7Yg/s4CdH+CXmx0Zcu6wcfCWILSftXPMosJHP5A=="], + "@tokenizer/inflate": ["@tokenizer/inflate@0.2.6", "", { "dependencies": { "debug": "^4.3.7", "fflate": "^0.8.2", "token-types": "^6.0.0" } }, "sha512-SdR/i05U7Xhnsq36iyIq/ZiGGw4PKzw4ww3bOq80Pjj4wyXpqyTcgrgdDdGlcatnlvzNJx8CQw3hp6QZvkUwhA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="], "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], @@ -218,8 +223,12 @@ "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + "file-type": ["file-type@20.1.0", "", { "dependencies": { "@tokenizer/inflate": "^0.2.6", "strtok3": "^10.2.0", "token-types": "^6.0.0", "uint8array-extras": "^1.4.0" } }, "sha512-XoxU+lETfCf+bYK3SXkxFusAvmtYQl1u/ZC4zw1DBLEsHUvh339uwYucgQnnSMz1mRCWYJrCzsbJJ95hsQbZ8A=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], @@ -242,6 +251,8 @@ "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -326,6 +337,8 @@ "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "peek-readable": ["peek-readable@6.1.1", "", {}, "sha512-7QmvgRKhxM0E2PGV4ocfROItVode+ELI27n4q+lpufZ+tRKBu/pBP8WOmw9HXn2ui/AUizqtvaVQhcJrOkRqYg=="], + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], @@ -362,12 +375,16 @@ "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strtok3": ["strtok3@10.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^6.1.1" } }, "sha512-Q2dTnW3UXokAvXmXvrvMoUj/me3LyJI76HNHeuGMh2o0As/vzd7eHV3ncLOyvu928vQIDbE7Vf9ldEnC7cwy1w=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], @@ -382,6 +399,8 @@ "typescript-eslint": ["typescript-eslint@8.23.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.23.0", "@typescript-eslint/parser": "8.23.0", "@typescript-eslint/utils": "8.23.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/LBRo3HrXr5LxmrdYSOCvoAMm7p2jNizNfbIpCgvG4HMsnoprRUOce/+8VJ9BDYWW68rqIENE/haVLWPeFZBVQ=="], + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], + "undici": ["undici@6.19.8", "", {}, "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], diff --git a/package.json b/package.json index add366a..2d3c14d 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@sapphire/discord.js-utilities": "^7.3.2", "cheerio": "^1.0.0", "discord.js": "^14.17.3", + "file-type": "^20.1.0", "ky": "^1.7.4", "lru-cache": "^11.0.2", "nanoid": "^5.0.9", diff --git a/src/api.ts b/src/api.ts deleted file mode 100644 index 1c46604..0000000 --- a/src/api.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { env } from "./env"; -import { API } from "@discordjs/core"; -import { REST } from "discord.js"; - -const rest = new REST().setToken(env.DISCORD_TOKEN); -export const api = new API(rest); diff --git a/src/client.ts b/src/client.ts index 4c36450..cb8d58a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,7 +1,6 @@ -import { Client, Collection, GatewayIntentBits } from "discord.js"; +import { Client, Collection, GatewayIntentBits, REST } from "discord.js"; import { env } from "./env"; import { ActivityType, API } from "@discordjs/core"; -import { api } from "./api"; import type { Command } from "./types/command"; import path from "node:path"; import fs from "node:fs/promises"; @@ -31,7 +30,8 @@ export class ArtemisClient extends Client { }, }); - this.api = api; + const rest = new REST().setToken(env.DISCORD_TOKEN); + this.api = new API(rest); this.on("error", (err) => { log.error("Unhandled Client Error", err); diff --git a/src/commands/language/wiktionary.ts b/src/commands/language/wiktionary.ts index 9d6791b..d0e433f 100644 --- a/src/commands/language/wiktionary.ts +++ b/src/commands/language/wiktionary.ts @@ -4,6 +4,7 @@ import { getDefinitions, getSuggestions } from "../../utils/wiktionary"; import { stripHtml } from "../../utils/functions"; import { PaginatedMessage } from "@sapphire/discord.js-utilities"; import { LRUCache } from "lru-cache"; +import { abort } from "../../utils/error"; const titleCache = new LRUCache({ max: 100 }); @@ -51,10 +52,7 @@ export default defineCommand({ const definitions = await getDefinitions(term); if (!definitions?.length) { - await interaction.reply({ - content: "No definitions found", - }); - return; + abort("No definitions found"); } const title = titleCache.get(term) ?? term; diff --git a/src/commands/owner/sync.ts b/src/commands/owner/sync.ts index 3a0c6a1..50a7c4b 100644 --- a/src/commands/owner/sync.ts +++ b/src/commands/owner/sync.ts @@ -1,6 +1,7 @@ import { MessageFlags, SlashCommandBuilder } from "discord.js"; import { client } from "../../client"; import { defineCommand } from ".."; +import { abort } from "../../utils/error"; export default defineCommand({ data: new SlashCommandBuilder() @@ -13,10 +14,7 @@ export default defineCommand({ const counts = await client.syncCommands(); if (!counts) { - await interaction.followUp({ - content: "No commands to sync", - }); - return; + abort("No commands to sync"); } const { guildCount, globalCount } = counts; diff --git a/src/commands/utility/ocr.ts b/src/commands/utility/ocr.ts new file mode 100644 index 0000000..9ff1e28 --- /dev/null +++ b/src/commands/utility/ocr.ts @@ -0,0 +1,61 @@ +import { codeBlock, inlineCode, SlashCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { downloadFile } from "../../utils/http"; +import { abort } from "../../utils/error"; +import { yandexOcr } from "../../utils/api"; + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("ocr") + .setDescription("OCR an image using Yandex") + .addAttachmentOption((option) => + option + .setName("image") + .setDescription("The image to OCR") + .setRequired(true) + ), + + async execute(interaction) { + const attachment = interaction.options.getAttachment("image", true); + if (!attachment.contentType?.startsWith("image/")) { + abort("The file must be an image!"); + } + + const { data, type } = await downloadFile(attachment.url); + if (!type?.mime.startsWith("image/")) { + abort("The file must be an image!"); + } + + await interaction.deferReply(); + + const { text, detected_lang } = await yandexOcr(data, type.mime); + + const languageName = + new Intl.DisplayNames(["en"], { type: "language" }).of( + detected_lang ?? "unknown" + ) ?? "unknown"; + + const content = `Detected language: ${inlineCode( + languageName + )}\n${codeBlock(text)}`; + + if (content.length > 2000) { + await interaction.editReply({ + content: `Detected language: ${inlineCode(languageName)}`, + files: [ + { + name: "ocr.txt", + attachment: text, + }, + attachment, + ], + }); + return; + } + + await interaction.editReply({ + content, + files: [attachment], + }); + }, +}); diff --git a/src/commands/utility/ping.ts b/src/commands/utility/ping.ts index de04df2..cebf826 100644 --- a/src/commands/utility/ping.ts +++ b/src/commands/utility/ping.ts @@ -1,6 +1,7 @@ import { inlineCode, SlashCommandBuilder } from "discord.js"; import { client } from "../../client"; import { defineCommand } from ".."; +import { abort } from "../../utils/error"; export default defineCommand({ data: new SlashCommandBuilder() @@ -9,10 +10,9 @@ export default defineCommand({ async execute(interaction) { if (client.ws.ping < 1) { - await interaction.reply( + abort( ":ping_pong: Pong!\nThe bot is still starting up, accurate latency will be available shortly." ); - return; } const msg = ( diff --git a/src/env.ts b/src/env.ts index 3867ff5..6cc410d 100644 --- a/src/env.ts +++ b/src/env.ts @@ -10,6 +10,8 @@ const envSchema = z.object({ .default("development"), DEV_GUILD_ID: z.string(), DEV_CHANNEL_ID: z.string(), + API_URL: z.string(), + API_TOKEN: z.string(), }); export const env = envSchema.parse(process.env); diff --git a/src/events/interactionCreate.ts b/src/events/interactionCreate.ts index 840e0ec..5a04466 100644 --- a/src/events/interactionCreate.ts +++ b/src/events/interactionCreate.ts @@ -8,7 +8,7 @@ import { import { client } from "../client"; import { log } from "../utils/logger"; import { defineEvent } from "."; -import { isCommandError, notifyError } from "../utils/error"; +import { isExplicitCommandError, notifyError } from "../utils/error"; import { nanoid } from "../utils/functions"; const running = new Map(); @@ -54,11 +54,11 @@ async function handleChatInputCommand( try { await command.execute(interaction); } catch (err) { - let content = isCommandError(err) + let content = isExplicitCommandError(err) ? err.message : "An unknown error occurred!"; - if (!isCommandError(err)) { + if (!isExplicitCommandError(err)) { const trace = nanoid(); content += `\ntrace: ${inlineCode(trace)}`; log.error("Unhandled Command Error", { trace, err }); diff --git a/src/utils/api.ts b/src/utils/api.ts new file mode 100644 index 0000000..2b469a1 --- /dev/null +++ b/src/utils/api.ts @@ -0,0 +1,26 @@ +import ky from "ky"; +import { env } from "../env"; +import { version } from "discord.js"; + +type OCRResult = { + text: string; + detected_lang?: string; +}; + +const client = ky.create({ + prefixUrl: env.API_URL, + headers: { + "User-Agent": `artemis (discord.js v${version})`, + Authorization: `Bearer ${env.API_TOKEN}`, + }, +}); + +export async function yandexOcr(image: Buffer, mime: string) { + const res = await client.post("ocr/yandex", { + json: { + file: image.toString("base64"), + mime, + }, + }); + return res.json(); +} diff --git a/src/utils/error.ts b/src/utils/error.ts index 681e45f..bf60009 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -2,10 +2,16 @@ import { codeBlock, type TextChannel } from "discord.js"; import { client } from "../client"; import { env } from "../env"; -export class CommandError extends Error {} +export class ExplicitCommandError extends Error {} -export function isCommandError(error: any): error is CommandError { - return error instanceof CommandError; +export function abort(message: string): never { + throw new ExplicitCommandError(message); +} + +export function isExplicitCommandError( + error: any +): error is ExplicitCommandError { + return error instanceof ExplicitCommandError; } export async function notifyError(trace: string, error: any) { diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..a93aeef --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,8 @@ +import { fileTypeFromBuffer } from "file-type"; + +export async function downloadFile(url: string) { + const res = await fetch(url); + const data = Buffer.from(await res.arrayBuffer()); + const type = await fileTypeFromBuffer(data); + return { data, type }; +}