refactor: reworked errors codegen

This commit is contained in:
alina 🌸 2023-09-06 23:54:51 +03:00
parent 91894e87cc
commit 4b7d7d2e35
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
27 changed files with 374 additions and 469 deletions

View file

@ -174,7 +174,7 @@ export async function start(
return me
} catch (e) {
if (!(e instanceof tl.errors.AuthKeyUnregisteredError)) throw e
if (!tl.RpcError.is(e, 'AUTH_KEY_UNREGISTERED')) throw e
}
if (!params.phone && !params.botToken) {
@ -221,19 +221,21 @@ export async function start(
for (;;) {
const code = await resolveMaybeDynamic(params.code)
if (!code) throw new tl.errors.PhoneCodeEmptyError()
if (!code) throw new tl.RpcError(400, 'PHONE_CODE_EMPTY')
try {
result = await this.signIn(phone, sentCode.phoneCodeHash, code)
} catch (e) {
if (e instanceof tl.errors.SessionPasswordNeededError) {
if (!tl.RpcError.is(e)) throw e
if (e.is('SESSION_PASSWORD_NEEDED')) {
has2fa = true
break
} else if (
e instanceof tl.errors.PhoneCodeEmptyError ||
e instanceof tl.errors.PhoneCodeExpiredError ||
e instanceof tl.errors.PhoneCodeHashEmptyError ||
e instanceof tl.errors.PhoneCodeInvalidError
e.is('PHONE_CODE_EMPTY') ||
e.is('PHONE_CODE_EXPIRED') ||
e.is('PHONE_CODE_INVALID') ||
e.is('PHONE_CODE_HASH_EMPTY')
) {
if (typeof params.code !== 'function') {
throw new MtArgumentError('Provided code was invalid')
@ -270,7 +272,7 @@ export async function start(
throw new MtArgumentError('Provided password was invalid')
}
if (e instanceof tl.errors.PasswordHashInvalidError) {
if (tl.RpcError.is(e, 'PASSWORD_HASH_INVALID')) {
if (params.invalidCodeCallback) {
await params.invalidCodeCallback('password')
} else {

View file

@ -1,7 +1,12 @@
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { ChatMember, InputPeerLike, MtInvalidPeerTypeError, PeersIndex } from '../../types'
import {
ChatMember,
InputPeerLike,
MtInvalidPeerTypeError,
PeersIndex,
} from '../../types'
import {
isInputPeerChannel,
isInputPeerChat,
@ -27,7 +32,9 @@ export async function getChatMember(
const chat = await this.resolvePeer(chatId)
if (isInputPeerChat(chat)) {
if (!isInputPeerUser(user)) { throw new MtInvalidPeerTypeError(userId, 'user') }
if (!isInputPeerUser(user)) {
throw new MtInvalidPeerTypeError(userId, 'user')
}
const res = await this.call({
_: 'messages.getFullChat',
@ -57,7 +64,7 @@ export async function getChatMember(
}
}
throw new tl.errors.UserNotParticipantError()
throw new tl.RpcError(404, 'USER_NOT_PARTICIPANT')
} else if (isInputPeerChannel(chat)) {
const res = await this.call({
_: 'channels.getParticipant',

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client'
import { ChatPreview, MtArgumentError, MtNotFoundError } from '../../types'
import { ChatPreview, MtArgumentError, MtPeerNotFoundError } from '../../types'
import { INVITE_LINK_REGEX } from '../../utils/peer-utils'
/**
@ -25,7 +25,7 @@ export async function getChatPreview(
})
if (res._ !== 'chatInvite') {
throw new MtNotFoundError('You have already joined this chat!')
throw new MtPeerNotFoundError('You have already joined this chat!')
}
return new ChatPreview(this, res, inviteLink)

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client'
import { Chat, InputPeerLike, MtNotFoundError } from '../../types'
import { Chat, InputPeerLike, MtPeerNotFoundError } from '../../types'
import {
INVITE_LINK_REGEX,
normalizeToInputChannel,
@ -40,7 +40,10 @@ export async function joinChat(
const res = await this.call({
_: 'channels.joinChannel',
channel: normalizeToInputChannel(await this.resolvePeer(chatId), chatId),
channel: normalizeToInputChannel(
await this.resolvePeer(chatId),
chatId,
),
})
assertIsUpdatesGroup('channels.joinChannel', res)

View file

@ -134,7 +134,9 @@ export async function* getDialogs(
return it.id === params!.folder || it.title === params!.folder
})
if (!found) { throw new MtArgumentError(`Could not find folder ${params.folder}`) }
if (!found) {
throw new MtArgumentError(`Could not find folder ${params.folder}`)
}
filters = found as tl.RawDialogFilter
} else {
@ -160,7 +162,9 @@ export async function* getDialogs(
!filters ||
filters._ === 'dialogFilterDefault' ||
!filters.pinnedPeers.length
) { return null }
) {
return null
}
const res = await this.call({
_: 'messages.getPeerDialogs',
peers: filters.pinnedPeers.map((peer) => ({
@ -229,8 +233,7 @@ export async function* getDialogs(
}
}
const filterFolder = filters ?
// if pinned is `only`, this wouldn't be reached
const filterFolder = filters ? // if pinned is `only`, this wouldn't be reached
// if pinned is `exclude`, we want to exclude them
// if pinned is `include`, we already yielded them, so we also want to exclude them
// if pinned is `keep`, we want to keep them
@ -266,7 +269,7 @@ export async function* getDialogs(
const last = dialogs[dialogs.length - 1]
offsetPeer = last.chat.inputPeer
offsetId = last.raw.topMessage
offsetDate = normalizeDate(last.lastMessage.date)!
offsetDate = normalizeDate(last.lastMessage?.date) ?? 0
for (const d of dialogs) {
if (filterFolder && !filterFolder(d)) continue

View file

@ -137,13 +137,14 @@ export async function* downloadAsIterable(
},
{ dcId, kind: connectionKind },
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
if (e.constructor === tl.errors.FileMigrateXError) {
dcId = e.new_dc
} catch (e: unknown) {
if (!tl.RpcError.is(e)) throw e
if (e.is('FILE_MIGRATE_%d')) {
dcId = e.newDc
return downloadChunk(chunk)
} else if (e.constructor === tl.errors.FilerefUpgradeNeededError) {
} else if (e.is('FILEREF_UPGRADE_NEEDED')) {
// todo: implement someday
// see: https://github.com/LonamiWebs/Telethon/blob/0e8bd8248cc649637b7c392616887c50986427a0/telethon/client/downloads.py#L99
throw new MtUnsupportedError('File ref expired!')

View file

@ -117,7 +117,7 @@ export async function editInlineMessage(
return
} catch (e) {
if (e instanceof tl.errors.MediaEmptyError) {
if (tl.RpcError.is(e, 'MEDIA_EMPTY')) {
continue
}

View file

@ -1,3 +1,4 @@
import { getMarkedPeerId } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
@ -5,6 +6,7 @@ import {
FormattedString,
InputPeerLike,
Message,
MtMessageNotFoundError,
ReplyMarkup,
} from '../../types'
@ -104,7 +106,13 @@ export async function sendCopy(
const msg = await this.getMessages(fromPeer, message)
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(fromPeer),
message,
'to copy',
)
}
return msg.sendCopy(toChatId, params)
}

View file

@ -1,4 +1,4 @@
import { randomLong } from '@mtcute/core'
import { getMarkedPeerId, randomLong } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
@ -8,6 +8,7 @@ import {
InputPeerLike,
Message,
MtArgumentError,
MtMessageNotFoundError,
PeersIndex,
ReplyMarkup,
} from '../../types'
@ -136,7 +137,13 @@ export async function sendMediaGroup(
const msg = await this.getMessages(peer, replyTo)
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(peer),
replyTo,
'to reply to',
)
}
}
const multiMedia: tl.RawInputSingleMedia[] = []

View file

@ -1,4 +1,4 @@
import { randomLong } from '@mtcute/core'
import { getMarkedPeerId, randomLong } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
@ -9,6 +9,7 @@ import {
InputPeerLike,
Message,
MtArgumentError,
MtMessageNotFoundError,
ReplyMarkup,
} from '../../types'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils'
@ -173,7 +174,13 @@ export async function sendMedia(
const msg = await this.getMessages(peer, replyTo)
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(peer),
replyTo,
'to reply to',
)
}
}
const res = await this.call({

View file

@ -8,6 +8,7 @@ import {
InputPeerLike,
Message,
MtArgumentError,
MtMessageNotFoundError,
MtTypeAssertionError,
PeersIndex,
ReplyMarkup,
@ -145,7 +146,13 @@ export async function sendText(
const msg = await this.getMessages(peer, replyTo)
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(peer),
replyTo,
'to reply to',
)
}
}
const res = await this.call({

View file

@ -1,10 +1,11 @@
import { MaybeArray } from '@mtcute/core'
import { getMarkedPeerId, MaybeArray } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import {
InputPeerLike,
MtArgumentError,
MtMessageNotFoundError,
MtTypeAssertionError,
PeersIndex,
Poll,
@ -40,9 +41,17 @@ export async function sendVote(
if (options.some((it) => typeof it === 'number')) {
const msg = await this.getMessages(peer, message)
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(peer),
message,
'to vote in',
)
}
if (!(msg.media instanceof Poll)) { throw new MtArgumentError('This message does not contain a poll') }
if (!(msg.media instanceof Poll)) {
throw new MtArgumentError('This message does not contain a poll')
}
poll = msg.media
options = options.map((opt) => {

View file

@ -10,7 +10,7 @@ import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import {
InputPeerLike,
MtNotFoundError,
MtPeerNotFoundError,
MtTypeAssertionError,
} from '../../types'
import { normalizeToInputPeer } from '../../utils/peer-utils'
@ -72,7 +72,7 @@ export async function resolvePeer(
}
}
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Could not find a peer by phone ${peerId}`,
)
} else {
@ -97,7 +97,7 @@ export async function resolvePeer(
// no access hash, we can't use it
// this may happen when bot resolves a username
// of a user who hasn't started a conversation with it
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Peer (user) with username ${peerId} was found, but it has no access hash`,
)
}
@ -131,7 +131,7 @@ export async function resolvePeer(
if (!found.accessHash) {
// shouldn't happen? but just in case
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Peer (channel) with username ${peerId} was found, but it has no access hash`,
)
}
@ -151,7 +151,7 @@ export async function resolvePeer(
)
}
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Could not find a peer by username ${peerId}`,
)
}
@ -178,7 +178,7 @@ export async function resolvePeer(
if (found && found._ === 'user') {
if (!found.accessHash) {
// shouldn't happen? but just in case
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Peer (user) with username ${peerId} was found, but it has no access hash`,
)
}
@ -235,7 +235,7 @@ export async function resolvePeer(
) {
if (!found.accessHash) {
// shouldn't happen? but just in case
throw new MtNotFoundError(
throw new MtPeerNotFoundError(
`Peer (channel) with username ${peerId} was found, but it has no access hash`,
)
}
@ -251,5 +251,5 @@ export async function resolvePeer(
}
}
throw new MtNotFoundError(`Could not find a peer by ID ${peerId}`)
throw new MtPeerNotFoundError(`Could not find a peer by ID ${peerId}`)
}

View file

@ -3,7 +3,7 @@ import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { encodeInlineMessageId } from '../../utils/inline-utils'
import { MtArgumentError } from '../errors'
import { MtArgumentError, MtMessageNotFoundError } from '../errors'
import { Message } from '../messages'
import { PeersIndex, User } from '../peers'
import { makeInspectable } from '../utils'
@ -182,11 +182,15 @@ export class CallbackQuery {
)
}
const msg = await this.client.getMessages(
const msg = await this.client.getMessages(this.raw.peer, this.raw.msgId)
if (!msg) {
throw new MtMessageNotFoundError(
getMarkedPeerId(this.raw.peer),
this.raw.msgId,
'with button',
)
if (!msg) throw new tl.errors.MessageNotFoundError()
}
return msg
}

View file

@ -7,7 +7,7 @@ import {
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../client'
import { MtArgumentError } from './errors'
import { MtArgumentError, MtTimeoutError } from './errors'
import { InputMediaLike } from './media'
import { Message } from './messages'
import { FormattedString } from './parser'
@ -101,14 +101,12 @@ export class Conversation {
this._chatId = getMarkedPeerId(this._inputPeer)
const dialog = await this.client.getPeerDialogs(this._inputPeer)
const lastMessage = dialog.lastMessage
try {
this._lastMessage = this._lastReceivedMessage =
dialog.lastMessage.id
} catch (e) {
if (e instanceof tl.errors.MessageNotFoundError) {
if (lastMessage) {
this._lastMessage = this._lastReceivedMessage = lastMessage.id
} else {
this._lastMessage = this._lastReceivedMessage = 0
} else throw e
}
this.client.on('new_message', this._onNewMessage)
this.client.on('edit_message', this._onEditMessage)
@ -279,7 +277,7 @@ export class Conversation {
if (timeout !== null) {
timer = setTimeout(() => {
console.log('timed out')
promise.reject(new tl.errors.TimeoutError())
promise.reject(new MtTimeoutError(timeout))
this._queuedNewMessage.removeBy((it) => it.promise === promise)
}, timeout)
}
@ -422,12 +420,13 @@ export class Conversation {
const promise = createControllablePromise<Message>()
let timer: NodeJS.Timeout | undefined = undefined
const timeout = params?.timeout
if (params?.timeout !== null) {
if (timeout) {
timer = setTimeout(() => {
promise.reject(new tl.errors.TimeoutError())
promise.reject(new MtTimeoutError(timeout))
delete this._pendingEditMessage[msgId]
}, params?.timeout ?? 15000)
}, timeout)
}
this._pendingEditMessage[msgId] = {
@ -477,7 +476,7 @@ export class Conversation {
if (timeout !== null) {
timer = setTimeout(() => {
promise.reject(new tl.errors.TimeoutError())
promise.reject(new MtTimeoutError(timeout))
delete this._pendingRead[msgId]
}, timeout)
}

View file

@ -12,9 +12,26 @@ export class MtClientError extends Error {}
export class MtArgumentError extends MtClientError {}
/**
* Could not find peer by provided information
* Could not find a peer by the provided information
*/
export class MtNotFoundError extends MtClientError {}
export class MtPeerNotFoundError extends MtClientError {}
/**
* Could not find a message by the provided information
*/
export class MtMessageNotFoundError extends MtClientError {
constructor(
readonly peerId: number,
readonly messageId: number,
readonly context?: string,
) {
super(
`Message${
context ? ' ' + context : ''
} ${messageId} not found in ${peerId}`,
)
}
}
/**
* Either you requested or the server returned something
@ -80,3 +97,9 @@ export class MtEmptyError extends MtClientError {
super('Property is not available on an empty object')
}
}
export class MtTimeoutError extends MtClientError {
constructor(readonly timeout?: number) {
super(`Request timed out${timeout ? ` after ${timeout}ms` : ''}`)
}
}

View file

@ -2,6 +2,7 @@ import { getMarkedPeerId } from '@mtcute/core'
import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client'
import { MtMessageNotFoundError } from '../errors'
import { Chat, PeersIndex } from '../peers'
import { makeInspectable } from '../utils'
import { DraftMessage } from './draft-message'
@ -94,7 +95,9 @@ export class Dialog {
// manual exclusion/inclusion and pins
if (include[chatId]) return true
if (exclude[chatId] || (excludePinned && pinned[chatId])) { return false }
if (exclude[chatId] || (excludePinned && pinned[chatId])) {
return false
}
// exclusions based on status
if (folder.excludeRead && !dialog.isUnread) return false
@ -196,7 +199,7 @@ export class Dialog {
/**
* The latest message sent in this chat
*/
get lastMessage(): Message {
get lastMessage(): Message | null {
if (!this._lastMessage) {
const cid = this.chat.id
@ -207,7 +210,7 @@ export class Dialog {
this._peers,
)
} else {
throw new tl.errors.MessageNotFoundError()
throw new MtMessageNotFoundError(cid, 0)
}
}

View file

@ -24,13 +24,13 @@ import { defaultTransportFactory, TransportFactory } from './transports'
export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall'
const CLIENT_ERRORS = {
'303': 1,
'400': 1,
'401': 1,
'403': 1,
'404': 1,
'406': 1,
'420': 1,
[tl.RpcError.BAD_REQUEST]: 1,
[tl.RpcError.UNAUTHORIZED]: 1,
[tl.RpcError.FORBIDDEN]: 1,
[tl.RpcError.NOT_FOUND]: 1,
[tl.RpcError.FLOOD]: 1,
[tl.RpcError.SEE_OTHER]: 1,
[tl.RpcError.NOT_ACCEPTABLE]: 1,
}
/**
@ -714,7 +714,12 @@ export class NetworkManager {
await sleep(delta)
delete this._floodWaitedRequests[message._]
} else {
throw new tl.errors.FloodWaitXError(delta / 1000)
const err = tl.RpcError.create(
tl.RpcError.FLOOD,
'FLOOD_WAIT_%d',
)
err.seconds = Math.ceil(delta / 1000)
throw err
}
}
@ -747,14 +752,16 @@ export class NetworkManager {
} catch (e: any) {
lastError = e as Error
if (e.code && !(e.code in CLIENT_ERRORS)) {
if (!tl.RpcError.is(e)) continue
if (!(e.code in CLIENT_ERRORS)) {
this._log.warn(
'Telegram is having internal issues: %d %s, retrying',
e.code,
e.message,
)
if (e.message === 'WORKER_BUSY_TOO_LONG_RETRY') {
if (e.text === 'WORKER_BUSY_TOO_LONG_RETRY') {
// according to tdlib, "it is dangerous to resend query without timeout, so use 1"
await sleep(1000)
}
@ -762,11 +769,11 @@ export class NetworkManager {
}
if (
e.constructor === tl.errors.FloodWaitXError ||
e.constructor === tl.errors.SlowmodeWaitXError ||
e.constructor === tl.errors.FloodTestPhoneWaitXError
e.is('FLOOD_WAIT_%d') ||
e.is('SLOWMODE_WAIT_%d') ||
e.is('FLOOD_TEST_PHONE_WAIT_%d')
) {
if (e.constructor !== tl.errors.SlowmodeWaitXError) {
if (e.text !== 'SLOWMODE_WAIT_%d') {
// SLOW_MODE_WAIT is chat-specific, not request-specific
this._floodWaitedRequests[message._] =
Date.now() + e.seconds * 1000
@ -775,7 +782,7 @@ export class NetworkManager {
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
// such a short amount will cause retries very fast leading to issues
if (e.seconds === 0) {
(e as tl.Mutable<typeof e>).seconds = 1
e.seconds = 1
}
if (e.seconds <= floodSleepThreshold) {
@ -787,21 +794,19 @@ export class NetworkManager {
if (manager === this._primaryDc) {
if (
e.constructor === tl.errors.PhoneMigrateXError ||
e.constructor === tl.errors.UserMigrateXError ||
e.constructor === tl.errors.NetworkMigrateXError
e.is('PHONE_MIGRATE_%d') ||
e.is('NETWORK_MIGRATE_%d') ||
e.is('USER_MIGRATE_%d')
) {
this._log.info('Migrate error, new dc = %d', e.new_dc)
this._log.info('Migrate error, new dc = %d', e.newDc)
await this.changePrimaryDc(e.new_dc)
await this.changePrimaryDc(e.newDc)
manager = this._primaryDc!
multi = manager[kind]
continue
}
} else if (
e.constructor === tl.errors.AuthKeyUnregisteredError
) {
} else if (e.is('AUTH_KEY_UNREGISTERED')) {
// we can try re-exporting auth from the primary connection
this._log.warn(
'exported auth key error, trying re-exporting..',

View file

@ -50,12 +50,8 @@ export interface SessionConnectionParams extends PersistentConnectionParams {
// destroy_auth_key#d1435160 = DestroyAuthKeyRes;
// const DESTROY_AUTH_KEY = Buffer.from('605134d1', 'hex')
function makeNiceStack(
error: tl.errors.RpcError,
stack: string,
method?: string,
) {
error.stack = `${error.constructor.name} (${error.code} ${error.text}): ${
function makeNiceStack(error: tl.RpcError, stack: string, method?: string) {
error.stack = `RpcError (${error.code} ${error.text}): ${
error.message
}\n at ${method}\n${stack.split('\n').slice(2).join('\n')}`
}
@ -859,7 +855,7 @@ export class SessionConnection extends PersistentConnection {
if (rpc.cancelled) return
const error = tl.errors.createRpcErrorFromTl(res)
const error = tl.RpcError.fromTl(res)
if (this.params.niceStacks !== false) {
makeNiceStack(error, rpc.stack!, rpc.method)
@ -1544,7 +1540,8 @@ export class SessionConnection extends PersistentConnection {
}
if (onTimeout) {
const error = new tl.errors.RpcTimeoutError()
// todo: replace with MtTimeoutError
const error = new tl.RpcError(-503, 'Timeout')
if (this.params.niceStacks !== false) {
makeNiceStack(error, rpc.stack!, rpc.method)

View file

@ -1,96 +1,83 @@
import { TlError, TlErrors } from '../types'
import { camelToPascal, jsComment, snakeToCamel } from './utils'
import { TlErrors } from '../types'
import { snakeToCamel } from './utils'
/**
* Transform TL error name to JS error name
*
* @param code TL error code
* @example 'MSG_ID_INVALID' -> 'MsgIdInvalidError'
*/
export function errorCodeToClassName(code: string): string {
let str =
camelToPascal(snakeToCamel(code.toLowerCase().replace(/ /g, '_'))) +
'Error'
if (str[0].match(/\d/)) {
str = '_' + str
const TEMPLATE_JS = `
const _descriptionsMap = {
{descriptionsMap}
}
return str
}
const RPC_ERROR_CLASS_JS = `
class RpcError extends Error {
constructor(code, text, description) {
super(description);
constructor(code, name) {
super(_descriptionsMap[name] || 'Unknown RPC error: [' + code + ':' + name + ']');
this.code = code;
this.text = text;
this.name = name;
}
static is(err, name) { return err.constructor === RpcError && (!name || err.name === name); }
is(name) { return this.name === name; }
}
RpcError.fromTl = function (obj) {
const err = new RpcError(obj.errorCode, obj.errorMessage);
if (err in _descriptionsMap) return err;
let match;
{matchers}
return err
}
{statics}
{exports}RpcError = RpcError;
`.trimStart()
const RPC_ERROR_CLASS_TS = `
const TEMPLATE_TS = `
type MtErrorText =
{texts}
| (string & {}) // to keep hints
interface MtErrorArgMap {
{argMap}
}
type RpcErrorWithArgs<T extends string> =
RpcError & { text: T } & (T extends keyof MtErrorArgMap ? (RpcError & MtErrorArgMap[T]) : {});
export class RpcError extends Error {
{statics}
readonly code: number;
readonly text: string;
constructor(code: number, text: string, description?: string);
readonly text: MtErrorText;
constructor(code: number, text: MtErrorText);
is<const T extends MtErrorText>(text: T): this is RpcErrorWithArgs<T>;
static is<const T extends MtErrorText>(err: unknown): err is RpcError;
static is<const T extends MtErrorText>(err: unknown, text: T): err is RpcErrorWithArgs<T>;
static create<const T extends MtErrorText>(code: number, text: T): RpcErrorWithArgs<T>;
static fromTl(obj: object): RpcError;
}
`.trimStart()
const BASE_ERROR_JS = `
class {className} extends RpcError {
constructor(name, description) {
super({code}, name, description);
}
}
{exports}{className} = {className}
`
const BASE_ERROR_TS = `
export class {className} extends RpcError {
constructor(name: string, description: string);
}
`.trimStart()
const ERROR_PRELUDE = `{ts}class {className} extends {base} {
constructor({arguments})`
const TL_BUILDER_TEMPLATE_JS = `
{exports}createRpcErrorFromTl = function (obj) {
if (obj.errorMessage in _byName) return new _byName[obj.errorMessage]();
let match;
{inner}
if (obj.errorCode in _byCode) return new _byCode[obj.errorCode](obj.errorMessage);
return new RpcError(obj.errorCode, obj.errorMessage);
}
`.trim()
const template = (str: string, params: Record<string, string | number>): string => {
const template = (
str: string,
params: Record<string, string | number>,
): string => {
return str.replace(/{([a-z]+)}/gi, (_, name) => String(params[name] ?? ''))
}
function parseCode(
err: string,
placeholders_?: string[],
): [string, string[], boolean] {
function parseCode(err: string, placeholders_?: string[]): [string, string[]] {
let addPlaceholders = false
if (!placeholders_) {
placeholders_ = []
addPlaceholders = true
} else {
placeholders_ = placeholders_.map(snakeToCamel)
}
const placeholders = placeholders_
let wildcard = false
err = err
.replace(/%[a-z]/g, (ph) => {
if (ph !== '%d') {
throw new Error(`Unsupported placeholder: ${ph}`)
err = err.replace(/%[a-z]/g, (placeholder) => {
if (placeholder !== '%d') {
throw new Error(`Unsupported placeholder: ${placeholder}`)
}
if (addPlaceholders) {
@ -98,15 +85,10 @@ function parseCode(
placeholders.push(`duration${idx === 0 ? '' : idx}`)
}
return 'X'
})
.replace(/_\*$/, () => {
wildcard = true
return ''
return placeholder
})
return [err, placeholders, wildcard]
return [err, placeholders]
}
function placeholderType(_name: string): string {
@ -127,164 +109,55 @@ export function generateCodeForErrors(
errors: TlErrors,
exports = 'exports.',
): [string, string] {
let ts = RPC_ERROR_CLASS_TS
let js = template(RPC_ERROR_CLASS_JS, { exports })
let descriptionsMap = ''
let texts = ''
let argMap = ''
let matchers = ''
let staticsJs = ''
let staticsTs = ''
const baseErrorsClasses: Record<number, string> = {}
for (const it of errors.base) {
const className = errorCodeToClassName(it.name)
baseErrorsClasses[it.code] = className
if (it.description) ts += jsComment(it.description) + '\n'
ts +=
template(BASE_ERROR_TS, {
className,
}) + '\n'
js += template(BASE_ERROR_JS, {
className,
code: it.code,
exports,
})
for (const [name, code] of Object.entries(errors.base)) {
staticsJs += `RpcError.${name} = ${code};\n`
staticsTs += ` static ${name} = ${code};\n`
}
const errorClasses: Record<string, string> = {}
const wildcardClasses: [string, string][] = []
const withPlaceholders: [string, string][] = []
for (const error of Object.values(errors.errors)) {
const [name, placeholders] = parseCode(error.name, error._paramNames)
function findBaseClass(it: TlError) {
for (const [prefix, cls] of wildcardClasses) {
if (it.name.startsWith(prefix)) return cls
if (error.description) {
descriptionsMap += ` '${name}': ${JSON.stringify(
error.description,
)},\n`
}
return baseErrorsClasses[it.code] ?? 'RpcError'
}
texts += ` | '${name}'\n`
for (const it of Object.values(errors.errors)) {
if (it._auto) {
// information about the error is incomplete
continue
}
const [name, placeholders, wildcard] = parseCode(
it.name,
it._paramNames,
)
const className = errorCodeToClassName(name)
const baseClass = findBaseClass(it)
if (!it.virtual && !wildcard) {
errorClasses[it.name] = className
}
if (wildcard) {
wildcardClasses.push([it.name.replace('*', ''), className])
}
if (placeholders.length) {
withPlaceholders.push([it.name, className])
}
const placeholderTypes = placeholders.map(placeholderType)
argMap +=
` '${name}': { ` +
placeholders
.map((it, i) => `${it}: ${placeholderTypes[i]}`)
.join(', ') +
' },\n'
js +=
template(ERROR_PRELUDE, {
className,
base: baseClass,
arguments: wildcard ?
'code, description' :
placeholders.join(', '),
}) + '{\n'
let description
let comment = ''
if (it.description) {
let idx = 0
description = JSON.stringify(it.description).replace(
/%[a-z]/g,
() => `" + ${placeholders[idx++]} + "`,
)
if (wildcard) {
description = description.replace(/"$/, ': " + description')
}
idx = 0
comment += it.description.replace(
/%[a-z]/g,
() => `{@link ${placeholders[idx++]}}`,
)
} else {
description = `"Unknown RPC error: [${it.code}:${it.name}]"`
}
if (it.virtual) {
if (comment) comment += '\n\n'
comment +=
'This is a *virtual* error, meaning that it may only occur when using mtcute APIs (not MTProto)'
}
if (wildcard) {
if (comment) comment += '\n\n'
comment +=
'This is an *abstract* error, meaning that only its subclasses may occur when using the API'
}
if (comment) ts += jsComment(comment) + '\n'
ts +=
template(ERROR_PRELUDE, {
ts: 'export ',
className,
base: baseClass,
arguments: placeholders
.map((it) => `${it}: ${placeholderType(it)}`)
.join(', '),
}) + ';'
if (baseClass === 'RpcError') {
js += `super(${it.code}, '${it.name}', ${description});`
} else if (wildcard) {
js += `super(code, ${description});`
} else {
js += `super('${it.name}', ${description});`
}
for (const ph of placeholders) {
js += `\nthis.${ph} = ${ph};`
ts += `\n readonly ${ph}: ${placeholderType(ph)}`
}
js += '\n }\n}\n'
js += `${exports}${className} = ${className};\n`
ts += '\n}\n'
}
ts += 'export function createRpcErrorFromTl (obj: object): RpcError;\n'
// and now we need to implement it
js += 'const _byName = {\n'
for (const [name, cls] of Object.entries(errorClasses)) {
js += `'${name.replace(/%[a-z]/gi, 'X')}': ${cls},\n`
}
js += '};\n'
js += 'const _byCode = {\n'
for (const [code, cls] of Object.entries(baseErrorsClasses)) {
js += `${code}: ${cls},\n`
}
js += '};\n'
// finally, the function itself
let inner = ''
for (const [name, cls] of withPlaceholders) {
const regex = name.replace('%d', '(\\d+)')
inner += `if ((match=obj.errorMessage.match(/^${regex}$/))!=null)return new ${cls}(parseInt(match[1]));\n`
const setters = placeholders.map(
(it, i) => `err.${it} = parseInt(match[${i + 1}])`,
)
const settersStr =
setters.length > 1 ? `{ ${setters.join('; ')} }` : setters[0]
matchers += ` if ((match=obj.errorMessage.match(/^${regex}$/))!=null)${settersStr}\n`
}
}
js += template(TL_BUILDER_TEMPLATE_JS, { inner, exports })
return [ts, js]
return [
template(TEMPLATE_TS, { statics: staticsTs, texts, argMap }),
template(TEMPLATE_JS, {
exports,
statics: staticsJs,
descriptionsMap,
matchers,
}),
]
}

View file

@ -1,6 +1,6 @@
import { TlEntry, TlErrors, TlFullSchema, TlTypeModifiers } from '../types'
import { groupTlEntriesByNamespace, splitNameToNamespace } from '../utils'
import { errorCodeToClassName, generateCodeForErrors } from './errors'
import { generateCodeForErrors } from './errors'
import { camelToPascal, indent, jsComment, snakeToCamel } from './utils'
/**
@ -117,9 +117,7 @@ export function generateTypescriptDefinitionsForTlEntry(
if (errors.throws[entry.name]) {
comment +=
'\n\nThis method *may* throw one of these errors: ' +
errors.throws[entry.name]
.map((it) => `{$see ${errorCodeToClassName(it)}`)
.join(', ')
errors.throws[entry.name].join(', ')
}
}
}
@ -250,15 +248,16 @@ export function generateTypescriptDefinitionsForTlSchema(
)
if (errors) {
ts += '\n namespace errors {\n'
js += 'ns.errors = {};\n(function(ns){\n'
// ts += '\n namespace errors {\n'
// js += 'ns.errors = {};\n(function(ns){\n'
const [_ts, _js] = generateCodeForErrors(errors, 'ns.')
ts += indent(8, _ts)
// ts += indent(8, _ts)
ts += _ts
js += _js
ts += '}\n'
js += '})(ns.errors);\n'
// ts += '}\n'
// js += '})(ns.errors);\n'
}
const namespaces = groupTlEntriesByNamespace(schema.entries)

View file

@ -237,11 +237,6 @@ export interface TlError {
*/
description?: string
/**
* Whether this is a "virtual" error (only thrown by mtcute itself)
*/
virtual?: true
// internal fields used by generator
/** @hidden */
@ -255,9 +250,9 @@ export interface TlError {
*/
export interface TlErrors {
/**
* Base errors
* Base errors (map of error names to error code, e.g. `BAD_REQUEST: 400`)
*/
base: TlError[]
base: Record<string, number>
/**
* Index of errors by name
@ -275,6 +270,11 @@ export interface TlErrors {
* Index of the methods only usable by user
*/
userOnly: Record<string, 1>
/**
* Index of the methods only usable by bots
*/
botOnly: Record<string, 1>
}
/**

View file

@ -23,7 +23,7 @@
"@mtcute/tl-utils": "workspace:^1.0.0",
"@types/js-yaml": "^4.0.5",
"cheerio": "1.0.0-rc.12",
"csv-parser": "3.0.0",
"csv-parse": "^5.5.0",
"js-yaml": "4.1.0"
},
"typedoc": {

File diff suppressed because one or more lines are too long

View file

@ -1,87 +1,23 @@
import csvParser from 'csv-parser'
import { parse } from 'csv-parse/sync'
import { writeFile } from 'fs/promises'
import { TlError, TlErrors } from '@mtcute/tl-utils'
import { TlErrors } from '@mtcute/tl-utils'
import { ERRORS_JSON_FILE } from './constants'
const ERRORS_PAGE_TG = 'https://corefork.telegram.org/api/errors'
const ERRORS_PAGE_TELETHON =
'https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_generator/data/errors.csv'
const baseErrors: TlError[] = [
{
code: 400,
name: 'BAD_REQUEST',
description:
'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',
},
{
code: 401,
name: 'UNAUTHORIZED',
description:
'There was an unauthorized attempt to use functionality available only to authorized users.',
},
{
code: 403,
name: 'FORBIDDEN',
description:
'Privacy violation. For example, an attempt to write a message ' +
'to someone who has blacklisted the current user.',
},
{
code: 404,
name: 'NOT_FOUND',
description:
'An attempt to invoke a non-existent object, such as a method.',
},
{
code: 420,
name: 'FLOOD',
description:
'The maximum allowed number of attempts to invoke the given method' +
'with the given input parameters has been exceeded. For example, in an' +
'attempt to request a large number of text messages (SMS) for the same' +
'phone number.',
},
{
code: 303,
name: 'SEE_OTHER',
description:
'The request must be repeated, but directed to a different data center',
},
{
code: 406,
name: 'NOT_ACCEPTABLE',
description:
'Similar to 400 BAD_REQUEST, but the app should not display any error messages to user ' +
'in UI as a result of this response. The error message will be delivered via ' +
'updateServiceNotification instead.',
},
{
code: 500,
name: 'INTERNAL',
description:
'An internal server error occurred while a request was being processed; ' +
'for example, there was a disruption while accessing a database or file storage.',
},
]
const virtualErrors: TlError[] = [
{
name: 'RPC_TIMEOUT',
code: 408,
description: 'The set RPC timeout has exceeded',
},
{
name: 'MESSAGE_NOT_FOUND',
code: 404,
description: 'Message was not found',
},
]
virtualErrors.forEach((it) => (it.virtual = true))
'https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_generator/data/errors.csv'
const baseErrors = {
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
FLOOD: 420,
SEE_OTHER: 303,
NOT_ACCEPTABLE: 406,
INTERNAL: 500,
} as const
interface TelegramErrorsSpec {
errors: Record<string, Record<string, string[]>>
@ -147,6 +83,47 @@ async function fetchFromTelegram(errors: TlErrors) {
}
json.user_only.forEach((it: string) => (errors.userOnly[it] = 1))
json.bot_only.forEach((it: string) => (errors.botOnly[it] = 1))
// process _* wildcard errors
// 1. add description to errors that are missing it
// 2. replace all wildcards in errors.throws with all matching errors
// 3. remove all _* such errors from errors.errors
for (const name of Object.keys(errors.errors)) {
if (!name.endsWith('_*')) continue
const base = name.slice(0, -2)
const matchingErrors: string[] = []
for (const inner of Object.keys(errors.errors)) {
if (!inner.startsWith(base) || inner === name) continue
matchingErrors.push(inner)
if (!errors.errors[inner].description) {
errors.errors[inner].description =
errors.errors[name].description
}
}
if (matchingErrors.length === 0) continue
for (const method of Object.keys(errors.throws)) {
const idx = errors.throws[method].indexOf(name)
if (idx === -1) continue
errors.throws[method].splice(idx, 1, ...matchingErrors)
}
delete errors.errors[name]
}
// clean up: remove duplicates in throws
for (const method of Object.keys(errors.throws)) {
errors.throws[method] = [...new Set(errors.throws[method])]
}
}
async function fetchFromTelethon(errors: TlErrors) {
@ -156,22 +133,29 @@ async function fetchFromTelethon(errors: TlErrors) {
throw new Error('No body in response')
}
const parser = csvParser()
const records = parse(await csv.text(), {
columns: true,
skip_empty_lines: true,
}) as {
name: string
codes: string
description: string
}[]
function addError(name: string, codes: string, description: string): void {
if (!codes) return
if (name === 'TIMEOUT') return
for (const { name: name_, codes, description } of records) {
if (!codes) continue
if (name_ === 'TIMEOUT') continue
let name = name_
const code = parseInt(codes)
if (isNaN(code)) {
throw new Error(`Invalid code: ${codes} (name: ${name})`)
}
// telethon uses numbers for parameters instead of printf-like
// telethon uses X for parameters instead of printf-like
// we'll convert it back to printf-like
// so far, only one param is supported
name = name.replace(/_0(_)?/g, '_%d$1')
name = name.replace(/_X(_)?/g, '_%d$1')
if (!(name in errors.errors)) {
errors.errors[name] = {
@ -209,28 +193,6 @@ async function fetchFromTelethon(errors: TlErrors) {
}
}
}
return new Promise<void>((resolve, reject) => {
parser
.on(
'data',
({
name,
codes,
description,
}: {
name: string
codes: string
description: string
}) => addError(name, codes, description),
)
.on('end', resolve)
.on('error', reject)
csv.text()
.then((it) => parser.write(it))
.catch(reject)
})
}
async function main() {
@ -239,6 +201,7 @@ async function main() {
errors: {},
throws: {},
userOnly: {},
botOnly: {},
}
console.log('Fetching errors from Telegram...')
@ -249,16 +212,6 @@ async function main() {
console.log('Fetching errors from Telethon...')
await fetchFromTelethon(errors)
virtualErrors.forEach((err) => {
if (err.name in errors.errors) {
console.log(`Error ${err.name} already exists and is not virtual`)
return
}
errors.errors[err.name] = err
})
console.log('Saving...')
await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors))

View file

@ -41,7 +41,6 @@ async function generateTypings(
mtpSchema,
0,
'mtp',
errors,
)
await writeFile(

View file

@ -322,9 +322,9 @@ importers:
cheerio:
specifier: 1.0.0-rc.12
version: 1.0.0-rc.12
csv-parser:
specifier: 3.0.0
version: 3.0.0
csv-parse:
specifier: ^5.5.0
version: 5.5.0
js-yaml:
specifier: 4.1.0
version: 4.1.0
@ -1834,12 +1834,8 @@ packages:
engines: {node: '>= 6'}
dev: true
/csv-parser@3.0.0:
resolution: {integrity: sha512-s6OYSXAK3IdKqYO33y09jhypG/bSDHPuyCme/IdEHfWpLf/jKcpitVFyOC6UemgGk8v7Q5u2XE0vvwmanxhGlQ==}
engines: {node: '>= 10'}
hasBin: true
dependencies:
minimist: 1.2.6
/csv-parse@5.5.0:
resolution: {integrity: sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==}
dev: true
/dargs@7.0.0: