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": {
"@types/long": "^4.0.1",
"@types/node": "^15.12.1",
"@mtcute/tl": "~134.0",
"@mtcute/tl": "~139.0",
"@mtcute/core": "^1.0.0",
"@mtcute/file-id": "^1.0.0",
"eager-async-pool": "^1.0.0",

View file

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

View file

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

View file

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

View file

@ -2,33 +2,32 @@ import { TelegramClient } from '../../client'
import { InputPeerLike, MtInvalidPeerTypeError } from '../../types'
import {
normalizeToInputChannel,
normalizeToInputUser,
normalizeToInputPeer,
} from '../../utils/peer-utils'
import { tl } from '@mtcute/tl'
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 userId User ID
* @param participantId User/channel ID
* @internal
*/
export async function deleteUserHistory(
this: TelegramClient,
chatId: InputPeerLike,
userId: InputPeerLike
participantId: InputPeerLike
): Promise<void> {
const channel = normalizeToInputChannel(await this.resolvePeer(chatId))
if (!channel) throw new MtInvalidPeerTypeError(chatId, 'channel')
const user = normalizeToInputUser(await this.resolvePeer(userId))
if (!user) throw new MtInvalidPeerTypeError(userId, 'user')
const peer = normalizeToInputPeer(await this.resolvePeer(participantId))
const res = await this.call({
_: 'channels.deleteUserHistory',
_: 'channels.deleteParticipantHistory',
channel,
userId: user,
participant: peer,
})
this._handleUpdate(

View file

@ -9,7 +9,6 @@ import {
import { assertTypeIs } from '../../utils/type-assertion'
import { tl } from '@mtcute/tl'
import { ChatMember } from '../../types'
import { UserNotParticipantError } from '@mtcute/tl/errors'
/**
* 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)) {
const res = await this.call({
_: 'channels.getParticipant',

View file

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

View file

@ -1,7 +1,6 @@
import { TelegramClient } from '../../client'
import { determinePartSize } from '../../utils/file-utils'
import { tl } from '@mtcute/tl'
import { FileMigrateError, FilerefUpgradeNeededError } from '@mtcute/tl/errors'
import {
MtArgumentError,
MtUnsupportedError,
@ -102,14 +101,14 @@ export async function* downloadAsIterable(
{ connection }
)
} catch (e) {
if (e.constructor === FileMigrateError) {
connection = this._downloadConnections[e.newDc]
if (e.constructor === tl.errors.FileMigrateXError) {
connection = this._downloadConnections[e.new_dc]
if (!connection) {
connection = await this.createAdditionalConnection(e.newDc)
this._downloadConnections[e.newDc] = connection
connection = await this.createAdditionalConnection(e.new_dc)
this._downloadConnections[e.new_dc] = connection
}
return requestCurrent()
} else if (e.constructor === FilerefUpgradeNeededError) {
} else if (e.constructor === tl.errors.FilerefUpgradeNeededError) {
// todo: implement someday
// see: https://github.com/LonamiWebs/Telethon/blob/0e8bd8248cc649637b7c392616887c50986427a0/telethon/client/downloads.py#L99
throw new MtUnsupportedError('File ref expired!')

View file

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

View file

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

View file

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

View file

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

View file

@ -6,9 +6,10 @@ import {
PeersIndex,
Poll,
} from '../../types'
import { MaybeArray, MessageNotFoundError } from '@mtcute/core'
import { MaybeArray } from '@mtcute/core'
import { assertTypeIs } from '../../utils/type-assertion'
import { assertIsUpdatesGroup } from '../../utils/updates-utils'
import { tl } from '@mtcute/tl'
/**
* Send or retract a vote in a poll.
@ -37,7 +38,7 @@ export async function sendVote(
if (options.some((it) => typeof it === 'number')) {
const msg = await this.getMessages(peer, message)
if (!msg) throw new MessageNotFoundError()
if (!msg) throw new tl.errors.MessageNotFoundError()
if (!(msg.media instanceof Poll))
throw new MtArgumentError(

View file

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

View file

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

View file

@ -9,6 +9,7 @@ const sentCodeMap: Record<
'auth.sentCodeTypeCall': 'call',
'auth.sentCodeTypeFlashCall': 'flash_call',
'auth.sentCodeTypeSms': 'sms',
'auth.sentCodeTypeMissedCall': 'missed_call',
}
const nextCodeMap: Record<
@ -18,6 +19,7 @@ const nextCodeMap: Record<
'auth.codeTypeCall': 'call',
'auth.codeTypeFlashCall': 'flash_call',
'auth.codeTypeSms': 'sms',
'auth.codeTypeMissedCall': 'missed_call',
}
export namespace SentCode {
@ -28,7 +30,7 @@ export namespace SentCode {
* - `call`: Code is sent via voice call
* - `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.

View file

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

View file

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

View file

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

View file

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

View file

@ -494,10 +494,19 @@ export class Chat {
/** @internal */
static _parseFull(
client: TelegramClient,
full: tl.messages.RawChatFull | tl.RawUserFull
full: tl.messages.RawChatFull | tl.users.TypeUserFull,
): Chat {
if (full._ === 'userFull') {
return new Chat(client, full.user, full)
if (full._ === 'users.userFull') {
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 {
const fullChat = full.fullChat
let chat: tl.TypeChat | undefined = undefined

View file

@ -1,6 +1,5 @@
import { tl } from '@mtcute/tl'
import { MtTypeAssertionError } from '../types'
import Long from 'long'
// 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

View file

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

View file

@ -24,17 +24,6 @@ import {
defaultTestDc,
defaultTestIpv6Dc,
} 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 { ITelegramStorage, MemoryStorage } from './storage'
import EventEmitter from 'events'
@ -375,7 +364,7 @@ export class BaseTelegramClient extends EventEmitter {
// so we just use getState so the server knows
// we still do need updates
this.call({ _: 'updates.getState' }).catch((e) => {
if (!(e instanceof RpcError)) {
if (!(e instanceof tl.errors.RpcError)) {
this.primaryConnection.reconnect()
}
})
@ -651,7 +640,7 @@ export class BaseTelegramClient extends EventEmitter {
await sleep(delta)
delete this._floodWaitedRequests[message._]
} else {
throw new FloodWaitError(delta / 1000)
throw new tl.errors.FloodWaitXError(delta / 1000)
}
}
@ -673,7 +662,7 @@ export class BaseTelegramClient extends EventEmitter {
} catch (e) {
lastError = e
if (e instanceof InternalError) {
if (e instanceof tl.errors.InternalError) {
this.log.warn('Telegram is having internal issues: %s', e)
if (e.message === 'WORKER_BUSY_TOO_LONG_RETRY') {
// according to tdlib, "it is dangerous to resend query without timeout, so use 1"
@ -683,11 +672,11 @@ export class BaseTelegramClient extends EventEmitter {
}
if (
e.constructor === FloodWaitError ||
e.constructor === SlowmodeWaitError ||
e.constructor === FloodTestPhoneWaitError
e.constructor === tl.errors.FloodWaitXError ||
e.constructor === tl.errors.SlowmodeWaitXError ||
e.constructor === tl.errors.FloodTestPhoneWaitXError
) {
if (e.constructor !== SlowmodeWaitError) {
if (e.constructor !== tl.errors.SlowmodeWaitXError) {
// SLOW_MODE_WAIT is chat-specific, not request-specific
this._floodWaitedRequests[message._] =
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
// such a short amount will cause retries very fast leading to issues
if (e.seconds === 0) {
e.seconds = 1
;(e as any).seconds = 1
}
if (
@ -711,16 +700,16 @@ export class BaseTelegramClient extends EventEmitter {
if (connection.params.dc.id === this._primaryDc.id) {
if (
e.constructor === PhoneMigrateError ||
e.constructor === UserMigrateError ||
e.constructor === NetworkMigrateError
e.constructor === tl.errors.PhoneMigrateXError ||
e.constructor === tl.errors.UserMigrateXError ||
e.constructor === tl.errors.NetworkMigrateXError
) {
this.log.info('Migrate error, new dc = %d', e.newDc)
await this.changeDc(e.newDc)
this.log.info('Migrate error, new dc = %d', e.new_dc)
await this.changeDc(e.new_dc)
continue
}
} else {
if (e.constructor === AuthKeyUnregisteredError) {
if (e.constructor === tl.errors.AuthKeyUnregisteredError) {
// we can try re-exporting auth from the primary connection
this.log.warn('exported auth key error, re-exporting..')

View file

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

View file

@ -13,11 +13,6 @@ import {
ControllablePromise,
createCancellablePromise,
} from '../utils/controllable-promise'
import {
createRpcErrorFromTl,
RpcError,
RpcTimeoutError,
} from '@mtcute/tl/errors'
import { gzipDeflate, gzipInflate } from '@mtcute/tl-runtime/src/platform/gzip'
import { SortedArray } from '../utils/sorted-array'
import { EarlyTimer } from '../utils/early-timer'
@ -96,7 +91,7 @@ type PendingMessage =
// todo
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.message
}\n at ${method}\n${stack.split('\n').slice(2).join('\n')}`
@ -526,7 +521,7 @@ export class SessionConnection extends PersistentConnection {
if (rpc.cancelled) return
const error = createRpcErrorFromTl(res)
const error = tl.errors.createRpcErrorFromTl(res)
if (this.params.niceStacks !== false) {
makeNiceStack(error, rpc.stack!, rpc.method)
}
@ -1144,7 +1139,7 @@ export class SessionConnection extends PersistentConnection {
}
if (onTimeout) {
const error = new RpcTimeoutError()
const error = new tl.errors.RpcTimeoutError()
if (this.params.niceStacks !== false) {
makeNiceStack(error, rpc.stack!, rpc.method)
}

View file

@ -1,9 +1,8 @@
import { describe, it } from 'mocha'
import { expect } from 'chai'
import { randomBytes } from '../../src'
import { sleep } from '../../src/utils/misc-utils'
import { UserMigrateError } from '@mtcute/tl/errors'
import { randomBytes, sleep } from '../../src'
import { createTestTelegramClient } from '../e2e/utils'
import { tl } from '@mtcute/tl'
require('dotenv-flow').config()
@ -62,7 +61,7 @@ describe('fuzz : session', async function () {
await client.waitUntilUsable()
const conn = await client.createAdditionalConnection(1)
await conn.sendForResult({ _: 'help.getConfig' })
await conn.sendRpc({ _: 'help.getConfig' })
await sleep(10000)
@ -116,7 +115,7 @@ describe('fuzz : session', async function () {
]
}, { connection: conn })
} catch (e) {
if (e instanceof UserMigrateError) {
if (e instanceof tl.errors.UserMigrateXError) {
hadError = true
}
}

View file

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

View file

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

View file

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

View file

@ -13,7 +13,7 @@
"docs": "npx typedoc"
},
"dependencies": {
"@mtcute/tl": "^134.0.0",
"@mtcute/tl": "~139.0",
"long": "^4.0.0"
},
"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 { camelToPascal, snakeToCamel } from './utils'
import { camelToPascal, indent, jsComment, snakeToCamel } from './utils'
import { errorCodeToClassName, generateCodeForErrors } from './errors'
const PRIMITIVE_TO_TS: Record<string, string> = {
int: 'number',
@ -14,23 +15,7 @@ const PRIMITIVE_TO_TS: Record<string, string> = {
Bool: 'boolean',
true: 'boolean',
null: 'null',
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)
any: 'any',
}
function fullTypeName(
@ -68,7 +53,8 @@ function entryFullTypeName(entry: TlEntry): string {
export function generateTypescriptDefinitionsForTlEntry(
entry: TlEntry,
baseNamespace = 'tl.'
baseNamespace = 'tl.',
errors?: TlErrors
): string {
let ret = ''
@ -83,6 +69,20 @@ export function generateTypescriptDefinitionsForTlEntry(
entry.type,
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'
@ -172,7 +172,8 @@ ns.LAYER = $LAYER$;
export function generateTypescriptDefinitionsForTlSchema(
schema: TlFullSchema,
layer: number,
namespace = 'tl'
namespace = 'tl',
errors?: TlErrors
): [string, string] {
let ts = PRELUDE.replace('$NS$', namespace).replace('$LAYER$', layer + '')
let js = PRELUDE_JS.replace('$NS$', namespace).replace(
@ -180,6 +181,18 @@ export function generateTypescriptDefinitionsForTlSchema(
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)
for (const ns in namespaces) {

View file

@ -6,3 +6,19 @@ export const snakeToCamel = (s: string): string => {
export const camelToPascal = (s: string): string =>
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>
}
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> {
added: T[]
removed: T[]

View file

@ -2,7 +2,7 @@
> 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
@ -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
import statements.
MTProto schema is available in namespace `mtp`, also exported by this package.
```typescript
import { tl } from '@mtcute/tl'
const obj: tl.RawInputPeerChat = { _: 'inputPeerChat', chatId: 42 }
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
> 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
---types---
dummyUpdate pts:int pts_count:int channel_id:int53 = Update;
// reactions
// taken from official docs coz why not lol
// ctor ids will be generated by the codegen
// *does* work with layer 131 (as of 25.07.21)
// taken from OLD official
// no longer work on newer layers because they changed this thing entirely
// kept only as a historical reference
---types---
updateMessageReactions peer:Peer msg_id:int reactions:MessageReactions = Update;
messageReactions flags:# min:flags.0?true results:Vector<ReactionCount> = MessageReactions;
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;
messageUserReaction user_id:int reaction:string = MessageUserReaction;
---functions---
messages.sendReaction flags:# peer:InputPeer msg_id:int reaction:flags.0?string = 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;
// ---types---
//
// updateMessageReactions peer:Peer msg_id:int reactions:MessageReactions = Update;
// messageReactions flags:# min:flags.0?true results:Vector<ReactionCount> = MessageReactions;
// 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;
// messageUserReaction user_id:int reaction:string = MessageUserReaction;
//
// ---functions---
//
// messages.sendReaction flags:# peer:InputPeer msg_id:int reaction:flags.0?string = 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;

View file

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

View file

@ -1,6 +1,6 @@
{
"name": "@mtcute/tl",
"version": "134.0.0",
"version": "139.0.0",
"description": "TL schema used for MTCute",
"main": "index.js",
"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 API_SCHEMA_JSON_FILE = join(__dirname, '../api-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 =
'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 { writeTlEntryToString } from '@mtcute/tl-utils/src/stringify'
import {
CORE_DOMAIN,
API_SCHEMA_JSON_FILE,
TDESKTOP_SCHEMA,
TDLIB_SCHEMA,
CORE_DOMAIN, API_SCHEMA_JSON_FILE, TDESKTOP_SCHEMA, TDLIB_SCHEMA, COREFORK_DOMAIN
} from './constants'
import { fetchRetry } from './utils'
import {
@ -69,8 +66,8 @@ async function fetchTdesktopSchema(): Promise<Schema> {
}
}
async function fetchCoreSchema(): Promise<Schema> {
const html = await fetchRetry(`${CORE_DOMAIN}/schema`)
async function fetchCoreSchema(domain = CORE_DOMAIN, name = 'Core'): Promise<Schema> {
const html = await fetchRetry(`${domain}/schema`)
const $ = cheerio.load(html)
// cheerio doesn't always unescape them
const schema = $('.page_scheme code')
@ -85,7 +82,7 @@ async function fetchCoreSchema(): Promise<Schema> {
if (!layer) throw new Error('Layer number not available')
return {
name: 'Core',
name,
layer: parseInt(layer[1]),
content: tlToFullSchema(schema),
}
@ -175,6 +172,7 @@ async function main() {
await fetchTdlibSchema(),
await fetchTdesktopSchema(),
await fetchCoreSchema(),
await fetchCoreSchema(COREFORK_DOMAIN, 'Corefork'),
{
name: 'Custom',
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 csvParser from 'csv-parser'
import { camelToPascal, snakeToCamel } from '@mtcute/tl-utils/src/codegen/utils'
import { writeFile } from 'fs/promises'
import { ERRORS_JSON_FILE } from './constants'
const OUT_TS_FILE = join(__dirname, '../errors.d.ts')
const OUT_JS_FILE = join(__dirname, '../errors.js')
const TELETHON_ERRORS_CSV =
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'
interface ErrorInfo {
base?: true
virtual?: true
codes: string
name: string
description: string
}
const baseErrors: ErrorInfo[] = [
const baseErrors: TlError[] = [
{
codes: '400',
code: 400,
name: 'BAD_REQUEST',
description:
'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',
},
{
codes: '401',
code: 401,
name: 'UNAUTHORIZED',
description:
'There was an unauthorized attempt to use functionality available only to authorized users.',
},
{
codes: '403',
code: 403,
name: 'FORBIDDEN',
description:
'Privacy violation. For example, an attempt to write a message ' +
'to someone who has blacklisted the current user.',
},
{
codes: '404',
code: 404,
name: 'NOT_FOUND',
description:
'An attempt to invoke a non-existent object, such as a method.',
},
{
codes: '420',
code: 420,
name: 'FLOOD',
description:
'The maximum allowed number of attempts to invoke the given method' +
@ -56,13 +46,13 @@ const baseErrors: ErrorInfo[] = [
'phone number.',
},
{
codes: '303',
code: 303,
name: 'SEE_OTHER',
description:
'The request must be repeated, but directed to a different data center',
},
{
codes: '406',
code: 406,
name: 'NOT_ACCEPTABLE',
description:
'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.',
},
{
codes: '500',
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.',
},
]
baseErrors.forEach((it) => (it.base = true))
const virtualErrors: ErrorInfo[] = [
const virtualErrors: TlError[] = [
{
name: 'RPC_TIMEOUT',
codes: '408',
code: 408,
description: 'The set RPC timeout has exceeded',
},
{
name: 'MESSAGE_NOT_FOUND',
codes: '404',
code: 404,
description: 'Message was not found',
},
]
virtualErrors.forEach((it) => (it.virtual = true))
const inheritanceTable: Record<string, string> = {
400: 'BadRequestError',
401: 'UnauthorizedError',
403: 'ForbiddenError',
404: 'NotFoundError',
420: 'FloodError',
303: 'SeeOtherError',
406: 'NotAcceptableError',
500: 'InternalError',
}
async function fetchFromTelegram(errors: TlErrors) {
const page = await fetch(ERRORS_PAGE_TG).then((it) => it.text())
const jsonUrl = page.match(
/can be found <a href="([^"]+)">here »<\/a>/i
)![1]
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 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 */'
const json = await fetch(new URL(jsonUrl, ERRORS_PAGE_TG)).then((it) =>
it.json()
)
// 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}`)
}
async function fetchTelethonErrors(): Promise<ErrorInfo[]> {
const stream = await fetch(TELETHON_ERRORS_CSV).then((i) => i.body!)
if (!(name in errors.errors)) {
errors.errors[name] = {
code: _code,
name,
}
}
return new Promise((resolve, reject) => {
const ret: ErrorInfo[] = []
for (const method of thrownBy) {
if (!(method in errors.throws)) {
errors.throws[method] = []
}
stream
.pipe(csvParser())
.on('data', (it) => ret.push(it))
.on('end', () => resolve(ret))
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 fetchFromTelethon(errors: TlErrors) {
const csv = await fetch(ERRORS_PAGE_TELETHON)
const parser = csvParser()
function addError(name: string, codes: string, description: string): void {
if (!codes) return
if (name === 'TIMEOUT') return
const code = parseInt(codes)
if (isNaN(code)) {
throw new Error(`Invalid code: ${codes} (name: ${name})`)
}
// 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)
})
}
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() {
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...')
await fetchFromTelethon(errors)
const errors = [
...baseErrors,
...(await fetchTelethonErrors()),
...virtualErrors,
].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`
virtualErrors.forEach((err) => {
if (errors.errors[err.name]) {
console.log(`Error ${err.name} already exists and is not virtual`)
return
}
if (err.argument) {
js += ` this.${err.argument} = ${err.argument};\n`
}
errors.errors[err.name] = err
})
js += ' }\n}\n'
js += `exports.${err.className} = ${err.className};\n`
}
console.log('Saving...')
// generate error class typings
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) {
ts += ` ${err.argument}: number;\n`
}
ts += ` constructor (${
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)
await writeFile(ERRORS_JSON_FILE, JSON.stringify(errors))
}
main().catch(console.error)

View file

@ -1,8 +1,13 @@
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 { 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 { generateTypescriptDefinitionsForTlSchema } from '@mtcute/tl-utils/src/codegen/types'
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(
apiSchema: TlFullSchema,
apiLayer: number,
mtpSchema: TlFullSchema
mtpSchema: TlFullSchema,
errors: TlErrors
) {
console.log('Generating typings...')
const [apiTs, apiJs] = generateTypescriptDefinitionsForTlSchema(
apiSchema,
apiLayer
apiLayer,
undefined,
errors
)
const [mtpTs, mtpJs] = generateTypescriptDefinitionsForTlSchema(
mtpSchema,
0,
'mtp'
'mtp',
errors
)
await writeFile(
@ -67,6 +76,10 @@ async function generateWriters(
}
async function main() {
const errors: TlErrors = JSON.parse(
await readFile(ERRORS_JSON_FILE, 'utf8')
)
const [apiSchema, apiLayer] = unpackTlSchema(
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'))
)
await generateTypings(apiSchema, apiLayer, mtpSchema)
await generateTypings(apiSchema, apiLayer, mtpSchema, errors)
await generateReaders(apiSchema, mtpSchema)
await generateWriters(apiSchema, mtpSchema)

View file

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

View file

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