diff --git a/src/api/gramjs/helpers/misc.ts b/src/api/gramjs/helpers/misc.ts index 943081196..85f01768e 100644 --- a/src/api/gramjs/helpers/misc.ts +++ b/src/api/gramjs/helpers/misc.ts @@ -2,6 +2,7 @@ import { Api as GramJs, errors } from '../../../lib/gramjs'; import type { RegularLangKey } from '../../../types/language'; import type { RegularLangFnParameters } from '../../../util/localization'; +import type { ApiError } from '../../types'; import { DEBUG } from '../../../config'; import { @@ -39,6 +40,10 @@ const ERROR_KEYS: Record = { PASSKEY_CREDENTIAL_NOT_FOUND: 'ErrorPasskeyUnknown', }; +function resolveErrorKey(errorMessage: string) { + return ERROR_KEYS[errorMessage] || ERROR_KEYS[errorMessage.replace(/_\d+$/, '')]; +} + export type MessageRepairContext = Pick; export type MediaRepairContext = MessageRepairContext; @@ -102,6 +107,20 @@ export function checkErrorType(error: unknown): error is Error { return true; } +export function buildApiError(error: Error): Pick { + if (error instanceof errors.RPCError) { + return { + message: error.errorMessage, + code: error.code, + hasErrorKey: true, + }; + } + + return { + message: error.message, + }; +} + export function wrapError(error: T): WrappedError { let messageKey: RegularLangFnParameters | undefined; @@ -123,9 +142,12 @@ export function wrapError(error: T): WrappedError { variables: { time: formatWait(error.seconds) }, }; } else if (error instanceof errors.RPCError) { - messageKey = { - key: ERROR_KEYS[error.errorMessage], - }; + const key = resolveErrorKey(error.errorMessage); + if (key) { + messageKey = { + key, + }; + } } if (!messageKey) { diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 21f8d7dee..08815e5dc 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -82,7 +82,9 @@ import { import { addPhotoToLocalDb, } from '../helpers/localDb'; -import { checkErrorType, isChatFolder, wrapError } from '../helpers/misc'; +import { + buildApiError, checkErrorType, isChatFolder, wrapError, +} from '../helpers/misc'; import { scheduleMutedChatUpdate } from '../scheduleUnmute'; import { sendApiUpdate } from '../updates/apiUpdateEmitter'; import { @@ -1676,12 +1678,10 @@ export async function addChatMembers(chat: ApiChat, users: ApiUser[]) { return addChatUsersResult.flat().filter(Boolean); } } catch (err: unknown) { - const message = err instanceof RPCError ? err.errorMessage : (err as Error).message; + const apiError = buildApiError(err as Error); sendApiUpdate({ '@type': 'error', - error: { - message, - }, + error: apiError, }); } return undefined; diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index 81fab9180..3bc3d2776 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -1,5 +1,6 @@ import { Api as GramJs, + errors, sessions, type Update, } from '../../../lib/gramjs'; @@ -40,6 +41,7 @@ import { addWebPageMediaToLocalDb, } from '../helpers/localDb'; import { + buildApiError, isResponseUpdate, log, } from '../helpers/misc'; import localDb, { clearLocalDb, type RepairInfo } from '../localDb'; @@ -451,9 +453,9 @@ export async function fetchCurrentUser() { } export function dispatchErrorUpdate(err: Error, request: T) { - const message = err instanceof RPCError ? err.errorMessage : err.message; + const { message, code } = buildApiError(err); - const isSlowMode = message === 'FLOOD' && ( + const isSlowMode = err instanceof errors.FloodError && ( request instanceof GramJs.messages.SendMessage || request instanceof GramJs.messages.SendMedia || request instanceof GramJs.messages.SendMultiMedia @@ -463,6 +465,7 @@ export function dispatchErrorUpdate(err: Error, req '@type': 'error', error: { message, + code, isSlowMode, hasErrorKey: true, }, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index b35b0df7f..f50527021 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -109,6 +109,7 @@ import { getEntityTypeById, } from '../gramjsBuilders'; import { + buildApiError, deserializeBytes, resolveMessageApiChatId, } from '../helpers/misc'; @@ -237,7 +238,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message }, ); } catch (err: any) { - const { message } = err; + const { message, code } = buildApiError(err); // When fetching messages for the bot @replies, there may be situations when the user was banned // in the comment group or this group was deleted @@ -246,6 +247,7 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message '@type': 'error', error: { message, + code, isSlowMode: false, hasErrorKey: true, }, @@ -821,12 +823,12 @@ export async function editMessage({ console.warn(err); } - const { message: messageErr } = err as Error; + const apiError = buildApiError(err as Error); sendApiUpdate({ '@type': 'error', error: { - message: messageErr, + ...apiError, hasErrorKey: true, }, }); @@ -887,12 +889,12 @@ export async function editTodo({ console.warn(err); } - const { message: messageErr } = err as Error; + const apiError = buildApiError(err as Error); sendApiUpdate({ '@type': 'error', error: { - message: messageErr, + ...apiError, hasErrorKey: true, }, }); @@ -936,12 +938,12 @@ export async function appendTodoList({ console.warn(err); } - const { message: messageErr } = err as Error; + const apiError = buildApiError(err as Error); sendApiUpdate({ '@type': 'error', error: { - message: messageErr, + ...apiError, hasErrorKey: true, }, }); diff --git a/src/api/gramjs/worker/connector.ts b/src/api/gramjs/worker/connector.ts index 28af80814..4ea301f69 100644 --- a/src/api/gramjs/worker/connector.ts +++ b/src/api/gramjs/worker/connector.ts @@ -1,6 +1,6 @@ import type { Api } from '../../../lib/gramjs'; import type { TypedBroadcastChannel } from '../../../util/browser/multitab'; -import type { ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; +import type { ApiError, ApiInitialArgs, ApiOnProgress, OnApiUpdate } from '../../types'; import type { LocalDb } from '../localDb'; import type { MethodArgs, MethodResponse, Methods } from '../methods/types'; import type { OriginPayload, ThenArg, WorkerMessageEvent } from './types'; @@ -313,7 +313,7 @@ function subscribeToWorker(onUpdate: OnApiUpdate) { export function handleMethodResponse(data: { messageId: string; response?: ThenArg>; - error?: { message: string }; + error?: Pick; }) { const requestState = requestStates.get(data.messageId); if (requestState) { diff --git a/src/api/gramjs/worker/types.ts b/src/api/gramjs/worker/types.ts index 1f4c03bcc..5a7efcb98 100644 --- a/src/api/gramjs/worker/types.ts +++ b/src/api/gramjs/worker/types.ts @@ -1,6 +1,6 @@ import type { DebugLevel } from '../../../util/debugConsole'; import type { - ApiInitialArgs, ApiUpdate, + ApiError, ApiInitialArgs, ApiUpdate, } from '../../types'; import type { LocalDb } from '../localDb'; import type { MethodArgs, MethodResponse, Methods } from '../methods/types'; @@ -17,7 +17,7 @@ export type WorkerPayload = type: 'methodResponse'; messageId: string; response?: ThenArg>; - error?: { message: string }; + error?: Pick; } | { diff --git a/src/api/gramjs/worker/worker.ts b/src/api/gramjs/worker/worker.ts index 9bc09a935..8cde7e2e7 100644 --- a/src/api/gramjs/worker/worker.ts +++ b/src/api/gramjs/worker/worker.ts @@ -7,7 +7,7 @@ import type { OriginMessageEvent, WorkerPayload } from './types'; import { DEBUG } from '../../../config'; import { DEBUG_LEVELS } from '../../../util/debugConsole'; import { throttleWithTickEnd } from '../../../util/schedulers'; -import { log } from '../helpers/misc'; +import { buildApiError, log } from '../helpers/misc'; import { callApi, cancelApiProgress, initApi } from '../methods/init'; declare const self: WorkerGlobalScope; @@ -110,7 +110,7 @@ onmessage = ({ data }: OriginMessageEvent) => { sendToOrigin({ type: 'methodResponse', messageId, - error: { message: error.message }, + error: buildApiError(error), }); } } diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index bdb83a7eb..2dae0b03f 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -174,6 +174,7 @@ export type ApiDialog = ApiDialogError | ApiDialogMessage | ApiDialogContact | A export type ApiError = { message: string; + code?: number; entities?: ApiMessageEntity[]; hasErrorKey?: boolean; isSlowMode?: boolean; diff --git a/src/global/actions/ui/misc.ts b/src/global/actions/ui/misc.ts index a5c5179d5..7ac0409f0 100644 --- a/src/global/actions/ui/misc.ts +++ b/src/global/actions/ui/misc.ts @@ -12,7 +12,7 @@ import { IS_WAVE_TRANSFORM_SUPPORTED } from '../../../util/browser/windowEnviron import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole'; import { getAllNotificationsCount } from '../../../util/folderManager'; import getIsAppUpdateNeeded from '../../../util/getIsAppUpdateNeeded'; -import getReadableErrorText from '../../../util/getReadableErrorText'; +import { shouldShowErrorDialog } from '../../../util/getReadableErrorText'; import { compact, unique } from '../../../util/iteratees'; import { refreshFromCache } from '../../../util/localization'; import * as langProvider from '../../../util/oldLangProvider'; @@ -388,7 +388,7 @@ addActionHandler('showDialog', (global, actions, payload): ActionReturnType => { const { data, tabId = getCurrentTabId() } = payload; // Filter out errors that we don't want to show to the user - if (data.type === 'error' && data.hasErrorKey && !getReadableErrorText(data)) { + if (data.type === 'error' && !shouldShowErrorDialog(data)) { return global; } diff --git a/src/lib/gramjs/errors/RPCBaseErrors.ts b/src/lib/gramjs/errors/RPCBaseErrors.ts index f9fdf2bc0..c4020524c 100644 --- a/src/lib/gramjs/errors/RPCBaseErrors.ts +++ b/src/lib/gramjs/errors/RPCBaseErrors.ts @@ -4,14 +4,14 @@ import type { Api } from '../tl'; * Base class for all Remote Procedure Call errors. */ export class RPCError extends Error { - public code: number | undefined; + public code: number; public errorMessage: string; - constructor(message: string, request: Api.AnyRequest, code?: number) { + constructor(message: string, request: Api.AnyRequest, code: number) { super( 'RPCError {0}: {1}{2}' - .replace('{0}', code?.toString() || '') + .replace('{0}', code.toString()) .replace('{1}', message) .replace('{2}', RPCError._fmtRequest(request)), ); @@ -32,63 +32,37 @@ export class RPCError extends Error { /** * The request must be repeated, but directed to a different data center. */ -export class InvalidDCError extends RPCError { - constructor(message: string, request: Api.AnyRequest, code?: number) { - super(message, request, code); - this.code = code || 303; - this.errorMessage = message || 'ERROR_SEE_OTHER'; - } -} +export class InvalidDCError extends RPCError {} /** * The query contains errors. In the event that a request was created * using a form and contains user generated data, the user should be * notified that the data must be corrected before the query is repeated. */ -export class BadRequestError extends RPCError { - code = 400; - - errorMessage = 'BAD_REQUEST'; -} +export class BadRequestError extends RPCError {} /** * There was an unauthorized attempt to use functionality available only * to authorized users. */ -export class UnauthorizedError extends RPCError { - code = 401; - - errorMessage = 'UNAUTHORIZED'; -} +export class UnauthorizedError extends RPCError {} /** * Privacy violation. For example, an attempt to write a message to * someone who has blacklisted the current user. */ -export class ForbiddenError extends RPCError { - code = 403; - - errorMessage = 'FORBIDDEN'; -} +export class ForbiddenError extends RPCError {} /** * An attempt to invoke a non-existent object, such as a method. */ -export class NotFoundError extends RPCError { - code = 404; - - errorMessage = 'NOT_FOUND'; -} +export class NotFoundError extends RPCError {} /** * Errors related to invalid authorization key, like * AUTH_KEY_DUPLICATED which can cause the connection to fail. */ -export class AuthKeyError extends RPCError { - code = 406; - - errorMessage = 'AUTH_KEY'; -} +export class AuthKeyError extends RPCError {} /** * The maximum allowed number of attempts to invoke the given method @@ -96,29 +70,21 @@ export class AuthKeyError extends RPCError { * attempt to request a large number of text messages (SMS) for the same * phone number. */ -export class FloodError extends RPCError { - code = 420; - - errorMessage = 'FLOOD'; -} +export class FloodError extends RPCError {} /** * An internal server error occurred while a request was being processed * for example, there was a disruption while accessing a database or file * storage */ -export class ServerError extends RPCError { - code = 500; // Also witnessed as -500 - - errorMessage = 'INTERNAL'; -} +export class ServerError extends RPCError {} /** * Clicking the inline buttons of bots that never (or take to long to) * call ``answerCallbackQuery`` will result in this "special" RPCError. */ export class TimedOutError extends RPCError { - code = 503; // Only witnessed as -503 - - errorMessage = 'Timeout'; + constructor(args: { request: Api.AnyRequest; code: number }) { + super('Timeout', args.request, args.code); // Only witnessed as -503 + } } diff --git a/src/lib/gramjs/errors/RPCErrorList.ts b/src/lib/gramjs/errors/RPCErrorList.ts index e542e3acf..0cfd95442 100644 --- a/src/lib/gramjs/errors/RPCErrorList.ts +++ b/src/lib/gramjs/errors/RPCErrorList.ts @@ -1,4 +1,3 @@ -/* eslint-disable @stylistic/max-len */ import { BadRequestError, FloodError, InvalidDCError, RPCError, TimedOutError, } from './RPCBaseErrors'; @@ -8,8 +7,7 @@ export class UserMigrateError extends InvalidDCError { constructor(args: any) { const newDc = Number(args.capture || 0); - super(`The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request); - this.message = `The user whose identity is being used to execute queries is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.newDc = newDc; } } @@ -19,8 +17,7 @@ export class PhoneMigrateError extends InvalidDCError { constructor(args: any) { const newDc = Number(args.capture || 0); - super(`The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, args.request); - this.message = `The phone number a user is trying to use for authorization is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.newDc = newDc; } } @@ -30,11 +27,7 @@ export class SlowModeWaitError extends FloodError { constructor(args: any) { const seconds = Number(args.capture || 0); - super( - `A wait of ${seconds} seconds is required before sending another message in this chat ${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `A wait of ${seconds} seconds is required before sending another message in this chat${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.seconds = seconds; } } @@ -44,11 +37,7 @@ export class FloodWaitError extends FloodError { constructor(args: any) { const seconds = Number(args.capture || 0); - super( - `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.seconds = seconds; } } @@ -56,21 +45,14 @@ export class FloodWaitError extends FloodError { export class FloodPremiumWaitError extends FloodWaitError { constructor(args: any) { const seconds = Number(args.capture || 0); - super(`A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`); - this.message = `A wait of ${seconds} seconds is required${RPCError._fmtRequest(args.request)}`; + super(args); this.seconds = seconds; } } export class MsgWaitError extends FloodError { constructor(args: any) { - super( - `Message failed to be sent.${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `Message failed to be sent.${RPCError._fmtRequest( - args.request, - )}`; + super(args.errorMessage, args.request, args.code); } } @@ -79,11 +61,7 @@ export class FloodTestPhoneWaitError extends FloodError { constructor(args: any) { const seconds = Number(args.capture || 0); - super( - `A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `A wait of ${seconds} seconds is required in the test servers${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.seconds = seconds; } } @@ -93,11 +71,7 @@ export class FileMigrateError extends InvalidDCError { constructor(args: any) { const newDc = Number(args.capture || 0); - super( - `The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `The file to be accessed is currently stored in DC ${newDc}${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.newDc = newDc; } } @@ -107,11 +81,7 @@ export class NetworkMigrateError extends InvalidDCError { constructor(args: any) { const newDc = Number(args.capture || 0); - super( - `The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`, - args.request, - ); - this.message = `The source IP address is associated with DC ${newDc}${RPCError._fmtRequest(args.request)}`; + super(args.errorMessage, args.request, args.code); this.newDc = newDc; } } @@ -121,17 +91,7 @@ export class EmailUnconfirmedError extends BadRequestError { constructor(args: any) { const codeLength = Number(args.capture || 0); - super( - `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest( - args.request, - )}`, - args.request, - 400, - ); - - this.message = `Email unconfirmed, the length of the code must be ${codeLength}${RPCError._fmtRequest( - args.request, - )}`; + super(args.errorMessage, args.request, args.code); this.codeLength = codeLength; } } @@ -141,9 +101,7 @@ export class PasswordFreshError extends BadRequestError { constructor(args: any) { const seconds = Number(args.capture || 0); - super(`The password was modified less than 24 hours ago, try again in ${seconds} seconds.`, args.request); - - this.message = `The password was modified less than 24 hours ago, try again in ${seconds} seconds.`; + super(args.errorMessage, args.request, args.code); this.seconds = seconds; } } @@ -153,9 +111,7 @@ export class SessionFreshError extends BadRequestError { constructor(args: any) { const seconds = Number(args.capture || 0); - super(`Session is fresh, please try again in ${seconds} seconds.`, args.request); - - this.message = `Session is fresh, please try again in ${seconds} seconds.`; + super(args.errorMessage, args.request, args.code); this.seconds = seconds; } } @@ -181,7 +137,7 @@ export class UserAlreadyAuthorizedError extends Error { export class PasskeyCredentialNotFoundError extends RPCError { constructor(args: any) { - super('PASSKEY_CREDENTIAL_NOT_FOUND', args.request); + super(args.errorMessage, args.request, args.code); } } diff --git a/src/lib/gramjs/errors/index.ts b/src/lib/gramjs/errors/index.ts index dc203cd1e..952fe3a9e 100644 --- a/src/lib/gramjs/errors/index.ts +++ b/src/lib/gramjs/errors/index.ts @@ -17,7 +17,12 @@ export function RPCMessageToError( const m = rpcError.errorMessage.match(msgRegex); if (m) { const capture = m.length === 2 ? parseInt(m[1], 10) : undefined; - return new Cls({ request, capture }); + return new Cls({ + request, + capture, + code: rpcError.errorCode, + errorMessage: rpcError.errorMessage, + }); } } return new RPCError(rpcError.errorMessage, request, rpcError.errorCode); diff --git a/src/util/getReadableErrorText.ts b/src/util/getReadableErrorText.ts index d664203c0..e460607e9 100644 --- a/src/util/getReadableErrorText.ts +++ b/src/util/getReadableErrorText.ts @@ -136,6 +136,10 @@ const FINAL_PAYMENT_ERRORS = new Set([ 'PAYMENT_FAILED', ]); +const ERROR_CODES_WITHOUT_DIALOG = new Set([ + 406, +]); + export default function getReadableErrorText(error: ApiError) { const { message, isSlowMode, textParams } = error; // Currently, Telegram API doesn't return `SLOWMODE_WAIT_X` error as described in the docs @@ -159,3 +163,10 @@ export function getShippingError(error: ApiError): ApiFieldError | undefined { export function shouldClosePaymentModal(error: ApiError): boolean { return FINAL_PAYMENT_ERRORS.has(error.message); } + +export function shouldShowErrorDialog(error: ApiError): boolean { + if (error.code && ERROR_CODES_WITHOUT_DIALOG.has(error.code)) return false; + if (error.hasErrorKey && !getReadableErrorText(error)) return false; + + return true; +}