From b72f13e81b972ea8fddb46f6d7c72318f33a6e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aura=20Rom=C3=A1n?= Date: Sun, 3 Nov 2024 10:16:15 +0100 Subject: [PATCH] fix(reddit): handle invalid JSON errors (HTML) Insert https://www.youtube.com/watch?v=nSKp2StlS6s --- src/commands/reddit.ts | 40 +++++++++++++++++++++++++++------------- src/lib/common/ansi.ts | 7 +++++++ 2 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 src/lib/common/ansi.ts diff --git a/src/commands/reddit.ts b/src/commands/reddit.ts index fffd7fd08..f47706747 100644 --- a/src/commands/reddit.ts +++ b/src/commands/reddit.ts @@ -1,3 +1,4 @@ +import { ansi8Foreground } from '#lib/common/ansi'; import { LanguageKeys } from '#lib/i18n/LanguageKeys'; import { isNsfwChannel } from '#lib/utilities/discord-utilities'; import { inlineCode, unorderedList } from '@discordjs/builders'; @@ -19,6 +20,7 @@ import { } from '@skyra/reddit-helpers'; import { isAbortError, type FetchError } from '@skyra/safe-fetch'; import { ApplicationIntegrationType, InteractionContextType, MessageFlags } from 'discord-api-types/v10'; +import { inspect } from 'node:util'; const Root = LanguageKeys.Commands.Reddit; @@ -29,6 +31,7 @@ const Root = LanguageKeys.Commands.Reddit; ) export class UserCommand extends Command { private readonly forbidden = new Collection(); + private readonly LogPrefix = ansi8Foreground(202, '[\uf281 REDDIT]'); @RegisterSubcommand((builder) => applyLocalizedBuilder(builder, Root.Subreddit) // @@ -107,33 +110,44 @@ export class UserCommand extends Command { return { content: this.handleErrorGetContent(interaction, reddit, error), flags: MessageFlags.Ephemeral }; } - private handleErrorGetContent(interaction: Command.ChatInputInteraction, reddit: string, error: FetchError | RedditParseException) { + private handleErrorGetContent(interaction: Command.ChatInputInteraction, reddit: string, error: FetchError | RedditParseException): string { if (isAbortError(error)) return resolveUserKey(interaction, Root.AbortError); if (error instanceof RedditParseException) { return resolveUserKey(interaction, Root.ParsePostException); } - const parsed = error.jsonBody as RedditError; - switch (parsed.error) { + let errorBody: RedditError; + try { + errorBody = error.jsonBody as RedditError; + } catch (parseError) { + this.container.logger.error(`${this.LogPrefix} [${error.code}] ${error.url}\nBody: ${red(error.body)}\nError: ${inspect(parseError)}`); + return resolveUserKey(interaction, Root.UnknownError); + } + + return this.handleRedditError(interaction, reddit, errorBody); + } + + private handleRedditError(interaction: Command.ChatInputInteraction, reddit: string, errorBody: RedditError): string { + switch (errorBody.error) { case 403: { - if (parsed.reason === 'private') return resolveUserKey(interaction, Root.UnavailableErrorPrivate); - if (parsed.reason === 'gold_only') return resolveUserKey(interaction, Root.UnavailableErrorGoldOnly); - if (parsed.reason === 'quarantined') { - const reason = this.forbid(reddit, ForbiddenType.Quarantined, parsed.quarantine_message); + if (errorBody.reason === 'private') return resolveUserKey(interaction, Root.UnavailableErrorPrivate); + if (errorBody.reason === 'gold_only') return resolveUserKey(interaction, Root.UnavailableErrorGoldOnly); + if (errorBody.reason === 'quarantined') { + const reason = this.forbid(reddit, ForbiddenType.Quarantined, errorBody.quarantine_message); return resolveUserKey(interaction, Root.UnavailableErrorQuarantined, { reason }); } - if (parsed.reason === 'gated') { - const reason = this.forbid(reddit, ForbiddenType.Quarantined, parsed.interstitial_warning_message); + if (errorBody.reason === 'gated') { + const reason = this.forbid(reddit, ForbiddenType.Quarantined, errorBody.interstitial_warning_message); return resolveUserKey(interaction, Root.UnavailableErrorGated, { reason }); } break; } case 404: { - if (!Reflect.has(parsed, 'reason')) { + if (!Reflect.has(errorBody, 'reason')) { return resolveUserKey(interaction, Root.UnavailableErrorNotFound); } - if (Reflect.get(parsed, 'reason') === 'banned') { + if (Reflect.get(errorBody, 'reason') === 'banned') { return resolveUserKey(interaction, Root.UnavailableErrorBanned); } break; @@ -146,14 +160,14 @@ export class UserCommand extends Command { } } - this.container.logger.error('[Reddit] Unknown Error', parsed); + this.container.logger.error(`${this.LogPrefix} Unknown Error`, errorBody); return resolveUserKey(interaction, Root.UnknownError); } private forbid(reddit: string, type: ForbiddenType, reason: string) { // Some messages send with 2 empty lines, where some *may* contain an empty whitespace. reason = reason.replaceAll(/(?:\s*\n){3,}/g, '\n\n'); - this.container.logger.info(`[REDDIT] Forbidding ${red(reddit)}. Reason: ${gray(reason.replaceAll('\n', '\\n'))}`); + this.container.logger.info(`${this.LogPrefix} Forbidding ${red(reddit)}. Reason: ${gray(reason.replaceAll('\n', '\\n'))}`); this.forbidden.set(reddit, { type, reason }); return reason; } diff --git a/src/lib/common/ansi.ts b/src/lib/common/ansi.ts new file mode 100644 index 000000000..89017d486 --- /dev/null +++ b/src/lib/common/ansi.ts @@ -0,0 +1,7 @@ +export function ansi8Foreground(code: number, content: string | number) { + return `\x1b[38;5;${code}m${content}\x1b[39m`; +} + +export function ansi8Background(code: number, content: string | number) { + return `\x1b[48;5;${code}m${content}\x1b[39m`; +}