build: updated to layer 139

didn't add any new layers' features, only bumped layer
This commit is contained in:
teidesu 2022-04-01 22:17:10 +03:00
parent ec736f8590
commit 9493759572
48 changed files with 651 additions and 405 deletions

View file

@ -14,7 +14,7 @@
"dependencies": { "dependencies": {
"@types/long": "^4.0.1", "@types/long": "^4.0.1",
"@types/node": "^15.12.1", "@types/node": "^15.12.1",
"@mtcute/tl": "~134.0", "@mtcute/tl": "~139.0",
"@mtcute/core": "^1.0.0", "@mtcute/core": "^1.0.0",
"@mtcute/file-id": "^1.0.0", "@mtcute/file-id": "^1.0.0",
"eager-async-pool": "^1.0.0", "eager-async-pool": "^1.0.0",

View file

@ -5,7 +5,6 @@ export {
tl, tl,
defaultDcs defaultDcs
} from '@mtcute/core' } from '@mtcute/core'
export * from '@mtcute/tl/errors'
export * from './types' export * from './types'
export * from './client' export * from './client'

View file

@ -1,5 +1,4 @@
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { tl } from '@mtcute/tl'
// @extension // @extension
interface AuthState { interface AuthState {

View file

@ -11,15 +11,7 @@ import {
resolveMaybeDynamic, resolveMaybeDynamic,
normalizePhoneNumber, normalizePhoneNumber,
} from '../../utils/misc-utils' } from '../../utils/misc-utils'
import { import { tl } from '@mtcute/tl'
AuthKeyUnregisteredError,
PasswordHashInvalidError,
PhoneCodeEmptyError,
PhoneCodeExpiredError,
PhoneCodeHashEmptyError,
PhoneCodeInvalidError,
SessionPasswordNeededError,
} from '@mtcute/tl/errors'
/** /**
* Start the client in an interactive and declarative manner, * Start the client in an interactive and declarative manner,
@ -174,7 +166,7 @@ export async function start(
return me return me
} catch (e) { } catch (e) {
if (!(e instanceof AuthKeyUnregisteredError)) throw e if (!(e instanceof tl.errors.AuthKeyUnregisteredError)) throw e
} }
if (!params.phone && !params.botToken) if (!params.phone && !params.botToken)
@ -215,19 +207,19 @@ export async function start(
for (;;) { for (;;) {
const code = await resolveMaybeDynamic(params.code) const code = await resolveMaybeDynamic(params.code)
if (!code) throw new PhoneCodeEmptyError() if (!code) throw new tl.errors.PhoneCodeEmptyError()
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 SessionPasswordNeededError) { if (e instanceof tl.errors.SessionPasswordNeededError) {
has2fa = true has2fa = true
break break
} else if ( } else if (
e instanceof PhoneCodeEmptyError || e instanceof tl.errors.PhoneCodeEmptyError ||
e instanceof PhoneCodeExpiredError || e instanceof tl.errors.PhoneCodeExpiredError ||
e instanceof PhoneCodeHashEmptyError || e instanceof tl.errors.PhoneCodeHashEmptyError ||
e instanceof PhoneCodeInvalidError e instanceof tl.errors.PhoneCodeInvalidError
) { ) {
if (typeof params.code !== 'function') { if (typeof params.code !== 'function') {
throw new MtArgumentError('Provided code was invalid') throw new MtArgumentError('Provided code was invalid')
@ -263,7 +255,7 @@ export async function start(
throw new MtArgumentError('Provided password was invalid') throw new MtArgumentError('Provided password was invalid')
} }
if (e instanceof PasswordHashInvalidError) { if (e instanceof tl.errors.PasswordHashInvalidError) {
if (params.invalidCodeCallback) { if (params.invalidCodeCallback) {
await params.invalidCodeCallback('password') await params.invalidCodeCallback('password')
} else { } else {

View file

@ -2,33 +2,32 @@ import { TelegramClient } from '../../client'
import { InputPeerLike, MtInvalidPeerTypeError } from '../../types' import { InputPeerLike, MtInvalidPeerTypeError } from '../../types'
import { import {
normalizeToInputChannel, normalizeToInputChannel,
normalizeToInputUser, normalizeToInputPeer,
} from '../../utils/peer-utils' } from '../../utils/peer-utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { createDummyUpdate } from '../../utils/updates-utils' import { createDummyUpdate } from '../../utils/updates-utils'
/** /**
* Delete all messages of a user in a supergroup * Delete all messages of a user (or channel) in a supergroup
* *
* @param chatId Chat ID * @param chatId Chat ID
* @param userId User ID * @param participantId User/channel ID
* @internal * @internal
*/ */
export async function deleteUserHistory( export async function deleteUserHistory(
this: TelegramClient, this: TelegramClient,
chatId: InputPeerLike, chatId: InputPeerLike,
userId: InputPeerLike participantId: InputPeerLike
): Promise<void> { ): Promise<void> {
const channel = normalizeToInputChannel(await this.resolvePeer(chatId)) const channel = normalizeToInputChannel(await this.resolvePeer(chatId))
if (!channel) throw new MtInvalidPeerTypeError(chatId, 'channel') if (!channel) throw new MtInvalidPeerTypeError(chatId, 'channel')
const user = normalizeToInputUser(await this.resolvePeer(userId)) const peer = normalizeToInputPeer(await this.resolvePeer(participantId))
if (!user) throw new MtInvalidPeerTypeError(userId, 'user')
const res = await this.call({ const res = await this.call({
_: 'channels.deleteUserHistory', _: 'channels.deleteParticipantHistory',
channel, channel,
userId: user, participant: peer,
}) })
this._handleUpdate( this._handleUpdate(

View file

@ -9,7 +9,6 @@ import {
import { assertTypeIs } from '../../utils/type-assertion' import { assertTypeIs } from '../../utils/type-assertion'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { ChatMember } from '../../types' import { ChatMember } from '../../types'
import { UserNotParticipantError } from '@mtcute/tl/errors'
/** /**
* Get information about a single chat member * Get information about a single chat member
@ -59,7 +58,7 @@ export async function getChatMember(
} }
} }
throw new UserNotParticipantError() throw new tl.errors.UserNotParticipantError()
} else if (isInputPeerChannel(chat)) { } else if (isInputPeerChannel(chat)) {
const res = await this.call({ const res = await this.call({
_: 'channels.getParticipant', _: 'channels.getParticipant',

View file

@ -44,7 +44,7 @@ export async function getFullChat(
const peer = await this.resolvePeer(chatId) const peer = await this.resolvePeer(chatId)
let res: tl.messages.TypeChatFull | tl.TypeUserFull let res: tl.messages.TypeChatFull | tl.users.TypeUserFull
if (isInputPeerChannel(peer)) { if (isInputPeerChannel(peer)) {
res = await this.call({ res = await this.call({
_: 'channels.getFullChannel', _: 'channels.getFullChannel',

View file

@ -1,7 +1,6 @@
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { determinePartSize } from '../../utils/file-utils' import { determinePartSize } from '../../utils/file-utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { FileMigrateError, FilerefUpgradeNeededError } from '@mtcute/tl/errors'
import { import {
MtArgumentError, MtArgumentError,
MtUnsupportedError, MtUnsupportedError,
@ -102,14 +101,14 @@ export async function* downloadAsIterable(
{ connection } { connection }
) )
} catch (e) { } catch (e) {
if (e.constructor === FileMigrateError) { if (e.constructor === tl.errors.FileMigrateXError) {
connection = this._downloadConnections[e.newDc] connection = this._downloadConnections[e.new_dc]
if (!connection) { if (!connection) {
connection = await this.createAdditionalConnection(e.newDc) connection = await this.createAdditionalConnection(e.new_dc)
this._downloadConnections[e.newDc] = connection this._downloadConnections[e.new_dc] = connection
} }
return requestCurrent() return requestCurrent()
} else if (e.constructor === FilerefUpgradeNeededError) { } else if (e.constructor === tl.errors.FilerefUpgradeNeededError) {
// 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

@ -1,7 +1,6 @@
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { InputPeerLike, Message, FormattedString, ReplyMarkup } from '../../types' import { InputPeerLike, Message, FormattedString, ReplyMarkup } from '../../types'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MessageNotFoundError } from '@mtcute/tl/errors'
/** /**
* Copy a message (i.e. send the same message, * Copy a message (i.e. send the same message,
@ -99,7 +98,7 @@ export async function sendCopy(
const msg = await this.getMessages(fromPeer, message) const msg = await this.getMessages(fromPeer, message)
if (!msg) throw new MessageNotFoundError() if (!msg) throw new tl.errors.MessageNotFoundError()
return msg.sendCopy(toChatId, params) return msg.sendCopy(toChatId, params)
} }

View file

@ -12,7 +12,6 @@ import {
} from '../../utils/misc-utils' } from '../../utils/misc-utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { assertIsUpdatesGroup } from '../../utils/updates-utils' import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { MessageNotFoundError } from '@mtcute/tl/errors'
import { randomLong } from '@mtcute/core' import { randomLong } from '@mtcute/core'
/** /**
@ -130,7 +129,7 @@ export async function sendMediaGroup(
const msg = await this.getMessages(peer, replyTo) const msg = await this.getMessages(peer, replyTo)
if (!msg) if (!msg)
throw new MessageNotFoundError() throw new tl.errors.MessageNotFoundError()
} }
const multiMedia: tl.RawInputSingleMedia[] = [] const multiMedia: tl.RawInputSingleMedia[] = []

View file

@ -10,7 +10,6 @@ import {
} from '../../types' } from '../../types'
import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils' import { normalizeDate, normalizeMessageId } from '../../utils/misc-utils'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MessageNotFoundError } from '@mtcute/tl/errors'
import { randomLong } from '@mtcute/core' import { randomLong } from '@mtcute/core'
/** /**
@ -156,7 +155,7 @@ export async function sendMedia(
const msg = await this.getMessages(peer, replyTo) const msg = await this.getMessages(peer, replyTo)
if (!msg) throw new MessageNotFoundError() if (!msg) throw new tl.errors.MessageNotFoundError()
} }
const res = await this.call({ const res = await this.call({

View file

@ -13,7 +13,7 @@ import {
MtTypeAssertionError, MtTypeAssertionError,
MtArgumentError, FormattedString, PeersIndex, MtArgumentError, FormattedString, PeersIndex,
} from '../../types' } from '../../types'
import { getMarkedPeerId, MessageNotFoundError, randomLong } from '@mtcute/core' import { getMarkedPeerId, randomLong } from '@mtcute/core'
import { createDummyUpdate } from '../../utils/updates-utils' import { createDummyUpdate } from '../../utils/updates-utils'
/** /**
@ -131,7 +131,7 @@ export async function sendText(
const msg = await this.getMessages(peer, replyTo) const msg = await this.getMessages(peer, replyTo)
if (!msg) if (!msg)
throw new MessageNotFoundError() throw new tl.errors.MessageNotFoundError()
} }
const res = await this.call({ const res = await this.call({

View file

@ -6,9 +6,10 @@ import {
PeersIndex, PeersIndex,
Poll, Poll,
} from '../../types' } from '../../types'
import { MaybeArray, MessageNotFoundError } from '@mtcute/core' import { MaybeArray } from '@mtcute/core'
import { assertTypeIs } from '../../utils/type-assertion' import { assertTypeIs } from '../../utils/type-assertion'
import { assertIsUpdatesGroup } from '../../utils/updates-utils' import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { tl } from '@mtcute/tl'
/** /**
* Send or retract a vote in a poll. * Send or retract a vote in a poll.
@ -37,7 +38,7 @@ 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 MessageNotFoundError() if (!msg) throw new tl.errors.MessageNotFoundError()
if (!(msg.media instanceof Poll)) if (!(msg.media instanceof Poll))
throw new MtArgumentError( throw new MtArgumentError(

View file

@ -1,5 +1,5 @@
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { InputStickerSetItem, StickerSet } from '../../types' import { InputStickerSetItem, MtTypeAssertionError, StickerSet } from '../../types'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
const MASK_POS = { const MASK_POS = {

View file

@ -35,6 +35,7 @@ export async function getStickerSet(
const res = await this.call({ const res = await this.call({
_: 'messages.getStickerSet', _: 'messages.getStickerSet',
stickerset: input, stickerset: input,
hash: 0
}) })
return new StickerSet(this, res) return new StickerSet(this, res)

View file

@ -9,6 +9,7 @@ const sentCodeMap: Record<
'auth.sentCodeTypeCall': 'call', 'auth.sentCodeTypeCall': 'call',
'auth.sentCodeTypeFlashCall': 'flash_call', 'auth.sentCodeTypeFlashCall': 'flash_call',
'auth.sentCodeTypeSms': 'sms', 'auth.sentCodeTypeSms': 'sms',
'auth.sentCodeTypeMissedCall': 'missed_call',
} }
const nextCodeMap: Record< const nextCodeMap: Record<
@ -18,6 +19,7 @@ const nextCodeMap: Record<
'auth.codeTypeCall': 'call', 'auth.codeTypeCall': 'call',
'auth.codeTypeFlashCall': 'flash_call', 'auth.codeTypeFlashCall': 'flash_call',
'auth.codeTypeSms': 'sms', 'auth.codeTypeSms': 'sms',
'auth.codeTypeMissedCall': 'missed_call',
} }
export namespace SentCode { export namespace SentCode {
@ -28,7 +30,7 @@ export namespace SentCode {
* - `call`: Code is sent via voice call * - `call`: Code is sent via voice call
* - `flash_call`: Code is the last 5 digits of the caller's phone number * - `flash_call`: Code is the last 5 digits of the caller's phone number
*/ */
export type DeliveryType = 'app' | 'sms' | 'call' | 'flash_call' export type DeliveryType = 'app' | 'sms' | 'call' | 'flash_call' | 'missed_call'
/** /**
* Type describing next code delivery type. * Type describing next code delivery type.

View file

@ -6,7 +6,6 @@ import { MtArgumentError } from '../errors'
import { BasicPeerType, getBasicPeerType, getMarkedPeerId } from '@mtcute/core' import { BasicPeerType, getBasicPeerType, getMarkedPeerId } from '@mtcute/core'
import { encodeInlineMessageId } from '../../utils/inline-utils' import { encodeInlineMessageId } from '../../utils/inline-utils'
import { User, PeersIndex } from '../peers' import { User, PeersIndex } from '../peers'
import { MessageNotFoundError } from '@mtcute/core'
/** /**
* An incoming callback query, originated from a callback button * An incoming callback query, originated from a callback button
@ -189,7 +188,7 @@ export class CallbackQuery {
getMarkedPeerId(this.raw.peer), getMarkedPeerId(this.raw.peer),
this.raw.msgId this.raw.msgId
) )
if (!msg) throw new MessageNotFoundError() if (!msg) throw new tl.errors.MessageNotFoundError()
return msg return msg
} }

View file

@ -11,7 +11,6 @@ import { HistoryReadUpdate } from './updates'
import { FormattedString } from './parser' import { FormattedString } from './parser'
import { Message } from './messages' import { Message } from './messages'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TimeoutError } from '@mtcute/tl/errors'
interface QueuedHandler<T> { interface QueuedHandler<T> {
promise: ControllablePromise<T> promise: ControllablePromise<T>
@ -270,7 +269,7 @@ export class Conversation {
let timer: NodeJS.Timeout | undefined = undefined let timer: NodeJS.Timeout | undefined = undefined
if (timeout !== null) { if (timeout !== null) {
timer = setTimeout(() => { timer = setTimeout(() => {
promise.reject(new TimeoutError()) promise.reject(new tl.errors.TimeoutError())
this._queuedNewMessage.removeBy((it) => it.promise === promise) this._queuedNewMessage.removeBy((it) => it.promise === promise)
}, timeout) }, timeout)
} }
@ -412,7 +411,7 @@ export class Conversation {
let timer: NodeJS.Timeout | undefined = undefined let timer: NodeJS.Timeout | undefined = undefined
if (params?.timeout !== null) { if (params?.timeout !== null) {
timer = setTimeout(() => { timer = setTimeout(() => {
promise.reject(new TimeoutError()) promise.reject(new tl.errors.TimeoutError())
delete this._pendingEditMessage[msgId] delete this._pendingEditMessage[msgId]
}, params?.timeout ?? 15000) }, params?.timeout ?? 15000)
} }
@ -458,7 +457,7 @@ export class Conversation {
let timer: NodeJS.Timeout | undefined = undefined let timer: NodeJS.Timeout | undefined = undefined
if (timeout !== null) { if (timeout !== null) {
timer = setTimeout(() => { timer = setTimeout(() => {
promise.reject(new TimeoutError()) promise.reject(new tl.errors.TimeoutError())
delete this._pendingRead[msgId] delete this._pendingRead[msgId]
}, timeout) }, timeout)
} }

View file

@ -4,7 +4,7 @@ import { Chat, PeersIndex } from '../peers'
import { Message } from './message' import { Message } from './message'
import { DraftMessage } from './draft-message' import { DraftMessage } from './draft-message'
import { makeInspectable } from '../utils' import { makeInspectable } from '../utils'
import { getMarkedPeerId, MessageNotFoundError } from '@mtcute/core' import { getMarkedPeerId } from '@mtcute/core'
/** /**
* A dialog. * A dialog.
@ -184,7 +184,7 @@ export class Dialog {
this._peers this._peers
) )
} else { } else {
throw new MessageNotFoundError() throw new tl.errors.MessageNotFoundError()
} }
} }

View file

@ -42,13 +42,19 @@ export class StickerSet {
constructor( constructor(
readonly client: TelegramClient, readonly client: TelegramClient,
raw: tl.RawStickerSet | tl.messages.RawStickerSet raw: tl.TypeStickerSet | tl.messages.TypeStickerSet
) { ) {
if (raw._ === 'messages.stickerSet') { if (raw._ === 'messages.stickerSet') {
this.full = raw this.full = raw
this.brief = raw.set this.brief = raw.set
} else { } else if (raw._ === 'stickerSet') {
this.brief = raw this.brief = raw
} else {
throw new MtTypeAssertionError(
'StickerSet',
'messages.stickerSet | stickerSet',
raw._
)
} }
this.isFull = raw._ === 'messages.stickerSet' this.isFull = raw._ === 'messages.stickerSet'

View file

@ -494,10 +494,19 @@ export class Chat {
/** @internal */ /** @internal */
static _parseFull( static _parseFull(
client: TelegramClient, client: TelegramClient,
full: tl.messages.RawChatFull | tl.RawUserFull full: tl.messages.RawChatFull | tl.users.TypeUserFull,
): Chat { ): Chat {
if (full._ === 'userFull') { if (full._ === 'users.userFull') {
return new Chat(client, full.user, full) const user = full.users.find((it) => it.id === full.fullUser.id)
if (!user || user._ === 'userEmpty') {
throw new MtTypeAssertionError(
'Chat._parseFull',
'user',
user?._ ?? 'undefined'
)
}
return new Chat(client, user, full.fullUser)
} else { } else {
const fullChat = full.fullChat const fullChat = full.fullChat
let chat: tl.TypeChat | undefined = undefined let chat: tl.TypeChat | undefined = undefined

View file

@ -1,6 +1,5 @@
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MtTypeAssertionError } from '../types' import { MtTypeAssertionError } from '../types'
import Long from 'long'
// dummy updates which are used for methods that return messages.affectedHistory. // dummy updates which are used for methods that return messages.affectedHistory.
// that is not an update, but it carries info about pts, and we need to handle it // that is not an update, but it carries info about pts, and we need to handle it

View file

@ -20,7 +20,7 @@
"dependencies": { "dependencies": {
"@types/node": "^15.12.1", "@types/node": "^15.12.1",
"@types/events": "^3.0.0", "@types/events": "^3.0.0",
"@mtcute/tl": "^134.0.0", "@mtcute/tl": "~139.0",
"@mtcute/tl-runtime": "^1.0.0", "@mtcute/tl-runtime": "^1.0.0",
"big-integer": "1.6.48", "big-integer": "1.6.48",
"long": "^4.0.0", "long": "^4.0.0",

View file

@ -24,17 +24,6 @@ import {
defaultTestDc, defaultTestDc,
defaultTestIpv6Dc, defaultTestIpv6Dc,
} from './utils/default-dcs' } from './utils/default-dcs'
import {
AuthKeyUnregisteredError,
FloodTestPhoneWaitError,
FloodWaitError,
InternalError,
NetworkMigrateError,
PhoneMigrateError,
RpcError,
SlowmodeWaitError,
UserMigrateError,
} from '@mtcute/tl/errors'
import { addPublicKey } from './utils/crypto/keys' import { addPublicKey } from './utils/crypto/keys'
import { ITelegramStorage, MemoryStorage } from './storage' import { ITelegramStorage, MemoryStorage } from './storage'
import EventEmitter from 'events' import EventEmitter from 'events'
@ -375,7 +364,7 @@ export class BaseTelegramClient extends EventEmitter {
// so we just use getState so the server knows // so we just use getState so the server knows
// we still do need updates // we still do need updates
this.call({ _: 'updates.getState' }).catch((e) => { this.call({ _: 'updates.getState' }).catch((e) => {
if (!(e instanceof RpcError)) { if (!(e instanceof tl.errors.RpcError)) {
this.primaryConnection.reconnect() this.primaryConnection.reconnect()
} }
}) })
@ -651,7 +640,7 @@ export class BaseTelegramClient extends EventEmitter {
await sleep(delta) await sleep(delta)
delete this._floodWaitedRequests[message._] delete this._floodWaitedRequests[message._]
} else { } else {
throw new FloodWaitError(delta / 1000) throw new tl.errors.FloodWaitXError(delta / 1000)
} }
} }
@ -673,7 +662,7 @@ export class BaseTelegramClient extends EventEmitter {
} catch (e) { } catch (e) {
lastError = e lastError = e
if (e instanceof InternalError) { if (e instanceof tl.errors.InternalError) {
this.log.warn('Telegram is having internal issues: %s', e) this.log.warn('Telegram is having internal issues: %s', e)
if (e.message === 'WORKER_BUSY_TOO_LONG_RETRY') { if (e.message === '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"
@ -683,11 +672,11 @@ export class BaseTelegramClient extends EventEmitter {
} }
if ( if (
e.constructor === FloodWaitError || e.constructor === tl.errors.FloodWaitXError ||
e.constructor === SlowmodeWaitError || e.constructor === tl.errors.SlowmodeWaitXError ||
e.constructor === FloodTestPhoneWaitError e.constructor === tl.errors.FloodTestPhoneWaitXError
) { ) {
if (e.constructor !== SlowmodeWaitError) { if (e.constructor !== tl.errors.SlowmodeWaitXError) {
// 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
@ -696,7 +685,7 @@ export class BaseTelegramClient extends EventEmitter {
// 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.seconds = 1 ;(e as any).seconds = 1
} }
if ( if (
@ -711,16 +700,16 @@ export class BaseTelegramClient extends EventEmitter {
if (connection.params.dc.id === this._primaryDc.id) { if (connection.params.dc.id === this._primaryDc.id) {
if ( if (
e.constructor === PhoneMigrateError || e.constructor === tl.errors.PhoneMigrateXError ||
e.constructor === UserMigrateError || e.constructor === tl.errors.UserMigrateXError ||
e.constructor === NetworkMigrateError e.constructor === tl.errors.NetworkMigrateXError
) { ) {
this.log.info('Migrate error, new dc = %d', e.newDc) this.log.info('Migrate error, new dc = %d', e.new_dc)
await this.changeDc(e.newDc) await this.changeDc(e.new_dc)
continue continue
} }
} else { } else {
if (e.constructor === AuthKeyUnregisteredError) { if (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('exported auth key error, re-exporting..') this.log.warn('exported auth key error, re-exporting..')

View file

@ -5,7 +5,6 @@ export * from './types'
export * from './utils' export * from './utils'
export * from '@mtcute/tl' export * from '@mtcute/tl'
export * from '@mtcute/tl/errors'
export * from '@mtcute/tl-runtime' export * from '@mtcute/tl-runtime'
export { defaultDcs } from './utils/default-dcs' export { defaultDcs } from './utils/default-dcs'

View file

@ -13,11 +13,6 @@ import {
ControllablePromise, ControllablePromise,
createCancellablePromise, createCancellablePromise,
} from '../utils/controllable-promise' } from '../utils/controllable-promise'
import {
createRpcErrorFromTl,
RpcError,
RpcTimeoutError,
} from '@mtcute/tl/errors'
import { gzipDeflate, gzipInflate } from '@mtcute/tl-runtime/src/platform/gzip' import { gzipDeflate, gzipInflate } from '@mtcute/tl-runtime/src/platform/gzip'
import { SortedArray } from '../utils/sorted-array' import { SortedArray } from '../utils/sorted-array'
import { EarlyTimer } from '../utils/early-timer' import { EarlyTimer } from '../utils/early-timer'
@ -96,7 +91,7 @@ type PendingMessage =
// todo // todo
const DESTROY_SESSION_ID = Buffer.from('262151e7', 'hex') const DESTROY_SESSION_ID = Buffer.from('262151e7', 'hex')
function makeNiceStack(error: RpcError, stack: string, method?: string) { function makeNiceStack(error: tl.errors.RpcError, stack: string, method?: string) {
error.stack = `${error.constructor.name} (${error.code} ${error.text}): ${ 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')}`
@ -526,7 +521,7 @@ export class SessionConnection extends PersistentConnection {
if (rpc.cancelled) return if (rpc.cancelled) return
const error = createRpcErrorFromTl(res) const error = tl.errors.createRpcErrorFromTl(res)
if (this.params.niceStacks !== false) { if (this.params.niceStacks !== false) {
makeNiceStack(error, rpc.stack!, rpc.method) makeNiceStack(error, rpc.stack!, rpc.method)
} }
@ -1144,7 +1139,7 @@ export class SessionConnection extends PersistentConnection {
} }
if (onTimeout) { if (onTimeout) {
const error = new RpcTimeoutError() const error = new tl.errors.RpcTimeoutError()
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,9 +1,8 @@
import { describe, it } from 'mocha' import { describe, it } from 'mocha'
import { expect } from 'chai' import { expect } from 'chai'
import { randomBytes } from '../../src' import { randomBytes, sleep } from '../../src'
import { sleep } from '../../src/utils/misc-utils'
import { UserMigrateError } from '@mtcute/tl/errors'
import { createTestTelegramClient } from '../e2e/utils' import { createTestTelegramClient } from '../e2e/utils'
import { tl } from '@mtcute/tl'
require('dotenv-flow').config() require('dotenv-flow').config()
@ -62,7 +61,7 @@ describe('fuzz : session', async function () {
await client.waitUntilUsable() await client.waitUntilUsable()
const conn = await client.createAdditionalConnection(1) const conn = await client.createAdditionalConnection(1)
await conn.sendForResult({ _: 'help.getConfig' }) await conn.sendRpc({ _: 'help.getConfig' })
await sleep(10000) await sleep(10000)
@ -116,7 +115,7 @@ describe('fuzz : session', async function () {
] ]
}, { connection: conn }) }, { connection: conn })
} catch (e) { } catch (e) {
if (e instanceof UserMigrateError) { if (e instanceof tl.errors.UserMigrateXError) {
hadError = true hadError = true
} }
} }

View file

@ -12,7 +12,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@mtcute/tl": "^134.0", "@mtcute/tl": "~139.0",
"@mtcute/core": "^1.0.0", "@mtcute/core": "^1.0.0",
"@mtcute/client": "^1.0.0", "@mtcute/client": "^1.0.0",
"events": "^3.2.0" "events": "^3.2.0"

View file

@ -12,7 +12,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"@mtcute/tl": "^134.0.0", "@mtcute/tl": "~139.0",
"@mtcute/tl-runtime": "^1.0.0", "@mtcute/tl-runtime": "^1.0.0",
"@mtcute/core": "^1.0.0", "@mtcute/core": "^1.0.0",
"long": "^4.0.0" "long": "^4.0.0"

View file

@ -13,7 +13,7 @@
"docs": "npx typedoc" "docs": "npx typedoc"
}, },
"dependencies": { "dependencies": {
"@mtcute/tl": "^134.0.0", "@mtcute/tl": "~139.0",
"htmlparser2": "^6.0.1", "htmlparser2": "^6.0.1",
"long": "^4.0.0" "long": "^4.0.0"
}, },

View file

@ -13,7 +13,7 @@
"docs": "npx typedoc" "docs": "npx typedoc"
}, },
"dependencies": { "dependencies": {
"@mtcute/tl": "^134.0.0", "@mtcute/tl": "~139.0",
"long": "^4.0.0" "long": "^4.0.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -0,0 +1,274 @@
import { camelToPascal, jsComment, snakeToCamel } from './utils'
import { TlError, TlErrors } from '../types'
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 {
constructor(code, text, description) {
super(description);
this.code = code;
this.text = text;
}
}
{exports}RpcError = RpcError;
`.trimStart()
const RPC_ERROR_CLASS_TS = `
export class RpcError extends Error {
readonly code: number;
readonly text: string;
constructor(code: number, text: string, description?: string);
}
`.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, any>): string => {
return str.replace(/{([a-z]+)}/gi, (_, name) => params[name] ?? '')
}
function parseCode(
err: string,
placeholders?: string[]
): [string, string[], boolean] {
let addPlaceholders = false
if (!placeholders) {
placeholders = []
addPlaceholders = true
}
let wildcard = false
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}`)
}
return 'X'
})
.replace(/_\*$/, () => {
wildcard = true
return ''
})
return [err, placeholders, wildcard]
}
function placeholderType(name: string): string {
// if (!name.startsWith('duration')) {
// throw new Error('Invalid placeholder name')
// }
return 'number'
}
export function generateCodeForErrors(
errors: TlErrors,
exports = 'exports.'
): [string, string] {
let ts = RPC_ERROR_CLASS_TS
let js = template(RPC_ERROR_CLASS_JS, { exports })
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,
})
}
const errorClasses: Record<string, string> = {}
const wildcardClasses: [string, string][] = []
const withPlaceholders: [string, string][] = []
function findBaseClass(it: TlError) {
for (const [prefix, cls] of wildcardClasses) {
if (it.name.startsWith(prefix)) return cls
}
return baseErrorsClasses[it.code] ?? 'RpcError'
}
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])
}
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,
() => `{@see ${placeholders[idx++]}}`
)
} else {
description = `"Unknown RPC error: [${it.code}:${it.name}]"`
}
if (it.virtual) {
if (comment) comment += '\n\n'
comment +=
'This is a *virtual* error, meaning that it may only occur when using MTCute APIs (not MTProto)'
}
if (wildcard) {
if (comment) comment += '\n\n'
comment +=
'This is an *abstract* error, meaning that only its subclasses may occur when using the API'
}
if (comment) ts += jsComment(comment) + '\n'
ts +=
template(ERROR_PRELUDE, {
ts: 'export ',
className,
base: baseClass,
arguments: placeholders
.map((it) => `${it}: ${placeholderType(it)}`)
.join(', '),
}) + ';'
if (baseClass === 'RpcError') {
js += `super(${it.code}, '${it.name}', ${description});`
} else if (wildcard) {
js += `super(code, ${description});`
} else {
js += `super('${it.name}', ${description});`
}
for (const ph of placeholders) {
js += `\nthis.${ph} = ${ph};`
ts += `\n readonly ${ph}: ${placeholderType(ph)}`
}
js += '\n }\n}\n'
js += `${exports}${className} = ${className};\n`
ts += '\n}\n'
}
ts += 'export function createRpcErrorFromTl (obj: object): RpcError;\n'
// and now we need to implement it
js += 'const _byName = {\n'
for (const [name, cls] of Object.entries(errorClasses)) {
js += `'${name.replace(/%[a-z]/gi, 'X')}': ${cls},\n`
}
js += '};\n'
js += 'const _byCode = {\n'
for (const [code, cls] of Object.entries(baseErrorsClasses)) {
js += `${code}: ${cls},\n`
}
js += '};\n'
// finally, the function itself
let inner = ''
for (const [name, cls] of withPlaceholders) {
const regex = name.replace('%d', '(\\d+)')
inner += `if ((match=obj.errorMessage.match(/^${regex}$/))!=null)return new ${cls}(parseInt(match[1]));\n`
}
js += template(TL_BUILDER_TEMPLATE_JS, { inner, exports })
return [ts, js]
}

View file

@ -1,6 +1,7 @@
import { TlEntry, TlFullSchema } from '../types' import { TlEntry, TlErrors, TlFullSchema } from '../types'
import { groupTlEntriesByNamespace, splitNameToNamespace } from '../utils' import { groupTlEntriesByNamespace, splitNameToNamespace } from '../utils'
import { camelToPascal, snakeToCamel } from './utils' import { camelToPascal, indent, jsComment, snakeToCamel } from './utils'
import { errorCodeToClassName, generateCodeForErrors } from './errors'
const PRIMITIVE_TO_TS: Record<string, string> = { const PRIMITIVE_TO_TS: Record<string, string> = {
int: 'number', int: 'number',
@ -14,23 +15,7 @@ const PRIMITIVE_TO_TS: Record<string, string> = {
Bool: 'boolean', Bool: 'boolean',
true: 'boolean', true: 'boolean',
null: 'null', null: 'null',
any: 'any' any: 'any',
}
function jsComment(s: string): string {
return (
'/**' +
s
.replace(/(?![^\n]{1,60}$)([^\n]{1,60})\s/g, '$1\n')
.replace(/\n|^/g, '\n * ') +
'\n */'
)
}
function indent(size: number, s: string): string {
let prefix = ''
while (size--) prefix += ' '
return prefix + s.replace(/\n/g, '\n' + prefix)
} }
function fullTypeName( function fullTypeName(
@ -68,7 +53,8 @@ function entryFullTypeName(entry: TlEntry): string {
export function generateTypescriptDefinitionsForTlEntry( export function generateTypescriptDefinitionsForTlEntry(
entry: TlEntry, entry: TlEntry,
baseNamespace = 'tl.' baseNamespace = 'tl.',
errors?: TlErrors
): string { ): string {
let ret = '' let ret = ''
@ -83,6 +69,20 @@ export function generateTypescriptDefinitionsForTlEntry(
entry.type, entry.type,
baseNamespace baseNamespace
)}}` )}}`
if (errors) {
if (errors.userOnly[entry.name]) {
comment += `\n\nThis method is **not** available for bots`
}
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(', ')
}
}
} }
if (comment) ret += jsComment(comment) + '\n' if (comment) ret += jsComment(comment) + '\n'
@ -172,7 +172,8 @@ ns.LAYER = $LAYER$;
export function generateTypescriptDefinitionsForTlSchema( export function generateTypescriptDefinitionsForTlSchema(
schema: TlFullSchema, schema: TlFullSchema,
layer: number, layer: number,
namespace = 'tl' namespace = 'tl',
errors?: TlErrors
): [string, string] { ): [string, string] {
let ts = PRELUDE.replace('$NS$', namespace).replace('$LAYER$', layer + '') let ts = PRELUDE.replace('$NS$', namespace).replace('$LAYER$', layer + '')
let js = PRELUDE_JS.replace('$NS$', namespace).replace( let js = PRELUDE_JS.replace('$NS$', namespace).replace(
@ -180,6 +181,18 @@ export function generateTypescriptDefinitionsForTlSchema(
layer + '' layer + ''
) )
if (errors) {
ts += `\n namespace errors {\n`
js += `ns.errors = {};\n(function(ns){\n`
const [_ts, _js] = generateCodeForErrors(errors, 'ns.')
ts += indent(8, _ts)
js += _js
ts += `}\n`
js += `})(ns.errors);\n`
}
const namespaces = groupTlEntriesByNamespace(schema.entries) const namespaces = groupTlEntriesByNamespace(schema.entries)
for (const ns in namespaces) { for (const ns in namespaces) {

View file

@ -6,3 +6,19 @@ export const snakeToCamel = (s: string): string => {
export const camelToPascal = (s: string): string => export const camelToPascal = (s: string): string =>
s[0].toUpperCase() + s.substr(1) s[0].toUpperCase() + s.substr(1)
export function jsComment(s: string): string {
return (
'/**' +
s
.replace(/(?![^\n]{1,60}$)([^\n]{1,60})\s/g, '$1\n')
.replace(/\n|^/g, '\n * ') +
'\n */'
)
}
export function indent(size: number, s: string): string {
let prefix = ''
while (size--) prefix += ' '
return prefix + s.replace(/\n/g, '\n' + prefix)
}

View file

@ -42,6 +42,24 @@ export interface TlFullSchema {
unions: Record<string, TlUnion> unions: Record<string, TlUnion>
} }
export interface TlError {
code: number
name: string
description?: string
virtual?: true
// internal fields used by generator
_auto?: true
_paramNames?: string[]
}
export interface TlErrors {
base: TlError[]
errors: Record<string, TlError>
throws: Record<string, string[]>
userOnly: Record<string, 1>
}
interface BasicDiff<T, K> { interface BasicDiff<T, K> {
added: T[] added: T[]
removed: T[] removed: T[]

View file

@ -2,7 +2,7 @@
> TL schema and related utils used for MTCute. > TL schema and related utils used for MTCute.
Generated from TL layer **134** (last updated on 20.11.2021). Generated from TL layer **139** (last updated on 23.03.2022).
## About ## About
@ -34,15 +34,31 @@ use-cases for mutable TL objects, so you can use exported
`tl` is exported as a namespace to allow better code insights, and to avoid cluttering global namespace and very long `tl` is exported as a namespace to allow better code insights, and to avoid cluttering global namespace and very long
import statements. import statements.
MTProto schema is available in namespace `mtp`, also exported by this package.
```typescript ```typescript
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
const obj: tl.RawInputPeerChat = { _: 'inputPeerChat', chatId: 42 } const obj: tl.RawInputPeerChat = { _: 'inputPeerChat', chatId: 42 }
console.log(tl.isAnyInputPeer(obj)) // true console.log(tl.isAnyInputPeer(obj)) // true
``` ```
### `@mtcute/tl/raw-schema`
[Documentation](./modules/raw_schema.html) RPC errors are also exposed in this package in `tl.errors` namespace:
```typescript
import { tl } from '@mtcute/tl'
try {
await client.call(...)
} catch (e) {
if (e instanceof tl.errors.ChatInvalidError) {
console.log('invalid chat')
} else throw e
}
```
### `@mtcute/tl/api-schema`
[Documentation](./modules/api_schema.html)
JSON file describing all available TL classes, methods and unions. Can be used to write custom code generators JSON file describing all available TL classes, methods and unions. Can be used to write custom code generators
> This very file is used to generate binary serialization and TypeScript typings for `@mtcute/tl`. > This very file is used to generate binary serialization and TypeScript typings for `@mtcute/tl`.

File diff suppressed because one or more lines are too long

View file

@ -1,29 +1,24 @@
// this file is sourced *before* actual schema.
// should be used for types that are not currently added to the schema,
// but seem to work
// in case of conflict, type from main schema is preferred
// for internal use // for internal use
---types--- ---types---
dummyUpdate pts:int pts_count:int channel_id:int53 = Update; dummyUpdate pts:int pts_count:int channel_id:int53 = Update;
// reactions // reactions
// taken from official docs coz why not lol // taken from OLD official
// ctor ids will be generated by the codegen // no longer work on newer layers because they changed this thing entirely
// *does* work with layer 131 (as of 25.07.21) // kept only as a historical reference
---types--- // ---types---
//
updateMessageReactions peer:Peer msg_id:int reactions:MessageReactions = Update; // updateMessageReactions peer:Peer msg_id:int reactions:MessageReactions = Update;
messageReactions flags:# min:flags.0?true results:Vector<ReactionCount> = MessageReactions; // messageReactions flags:# min:flags.0?true results:Vector<ReactionCount> = MessageReactions;
reactionCount flags:# chosen:flags.0?true reaction:string count:int = ReactionCount; // reactionCount flags:# chosen:flags.0?true reaction:string count:int = ReactionCount;
//
messageReactionsList flags:# count:int reactions:Vector<MessageUserReaction> users:Vector<User> next_offset:flags.0?string = MessageReactionsList; // messageReactionsList flags:# count:int reactions:Vector<MessageUserReaction> users:Vector<User> next_offset:flags.0?string = MessageReactionsList;
messageUserReaction user_id:int reaction:string = MessageUserReaction; // messageUserReaction user_id:int reaction:string = MessageUserReaction;
//
---functions--- // ---functions---
//
messages.sendReaction flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates; // messages.sendReaction flags:# peer:InputPeer msg_id:int reaction:flags.0?string = Updates;
messages.getMessagesReactions peer:InputPeer id:Vector<int> = Updates; // messages.getMessagesReactions peer:InputPeer id:Vector<int> = Updates;
messages.getMessageReactionsList flags:# peer:InputPeer id:int reaction:flags.0?string offset:flags.1?string limit:int = MessageReactionsList; // messages.getMessageReactionsList flags:# peer:InputPeer id:int reaction:flags.0?string offset:flags.1?string limit:int = MessageReactionsList;

View file

@ -47,6 +47,7 @@
"inputPrivacyValueDisallowChatParticipants": ["chats"], "inputPrivacyValueDisallowChatParticipants": ["chats"],
"inputUser": ["user_id"], "inputUser": ["user_id"],
"inputUserFromMessage": ["user_id"], "inputUserFromMessage": ["user_id"],
"keyboardButtonUserProfile": ["user_id"],
"message": ["via_bot_id"], "message": ["via_bot_id"],
"messageActionChannelMigrateFrom": ["chat_id"], "messageActionChannelMigrateFrom": ["chat_id"],
"messageActionChatAddUser": ["users"], "messageActionChatAddUser": ["users"],
@ -120,6 +121,7 @@
"updateUserTyping": ["user_id"], "updateUserTyping": ["user_id"],
"user": ["id"], "user": ["id"],
"userEmpty": ["id"], "userEmpty": ["id"],
"userFull": ["id"],
"webAuthorization": ["bot_id"], "webAuthorization": ["bot_id"],
"_": "Dummy line teehee~" "_": "Dummy line teehee~"
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "@mtcute/tl", "name": "@mtcute/tl",
"version": "134.0.0", "version": "139.0.0",
"description": "TL schema used for MTCute", "description": "TL schema used for MTCute",
"main": "index.js", "main": "index.js",
"author": "Alisa Sireneva <me@tei.su>", "author": "Alisa Sireneva <me@tei.su>",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,8 +4,10 @@ export const DOC_CACHE_FILE = join(__dirname, '.documentation.cache.json')
export const DESCRIPTIONS_YAML_FILE = join(__dirname, '../data/descriptions.yaml') export const DESCRIPTIONS_YAML_FILE = join(__dirname, '../data/descriptions.yaml')
export const API_SCHEMA_JSON_FILE = join(__dirname, '../api-schema.json') export const API_SCHEMA_JSON_FILE = join(__dirname, '../api-schema.json')
export const MTP_SCHEMA_JSON_FILE = join(__dirname, '../mtp-schema.json') export const MTP_SCHEMA_JSON_FILE = join(__dirname, '../mtp-schema.json')
export const ERRORS_JSON_FILE = join(__dirname, '../raw-errors.json')
export const CORE_DOMAIN = 'https://corefork.telegram.org' export const CORE_DOMAIN = 'https://core.telegram.org'
export const COREFORK_DOMAIN = 'https://core.telegram.org'
export const TDESKTOP_SCHEMA = export const TDESKTOP_SCHEMA =
'https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/Telegram/Resources/tl/api.tl' 'https://raw.githubusercontent.com/telegramdesktop/tdesktop/dev/Telegram/Resources/tl/api.tl'

View file

@ -15,10 +15,7 @@ import fetch from 'node-fetch'
import readline from 'readline' import readline from 'readline'
import { writeTlEntryToString } from '@mtcute/tl-utils/src/stringify' import { writeTlEntryToString } from '@mtcute/tl-utils/src/stringify'
import { import {
CORE_DOMAIN, CORE_DOMAIN, API_SCHEMA_JSON_FILE, TDESKTOP_SCHEMA, TDLIB_SCHEMA, COREFORK_DOMAIN
API_SCHEMA_JSON_FILE,
TDESKTOP_SCHEMA,
TDLIB_SCHEMA,
} from './constants' } from './constants'
import { fetchRetry } from './utils' import { fetchRetry } from './utils'
import { import {
@ -69,8 +66,8 @@ async function fetchTdesktopSchema(): Promise<Schema> {
} }
} }
async function fetchCoreSchema(): Promise<Schema> { async function fetchCoreSchema(domain = CORE_DOMAIN, name = 'Core'): Promise<Schema> {
const html = await fetchRetry(`${CORE_DOMAIN}/schema`) const html = await fetchRetry(`${domain}/schema`)
const $ = cheerio.load(html) const $ = cheerio.load(html)
// cheerio doesn't always unescape them // cheerio doesn't always unescape them
const schema = $('.page_scheme code') const schema = $('.page_scheme code')
@ -85,7 +82,7 @@ async function fetchCoreSchema(): Promise<Schema> {
if (!layer) throw new Error('Layer number not available') if (!layer) throw new Error('Layer number not available')
return { return {
name: 'Core', name,
layer: parseInt(layer[1]), layer: parseInt(layer[1]),
content: tlToFullSchema(schema), content: tlToFullSchema(schema),
} }
@ -175,6 +172,7 @@ async function main() {
await fetchTdlibSchema(), await fetchTdlibSchema(),
await fetchTdesktopSchema(), await fetchTdesktopSchema(),
await fetchCoreSchema(), await fetchCoreSchema(),
await fetchCoreSchema(COREFORK_DOMAIN, 'Corefork'),
{ {
name: 'Custom', name: 'Custom',
layer: 0, // handled manually layer: 0, // handled manually

View file

@ -1,26 +1,16 @@
import { join } from 'path' import { TlError, TlErrors } from '@mtcute/tl-utils/src/types'
import fetch from 'node-fetch' import fetch from 'node-fetch'
import csvParser from 'csv-parser' import csvParser from 'csv-parser'
import { camelToPascal, snakeToCamel } from '@mtcute/tl-utils/src/codegen/utils'
import { writeFile } from 'fs/promises' import { writeFile } from 'fs/promises'
import { ERRORS_JSON_FILE } from './constants'
const OUT_TS_FILE = join(__dirname, '../errors.d.ts') const ERRORS_PAGE_TG = 'https://corefork.telegram.org/api/errors'
const OUT_JS_FILE = join(__dirname, '../errors.js') const ERRORS_PAGE_TELETHON =
const TELETHON_ERRORS_CSV =
'https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_generator/data/errors.csv' 'https://raw.githubusercontent.com/LonamiWebs/Telethon/master/telethon_generator/data/errors.csv'
interface ErrorInfo { const baseErrors: TlError[] = [
base?: true
virtual?: true
codes: string
name: string
description: string
}
const baseErrors: ErrorInfo[] = [
{ {
codes: '400', code: 400,
name: 'BAD_REQUEST', name: 'BAD_REQUEST',
description: description:
'The query contains errors. In the event that a request was created using a form ' + 'The query contains errors. In the event that a request was created using a form ' +
@ -28,26 +18,26 @@ const baseErrors: ErrorInfo[] = [
'be corrected before the query is repeated', 'be corrected before the query is repeated',
}, },
{ {
codes: '401', code: 401,
name: 'UNAUTHORIZED', name: 'UNAUTHORIZED',
description: description:
'There was an unauthorized attempt to use functionality available only to authorized users.', 'There was an unauthorized attempt to use functionality available only to authorized users.',
}, },
{ {
codes: '403', code: 403,
name: 'FORBIDDEN', name: 'FORBIDDEN',
description: description:
'Privacy violation. For example, an attempt to write a message ' + 'Privacy violation. For example, an attempt to write a message ' +
'to someone who has blacklisted the current user.', 'to someone who has blacklisted the current user.',
}, },
{ {
codes: '404', code: 404,
name: 'NOT_FOUND', name: 'NOT_FOUND',
description: description:
'An attempt to invoke a non-existent object, such as a method.', 'An attempt to invoke a non-existent object, such as a method.',
}, },
{ {
codes: '420', code: 420,
name: 'FLOOD', name: 'FLOOD',
description: description:
'The maximum allowed number of attempts to invoke the given method' + 'The maximum allowed number of attempts to invoke the given method' +
@ -56,13 +46,13 @@ const baseErrors: ErrorInfo[] = [
'phone number.', 'phone number.',
}, },
{ {
codes: '303', code: 303,
name: 'SEE_OTHER', name: 'SEE_OTHER',
description: description:
'The request must be repeated, but directed to a different data center', 'The request must be repeated, but directed to a different data center',
}, },
{ {
codes: '406', code: 406,
name: 'NOT_ACCEPTABLE', name: 'NOT_ACCEPTABLE',
description: description:
'Similar to 400 BAD_REQUEST, but the app should not display any error messages to user ' + 'Similar to 400 BAD_REQUEST, but the app should not display any error messages to user ' +
@ -70,239 +60,174 @@ const baseErrors: ErrorInfo[] = [
'updateServiceNotification instead.', 'updateServiceNotification instead.',
}, },
{ {
codes: '500', code: 500,
name: 'INTERNAL', name: 'INTERNAL',
description: description:
'An internal server error occurred while a request was being processed; ' + 'An internal server error occurred while a request was being processed; ' +
'for example, there was a disruption while accessing a database or file storage.', 'for example, there was a disruption while accessing a database or file storage.',
}, },
] ]
baseErrors.forEach((it) => (it.base = true))
const virtualErrors: ErrorInfo[] = [ const virtualErrors: TlError[] = [
{ {
name: 'RPC_TIMEOUT', name: 'RPC_TIMEOUT',
codes: '408', code: 408,
description: 'The set RPC timeout has exceeded', description: 'The set RPC timeout has exceeded',
}, },
{ {
name: 'MESSAGE_NOT_FOUND', name: 'MESSAGE_NOT_FOUND',
codes: '404', code: 404,
description: 'Message was not found', description: 'Message was not found',
}, },
] ]
virtualErrors.forEach((it) => (it.virtual = true)) virtualErrors.forEach((it) => (it.virtual = true))
const inheritanceTable: Record<string, string> = { async function fetchFromTelegram(errors: TlErrors) {
400: 'BadRequestError', const page = await fetch(ERRORS_PAGE_TG).then((it) => it.text())
401: 'UnauthorizedError', const jsonUrl = page.match(
403: 'ForbiddenError', /can be found <a href="([^"]+)">here »<\/a>/i
404: 'NotFoundError', )![1]
420: 'FloodError',
303: 'SeeOtherError',
406: 'NotAcceptableError',
500: 'InternalError',
}
const RPC_ERROR_CLASS_JS = ` const json = await fetch(new URL(jsonUrl, ERRORS_PAGE_TG)).then((it) =>
class RpcError extends Error { it.json()
constructor(code, text, description) {
super(description);
this.code = code;
this.text = text;
}
}
exports.RpcError = RpcError;
`.trimStart()
const RPC_ERROR_CLASS_TS = `
export declare class RpcError extends Error {
code: number;
text: string;
constructor (code: number, text: string, description?: string);
}
`.trimStart()
const BASE_ERROR_TEMPLATE_JS = `
class {className} extends RpcError {
constructor(name, description) {
super({code}, name, description);
}
}
exports.{className} = {className}
`
const BASE_ERROR_TEMPLATE_TS = `
export declare class {className} extends RpcError {
constructor (name: string, description: string);
}
`
const TL_BUILDER_TEMPLATE_JS = `
exports.createRpcErrorFromTl = function (obj) {
if (obj.errorMessage in _staticNameErrors) return new _staticNameErrors[obj.errorMessage]();
let match;
{parametrized}
if (obj.errorCode in _baseCodeErrors) return new _baseCodeErrors[obj.errorCode](obj.errorMessage);
return new RpcError(obj.errorCode, obj.errorMessage);
}
`.trim()
const DESCRIPTION_PARAM_RE = /_X_|_X$|^X_/
function jsComment(s: string): string {
return (
'/**' +
s
.replace(/(?![^\n]{1,60}$)([^\n]{1,60})\s/g, '$1\n')
.replace(/\n|^/g, '\n * ') +
'\n */'
) )
// since nobody fucking guarantees that .descriptions
// will have description for each described here (or vice versa),
// we will process them independently
for (const code of Object.keys(json.errors)) {
for (const name of Object.keys(json.errors[code])) {
const thrownBy = json.errors[code][name]
const _code = parseInt(code)
if (isNaN(_code)) {
throw new Error(`Invalid code: ${code}`)
}
if (!(name in errors.errors)) {
errors.errors[name] = {
code: _code,
name,
}
}
for (const method of thrownBy) {
if (!(method in errors.throws)) {
errors.throws[method] = []
}
if (errors.throws[method].indexOf(name) === -1) {
errors.throws[method].push(name)
}
}
}
}
for (const name of Object.keys(json.descriptions)) {
if (!(name in errors.errors)) {
errors.errors[name] = {
_auto: true,
code: 400,
name,
}
}
errors.errors[name].description = json.descriptions[name]
}
json.user_only.forEach((it: string) => (errors.userOnly[it] = 1))
} }
async function fetchTelethonErrors(): Promise<ErrorInfo[]> { async function fetchFromTelethon(errors: TlErrors) {
const stream = await fetch(TELETHON_ERRORS_CSV).then((i) => i.body!) const csv = await fetch(ERRORS_PAGE_TELETHON)
const parser = csvParser()
return new Promise((resolve, reject) => { function addError(name: string, codes: string, description: string): void {
const ret: ErrorInfo[] = [] if (!codes) return
if (name === 'TIMEOUT') return
stream const code = parseInt(codes)
.pipe(csvParser()) if (isNaN(code)) {
.on('data', (it) => ret.push(it)) throw new Error(`Invalid code: ${codes} (name: ${name})`)
.on('end', () => resolve(ret)) }
// telethon uses numbers 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')
if (!(name in errors.errors)) {
errors.errors[name] = {
code,
name,
}
}
const obj = errors.errors[name]
if (obj._auto) {
obj.code = code
delete obj._auto
}
// same with descriptions, telethon uses python-like formatting
// strings. we'll convert it to printf-like, while also saving parameter
// names for better code insights
// we also prefer description from telegram, if it's available and doesn't use placeholders
if (description) {
const desc = description.replace(/{([a-z0-9_]+)}/gi, (_, name) => {
if (!obj._paramNames) {
obj._paramNames = []
}
obj._paramNames.push(name)
return '%d'
})
if (!obj.description || obj._paramNames?.length) {
obj.description = desc
}
}
}
return new Promise<void>((resolve, reject) => {
csv.body
.pipe(parser)
.on('data', ({ name, codes, description }) =>
addError(name, codes, description)
)
.on('end', resolve)
.on('error', reject) .on('error', reject)
}) })
} }
interface ExtendedErrorInfo extends ErrorInfo {
className: string
code: string
inherits: string
argument: string | null
}
function getExtendedErrorInfo(err: ErrorInfo): ExtendedErrorInfo {
const [, argument] = err.description.match(/{([a-z_]+)}/i) ?? [, null]
const ret = err as ExtendedErrorInfo
let className = err.name
if (className[0].match(/[0-9]/)) {
className = '_' + className
}
className = className.replace(DESCRIPTION_PARAM_RE, (i) =>
i === '_X_' ? '_' : ''
)
ret.className =
camelToPascal(snakeToCamel(className.toLowerCase())) + 'Error'
ret.code = err.codes.split(' ')[0]
ret.inherits = err.base
? 'RpcError'
: inheritanceTable[ret.code] ?? 'RpcError'
ret.argument = argument ? snakeToCamel(argument) : null
return ret
}
async function main() { async function main() {
const errors: TlErrors = {
base: baseErrors,
errors: {},
throws: {},
userOnly: {},
}
console.log('Fetching errors from Telegram...')
await fetchFromTelegram(errors)
// using some incredible fucking crutches we are also able to parse telethon errors file
// and add missing error descriptions
console.log('Fetching errors from Telethon...') console.log('Fetching errors from Telethon...')
await fetchFromTelethon(errors)
const errors = [ virtualErrors.forEach((err) => {
...baseErrors, if (errors.errors[err.name]) {
...(await fetchTelethonErrors()), console.log(`Error ${err.name} already exists and is not virtual`)
...virtualErrors, return
].map(getExtendedErrorInfo)
console.log('Generating code...')
let js = RPC_ERROR_CLASS_JS
let ts = RPC_ERROR_CLASS_TS
for (const err of errors) {
// generate error class in js
if (err.base) {
js += BASE_ERROR_TEMPLATE_JS.replace(
/{className}/g,
err.className
).replace(/{code}/g, err.code)
} else {
js += `class ${err.className} extends ${err.inherits} {\n`
js += ` constructor(${err.argument ?? ''}) {\n`
const description = JSON.stringify(err.description).replace(
/{([a-z_]+)}/gi,
(_, $1) => `" + ${snakeToCamel($1)} + "`
)
if (err.inherits === 'RpcError') {
js += ` super(${err.code}, '${err.name}', ${description});\n`
} else {
js += ` super('${err.name}', ${description});\n`
}
if (err.argument) {
js += ` this.${err.argument} = ${err.argument};\n`
}
js += ' }\n}\n'
js += `exports.${err.className} = ${err.className};\n`
} }
// generate error class typings errors.errors[err.name] = err
if (err.description) { })
ts += jsComment(err.description) + '\n'
}
if (err.base) {
ts += BASE_ERROR_TEMPLATE_TS.replace(/{className}/g, err.className)
} else {
ts += `export declare class ${err.className} extends ${err.inherits} {\n`
if (err.argument) { console.log('Saving...')
ts += ` ${err.argument}: number;\n`
}
ts += ` constructor (${ await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors))
err.argument ? err.argument + ': number' : ''
});\n`
ts += '}\n'
}
}
ts +=
'export declare function createRpcErrorFromTl (obj: object): RpcError;\n'
js += 'const _staticNameErrors = {\n'
for (const err of errors) {
if (err.virtual || err.argument) continue
js += ` '${err.name}': ${err.className},\n`
}
js += ` 'Timeout': TimeoutError,\n` // because telegram c:
js += '};\n'
js += 'const _baseCodeErrors = {\n'
for (const [code, error] of Object.entries(inheritanceTable)) {
js += ` ${code}: ${error},\n`
}
js += '};\n'
let builderInner = ''
for (const err of errors) {
if (err.virtual || !err.argument) continue
const regex = err.name.replace(DESCRIPTION_PARAM_RE, (s) =>
s.replace('X', '(\\d+)')
)
builderInner +=
`if ((match=obj.errorMessage.match(/${regex}/))!=null)` +
`return new ${err.className}(parseInt(match[1]));\n`
}
js += TL_BUILDER_TEMPLATE_JS.replace('{parametrized}', builderInner)
await writeFile(OUT_JS_FILE, js)
await writeFile(OUT_TS_FILE, ts)
} }
main().catch(console.error) main().catch(console.error)

View file

@ -1,8 +1,13 @@
import { unpackTlSchema } from './schema' import { unpackTlSchema } from './schema'
import { API_SCHEMA_JSON_FILE, MTP_SCHEMA_JSON_FILE, ESM_PRELUDE } from './constants' import {
API_SCHEMA_JSON_FILE,
MTP_SCHEMA_JSON_FILE,
ESM_PRELUDE,
ERRORS_JSON_FILE,
} from './constants'
import { readFile, writeFile } from 'fs/promises' import { readFile, writeFile } from 'fs/promises'
import { parseFullTlSchema } from '@mtcute/tl-utils/src/schema' import { parseFullTlSchema } from '@mtcute/tl-utils/src/schema'
import { TlFullSchema } from '@mtcute/tl-utils/src/types' import { TlErrors, TlFullSchema } from '@mtcute/tl-utils/src/types'
import { join } from 'path' import { join } from 'path'
import { generateTypescriptDefinitionsForTlSchema } from '@mtcute/tl-utils/src/codegen/types' import { generateTypescriptDefinitionsForTlSchema } from '@mtcute/tl-utils/src/codegen/types'
import { generateReaderCodeForTlEntries } from '@mtcute/tl-utils/src/codegen/reader' import { generateReaderCodeForTlEntries } from '@mtcute/tl-utils/src/codegen/reader'
@ -16,17 +21,21 @@ const OUT_WRITERS_FILE = join(__dirname, '../binary/writer.js')
async function generateTypings( async function generateTypings(
apiSchema: TlFullSchema, apiSchema: TlFullSchema,
apiLayer: number, apiLayer: number,
mtpSchema: TlFullSchema mtpSchema: TlFullSchema,
errors: TlErrors
) { ) {
console.log('Generating typings...') console.log('Generating typings...')
const [apiTs, apiJs] = generateTypescriptDefinitionsForTlSchema( const [apiTs, apiJs] = generateTypescriptDefinitionsForTlSchema(
apiSchema, apiSchema,
apiLayer apiLayer,
undefined,
errors
) )
const [mtpTs, mtpJs] = generateTypescriptDefinitionsForTlSchema( const [mtpTs, mtpJs] = generateTypescriptDefinitionsForTlSchema(
mtpSchema, mtpSchema,
0, 0,
'mtp' 'mtp',
errors
) )
await writeFile( await writeFile(
@ -67,6 +76,10 @@ async function generateWriters(
} }
async function main() { async function main() {
const errors: TlErrors = JSON.parse(
await readFile(ERRORS_JSON_FILE, 'utf8')
)
const [apiSchema, apiLayer] = unpackTlSchema( const [apiSchema, apiLayer] = unpackTlSchema(
JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8')) JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8'))
) )
@ -74,7 +87,7 @@ async function main() {
JSON.parse(await readFile(MTP_SCHEMA_JSON_FILE, 'utf8')) JSON.parse(await readFile(MTP_SCHEMA_JSON_FILE, 'utf8'))
) )
await generateTypings(apiSchema, apiLayer, mtpSchema) await generateTypings(apiSchema, apiLayer, mtpSchema, errors)
await generateReaders(apiSchema, mtpSchema) await generateReaders(apiSchema, mtpSchema)
await generateWriters(apiSchema, mtpSchema) await generateWriters(apiSchema, mtpSchema)

View file

@ -2,22 +2,14 @@
// This is a test for TypeScript typings // This is a test for TypeScript typings
// This file is never executed, only compiled // This file is never executed, only compiled
import * as rawSchema from '../raw-schema'
import readerMap from '../binary/reader' import readerMap from '../binary/reader'
import writerMap from '../binary/writer' import writerMap from '../binary/writer'
import { BadRequestError, RpcError } from '../errors' import { tl } from '../'
const layer: string = rawSchema.apiLayer readerMap[0].call(null, null)
Object.entries(rawSchema.api).forEach(([ns, content]) => { writerMap['mt_message'].call(null, null, {})
content.classes.forEach((cls) => {
const name: string = cls.type
})
})
readerMap[0].call(null) const error: tl.errors.RpcError = new tl.errors.BadRequestError(
writerMap['mt_message'].call(null, {})
const error: RpcError = new BadRequestError(
'BAD_REQUEST', 'BAD_REQUEST',
'Client has issued an invalid request' 'Client has issued an invalid request'
) )

View file

@ -1121,7 +1121,7 @@
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
"@types/integer@*": "@types/integer@*":
version "4.0.0" version "5.4.1
resolved "https://registry.yarnpkg.com/@types/integer/-/integer-4.0.0.tgz#3b778715df72d2cf8ba73bad27bd9d830907f944" resolved "https://registry.yarnpkg.com/@types/integer/-/integer-4.0.0.tgz#3b778715df72d2cf8ba73bad27bd9d830907f944"
integrity sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw== integrity sha512-2U1i6bIRiqizl6O+ETkp2HhUZIxg7g+burUabh9tzGd0qcszfNaFRaY9bGNlQKgEU7DCsH5qMajRDW5QamWQbw==