refactor: reworked errors codegen
This commit is contained in:
parent
91894e87cc
commit
4b7d7d2e35
27 changed files with 374 additions and 469 deletions
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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!')
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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[] = []
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
getMarkedPeerId(this.raw.peer),
|
||||
this.raw.msgId,
|
||||
)
|
||||
if (!msg) throw new tl.errors.MessageNotFoundError()
|
||||
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',
|
||||
)
|
||||
}
|
||||
|
||||
return msg
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
this._lastMessage = this._lastReceivedMessage = 0
|
||||
} else throw e
|
||||
if (lastMessage) {
|
||||
this._lastMessage = this._lastReceivedMessage = lastMessage.id
|
||||
} else {
|
||||
this._lastMessage = this._lastReceivedMessage = 0
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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` : ''}`)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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..',
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,112 +1,94 @@
|
|||
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
|
||||
}
|
||||
|
||||
return str
|
||||
const TEMPLATE_JS = `
|
||||
const _descriptionsMap = {
|
||||
{descriptionsMap}
|
||||
}
|
||||
|
||||
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, (placeholder) => {
|
||||
if (placeholder !== '%d') {
|
||||
throw new Error(`Unsupported placeholder: ${placeholder}`)
|
||||
}
|
||||
|
||||
err = err
|
||||
.replace(/%[a-z]/g, (ph) => {
|
||||
if (ph !== '%d') {
|
||||
throw new Error(`Unsupported placeholder: ${ph}`)
|
||||
}
|
||||
if (addPlaceholders) {
|
||||
const idx = placeholders.length
|
||||
placeholders.push(`duration${idx === 0 ? '' : idx}`)
|
||||
}
|
||||
|
||||
if (addPlaceholders) {
|
||||
const idx = placeholders.length
|
||||
placeholders.push(`duration${idx === 0 ? '' : idx}`)
|
||||
}
|
||||
return placeholder
|
||||
})
|
||||
|
||||
return 'X'
|
||||
})
|
||||
.replace(/_\*$/, () => {
|
||||
wildcard = true
|
||||
|
||||
return ''
|
||||
})
|
||||
|
||||
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++]} + "`,
|
||||
const regex = name.replace('%d', '(\\d+)')
|
||||
const setters = placeholders.map(
|
||||
(it, i) => `err.${it} = parseInt(match[${i + 1}])`,
|
||||
)
|
||||
|
||||
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}]"`
|
||||
const settersStr =
|
||||
setters.length > 1 ? `{ ${setters.join('; ')} }` : setters[0]
|
||||
matchers += ` if ((match=obj.errorMessage.match(/^${regex}$/))!=null)${settersStr}\n`
|
||||
}
|
||||
|
||||
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`
|
||||
}
|
||||
|
||||
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,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
@ -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))
|
||||
|
|
|
@ -41,7 +41,6 @@ async function generateTypings(
|
|||
mtpSchema,
|
||||
0,
|
||||
'mtp',
|
||||
errors,
|
||||
)
|
||||
|
||||
await writeFile(
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue