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

View file

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

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { ChatPreview, MtArgumentError, MtNotFoundError } from '../../types' import { ChatPreview, MtArgumentError, MtPeerNotFoundError } from '../../types'
import { INVITE_LINK_REGEX } from '../../utils/peer-utils' import { INVITE_LINK_REGEX } from '../../utils/peer-utils'
/** /**
@ -25,7 +25,7 @@ export async function getChatPreview(
}) })
if (res._ !== 'chatInvite') { 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) return new ChatPreview(this, res, inviteLink)

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import { getMarkedPeerId } from '@mtcute/core'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -5,6 +6,7 @@ import {
FormattedString, FormattedString,
InputPeerLike, InputPeerLike,
Message, Message,
MtMessageNotFoundError,
ReplyMarkup, ReplyMarkup,
} from '../../types' } from '../../types'
@ -104,7 +106,13 @@ export async function sendCopy(
const msg = await this.getMessages(fromPeer, message) 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) 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 { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -8,6 +8,7 @@ import {
InputPeerLike, InputPeerLike,
Message, Message,
MtArgumentError, MtArgumentError,
MtMessageNotFoundError,
PeersIndex, PeersIndex,
ReplyMarkup, ReplyMarkup,
} from '../../types' } from '../../types'
@ -136,7 +137,13 @@ export async function sendMediaGroup(
const msg = await this.getMessages(peer, replyTo) 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[] = [] 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 { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -9,6 +9,7 @@ import {
InputPeerLike, InputPeerLike,
Message, Message,
MtArgumentError, MtArgumentError,
MtMessageNotFoundError,
ReplyMarkup, ReplyMarkup,
} from '../../types' } from '../../types'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils' import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils'
@ -173,7 +174,13 @@ export async function sendMedia(
const msg = await this.getMessages(peer, replyTo) 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({ const res = await this.call({

View file

@ -8,6 +8,7 @@ import {
InputPeerLike, InputPeerLike,
Message, Message,
MtArgumentError, MtArgumentError,
MtMessageNotFoundError,
MtTypeAssertionError, MtTypeAssertionError,
PeersIndex, PeersIndex,
ReplyMarkup, ReplyMarkup,
@ -145,7 +146,13 @@ export async function sendText(
const msg = await this.getMessages(peer, replyTo) 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({ 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 { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { import {
InputPeerLike, InputPeerLike,
MtArgumentError, MtArgumentError,
MtMessageNotFoundError,
MtTypeAssertionError, MtTypeAssertionError,
PeersIndex, PeersIndex,
Poll, Poll,
@ -40,9 +41,17 @@ export async function sendVote(
if (options.some((it) => typeof it === 'number')) { if (options.some((it) => typeof it === 'number')) {
const msg = await this.getMessages(peer, message) 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 poll = msg.media
options = options.map((opt) => { options = options.map((opt) => {

View file

@ -10,7 +10,7 @@ import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { import {
InputPeerLike, InputPeerLike,
MtNotFoundError, MtPeerNotFoundError,
MtTypeAssertionError, MtTypeAssertionError,
} from '../../types' } from '../../types'
import { normalizeToInputPeer } from '../../utils/peer-utils' 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}`, `Could not find a peer by phone ${peerId}`,
) )
} else { } else {
@ -97,7 +97,7 @@ export async function resolvePeer(
// no access hash, we can't use it // no access hash, we can't use it
// this may happen when bot resolves a username // this may happen when bot resolves a username
// of a user who hasn't started a conversation with it // 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`, `Peer (user) with username ${peerId} was found, but it has no access hash`,
) )
} }
@ -131,7 +131,7 @@ export async function resolvePeer(
if (!found.accessHash) { if (!found.accessHash) {
// shouldn't happen? but just in case // 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`, `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}`, `Could not find a peer by username ${peerId}`,
) )
} }
@ -178,7 +178,7 @@ export async function resolvePeer(
if (found && found._ === 'user') { if (found && found._ === 'user') {
if (!found.accessHash) { if (!found.accessHash) {
// shouldn't happen? but just in case // 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`, `Peer (user) with username ${peerId} was found, but it has no access hash`,
) )
} }
@ -235,7 +235,7 @@ export async function resolvePeer(
) { ) {
if (!found.accessHash) { if (!found.accessHash) {
// shouldn't happen? but just in case // 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`, `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 { TelegramClient } from '../../client'
import { encodeInlineMessageId } from '../../utils/inline-utils' import { encodeInlineMessageId } from '../../utils/inline-utils'
import { MtArgumentError } from '../errors' import { MtArgumentError, MtMessageNotFoundError } from '../errors'
import { Message } from '../messages' import { Message } from '../messages'
import { PeersIndex, User } from '../peers' import { PeersIndex, User } from '../peers'
import { makeInspectable } from '../utils' 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), getMarkedPeerId(this.raw.peer),
this.raw.msgId, this.raw.msgId,
'with button',
) )
if (!msg) throw new tl.errors.MessageNotFoundError() }
return msg return msg
} }

View file

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

View file

@ -12,9 +12,26 @@ export class MtClientError extends Error {}
export class MtArgumentError extends MtClientError {} 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 * 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') 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 { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { MtMessageNotFoundError } from '../errors'
import { Chat, PeersIndex } from '../peers' import { Chat, PeersIndex } from '../peers'
import { makeInspectable } from '../utils' import { makeInspectable } from '../utils'
import { DraftMessage } from './draft-message' import { DraftMessage } from './draft-message'
@ -94,7 +95,9 @@ export class Dialog {
// manual exclusion/inclusion and pins // manual exclusion/inclusion and pins
if (include[chatId]) return true 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 // exclusions based on status
if (folder.excludeRead && !dialog.isUnread) return false if (folder.excludeRead && !dialog.isUnread) return false
@ -196,7 +199,7 @@ export class Dialog {
/** /**
* The latest message sent in this chat * The latest message sent in this chat
*/ */
get lastMessage(): Message { get lastMessage(): Message | null {
if (!this._lastMessage) { if (!this._lastMessage) {
const cid = this.chat.id const cid = this.chat.id
@ -207,7 +210,7 @@ export class Dialog {
this._peers, this._peers,
) )
} else { } 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' export type ConnectionKind = 'main' | 'upload' | 'download' | 'downloadSmall'
const CLIENT_ERRORS = { const CLIENT_ERRORS = {
'303': 1, [tl.RpcError.BAD_REQUEST]: 1,
'400': 1, [tl.RpcError.UNAUTHORIZED]: 1,
'401': 1, [tl.RpcError.FORBIDDEN]: 1,
'403': 1, [tl.RpcError.NOT_FOUND]: 1,
'404': 1, [tl.RpcError.FLOOD]: 1,
'406': 1, [tl.RpcError.SEE_OTHER]: 1,
'420': 1, [tl.RpcError.NOT_ACCEPTABLE]: 1,
} }
/** /**
@ -714,7 +714,12 @@ export class NetworkManager {
await sleep(delta) await sleep(delta)
delete this._floodWaitedRequests[message._] delete this._floodWaitedRequests[message._]
} else { } 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) { } catch (e: any) {
lastError = e as Error 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( this._log.warn(
'Telegram is having internal issues: %d %s, retrying', 'Telegram is having internal issues: %d %s, retrying',
e.code, e.code,
e.message, 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" // according to tdlib, "it is dangerous to resend query without timeout, so use 1"
await sleep(1000) await sleep(1000)
} }
@ -762,11 +769,11 @@ export class NetworkManager {
} }
if ( if (
e.constructor === tl.errors.FloodWaitXError || e.is('FLOOD_WAIT_%d') ||
e.constructor === tl.errors.SlowmodeWaitXError || e.is('SLOWMODE_WAIT_%d') ||
e.constructor === tl.errors.FloodTestPhoneWaitXError 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 // SLOW_MODE_WAIT is chat-specific, not request-specific
this._floodWaitedRequests[message._] = this._floodWaitedRequests[message._] =
Date.now() + e.seconds * 1000 Date.now() + e.seconds * 1000
@ -775,7 +782,7 @@ export class NetworkManager {
// In test servers, FLOOD_WAIT_0 has been observed, and sleeping for // In test servers, FLOOD_WAIT_0 has been observed, and sleeping for
// such a short amount will cause retries very fast leading to issues // such a short amount will cause retries very fast leading to issues
if (e.seconds === 0) { if (e.seconds === 0) {
(e as tl.Mutable<typeof e>).seconds = 1 e.seconds = 1
} }
if (e.seconds <= floodSleepThreshold) { if (e.seconds <= floodSleepThreshold) {
@ -787,21 +794,19 @@ export class NetworkManager {
if (manager === this._primaryDc) { if (manager === this._primaryDc) {
if ( if (
e.constructor === tl.errors.PhoneMigrateXError || e.is('PHONE_MIGRATE_%d') ||
e.constructor === tl.errors.UserMigrateXError || e.is('NETWORK_MIGRATE_%d') ||
e.constructor === tl.errors.NetworkMigrateXError 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! manager = this._primaryDc!
multi = manager[kind] multi = manager[kind]
continue continue
} }
} else if ( } else if (e.is('AUTH_KEY_UNREGISTERED')) {
e.constructor === tl.errors.AuthKeyUnregisteredError
) {
// we can try re-exporting auth from the primary connection // we can try re-exporting auth from the primary connection
this._log.warn( this._log.warn(
'exported auth key error, trying re-exporting..', 'exported auth key error, trying re-exporting..',

View file

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

View file

@ -1,96 +1,83 @@
import { TlError, TlErrors } from '../types' import { TlErrors } from '../types'
import { camelToPascal, jsComment, snakeToCamel } from './utils' import { snakeToCamel } from './utils'
/** const TEMPLATE_JS = `
* Transform TL error name to JS error name const _descriptionsMap = {
* {descriptionsMap}
* @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
} }
return str
}
const RPC_ERROR_CLASS_JS = `
class RpcError extends Error { class RpcError extends Error {
constructor(code, text, description) { constructor(code, name) {
super(description); super(_descriptionsMap[name] || 'Unknown RPC error: [' + code + ':' + name + ']');
this.code = code; 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; {exports}RpcError = RpcError;
`.trimStart() `.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 { export class RpcError extends Error {
{statics}
readonly code: number; readonly code: number;
readonly text: string; readonly text: MtErrorText;
constructor(code: number, text: string, description?: string); 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() `.trimStart()
const BASE_ERROR_JS = ` const template = (
class {className} extends RpcError { str: string,
constructor(name, description) { params: Record<string, string | number>,
super({code}, name, description); ): string => {
}
}
{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 => {
return str.replace(/{([a-z]+)}/gi, (_, name) => String(params[name] ?? '')) return str.replace(/{([a-z]+)}/gi, (_, name) => String(params[name] ?? ''))
} }
function parseCode( function parseCode(err: string, placeholders_?: string[]): [string, string[]] {
err: string,
placeholders_?: string[],
): [string, string[], boolean] {
let addPlaceholders = false let addPlaceholders = false
if (!placeholders_) { if (!placeholders_) {
placeholders_ = [] placeholders_ = []
addPlaceholders = true addPlaceholders = true
} else {
placeholders_ = placeholders_.map(snakeToCamel)
} }
const placeholders = placeholders_ const placeholders = placeholders_
let wildcard = false err = err.replace(/%[a-z]/g, (placeholder) => {
if (placeholder !== '%d') {
err = err throw new Error(`Unsupported placeholder: ${placeholder}`)
.replace(/%[a-z]/g, (ph) => {
if (ph !== '%d') {
throw new Error(`Unsupported placeholder: ${ph}`)
} }
if (addPlaceholders) { if (addPlaceholders) {
@ -98,15 +85,10 @@ function parseCode(
placeholders.push(`duration${idx === 0 ? '' : idx}`) placeholders.push(`duration${idx === 0 ? '' : idx}`)
} }
return 'X' return placeholder
})
.replace(/_\*$/, () => {
wildcard = true
return ''
}) })
return [err, placeholders, wildcard] return [err, placeholders]
} }
function placeholderType(_name: string): string { function placeholderType(_name: string): string {
@ -127,164 +109,55 @@ export function generateCodeForErrors(
errors: TlErrors, errors: TlErrors,
exports = 'exports.', exports = 'exports.',
): [string, string] { ): [string, string] {
let ts = RPC_ERROR_CLASS_TS let descriptionsMap = ''
let js = template(RPC_ERROR_CLASS_JS, { exports }) let texts = ''
let argMap = ''
let matchers = ''
let staticsJs = ''
let staticsTs = ''
const baseErrorsClasses: Record<number, string> = {} for (const [name, code] of Object.entries(errors.base)) {
staticsJs += `RpcError.${name} = ${code};\n`
for (const it of errors.base) { staticsTs += ` static ${name} = ${code};\n`
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,
})
} }
const errorClasses: Record<string, string> = {} for (const error of Object.values(errors.errors)) {
const wildcardClasses: [string, string][] = [] const [name, placeholders] = parseCode(error.name, error._paramNames)
const withPlaceholders: [string, string][] = []
function findBaseClass(it: TlError) { if (error.description) {
for (const [prefix, cls] of wildcardClasses) { descriptionsMap += ` '${name}': ${JSON.stringify(
if (it.name.startsWith(prefix)) return cls 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) { 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+)') 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 [
template(TEMPLATE_TS, { statics: staticsTs, texts, argMap }),
return [ts, js] template(TEMPLATE_JS, {
exports,
statics: staticsJs,
descriptionsMap,
matchers,
}),
]
} }

View file

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

View file

@ -237,11 +237,6 @@ export interface TlError {
*/ */
description?: string description?: string
/**
* Whether this is a "virtual" error (only thrown by mtcute itself)
*/
virtual?: true
// internal fields used by generator // internal fields used by generator
/** @hidden */ /** @hidden */
@ -255,9 +250,9 @@ export interface TlError {
*/ */
export interface TlErrors { 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 * Index of errors by name
@ -275,6 +270,11 @@ export interface TlErrors {
* Index of the methods only usable by user * Index of the methods only usable by user
*/ */
userOnly: Record<string, 1> 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", "@mtcute/tl-utils": "workspace:^1.0.0",
"@types/js-yaml": "^4.0.5", "@types/js-yaml": "^4.0.5",
"cheerio": "1.0.0-rc.12", "cheerio": "1.0.0-rc.12",
"csv-parser": "3.0.0", "csv-parse": "^5.5.0",
"js-yaml": "4.1.0" "js-yaml": "4.1.0"
}, },
"typedoc": { "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 { writeFile } from 'fs/promises'
import { TlError, TlErrors } from '@mtcute/tl-utils' import { TlErrors } from '@mtcute/tl-utils'
import { ERRORS_JSON_FILE } from './constants' import { ERRORS_JSON_FILE } from './constants'
const ERRORS_PAGE_TG = 'https://corefork.telegram.org/api/errors' const ERRORS_PAGE_TG = 'https://corefork.telegram.org/api/errors'
const ERRORS_PAGE_TELETHON = const ERRORS_PAGE_TELETHON =
'https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_generator/data/errors.csv' 'https://raw.githubusercontent.com/LonamiWebs/Telethon/v1/telethon_generator/data/errors.csv'
const baseErrors = {
const baseErrors: TlError[] = [ BAD_REQUEST: 400,
{ UNAUTHORIZED: 401,
code: 400, FORBIDDEN: 403,
name: 'BAD_REQUEST', NOT_FOUND: 404,
description: FLOOD: 420,
'The query contains errors. In the event that a request was created using a form ' + SEE_OTHER: 303,
'and contains user generated data, the user should be notified that the data must ' + NOT_ACCEPTABLE: 406,
'be corrected before the query is repeated', INTERNAL: 500,
}, } as const
{
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))
interface TelegramErrorsSpec { interface TelegramErrorsSpec {
errors: Record<string, Record<string, string[]>> 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.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) { async function fetchFromTelethon(errors: TlErrors) {
@ -156,22 +133,29 @@ async function fetchFromTelethon(errors: TlErrors) {
throw new Error('No body in response') 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 { for (const { name: name_, codes, description } of records) {
if (!codes) return if (!codes) continue
if (name === 'TIMEOUT') return if (name_ === 'TIMEOUT') continue
let name = name_
const code = parseInt(codes) const code = parseInt(codes)
if (isNaN(code)) { if (isNaN(code)) {
throw new Error(`Invalid code: ${codes} (name: ${name})`) 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 // we'll convert it back to printf-like
// so far, only one param is supported name = name.replace(/_X(_)?/g, '_%d$1')
name = name.replace(/_0(_)?/g, '_%d$1')
if (!(name in errors.errors)) { if (!(name in errors.errors)) {
errors.errors[name] = { 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() { async function main() {
@ -239,6 +201,7 @@ async function main() {
errors: {}, errors: {},
throws: {}, throws: {},
userOnly: {}, userOnly: {},
botOnly: {},
} }
console.log('Fetching errors from Telegram...') console.log('Fetching errors from Telegram...')
@ -249,16 +212,6 @@ async function main() {
console.log('Fetching errors from Telethon...') console.log('Fetching errors from Telethon...')
await fetchFromTelethon(errors) 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...') console.log('Saving...')
await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors)) await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors))

View file

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

View file

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