chore: improved eslint config

closes MTQ-54
This commit is contained in:
alina 🌸 2023-09-03 02:37:51 +03:00
parent e7171e32c7
commit 81ce550604
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
97 changed files with 778 additions and 541 deletions

View file

@ -176,7 +176,7 @@ module.exports = {
files: ['**/*.ts', '**/*.tsx'], files: ['**/*.ts', '**/*.tsx'],
env: { browser: true, es6: true, node: true }, env: { browser: true, es6: true, node: true },
extends: [ extends: [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/strict-type-checked',
'plugin:import/typescript', 'plugin:import/typescript',
], ],
globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly' }, globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly' },
@ -213,6 +213,18 @@ module.exports = {
], ],
'@typescript-eslint/no-non-null-assertion': 'off', // todo MTQ-36 '@typescript-eslint/no-non-null-assertion': 'off', // todo MTQ-36
'@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-confusing-void-expression': 'off',
'@typescript-eslint/no-unnecessary-condition': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/restrict-template-expressions': [
'error',
{ allowNever: true },
],
'@typescript-eslint/no-unsafe-enum-comparison': 'off',
'@typescript-eslint/no-invalid-void-type': 'off',
'@typescript-eslint/unbound-method': 'off',
'@typescript-eslint/no-dynamic-delete': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
}, },
settings: { settings: {
'import/resolver': { 'import/resolver': {

View file

@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */ /* eslint-disable @typescript-eslint/no-unsafe-declaration-merging, @typescript-eslint/unified-signatures */
/* THIS FILE WAS AUTO-GENERATED */ /* THIS FILE WAS AUTO-GENERATED */
import { Readable } from 'stream' import { Readable } from 'stream'

View file

@ -14,7 +14,8 @@ export async function logOut(this: TelegramClient): Promise<true> {
this._userId = null this._userId = null
this._isBot = false this._isBot = false
// eslint-disable-next-line @typescript-eslint/no-explicit-any // some implicit magic in favor of performance
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
this._pts = this._seq = this._date = undefined as any this._pts = this._seq = this._date = undefined as any
this._selfUsername = null this._selfUsername = null
this._selfChanged = true this._selfChanged = true

View file

@ -97,7 +97,11 @@ export async function answerInlineQuery(
): Promise<void> { ): Promise<void> {
if (!params) params = {} if (!params) params = {}
const [gallery, tlResults] = await BotInline._convertToTl(this, results, params!.parseMode) const [gallery, tlResults] = await BotInline._convertToTl(
this,
results,
params.parseMode,
)
await this.call({ await this.call({
_: 'messages.setInlineBotResults', _: 'messages.setInlineBotResults',

View file

@ -53,7 +53,7 @@ export async function getInlineGameHighScores(
messageId: string | tl.TypeInputBotInlineMessageID, messageId: string | tl.TypeInputBotInlineMessageID,
userId?: InputPeerLike, userId?: InputPeerLike,
): Promise<GameHighScore[]> { ): Promise<GameHighScore[]> {
const id = await normalizeInlineId(messageId) const id = normalizeInlineId(messageId)
let user: tl.TypeInputUser let user: tl.TypeInputUser

View file

@ -87,7 +87,7 @@ export async function setInlineGameScore(
const user = normalizeToInputUser(await this.resolvePeer(userId), userId) const user = normalizeToInputUser(await this.resolvePeer(userId), userId)
const id = await normalizeInlineId(messageId) const id = normalizeInlineId(messageId)
await this.call( await this.call(
{ {

View file

@ -45,10 +45,7 @@ export async function addChatMembers(
const updates = await this.call({ const updates = await this.call({
_: 'channels.inviteToChannel', _: 'channels.inviteToChannel',
channel: normalizeToInputChannel(chat), channel: normalizeToInputChannel(chat),
users: await this.resolvePeerMany( users: await this.resolvePeerMany(users, normalizeToInputUser),
users as InputPeerLike[],
normalizeToInputUser,
),
}) })
this._handleUpdate(updates) this._handleUpdate(updates)
} else throw new MtInvalidPeerTypeError(chatId, 'chat or channel') } else throw new MtInvalidPeerTypeError(chatId, 'chat or channel')

View file

@ -24,10 +24,7 @@ export async function createGroup(
): Promise<Chat> { ): Promise<Chat> {
if (!Array.isArray(users)) users = [users] if (!Array.isArray(users)) users = [users]
const peers = await this.resolvePeerMany( const peers = await this.resolvePeerMany(users, normalizeToInputUser)
users as InputPeerLike[],
normalizeToInputUser,
)
const res = await this.call({ const res = await this.call({
_: 'messages.createChat', _: 'messages.createChat',

View file

@ -28,7 +28,7 @@ export function _pushConversationMessage(
const chatId = getMarkedPeerId(msg.raw.peerId) const chatId = getMarkedPeerId(msg.raw.peerId)
const msgId = msg.raw.id const msgId = msg.raw.id
this._pendingConversations[chatId]?.forEach((conv) => { this._pendingConversations[chatId].forEach((conv) => {
conv['_lastMessage'] = msgId conv['_lastMessage'] = msgId
if (incoming) conv['_lastReceivedMessage'] = msgId if (incoming) conv['_lastReceivedMessage'] = msgId
}) })

View file

@ -5,11 +5,10 @@ import {
MtUnsupportedError, MtUnsupportedError,
} from '../../types' } from '../../types'
// eslint-disable-next-line @typescript-eslint/no-explicit-any let fs: typeof import('fs') | null = null
let fs: any = null
try { try {
fs = require('fs') fs = require('fs') as typeof import('fs')
} catch (e) {} } catch (e) {}
/** /**
@ -39,7 +38,7 @@ export function downloadToFile(
const buf = params.location.location const buf = params.location.location
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile(filename, buf, (err?: Error) => { fs!.writeFile(filename, buf, (err) => {
if (err) reject(err) if (err) reject(err)
else resolve() else resolve()
}) })

View file

@ -47,19 +47,20 @@ export async function* downloadAsIterable(
const input = params.location const input = params.location
let location: tl.TypeInputFileLocation | tl.TypeInputWebFileLocation let location: tl.TypeInputFileLocation | tl.TypeInputWebFileLocation
if (input instanceof FileLocation) { if (input instanceof FileLocation) {
if (typeof input.location === 'function') { let locationInner = input.location
(input as tl.Mutable<FileLocation>).location = input.location()
if (typeof locationInner === 'function') {
locationInner = locationInner()
} }
if (Buffer.isBuffer(input.location)) { if (Buffer.isBuffer(locationInner)) {
yield input.location yield locationInner
return return
} }
if (!dcId) dcId = input.dcId if (!dcId) dcId = input.dcId
if (!fileSize) fileSize = input.fileSize if (!fileSize) fileSize = input.fileSize
// eslint-disable-next-line @typescript-eslint/no-explicit-any location = locationInner
location = input.location as any
} else if (typeof input === 'string') { } else if (typeof input === 'string') {
const parsed = parseFileId(input) const parsed = parseFileId(input)
@ -129,7 +130,7 @@ export async function* downloadAsIterable(
result = await this.call( result = await this.call(
{ {
_: isWeb ? 'upload.getWebFile' : 'upload.getFile', _: isWeb ? 'upload.getWebFile' : 'upload.getFile',
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
location: location as any, location: location as any,
offset: chunkSize * chunk, offset: chunkSize * chunk,
limit: chunkSize, limit: chunkSize,
@ -182,7 +183,7 @@ export async function* downloadAsIterable(
} }
let error: unknown = undefined let error: unknown = undefined
Promise.all( void Promise.all(
Array.from( Array.from(
{ length: Math.min(poolSize * REQUESTS_PER_CONNECTION, numChunks) }, { length: Math.min(poolSize * REQUESTS_PER_CONNECTION, numChunks) },
downloadChunk, downloadChunk,
@ -202,6 +203,7 @@ export async function* downloadAsIterable(
while (position < limitBytes) { while (position < limitBytes) {
await nextChunkCv.wait() await nextChunkCv.wait()
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (error) throw error if (error) throw error
while (nextChunkIdx in buffer) { while (nextChunkIdx in buffer) {

View file

@ -26,6 +26,7 @@ export function downloadAsStream(
async read() {}, async read() {},
}) })
// eslint-disable-next-line @typescript-eslint/no-misused-promises
setTimeout(async () => { setTimeout(async () => {
try { try {
for await (const chunk of this.downloadAsIterable(params)) { for await (const chunk of this.downloadAsIterable(params)) {

View file

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { fromBuffer as fileTypeFromBuffer } from 'file-type' import { fromBuffer as fileTypeFromBuffer } from 'file-type'
import type { ReadStream } from 'fs' import type { ReadStream } from 'fs'
import { Readable } from 'stream' import { Readable } from 'stream'
@ -15,12 +14,12 @@ import {
readBytesFromStream, readBytesFromStream,
} from '../../utils/stream-utils' } from '../../utils/stream-utils'
let fs: any = null let fs: typeof import('fs') | null = null
let path: any = null let path: typeof import('path') | null = null
try { try {
fs = require('fs') fs = require('fs') as typeof import('fs')
path = require('path') path = require('path') as typeof import('path')
} catch (e) {} } catch (e) {}
const OVERRIDE_MIME: Record<string, string> = { const OVERRIDE_MIME: Record<string, string> = {
@ -132,15 +131,12 @@ export async function uploadFile(
} }
if (fs && file instanceof fs.ReadStream) { if (fs && file instanceof fs.ReadStream) {
fileName = path.basename((file as ReadStream).path.toString()) fileName = path!.basename(file.path.toString())
fileSize = await new Promise((res, rej) => { fileSize = await new Promise((res, rej) => {
fs.stat( fs!.stat((file as ReadStream).path.toString(), (err, stat) => {
(file as ReadStream).path.toString(), if (err) rej(err)
(err?: any, stat?: any) => { res(stat.size)
if (err) rej(err) })
res(stat.size)
},
)
}) })
// fs.ReadStream is a subclass of Readable, no conversion needed // fs.ReadStream is a subclass of Readable, no conversion needed
} }
@ -171,7 +167,7 @@ export async function uploadFile(
if (idx > -1) { if (idx > -1) {
const raw = disposition.slice(idx + 9).split(';')[0] const raw = disposition.slice(idx + 9).split(';')[0]
fileName = JSON.parse(raw) fileName = JSON.parse(raw) as string
} }
} }

View file

@ -91,7 +91,7 @@ export async function uploadMedia(
assertTypeIs('uploadMedia', res, 'messageMediaDocument') assertTypeIs('uploadMedia', res, 'messageMediaDocument')
assertTypeIs('uploadMedia', res.document!, 'document') assertTypeIs('uploadMedia', res.document!, 'document')
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
return parseDocument(this, res.document) as any return parseDocument(this, res.document) as any
case 'inputMediaStory': case 'inputMediaStory':
throw new MtArgumentError("This media (story) can't be uploaded") throw new MtArgumentError("This media (story) can't be uploaded")

View file

@ -76,7 +76,7 @@ export async function editInlineMessage(
let entities: tl.TypeMessageEntity[] | undefined let entities: tl.TypeMessageEntity[] | undefined
let media: tl.TypeInputMedia | undefined = undefined let media: tl.TypeInputMedia | undefined = undefined
const id = await normalizeInlineId(messageId) const id = normalizeInlineId(messageId)
if (params.media) { if (params.media) {
media = await this._normalizeInputMedia(params.media, params, true) media = await this._normalizeInputMedia(params.media, params, true)

View file

@ -114,6 +114,6 @@ export async function editMessage(
media, media,
}) })
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
return this._findMessageInUpdate(res, true) as any return this._findMessageInUpdate(res, true) as any
} }

View file

@ -291,13 +291,18 @@ export async function forwardMessages(
}, },
): Promise<MaybeArray<Message>> { ): Promise<MaybeArray<Message>> {
if (!params) params = {} if (!params) params = {}
const isSingle = !Array.isArray(messages)
if (isSingle) messages = [messages as number] let isSingle = false
if (!Array.isArray(messages)) {
isSingle = true
messages = [messages]
}
// sending more than 100 will not result in a server-sent // sending more than 100 will not result in a server-sent
// error, instead only first 100 IDs will be forwarded, // error, instead only first 100 IDs will be forwarded,
// which is definitely not the best outcome. // which is definitely not the best outcome.
if ((messages as number[]).length > 100) { if (messages.length > 100) {
throw new MtArgumentError( throw new MtArgumentError(
'You can forward no more than 100 messages at once', 'You can forward no more than 100 messages at once',
) )
@ -338,12 +343,10 @@ export async function forwardMessages(
_: 'messages.forwardMessages', _: 'messages.forwardMessages',
toPeer, toPeer,
fromPeer: await this.resolvePeer(fromChatId), fromPeer: await this.resolvePeer(fromChatId),
id: messages as number[], id: messages,
silent: params.silent, silent: params.silent,
scheduleDate: normalizeDate(params.schedule), scheduleDate: normalizeDate(params.schedule),
randomId: [...Array((messages as number[]).length)].map(() => randomId: Array.from({ length: messages.length }, () => randomLong()),
randomLong(),
),
dropAuthor: params.noAuthor, dropAuthor: params.noAuthor,
dropMediaCaptions: params.noCaption, dropMediaCaptions: params.noCaption,
noforwards: params.forbidForwards, noforwards: params.forbidForwards,

View file

@ -56,7 +56,7 @@ export async function addStickerToSet(
maskCoords: sticker.maskPosition ? maskCoords: sticker.maskPosition ?
{ {
_: 'maskCoords', _: 'maskCoords',
n: MASK_POS[sticker.maskPosition.point as keyof typeof MASK_POS], n: MASK_POS[sticker.maskPosition.point],
x: sticker.maskPosition.x, x: sticker.maskPosition.x,
y: sticker.maskPosition.y, y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale, zoom: sticker.maskPosition.scale,

View file

@ -106,7 +106,10 @@ export async function createStickerSet(
) )
} }
const owner = normalizeToInputUser(await this.resolvePeer(params.owner), params.owner) const owner = normalizeToInputUser(
await this.resolvePeer(params.owner),
params.owner,
)
const inputStickers: tl.TypeInputStickerSetItem[] = [] const inputStickers: tl.TypeInputStickerSetItem[] = []
@ -124,7 +127,7 @@ export async function createStickerSet(
maskCoords: sticker.maskPosition ? maskCoords: sticker.maskPosition ?
{ {
_: 'maskCoords', _: 'maskCoords',
n: MASK_POS[sticker.maskPosition.point as keyof typeof MASK_POS], n: MASK_POS[sticker.maskPosition.point],
x: sticker.maskPosition.x, x: sticker.maskPosition.x,
y: sticker.maskPosition.y, y: sticker.maskPosition.y,
zoom: sticker.maskPosition.scale, zoom: sticker.maskPosition.scale,

View file

@ -33,6 +33,6 @@ export async function getCustomEmojis(
) )
} }
return doc as Sticker return doc
}) })
} }

View file

@ -940,7 +940,7 @@ function _fetchChannelDifferenceLater(
fallbackPts?: number, fallbackPts?: number,
force = false, force = false,
): void { ): void {
if (!requestedDiff[channelId]) { if (!(channelId in requestedDiff)) {
requestedDiff[channelId] = _fetchChannelDifference requestedDiff[channelId] = _fetchChannelDifference
.call(this, channelId, fallbackPts, force) .call(this, channelId, fallbackPts, force)
.catch((err) => { .catch((err) => {
@ -1074,7 +1074,7 @@ function _fetchDifferenceLater(
this: TelegramClient, this: TelegramClient,
requestedDiff: Record<number, Promise<void>>, requestedDiff: Record<number, Promise<void>>,
): void { ): void {
if (!requestedDiff[0]) { if (!(0 in requestedDiff)) {
requestedDiff[0] = _fetchDifference requestedDiff[0] = _fetchDifference
.call(this, requestedDiff) .call(this, requestedDiff)
.catch((err) => { .catch((err) => {

View file

@ -8,7 +8,11 @@ import {
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
import { InputPeerLike, MtNotFoundError, MtTypeAssertionError } from '../../types' import {
InputPeerLike,
MtNotFoundError,
MtTypeAssertionError,
} from '../../types'
import { normalizeToInputPeer } from '../../utils/peer-utils' import { normalizeToInputPeer } from '../../utils/peer-utils'
import { assertTypeIs } from '../../utils/type-assertion' import { assertTypeIs } from '../../utils/type-assertion'
@ -109,7 +113,12 @@ export async function resolvePeer(
const found = res.chats.find((it) => it.id === id) const found = res.chats.find((it) => it.id === id)
if (found) { if (found) {
if (!(found._ === 'channel' || found._ === 'channelForbidden')) { if (
!(
found._ === 'channel' ||
found._ === 'channelForbidden'
)
) {
// chats can't have usernames // chats can't have usernames
// furthermore, our id is a channel id, so it must be a channel // furthermore, our id is a channel id, so it must be a channel
// this should never happen, unless Telegram goes crazy // this should never happen, unless Telegram goes crazy
@ -205,7 +214,7 @@ export async function resolvePeer(
// break // break
} }
case 'channel': { case 'channel': {
const id = toggleChannelIdMark(peerId as number) const id = toggleChannelIdMark(peerId)
const res = await this.call({ const res = await this.call({
_: 'channels.getChannels', _: 'channels.getChannels',

View file

@ -251,6 +251,7 @@ export class Conversation {
this.stop() this.stop()
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (err) throw err if (err) throw err
return res! return res!
@ -501,7 +502,7 @@ export class Conversation {
const it = this._queuedNewMessage.peekFront()! const it = this._queuedNewMessage.peekFront()!
// order does matter for new messages // order does matter for new messages
this._lock.acquire().then(async () => { void this._lock.acquire().then(async () => {
try { try {
if (!it.check || (await it.check(msg))) { if (!it.check || (await it.check(msg))) {
if (it.timeout) clearTimeout(it.timeout) if (it.timeout) clearTimeout(it.timeout)

View file

@ -140,7 +140,7 @@ export class Sticker extends RawDocument {
*/ */
get customEmojiFree(): boolean { get customEmojiFree(): boolean {
return this.attr._ === 'documentAttributeCustomEmoji' ? return this.attr._ === 'documentAttributeCustomEmoji' ?
this.attr?.free ?? false : this.attr.free ?? false :
false false
} }

View file

@ -329,7 +329,7 @@ export class Message {
get replyToMessageId(): number | null { get replyToMessageId(): number | null {
if (this.raw.replyTo?._ !== 'messageReplyHeader') return null if (this.raw.replyTo?._ !== 'messageReplyHeader') return null
return this.raw.replyTo?.replyToMsgId ?? null return this.raw.replyTo.replyToMsgId ?? null
} }
/** /**
@ -339,7 +339,7 @@ export class Message {
get replyToThreadId(): number | null { get replyToThreadId(): number | null {
if (this.raw.replyTo?._ !== 'messageReplyHeader') return null if (this.raw.replyTo?._ !== 'messageReplyHeader') return null
return this.raw.replyTo?.replyToTopId ?? null return this.raw.replyTo.replyToTopId ?? null
} }
/** /**

View file

@ -239,7 +239,7 @@ export class StickerSet {
*/ */
getStickersByEmoji(emoji: string): StickerInfo[] { getStickersByEmoji(emoji: string): StickerInfo[] {
return this.stickers.filter( return this.stickers.filter(
(it) => it.alt === emoji || it.emoji.indexOf(emoji) !== -1, (it) => it.alt === emoji || it.emoji.includes(emoji),
) )
} }
@ -271,8 +271,8 @@ export class StickerSet {
private _getInputDocument(idx: number): tl.TypeInputDocument { private _getInputDocument(idx: number): tl.TypeInputDocument {
if (!this.full) throw new MtEmptyError() if (!this.full) throw new MtEmptyError()
if (idx < 0) idx = this.full!.documents.length + idx if (idx < 0) idx = this.full.documents.length + idx
const doc = this.full!.documents[idx] as tl.RawDocument const doc = this.full.documents[idx] as tl.RawDocument
if (!doc) { if (!doc) {
throw new RangeError(`Sticker set does not have sticker ${idx}`) throw new RangeError(`Sticker set does not have sticker ${idx}`)

View file

@ -33,6 +33,7 @@ export class TakeoutSession {
message: MustEqual<T, tl.RpcMethod>, message: MustEqual<T, tl.RpcMethod>,
params?: RpcCallOptions, params?: RpcCallOptions,
): Promise<tl.RpcCallReturn[T['_']]> { ): Promise<tl.RpcCallReturn[T['_']]> {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return this.client.call( return this.client.call(
{ {
_: 'invokeWithTakeout', _: 'invokeWithTakeout',
@ -76,6 +77,7 @@ export class TakeoutSession {
get(target, prop, receiver) { get(target, prop, receiver) {
if (prop === 'call') return boundCall if (prop === 'call') return boundCall
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return Reflect.get(target, prop, receiver) return Reflect.get(target, prop, receiver)
}, },
}) })

View file

@ -98,6 +98,7 @@ export class ChatPhotoSize extends FileLocation {
{ {
_: 'photo', _: 'photo',
id: this.obj.photoId, id: this.obj.photoId,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
source: { source: {
_: 'dialogPhoto', _: 'dialogPhoto',
big: this.big, big: this.big,

View file

@ -120,7 +120,7 @@ export class Chat {
} }
} }
return this._inputPeer! return this._inputPeer
} }
private _chatType?: ChatType private _chatType?: ChatType
@ -148,7 +148,7 @@ export class Chat {
} }
} }
return this._chatType! return this._chatType
} }
/** /**
@ -594,7 +594,7 @@ export class Chat {
return new FormattedString( return new FormattedString(
this.client.getParseMode(parseMode).unparse(text, [ this.client.getParseMode(parseMode).unparse(text, [
{ {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
raw: undefined as any, raw: undefined as any,
type: 'text_link', type: 'text_link',
offset: 0, offset: 0,
@ -602,7 +602,7 @@ export class Chat {
url: `https://t.me/${this.username}`, url: `https://t.me/${this.username}`,
}, },
]), ]),
parseMode!, parseMode,
) )
} }

View file

@ -6,8 +6,8 @@ const ERROR_MSG =
'Given peer is not available in this index. This is most likely an internal library error.' 'Given peer is not available in this index. This is most likely an internal library error.'
export class PeersIndex { export class PeersIndex {
readonly users: Record<number, tl.TypeUser> = Object.create(null) readonly users: Record<number, tl.TypeUser> = {}
readonly chats: Record<number, tl.TypeChat> = Object.create(null) readonly chats: Record<number, tl.TypeChat> = {}
hasMin = false hasMin = false

View file

@ -324,7 +324,7 @@ export class User {
return new FormattedString( return new FormattedString(
this.client.getParseMode(parseMode).unparse(text, [ this.client.getParseMode(parseMode).unparse(text, [
{ {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
raw: undefined as any, raw: undefined as any,
type: 'text_mention', type: 'text_mention',
offset: 0, offset: 0,
@ -332,7 +332,7 @@ export class User {
userId: this.raw.id, userId: this.raw.id,
}, },
]), ]),
parseMode!, parseMode,
) )
} }
@ -383,7 +383,7 @@ export class User {
return new FormattedString( return new FormattedString(
this.client.getParseMode(parseMode).unparse(text, [ this.client.getParseMode(parseMode).unparse(text, [
{ {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
raw: undefined as any, raw: undefined as any,
type: 'text_link', type: 'text_link',
offset: 0, offset: 0,
@ -393,7 +393,7 @@ export class User {
}&hash=${this.raw.accessHash.toString(16)}`, }&hash=${this.raw.accessHash.toString(16)}`,
}, },
]), ]),
parseMode!, parseMode,
) )
} }

View file

@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
// ^^ will be looked into in MTQ-35 // ^^ will be looked into in MTQ-35
import { getMarkedPeerId } from '@mtcute/core'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -37,6 +37,26 @@ export type ChatMemberUpdateType =
| 'new_owner' | 'new_owner'
| 'other' | 'other'
function extractPeerId(
raw?: tl.TypeChatParticipant | tl.TypeChannelParticipant,
) {
if (!raw) return 0
if (tl.isAnyChatParticipant(raw)) {
return raw.userId
}
switch (raw._) {
case 'channelParticipant':
case 'channelParticipantSelf':
case 'channelParticipantCreator':
case 'channelParticipantAdmin':
return raw.userId
default:
return getMarkedPeerId(raw.peer)
}
}
/** /**
* Update representing a change in the status * Update representing a change in the status
* of a chat/channel member. * of a chat/channel member.
@ -82,12 +102,8 @@ export class ChatMemberUpdate {
const old = this.raw.prevParticipant const old = this.raw.prevParticipant
const cur = this.raw.newParticipant const cur = this.raw.newParticipant
const oldId = const oldId = extractPeerId(old)
(old && ((old as any).userId || (old as any).peer.userId)) || const curId = extractPeerId(cur)
null
const curId =
(cur && ((cur as any).userId || (cur as any).peer.userId)) ||
null
const actorId = this.raw.actorId const actorId = this.raw.actorId

View file

@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-argument */
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { TelegramClient } from '../../client' import { TelegramClient } from '../../client'
@ -19,13 +19,14 @@ import {
PollVoteUpdate, PollVoteUpdate,
PreCheckoutQuery, PreCheckoutQuery,
UserStatusUpdate, UserStatusUpdate,
UserTypingUpdate } from '../index' UserTypingUpdate,
} from '../index'
type ParserFunction = ( type ParserFunction = (
client: TelegramClient, client: TelegramClient,
upd: tl.TypeUpdate | tl.TypeMessage, upd: tl.TypeUpdate | tl.TypeMessage,
peers: PeersIndex peers: PeersIndex
) => any ) => ParsedUpdate['data']
type UpdateParser = [ParsedUpdate['name'], ParserFunction] type UpdateParser = [ParsedUpdate['name'], ParserFunction]
const baseMessageParser: ParserFunction = ( const baseMessageParser: ParserFunction = (
@ -35,7 +36,9 @@ const baseMessageParser: ParserFunction = (
) => ) =>
new Message( new Message(
client, client,
tl.isAnyMessage(upd) ? upd : (upd as any).message, tl.isAnyMessage(upd) ?
upd :
(upd as { message: tl.TypeMessage }).message,
peers, peers,
upd._ === 'updateNewScheduledMessage', upd._ === 'updateNewScheduledMessage',
) )
@ -142,7 +145,7 @@ export function _parseUpdate(
return { return {
name: pair[0], name: pair[0],
data: pair[1](client, update, peers), data: pair[1](client, update, peers),
} } as ParsedUpdate
} }
return null return null

View file

@ -1,14 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-argument */
import { MaybeAsync } from '@mtcute/core' import { MaybeAsync } from '@mtcute/core'
export type MaybeDynamic<T> = MaybeAsync<T> | (() => MaybeAsync<T>) export type MaybeDynamic<T> = MaybeAsync<T> | (() => MaybeAsync<T>)
export type ArrayWithTotal<T> = T[] & { total: number } export type ArrayWithTotal<T> = T[] & { total: number }
let util: any | null = null let util: typeof import('util') | null = null
try { try {
util = require('util') util = require('util') as typeof import('util')
} catch (e) {} } catch (e) {}
// get all property names. unlike Object.getOwnPropertyNames, // get all property names. unlike Object.getOwnPropertyNames,
@ -21,7 +22,7 @@ function getAllGettersNames(obj: object): string[] {
if ( if (
prop !== '__proto__' && prop !== '__proto__' &&
Object.getOwnPropertyDescriptor(obj, prop)?.get && Object.getOwnPropertyDescriptor(obj, prop)?.get &&
getters.indexOf(prop) === -1 !getters.includes(prop)
) { ) {
getters.push(prop) getters.push(prop)
} }
@ -53,10 +54,11 @@ export function makeInspectable(
const getters: string[] = props ? props : [] const getters: string[] = props ? props : []
for (const key of getAllGettersNames(obj.prototype)) { for (const key of getAllGettersNames(obj.prototype)) {
if (!hide || hide.indexOf(key) === -1) getters.push(key) if (!hide || !hide.includes(key)) getters.push(key)
} }
// dirty hack to set name for inspect result // dirty hack to set name for inspect result
// eslint-disable-next-line @typescript-eslint/no-implied-eval
const proto = new Function(`return function ${obj.name}(){}`)().prototype const proto = new Function(`return function ${obj.name}(){}`)().prototype
obj.prototype.toJSON = function (nested = false) { obj.prototype.toJSON = function (nested = false) {
@ -86,6 +88,7 @@ export function makeInspectable(
Buffer.prototype.toJSON = bufferToJsonOriginal Buffer.prototype.toJSON = bufferToJsonOriginal
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return ret return ret
} }
if (util) { if (util) {

View file

@ -21,27 +21,30 @@ class NodeReadable extends Readable {
this._reading = true this._reading = true
const doRead = () => { const doRead = () => {
this._reader.read().then((res) => { this._reader
if (this._doneReading) { .read()
.then((res) => {
if (this._doneReading) {
this._reading = false
this._reader.releaseLock()
this._doneReading()
}
if (res.done) {
this.push(null)
this._reading = false
this._reader.releaseLock()
return
}
if (this.push(res.value)) {
doRead()
return
}
this._reading = false this._reading = false
this._reader.releaseLock() this._reader.releaseLock()
this._doneReading() })
} .catch((err) => this.emit('error', err))
if (res.done) {
this.push(null)
this._reading = false
this._reader.releaseLock()
return
}
if (this.push(res.value)) {
doRead()
return
}
this._reading = false
this._reader.releaseLock()
})
} }
doRead() doRead()
} }
@ -51,9 +54,11 @@ class NodeReadable extends Readable {
const promise = new Promise<void>((resolve) => { const promise = new Promise<void>((resolve) => {
this._doneReading = resolve this._doneReading = resolve
}) })
promise.then(() => { promise
this._handleDestroy(err, callback) .then(() => {
}) this._handleDestroy(err, callback)
})
.catch((err) => this.emit('error', err))
} else { } else {
this._handleDestroy(err, callback) this._handleDestroy(err, callback)
} }
@ -63,8 +68,10 @@ class NodeReadable extends Readable {
err: Error | null, err: Error | null,
callback: (error?: Error | null) => void, callback: (error?: Error | null) => void,
) { ) {
this._webStream.cancel() this._webStream
super._destroy(err, callback) .cancel()
.then(() => super._destroy(err, callback))
.catch((err: Error) => callback(err))
} }
} }
@ -90,12 +97,12 @@ export async function readBytesFromStream(
): Promise<Buffer | null> { ): Promise<Buffer | null> {
if (stream.readableEnded) return null if (stream.readableEnded) return null
let res = stream.read(size) let res = stream.read(size) as Buffer
if (!res) { if (!res) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
stream.on('readable', function handler() { stream.on('readable', function handler() {
res = stream.read(size) res = stream.read(size) as Buffer
if (res) { if (res) {
stream.off('readable', handler) stream.off('readable', handler)

View file

@ -4,6 +4,7 @@
"outDir": "./dist", "outDir": "./dist",
}, },
"include": [ "include": [
"./src" "./src",
"./tests"
] ]
} }

View file

@ -18,17 +18,18 @@
"./storage/json-file.js": false "./storage/json-file.js": false
}, },
"dependencies": { "dependencies": {
"@types/node": "18.16.0",
"@types/events": "3.0.0",
"@mtcute/tl": "workspace:^160.0.0", "@mtcute/tl": "workspace:^160.0.0",
"@mtcute/tl-runtime": "workspace:^1.0.0", "@mtcute/tl-runtime": "workspace:^1.0.0",
"@types/events": "3.0.0",
"@types/node": "18.16.0",
"big-integer": "1.6.51", "big-integer": "1.6.51",
"long": "5.2.3", "events": "3.2.0",
"events": "3.2.0" "long": "5.2.3"
}, },
"devDependencies": { "devDependencies": {
"@mtcute/dispatcher": "workspace:^1.0.0", "@mtcute/dispatcher": "workspace:^1.0.0",
"@types/ws": "8.5.4", "@types/ws": "8.5.4",
"exit-hook": "^4.0.0",
"ws": "8.13.0" "ws": "8.13.0"
} }
} }

View file

@ -388,7 +388,7 @@ export class BaseTelegramClient extends EventEmitter {
promise.resolve() promise.resolve()
this._connected = true this._connected = true
}) })
.catch((err) => this._emitError(err)) .catch((err: Error) => this._emitError(err))
} }
/** /**
@ -401,7 +401,7 @@ export class BaseTelegramClient extends EventEmitter {
* Close all connections and finalize the client. * Close all connections and finalize the client.
*/ */
async close(): Promise<void> { async close(): Promise<void> {
await this._onClose() this._onClose()
this._config.destroy() this._config.destroy()
this.network.destroy() this.network.destroy()
@ -436,6 +436,7 @@ export class BaseTelegramClient extends EventEmitter {
const res = await this.network.call(message, params, stack) const res = await this.network.call(message, params, stack)
await this._cachePeersFrom(res) await this._cachePeersFrom(res)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return res return res
} }
@ -487,7 +488,7 @@ export class BaseTelegramClient extends EventEmitter {
let hadMin = false let hadMin = false
let count = 0 let count = 0
for (const peer of getAllPeersFrom(obj as any)) { for (const peer of getAllPeersFrom(obj as tl.TlObject)) {
if ((peer as any).min) { if ((peer as any).min) {
// absolutely incredible min peer handling, courtesy of levlam. // absolutely incredible min peer handling, courtesy of levlam.
// see this thread: https://t.me/tdlibchat/15084 // see this thread: https://t.me/tdlibchat/15084

View file

@ -178,6 +178,7 @@ async function rsaPad(
const decryptedDataBigint = bufferToBigInt(decryptedData) const decryptedDataBigint = bufferToBigInt(decryptedData)
if (decryptedDataBigint.geq(keyModulus)) { if (decryptedDataBigint.geq(keyModulus)) {
console.log('retrying because decrypted data is too big')
continue continue
} }
@ -224,6 +225,7 @@ export async function doAuthorization(
const session = connection['_session'] const session = connection['_session']
const readerMap = session._readerMap const readerMap = session._readerMap
const writerMap = session._writerMap const writerMap = session._writerMap
const log = connection.log.create('auth')
function sendPlainMessage(message: mtp.TlObject): Promise<void> { function sendPlainMessage(message: mtp.TlObject): Promise<void> {
const length = TlSerializationCounter.countNeededBytes( const length = TlSerializationCounter.countNeededBytes(
@ -234,6 +236,7 @@ export async function doAuthorization(
const messageId = session.getMessageId() const messageId = session.getMessageId()
log.verbose('[PLAIN] >>> %j', message)
writer.long(Long.ZERO) writer.long(Long.ZERO)
writer.long(messageId) writer.long(messageId)
writer.uint(length) writer.uint(length)
@ -243,14 +246,17 @@ export async function doAuthorization(
} }
async function readNext(): Promise<mtp.TlObject> { async function readNext(): Promise<mtp.TlObject> {
return TlBinaryReader.deserializeObject( const res = TlBinaryReader.deserializeObject<mtp.TlObject>(
readerMap, readerMap,
await connection.waitForUnencryptedMessage(), await connection.waitForUnencryptedMessage(),
20, // skip mtproto header 20, // skip mtproto header
) )
log.verbose('[PLAIN] <<< %j', res)
return res
} }
const log = connection.log.create('auth')
if (expiresIn) log.prefix = '[PFS] ' if (expiresIn) log.prefix = '[PFS] '
const nonce = randomBytes(16) const nonce = randomBytes(16)
@ -508,8 +514,7 @@ export async function doAuthorization(
if (!buffersEqual(expectedHash.slice(4, 20), dhGen.newNonceHash2)) { if (!buffersEqual(expectedHash.slice(4, 20), dhGen.newNonceHash2)) {
throw Error('Step 4: invalid retry nonce hash from server') throw Error('Step 4: invalid retry nonce hash from server')
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any retryId = Long.fromBytesLE(authKeyAuxHash as unknown as number[])
retryId = Long.fromBytesLE(authKeyAuxHash as any)
continue continue
} }

View file

@ -32,7 +32,7 @@ export class ConfigManager {
if (this._updateTimeout) clearTimeout(this._updateTimeout) if (this._updateTimeout) clearTimeout(this._updateTimeout)
this._updateTimeout = setTimeout( this._updateTimeout = setTimeout(
() => this.update(), () => void this.update().catch(() => {}),
(config.expires - Date.now() / 1000) * 1000, (config.expires - Date.now() / 1000) * 1000,
) )

View file

@ -250,7 +250,7 @@ export class DcConnectionManager {
private _setupMulti(kind: ConnectionKind): void { private _setupMulti(kind: ConnectionKind): void {
const connection = this[kind] const connection = this[kind]
connection.on('key-change', (idx, key) => { connection.on('key-change', (idx, key: Buffer | null) => {
if (kind !== 'main') { if (kind !== 'main') {
// main connection is responsible for authorization, // main connection is responsible for authorization,
// and keys are then sent to other connections // and keys are then sent to other connections
@ -266,51 +266,58 @@ export class DcConnectionManager {
this.dcId, this.dcId,
idx, idx,
) )
this.manager._storage.setAuthKeyFor(this.dcId, key)
// send key to other connections // send key to other connections
Promise.all([ Promise.all([
this.manager._storage.setAuthKeyFor(this.dcId, key),
this.upload.setAuthKey(key), this.upload.setAuthKey(key),
this.download.setAuthKey(key), this.download.setAuthKey(key),
this.downloadSmall.setAuthKey(key), this.downloadSmall.setAuthKey(key),
]).then(() => { ])
this.upload.notifyKeyChange() .then(() => {
this.download.notifyKeyChange() this.upload.notifyKeyChange()
this.downloadSmall.notifyKeyChange() this.download.notifyKeyChange()
}) this.downloadSmall.notifyKeyChange()
})
.catch((e: Error) => this.manager.params._emitError(e))
}) })
connection.on('tmp-key-change', (idx, key, expires) => { connection.on(
if (kind !== 'main') { 'tmp-key-change',
this.manager._log.warn( (idx: number, key: Buffer | null, expires: number) => {
'got tmp-key-change from non-main connection, ignoring', if (kind !== 'main') {
this.manager._log.warn(
'got tmp-key-change from non-main connection, ignoring',
)
return
}
this.manager._log.debug(
'temp key change for dc %d from connection %d',
this.dcId,
idx,
) )
return // send key to other connections
} Promise.all([
this.manager._storage.setTempAuthKeyFor(
this.manager._log.debug( this.dcId,
'temp key change for dc %d from connection %d', idx,
this.dcId, key,
idx, expires * 1000,
) ),
this.manager._storage.setTempAuthKeyFor( this.upload.setAuthKey(key, true),
this.dcId, this.download.setAuthKey(key, true),
idx, this.downloadSmall.setAuthKey(key, true),
key, ])
expires * 1000, .then(() => {
) this.upload.notifyKeyChange()
this.download.notifyKeyChange()
// send key to other connections this.downloadSmall.notifyKeyChange()
Promise.all([ })
this.upload.setAuthKey(key, true), .catch((e: Error) => this.manager.params._emitError(e))
this.download.setAuthKey(key, true), },
this.downloadSmall.setAuthKey(key, true), )
]).then(() => {
this.upload.notifyKeyChange()
this.download.notifyKeyChange()
this.downloadSmall.notifyKeyChange()
})
})
connection.on('auth-begin', () => { connection.on('auth-begin', () => {
// we need to propagate auth-begin to all connections // we need to propagate auth-begin to all connections
@ -334,7 +341,7 @@ export class DcConnectionManager {
this.main.requestAuth() this.main.requestAuth()
}) })
connection.on('error', (err, conn) => { connection.on('error', (err: Error, conn: SessionConnection) => {
this.manager.params._emitError(err, conn) this.manager.params._emitError(err, conn)
}) })
} }
@ -429,19 +436,21 @@ export class NetworkManager {
let deviceModel = 'mtcute on ' let deviceModel = 'mtcute on '
let appVersion = 'unknown' let appVersion = 'unknown'
if (typeof process !== 'undefined' && typeof require !== 'undefined') { if (typeof process !== 'undefined' && typeof require !== 'undefined') {
// eslint-disable-next-line @typescript-eslint/no-var-requires const os = require('os') as typeof import('os')
const os = require('os')
deviceModel += `${os.type()} ${os.arch()} ${os.release()}` deviceModel += `${os.type()} ${os.arch()} ${os.release()}`
try { try {
// for production builds // for production builds
// eslint-disable-next-line @typescript-eslint/no-var-requires
appVersion = require('../package.json').version appVersion = (require('../package.json') as { version: string })
.version
} catch (e) { } catch (e) {
try { try {
// for development builds (additional /src/ in path) // for development builds (additional /src/ in path)
// eslint-disable-next-line @typescript-eslint/no-var-requires
appVersion = require('../../package.json').version appVersion = (
require('../../package.json') as { version: string }
).version
} catch (e) {} } catch (e) {}
} }
} else if (typeof navigator !== 'undefined') { } else if (typeof navigator !== 'undefined') {
@ -458,7 +467,7 @@ export class NetworkManager {
langCode: 'en', langCode: 'en',
...(params.initConnectionOptions ?? {}), ...(params.initConnectionOptions ?? {}),
apiId: params.apiId, apiId: params.apiId,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line
query: null as any, query: null as any,
} }
@ -484,7 +493,7 @@ export class NetworkManager {
this._lastUpdateTime = Date.now() this._lastUpdateTime = Date.now()
if (this._keepAliveInterval) clearInterval(this._keepAliveInterval) if (this._keepAliveInterval) clearInterval(this._keepAliveInterval)
this._keepAliveInterval = setInterval(async () => { this._keepAliveInterval = setInterval(() => {
if (Date.now() - this._lastUpdateTime > 900_000) { if (Date.now() - this._lastUpdateTime > 900_000) {
// telegram asks to fetch pending updates if there are no updates for 15 minutes. // telegram asks to fetch pending updates if there are no updates for 15 minutes.
// it is up to the user to decide whether to do it or not // it is up to the user to decide whether to do it or not
@ -494,27 +503,21 @@ export class NetworkManager {
} }
}, 60_000) }, 60_000)
Promise.resolve(this._storage.getSelf()).then((self) => { Promise.resolve(this._storage.getSelf())
if (self?.isBot) { .then((self) => {
// bots may receive tmpSessions, which we should respect if (self?.isBot) {
this.config.update(true).catch((e) => { // bots may receive tmpSessions, which we should respect
this.params._emitError(e) return this.config.update(true)
}) }
} })
}) .catch((e: Error) => this.params._emitError(e))
}) })
dc.main.on('update', (update) => { dc.main.on('update', (update: tl.TypeUpdates) => {
this._lastUpdateTime = Date.now() this._lastUpdateTime = Date.now()
this._updateHandler(update) this._updateHandler(update)
}) })
dc.loadKeys() return dc.loadKeys().then(() => dc.main.ensureConnected())
.catch((e) => {
this.params._emitError(e)
})
.then(() => {
dc.main.ensureConnected()
})
} }
private _dcCreationPromise: Record<number, Promise<void>> = {} private _dcCreationPromise: Record<number, Promise<void>> = {}
@ -574,7 +577,7 @@ export class NetworkManager {
const dc = new DcConnectionManager(this, defaultDc.id, defaultDc) const dc = new DcConnectionManager(this, defaultDc.id, defaultDc)
this._dcConnections[defaultDc.id] = dc this._dcConnections[defaultDc.id] = dc
this._switchPrimaryDc(dc) await this._switchPrimaryDc(dc)
} }
private async _exportAuthTo(manager: DcConnectionManager): Promise<void> { private async _exportAuthTo(manager: DcConnectionManager): Promise<void> {
@ -681,9 +684,9 @@ export class NetworkManager {
) )
} }
this._storage.setDefaultDc(option) await this._storage.setDefaultDc(option)
this._switchPrimaryDc(this._dcConnections[newDc]) await this._switchPrimaryDc(this._dcConnections[newDc])
} }
private _floodWaitedRequests: Record<string, number> = {} private _floodWaitedRequests: Record<string, number> = {}
@ -738,10 +741,11 @@ export class NetworkManager {
this._lastUpdateTime = Date.now() this._lastUpdateTime = Date.now()
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return res return res
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) { } catch (e: any) {
lastError = e lastError = e as Error
if (e.code && !(e.code in CLIENT_ERRORS)) { if (e.code && !(e.code in CLIENT_ERRORS)) {
this._log.warn( this._log.warn(
@ -811,7 +815,7 @@ export class NetworkManager {
} }
} }
throw lastError throw lastError!
} }
setUpdateHandler(handler: NetworkManager['_updateHandler']): void { setUpdateHandler(handler: NetworkManager['_updateHandler']): void {

View file

@ -115,7 +115,7 @@ export class SessionConnection extends PersistentConnection {
this._isPfsBindingPending = false this._isPfsBindingPending = false
this._isPfsBindingPendingInBackground = false this._isPfsBindingPendingInBackground = false
this._session._authKeyTemp.reset() this._session._authKeyTemp.reset()
clearTimeout(this._pfsUpdateTimeout!) clearTimeout(this._pfsUpdateTimeout)
} }
this._resetSession() this._resetSession()
@ -296,7 +296,7 @@ export class SessionConnection extends PersistentConnection {
} }
this.onConnectionUsable() this.onConnectionUsable()
}) })
.catch((err) => { .catch((err: Error) => {
this._session.authorizationPending = false this._session.authorizationPending = false
this.log.error('Authorization error: %s', err.message) this.log.error('Authorization error: %s', err.message)
this.onError(err) this.onError(err)
@ -338,7 +338,7 @@ export class SessionConnection extends PersistentConnection {
return return
} }
const tempKey = await this._session._authKeyTempSecondary const tempKey = this._session._authKeyTempSecondary
await tempKey.setup(tempAuthKey) await tempKey.setup(tempAuthKey)
const msgId = this._session.getMessageId() const msgId = this._session.getMessageId()
@ -490,7 +490,7 @@ export class SessionConnection extends PersistentConnection {
this._authorizePfs(true) this._authorizePfs(true)
}, (TEMP_AUTH_KEY_EXPIRY - 60) * 1000) }, (TEMP_AUTH_KEY_EXPIRY - 60) * 1000)
}) })
.catch((err) => { .catch((err: Error) => {
this.log.error('PFS Authorization error: %s', err.message) this.log.error('PFS Authorization error: %s', err.message)
if (this._isPfsBindingPendingInBackground) { if (this._isPfsBindingPendingInBackground) {
@ -699,6 +699,8 @@ export class SessionConnection extends PersistentConnection {
this._rescheduleInactivity() this._rescheduleInactivity()
} }
this.log.verbose('<<< %j', message)
if (this.params.disableUpdates) { if (this.params.disableUpdates) {
this.log.warn( this.log.warn(
'received updates, but updates are disabled', 'received updates, but updates are disabled',
@ -727,6 +729,7 @@ export class SessionConnection extends PersistentConnection {
let resultType let resultType
try { try {
// eslint-disable-next-line
resultType = (message.object() as any)._ resultType = (message.object() as any)._
} catch (err) { } catch (err) {
resultType = message.peekUint() resultType = message.peekUint()
@ -745,6 +748,7 @@ export class SessionConnection extends PersistentConnection {
let result let result
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
result = message.object() as any result = message.object() as any
} catch (err) { } catch (err) {
result = '[failed to parse]' result = '[failed to parse]'
@ -1490,6 +1494,7 @@ export class SessionConnection extends PersistentConnection {
const pending: PendingRpc = { const pending: PendingRpc = {
method, method,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
promise: undefined as any, // because we need the object to make a promise promise: undefined as any, // because we need the object to make a promise
data: content, data: content,
stack, stack,
@ -1599,7 +1604,7 @@ export class SessionConnection extends PersistentConnection {
try { try {
this._doFlush() this._doFlush()
} catch (e: any) { } catch (e: any) {
this.log.error('flush error: %s', e.stack) this.log.error('flush error: %s', (e as Error).stack)
// should not happen unless there's a bug in the code // should not happen unless there's a bug in the code
} }
@ -1718,7 +1723,7 @@ export class SessionConnection extends PersistentConnection {
} }
const idx = this._session.getStateSchedule.index( const idx = this._session.getStateSchedule.index(
{ getState: now } as any, { getState: now } as PendingRpc,
true, true,
) )
@ -2035,7 +2040,7 @@ export class SessionConnection extends PersistentConnection {
this._session this._session
.encryptMessage(result) .encryptMessage(result)
.then((enc) => this.send(enc)) .then((enc) => this.send(enc))
.catch((err) => { .catch((err: Error) => {
this.log.error( this.log.error(
'error while sending pending messages (root msg_id = %l): %s', 'error while sending pending messages (root msg_id = %l): %s',
rootMsgId, rootMsgId,

View file

@ -98,8 +98,13 @@ export class ObfuscatedPacketCodec
feed(data: Buffer): void { feed(data: Buffer): void {
const dec = this._decryptor!.decrypt(data) const dec = this._decryptor!.decrypt(data)
if (Buffer.isBuffer(dec)) this._inner.feed(dec) if (Buffer.isBuffer(dec)) this._inner.feed(dec)
else dec.then((dec) => this._inner.feed(dec)) else {
dec.then((dec) => this._inner.feed(dec)).catch((err) =>
this.emit('error', err),
)
}
} }
reset(): void { reset(): void {

View file

@ -96,28 +96,31 @@ export abstract class BaseTcpTransport
this.emit('error', error) this.emit('error', error)
} }
async handleConnect(): Promise<void> { handleConnect(): void {
this.log.info('connected') this.log.info('connected')
const initialMessage = await this._packetCodec.tag()
if (initialMessage.length) { Promise.resolve(this._packetCodec.tag())
this._socket!.write(initialMessage, (err) => { .then((initialMessage) => {
if (err) { if (initialMessage.length) {
this.log.error( this._socket!.write(initialMessage, (err) => {
'failed to write initial message: %s', if (err) {
err.stack, this.log.error(
) 'failed to write initial message: %s',
this.emit('error') err.stack,
this.close() )
this.emit('error')
this.close()
} else {
this._state = TransportState.Ready
this.emit('ready')
}
})
} else { } else {
this._state = TransportState.Ready this._state = TransportState.Ready
this.emit('ready') this.emit('ready')
} }
}) })
} else { .catch((err) => this.emit('error', err))
this._state = TransportState.Ready
this.emit('ready')
}
} }
async send(bytes: Buffer): Promise<void> { async send(bytes: Buffer): Promise<void> {

View file

@ -1,5 +1,4 @@
import EventEmitter from 'events' import EventEmitter from 'events'
import type WebSocket from 'ws'
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
@ -14,13 +13,13 @@ let ws: {
if (typeof window === 'undefined' || typeof window.WebSocket === 'undefined') { if (typeof window === 'undefined' || typeof window.WebSocket === 'undefined') {
try { try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ws = require('ws') ws = require('ws')
} catch (e) { } catch (e) {
ws = null ws = null
} }
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any ws = window.WebSocket
ws = window.WebSocket as any
} }
const subdomainsMap: Record<string, string> = { const subdomainsMap: Record<string, string> = {
@ -93,7 +92,9 @@ export abstract class BaseWebSocketTransport
} }
connect(dc: tl.RawDcOption, testMode: boolean): void { connect(dc: tl.RawDcOption, testMode: boolean): void {
if (this._state !== TransportState.Idle) { throw new Error('Transport is not IDLE') } if (this._state !== TransportState.Idle) {
throw new Error('Transport is not IDLE')
}
if (!this.packetCodecInitialized) { if (!this.packetCodecInitialized) {
this._packetCodec.setup?.(this._crypto, this.log) this._packetCodec.setup?.(this._crypto, this.log)
@ -117,8 +118,9 @@ export abstract class BaseWebSocketTransport
this._socket.binaryType = 'arraybuffer' this._socket.binaryType = 'arraybuffer'
this._socket.addEventListener('message', (evt) => this._socket.addEventListener('message', (evt) =>
// eslint-disable-next-line @typescript-eslint/no-explicit-any this._packetCodec.feed(
this._packetCodec.feed(typedArrayToBuffer(evt.data as any)), typedArrayToBuffer(evt.data as NodeJS.TypedArray),
),
) )
this._socket.addEventListener('open', this.handleConnect.bind(this)) this._socket.addEventListener('open', this.handleConnect.bind(this))
this._socket.addEventListener('error', this.handleError.bind(this)) this._socket.addEventListener('error', this.handleError.bind(this))
@ -138,22 +140,32 @@ export abstract class BaseWebSocketTransport
this._packetCodec.reset() this._packetCodec.reset()
} }
async handleError({ error }: { error: Error }): Promise<void> { handleError(event: Event | { error: Error }): void {
const error =
'error' in event ?
event.error :
new Error('unknown WebSocket error')
this.log.error('error: %s', error.stack) this.log.error('error: %s', error.stack)
this.emit('error', error) this.emit('error', error)
} }
async handleConnect(): Promise<void> { handleConnect(): void {
this.log.info('connected') this.log.info('connected')
const initialMessage = await this._packetCodec.tag()
this._socket!.send(initialMessage) Promise.resolve(this._packetCodec.tag())
this._state = TransportState.Ready .then((initialMessage) => {
this.emit('ready') this._socket!.send(initialMessage)
this._state = TransportState.Ready
this.emit('ready')
})
.catch((err) => this.emit('error', err))
} }
async send(bytes: Buffer): Promise<void> { async send(bytes: Buffer): Promise<void> {
if (this._state !== TransportState.Ready) { throw new Error('Transport is not READY') } if (this._state !== TransportState.Ready) {
throw new Error('Transport is not READY')
}
const framed = await this._packetCodec.encode(bytes) const framed = await this._packetCodec.encode(bytes)

View file

@ -1,16 +1,20 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import type * as exitHookNs from 'exit-hook'
import type * as fsNs from 'fs'
import { JsonMemoryStorage } from './json' import { JsonMemoryStorage } from './json'
let fs: any = null type fs = typeof fsNs
let fs: fs | null = null
try { try {
fs = require('fs') fs = require('fs') as fs
} catch (e) {} } catch (e) {}
let exitHook: any = null type exitHook = typeof exitHookNs
let exitHook: exitHook | null = null
try { try {
exitHook = require('exit-hook') exitHook = require('exit-hook') as exitHook
} catch (e) {} } catch (e) {}
export class JsonFileStorage extends JsonMemoryStorage { export class JsonFileStorage extends JsonMemoryStorage {
@ -47,7 +51,9 @@ export class JsonFileStorage extends JsonMemoryStorage {
) { ) {
super() super()
if (!fs || !fs.readFile) { throw new Error('Node fs module is not available!') } if (!fs || !fs.readFile) {
throw new Error('Node fs module is not available!')
}
this._filename = filename this._filename = filename
this._safe = params?.safe ?? true this._safe = params?.safe ?? true
@ -60,7 +66,9 @@ export class JsonFileStorage extends JsonMemoryStorage {
} }
if (this._cleanup) { if (this._cleanup) {
this._unsubscribe = exitHook(this._onProcessExit.bind(this)) this._unsubscribe = exitHook!.default(
this._onProcessExit.bind(this),
)
} }
} }
@ -68,12 +76,8 @@ export class JsonFileStorage extends JsonMemoryStorage {
try { try {
this._loadJson( this._loadJson(
await new Promise((res, rej) => await new Promise((res, rej) =>
fs.readFile( fs!.readFile(this._filename, 'utf-8', (err, data) =>
this._filename, err ? rej(err) : res(data),
'utf-8',
(err?: Error, data?: string) =>
err ? rej(err) : res(data!),
), ),
), ),
) )
@ -82,16 +86,16 @@ export class JsonFileStorage extends JsonMemoryStorage {
save(): Promise<void> { save(): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fs.writeFile( fs!.writeFile(
this._safe ? this._filename + '.tmp' : this._filename, this._safe ? this._filename + '.tmp' : this._filename,
this._saveJson(), this._saveJson(),
(err?: Error) => { (err) => {
if (err) reject(err) if (err) reject(err)
else if (this._safe) { else if (this._safe) {
fs.rename( fs!.rename(
this._filename + '.tmp', this._filename + '.tmp',
this._filename, this._filename,
(err?: any) => { (err) => {
if (err && err.code !== 'ENOENT') reject(err) if (err && err.code !== 'ENOENT') reject(err)
else resolve() else resolve()
}, },
@ -106,12 +110,12 @@ export class JsonFileStorage extends JsonMemoryStorage {
// on exit handler must be synchronous, thus we use sync methods here // on exit handler must be synchronous, thus we use sync methods here
try { try {
fs.writeFileSync(this._filename, this._saveJson()) fs!.writeFileSync(this._filename, this._saveJson())
} catch (e) {} } catch (e) {}
if (this._safe) { if (this._safe) {
try { try {
fs.unlinkSync(this._filename + '.tmp') fs!.unlinkSync(this._filename + '.tmp')
} catch (e) {} } catch (e) {}
} }
} }

View file

@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { tl } from '@mtcute/tl'
import { longFromFastString, longToFastString } from '../utils' import { longFromFastString, longToFastString } from '../utils'
import { MemoryStorage } from './memory' import { MemorySessionState, MemoryStorage } from './memory'
/** /**
* Helper class that provides json serialization functions * Helper class that provides json serialization functions
@ -12,7 +15,7 @@ export class JsonMemoryStorage extends MemoryStorage {
if (key === 'authKeys') { if (key === 'authKeys') {
const ret: Record<string, Buffer> = {} const ret: Record<string, Buffer> = {}
value.split('|').forEach((pair: string) => { ;(value as string).split('|').forEach((pair: string) => {
const [dcId, b64] = pair.split(',') const [dcId, b64] = pair.split(',')
ret[dcId] = Buffer.from(b64, 'base64') ret[dcId] = Buffer.from(b64, 'base64')
}) })
@ -21,27 +24,26 @@ export class JsonMemoryStorage extends MemoryStorage {
} }
if (key === 'accessHash') { if (key === 'accessHash') {
return longFromFastString(value) return longFromFastString(value as string)
} }
return value return value
}), }) as MemorySessionState,
) )
} }
protected _saveJson(): string { protected _saveJson(): string {
return JSON.stringify(this._state, (key, value) => { return JSON.stringify(this._state, (key, value) => {
if (key === 'authKeys') { if (key === 'authKeys') {
return Object.entries(value) const value_ = value as Record<string, Buffer | null>
.filter((it) => it[1] !== null)
.map( return Object.entries(value_)
([dcId, key]) => .filter((it): it is [string, Buffer] => it[1] !== null)
dcId + ',' + (key as Buffer).toString('base64'), .map(([dcId, key]) => dcId + ',' + key.toString('base64'))
)
.join('|') .join('|')
} }
if (key === 'accessHash') { if (key === 'accessHash') {
return longToFastString(value) return longToFastString(value as tl.Long)
} }
return value return value

View file

@ -15,7 +15,7 @@ export class LocalstorageStorage extends JsonMemoryStorage {
load(): void { load(): void {
try { try {
this._loadJson(localStorage[this._key]) this._loadJson(localStorage[this._key] as string)
} catch (e) {} } catch (e) {}
} }

View file

@ -210,7 +210,9 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
if (tempIndex !== undefined) { if (tempIndex !== undefined) {
const k = `${dcId}:${tempIndex}` const k = `${dcId}:${tempIndex}`
if (Date.now() > (this._state.authKeysTempExpiry[k] ?? 0)) { return null } if (Date.now() > (this._state.authKeysTempExpiry[k] ?? 0)) {
return null
}
return this._state.authKeysTemp[k] return this._state.authKeysTemp[k]
} }
@ -248,7 +250,9 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
} }
} }
if (peer.username) { this._state.usernameIndex[peer.username.toLowerCase()] = peer.id } if (peer.username) {
this._state.usernameIndex[peer.username.toLowerCase()] = peer.id
}
if (peer.phone) this._state.phoneIndex[peer.phone] = peer.id if (peer.phone) this._state.phoneIndex[peer.phone] = peer.id
this._state.entities[peer.id] = peer this._state.entities[peer.id] = peer
} }
@ -357,7 +361,7 @@ export class MemoryStorage implements ITelegramStorage, IStateStorage {
// IStateStorage implementation // IStateStorage implementation
getState(key: string): unknown | null { getState(key: string): unknown {
const val = this._state.fsm[key] const val = this._state.fsm[key]
if (!val) return null if (!val) return null

View file

@ -35,9 +35,10 @@ export class AsyncLock {
return this.acquire() return this.acquire()
.then(() => func()) .then(() => func())
.catch((e) => (err = e)) .catch((e) => void (err = e))
.then(() => { .then(() => {
this.release() this.release()
// eslint-disable-next-line @typescript-eslint/no-throw-literal
if (err) throw err if (err) throw err
}) })
} }

View file

@ -1,3 +1,5 @@
import type * as forgeNs from 'node-forge'
import { MaybeAsync } from '../../types' import { MaybeAsync } from '../../types'
import { import {
BaseCryptoProvider, BaseCryptoProvider,
@ -5,11 +7,11 @@ import {
IEncryptionScheme, IEncryptionScheme,
} from './abstract' } from './abstract'
// eslint-disable-next-line @typescript-eslint/no-explicit-any type forge = typeof forgeNs
let forge: any = null let forge: forge | null = null
try { try {
forge = require('node-forge') forge = require('node-forge') as forge
} catch (e) {} } catch (e) {}
export class ForgeCryptoProvider export class ForgeCryptoProvider
@ -26,14 +28,14 @@ export class ForgeCryptoProvider
} }
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme { createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
const cipher = forge.cipher[ const cipher = forge!.cipher[
encrypt ? 'createCipher' : 'createDecipher' encrypt ? 'createCipher' : 'createDecipher'
]('AES-CTR', key.toString('binary')) ]('AES-CTR', key.toString('binary'))
cipher.start({ iv: iv.toString('binary') }) cipher.start({ iv: iv.toString('binary') })
const update = (data: Buffer): Buffer => { const update = (data: Buffer): Buffer => {
cipher.output.data = '' cipher.output.data = ''
cipher.update(forge.util.createBuffer(data.toString('binary'))) cipher.update(forge!.util.createBuffer(data.toString('binary')))
return Buffer.from(cipher.output.data, 'binary') return Buffer.from(cipher.output.data, 'binary')
} }
@ -49,19 +51,24 @@ export class ForgeCryptoProvider
return { return {
encrypt(data: Buffer) { encrypt(data: Buffer) {
const cipher = forge.cipher.createCipher('AES-ECB', keyBuffer) const cipher = forge!.cipher.createCipher('AES-ECB', keyBuffer)
cipher.start({}) cipher.start({})
// @ts-expect-error wrong types
cipher.mode.pad = cipher.mode.unpad = false cipher.mode.pad = cipher.mode.unpad = false
cipher.update(forge.util.createBuffer(data.toString('binary'))) cipher.update(forge!.util.createBuffer(data.toString('binary')))
cipher.finish() cipher.finish()
return Buffer.from(cipher.output.data, 'binary') return Buffer.from(cipher.output.data, 'binary')
}, },
decrypt(data: Buffer) { decrypt(data: Buffer) {
const cipher = forge.cipher.createDecipher('AES-ECB', keyBuffer) const cipher = forge!.cipher.createDecipher(
'AES-ECB',
keyBuffer,
)
cipher.start({}) cipher.start({})
// @ts-expect-error wrong types
cipher.mode.pad = cipher.mode.unpad = false cipher.mode.pad = cipher.mode.unpad = false
cipher.update(forge.util.createBuffer(data.toString('binary'))) cipher.update(forge!.util.createBuffer(data.toString('binary')))
cipher.finish() cipher.finish()
return Buffer.from(cipher.output.data, 'binary') return Buffer.from(cipher.output.data, 'binary')
@ -77,12 +84,13 @@ export class ForgeCryptoProvider
algo = 'sha512', algo = 'sha512',
): MaybeAsync<Buffer> { ): MaybeAsync<Buffer> {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
forge.pkcs5.pbkdf2( forge!.pkcs5.pbkdf2(
password.toString('binary'), password.toString('binary'),
salt.toString('binary'), salt.toString('binary'),
iterations, iterations,
keylen, keylen,
forge.md[algo].create(), // eslint-disable-next-line
(forge!.md as any)[algo].create(),
(err: Error | null, buf: string) => (err: Error | null, buf: string) =>
err !== null ? err !== null ?
reject(err) : reject(err) :
@ -93,7 +101,7 @@ export class ForgeCryptoProvider
sha1(data: Buffer): MaybeAsync<Buffer> { sha1(data: Buffer): MaybeAsync<Buffer> {
return Buffer.from( return Buffer.from(
forge.md.sha1.create().update(data.toString('binary')).digest() forge!.md.sha1.create().update(data.toString('binary')).digest()
.data, .data,
'binary', 'binary',
) )
@ -101,14 +109,14 @@ export class ForgeCryptoProvider
sha256(data: Buffer): MaybeAsync<Buffer> { sha256(data: Buffer): MaybeAsync<Buffer> {
return Buffer.from( return Buffer.from(
forge.md.sha256.create().update(data.toString('binary')).digest() forge!.md.sha256.create().update(data.toString('binary')).digest()
.data, .data,
'binary', 'binary',
) )
} }
hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> { hmacSha256(data: Buffer, key: Buffer): MaybeAsync<Buffer> {
const hmac = forge.hmac.create() const hmac = forge!.hmac.create()
hmac.start('sha256', key.toString('binary')) hmac.start('sha256', key.toString('binary'))
hmac.update(data.toString('binary')) hmac.update(data.toString('binary'))

View file

@ -16,10 +16,6 @@ import {
export class NodeCryptoProvider export class NodeCryptoProvider
extends BaseCryptoProvider extends BaseCryptoProvider
implements ICryptoProvider { implements ICryptoProvider {
constructor() {
super()
}
createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme { createAesCtr(key: Buffer, iv: Buffer, encrypt: boolean): IEncryptionScheme {
const cipher = (encrypt ? createCipheriv : createDecipheriv)( const cipher = (encrypt ? createCipheriv : createDecipheriv)(
`aes-${key.length * 8}-ctr`, `aes-${key.length * 8}-ctr`,

View file

@ -81,7 +81,7 @@ export async function computeSrpParams(
// nice naming thx durov // nice naming thx durov
if ( if (
!request.currentAlgo || !request.currentAlgo ||
request.currentAlgo?._ !== request.currentAlgo._ !==
'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow' 'passwordKdfAlgoSHA256SHA256PBKDF2HMACSHA512iter100000SHA256ModPow'
) { ) {
throw new Error(`Unknown algo ${request.currentAlgo?._}`) throw new Error(`Unknown algo ${request.currentAlgo?._}`)

View file

@ -51,7 +51,7 @@ export class Deque<T> {
} }
} }
this._elements = new Array(capacity) this._elements = new Array<T | undefined>(capacity)
this._capacity = capacity this._capacity = capacity
} }
@ -63,7 +63,7 @@ export class Deque<T> {
const newCapacity = n << 1 const newCapacity = n << 1
if (newCapacity < 0) throw new Error('Deque is too big') if (newCapacity < 0) throw new Error('Deque is too big')
const arr = new Array(newCapacity) const arr = new Array<T | undefined>(newCapacity)
// copy items to the new array // copy items to the new array
// copy head till the end of arr // copy head till the end of arr
@ -251,7 +251,7 @@ export class Deque<T> {
} }
clear(): void { clear(): void {
this._elements = new Array(this._capacity) this._elements = new Array<T | undefined>(this._capacity)
this._head = this._tail = 0 this._head = this._tail = 0
} }
} }

View file

@ -9,7 +9,7 @@ if (typeof process !== 'undefined') {
defaultLogLevel = envLogLevel defaultLogLevel = envLogLevel
} }
} else if (typeof localStorage !== 'undefined') { } else if (typeof localStorage !== 'undefined') {
const localLogLevel = parseInt(localStorage.MTCUTE_LOG_LEVEL) const localLogLevel = parseInt(localStorage.MTCUTE_LOG_LEVEL as string)
if (!isNaN(localLogLevel)) { if (!isNaN(localLogLevel)) {
defaultLogLevel = localLogLevel defaultLogLevel = localLogLevel
@ -62,10 +62,10 @@ export class Logger {
// custom formatters // custom formatters
if ( if (
fmt.indexOf('%h') > -1 || fmt.includes('%h') ||
fmt.indexOf('%b') > -1 || fmt.includes('%b') ||
fmt.indexOf('%j') > -1 || fmt.includes('%j') ||
fmt.indexOf('%l') > -1 fmt.includes('%l')
) { ) {
let idx = 0 let idx = 0
fmt = fmt.replace(FORMATTER_RE, (m) => { fmt = fmt.replace(FORMATTER_RE, (m) => {
@ -89,7 +89,9 @@ export class Logger {
v.type === 'Buffer' && v.type === 'Buffer' &&
Array.isArray(v.data) Array.isArray(v.data)
) { ) {
let str = Buffer.from(v.data).toString('base64') let str = Buffer.from(
v.data as number[],
).toString('base64')
if (str.length > 300) { if (str.length > 300) {
str = str.slice(0, 300) + '...' str = str.slice(0, 300) + '...'
@ -98,6 +100,7 @@ export class Logger {
return str return str
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return v return v
}) })
} }
@ -152,7 +155,7 @@ export class LogManager extends Logger {
constructor(tag = 'base') { constructor(tag = 'base') {
// workaround because we cant pass this to super // workaround because we cant pass this to super
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-argument
super(null as any, tag) super(null as any, tag)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
;(this as any).mgr = this ;(this as any).mgr = this

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return */
// ^^ because of performance reasons // ^^ because of performance reasons
import Long from 'long' import Long from 'long'
@ -84,7 +85,9 @@ export function longFromFastString(val: string, unsigned = false): Long {
const low = parseInt(parts[0]) const low = parseInt(parts[0])
const high = parseInt(parts[1]) const high = parseInt(parts[1])
if (isNaN(low) || isNaN(high)) { throw new Error(`Invalid long fast string: ${val}`) } if (isNaN(low) || isNaN(high)) {
throw new Error(`Invalid long fast string: ${val}`)
}
return new Long(low, high, unsigned) return new Long(low, high, unsigned)
} }
@ -166,19 +169,19 @@ export class LongMap<V> {
} }
private _setForObj(key: Long, value: V): void { private _setForObj(key: Long, value: V): void {
this._obj![longToFastString(key)] = value this._obj[longToFastString(key)] = value
} }
private _hasForObj(key: Long): boolean { private _hasForObj(key: Long): boolean {
return longToFastString(key) in this._obj! return longToFastString(key) in this._obj
} }
private _getForObj(key: Long): V | undefined { private _getForObj(key: Long): V | undefined {
return this._obj![longToFastString(key)] return this._obj[longToFastString(key)]
} }
private _deleteForObj(key: Long): void { private _deleteForObj(key: Long): void {
delete this._obj![longToFastString(key)] delete this._obj[longToFastString(key)]
} }
private *_keysForObj(unsigned?: boolean): IterableIterator<Long> { private *_keysForObj(unsigned?: boolean): IterableIterator<Long> {
@ -188,7 +191,7 @@ export class LongMap<V> {
} }
private *_valuesForObj(): IterableIterator<V> { private *_valuesForObj(): IterableIterator<V> {
yield* Object.values(this._obj!) as any yield* Object.values(this._obj) as any
} }
private _clearForObj(): void { private _clearForObj(): void {
@ -254,22 +257,22 @@ export class LongSet {
private _addForObj(val: Long) { private _addForObj(val: Long) {
const k = longToFastString(val) const k = longToFastString(val)
if (k in this._obj!) return if (k in this._obj) return
this._obj![k] = true this._obj[k] = true
this._objSize! += 1 this._objSize! += 1
} }
private _deleteForObj(val: Long) { private _deleteForObj(val: Long) {
const k = longToFastString(val) const k = longToFastString(val)
if (!(k in this._obj!)) return if (!(k in this._obj)) return
delete this._obj![k] delete this._obj[k]
this._objSize! -= 1 this._objSize! -= 1
} }
private _hasForObj(val: Long) { private _hasForObj(val: Long) {
return longToFastString(val) in this._obj! return longToFastString(val) in this._obj
} }
private _clearForObj() { private _clearForObj() {

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return */
// ^^ because of performance reasons // ^^ because of performance reasons
import { LongMap } from './long-utils' import { LongMap } from './long-utils'
@ -130,7 +131,7 @@ export class LruMap<K extends string | number, V> {
if (oldest) { if (oldest) {
if (oldest.p) { if (oldest.p) {
this._last = oldest.p this._last = oldest.p
this._last!.n = undefined this._last.n = undefined
} else { } else {
// exhausted // exhausted
this._last = undefined this._last = undefined

View file

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
// ^^ because of performance reasons // ^^ because of performance reasons
import Long from 'long' import Long from 'long'

View file

@ -12,7 +12,7 @@ describe('IntermediatePacketCodec', () => {
it('should correctly parse immediate framing', (done) => { it('should correctly parse immediate framing', (done) => {
const codec = new IntermediatePacketCodec() const codec = new IntermediatePacketCodec()
codec.on('packet', (data) => { codec.on('packet', (data: Buffer) => {
expect([...data]).eql([5, 1, 2, 3, 4]) expect([...data]).eql([5, 1, 2, 3, 4])
done() done()
}) })
@ -21,7 +21,7 @@ describe('IntermediatePacketCodec', () => {
it('should correctly parse incomplete framing', (done) => { it('should correctly parse incomplete framing', (done) => {
const codec = new IntermediatePacketCodec() const codec = new IntermediatePacketCodec()
codec.on('packet', (data) => { codec.on('packet', (data: Buffer) => {
expect([...data]).eql([5, 1, 2, 3, 4]) expect([...data]).eql([5, 1, 2, 3, 4])
done() done()
}) })
@ -34,7 +34,7 @@ describe('IntermediatePacketCodec', () => {
let number = 0 let number = 0
codec.on('packet', (data) => { codec.on('packet', (data: Buffer) => {
if (number === 0) { if (number === 0) {
expect([...data]).eql([5, 1, 2, 3, 4]) expect([...data]).eql([5, 1, 2, 3, 4])
number = 1 number = 1
@ -51,7 +51,7 @@ describe('IntermediatePacketCodec', () => {
it('should correctly parse transport errors', (done) => { it('should correctly parse transport errors', (done) => {
const codec = new IntermediatePacketCodec() const codec = new IntermediatePacketCodec()
codec.on('error', (err) => { codec.on('error', (err: TransportError) => {
expect(err).to.have.instanceOf(TransportError) expect(err).to.have.instanceOf(TransportError)
expect(err.code).eq(404) expect(err.code).eq(404)
done() done()
@ -63,7 +63,7 @@ describe('IntermediatePacketCodec', () => {
it('should reset when called reset()', (done) => { it('should reset when called reset()', (done) => {
const codec = new IntermediatePacketCodec() const codec = new IntermediatePacketCodec()
codec.on('packet', (data) => { codec.on('packet', (data: Buffer) => {
expect([...data]).eql([1, 2, 3, 4, 5]) expect([...data]).eql([1, 2, 3, 4, 5])
done() done()
}) })

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests"
] ]
} }

View file

@ -36,7 +36,7 @@ export class CallbackDataBuilder<T extends string> {
.map((f) => { .map((f) => {
const val = obj[f] const val = obj[f]
if (val.indexOf(this.sep) > -1) { if (val.includes(this.sep)) {
throw new MtArgumentError( throw new MtArgumentError(
`Value for ${f} ${val} contains separator ${this.sep} and cannot be used.`, `Value for ${f} ${val} contains separator ${this.sep} and cannot be used.`,
) )

View file

@ -1,4 +1,4 @@
/* eslint-disable max-depth */ /* eslint-disable */
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */ /* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types */
// ^^ will be looked into in MTQ-29 // ^^ will be looked into in MTQ-29
@ -122,7 +122,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
constructor( constructor(
client?: TelegramClient | IStateStorage | StateKeyDelegate, client?: TelegramClient | IStateStorage | StateKeyDelegate,
storage?: IStateStorage | StateKeyDelegate, storage?: IStateStorage | StateKeyDelegate,
key?: StateKeyDelegate, key?: StateKeyDelegate
) { ) {
this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this) this.dispatchRawUpdate = this.dispatchRawUpdate.bind(this)
this.dispatchUpdate = this.dispatchUpdate.bind(this) this.dispatchUpdate = this.dispatchUpdate.bind(this)
@ -196,15 +196,14 @@ export class Dispatcher<State = never, SceneName extends string = string> {
*/ */
dispatchRawUpdate( dispatchRawUpdate(
update: tl.TypeUpdate | tl.TypeMessage, update: tl.TypeUpdate | tl.TypeMessage,
peers: PeersIndex, peers: PeersIndex
): void { ): void {
if (!this._client) return if (!this._client) return
// order does not matter in the dispatcher, // order does not matter in the dispatcher,
// so we can handle each update in its own task // so we can handle each update in its own task
this.dispatchRawUpdateNow(update, peers).catch((err) => this.dispatchRawUpdateNow(update, peers).catch((err) =>
// eslint-disable-next-line dot-notation this._client!['_emitError'](err)
this._client!['_emitError'](err),
) )
} }
@ -222,7 +221,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
*/ */
async dispatchRawUpdateNow( async dispatchRawUpdateNow(
update: tl.TypeUpdate | tl.TypeMessage, update: tl.TypeUpdate | tl.TypeMessage,
peers: PeersIndex, peers: PeersIndex
): Promise<boolean> { ): Promise<boolean> {
if (!this._client) return false if (!this._client) return false
@ -282,8 +281,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
// order does not matter in the dispatcher, // order does not matter in the dispatcher,
// so we can handle each update in its own task // so we can handle each update in its own task
this.dispatchUpdateNow(update).catch((err) => this.dispatchUpdateNow(update).catch((err) =>
// eslint-disable-next-line dot-notation this._client!['_emitError'](err)
this._client!['_emitError'](err),
) )
} }
@ -307,7 +305,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
// this is getting a bit crazy lol // this is getting a bit crazy lol
parsedState?: UpdateState<State, SceneName> | null, parsedState?: UpdateState<State, SceneName> | null,
parsedScene?: string | null, parsedScene?: string | null,
forceScene?: true, forceScene?: true
): Promise<boolean> { ): Promise<boolean> {
if (!this._client) return false if (!this._client) return false
@ -349,7 +347,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
update, update,
parsedState, parsedState,
parsedScene, parsedScene,
true, true
) )
} }
} }
@ -369,17 +367,16 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if ( if (
!this._customStateKeyDelegate || !this._customStateKeyDelegate ||
(customKey = await this._customStateKeyDelegate( (customKey = await this._customStateKeyDelegate(
update.data, update.data
)) ))
) { ) {
parsedState = new UpdateState( parsedState = new UpdateState(
this._storage,
this._storage!,
key, key,
this._scene ?? null, this._scene ?? null,
this._sceneScoped, this._sceneScoped,
this._customStorage, this._customStorage,
customKey, customKey
) )
} }
} else { } else {
@ -421,12 +418,12 @@ export class Dispatcher<State = never, SceneName extends string = string> {
!h.check || !h.check ||
(await h.check( (await h.check(
update.data as any, update.data as any,
parsedState as never, parsedState as never
)) ))
) { ) {
result = await h.callback( result = await h.callback(
update.data as any, update.data as any,
parsedState as never, parsedState as never
) )
handled = true handled = true
} else continue } else continue
@ -443,16 +440,15 @@ export class Dispatcher<State = never, SceneName extends string = string> {
case 'scene': { case 'scene': {
if (!parsedState) { if (!parsedState) {
throw new MtArgumentError( throw new MtArgumentError(
'Cannot use ToScene without state', 'Cannot use ToScene without state'
) )
} }
// eslint-disable-next-line dot-notation
const scene = parsedState['_scene'] const scene = parsedState['_scene']
if (!scene) { if (!scene) {
throw new MtArgumentError( throw new MtArgumentError(
'Cannot use ToScene without entering a scene', 'Cannot use ToScene without entering a scene'
) )
} }
@ -462,7 +458,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
update, update,
undefined, undefined,
scene, scene,
true, true
) )
} }
} }
@ -474,7 +470,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
const handled = await this._errorHandler( const handled = await this._errorHandler(
e, e,
update, update,
parsedState as never, parsedState as never
) )
if (!handled) throw e if (!handled) throw e
} else { } else {
@ -527,7 +523,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
*/ */
removeUpdateHandler( removeUpdateHandler(
handler: UpdateHandler | UpdateHandler['name'] | 'all', handler: UpdateHandler | UpdateHandler['name'] | 'all',
group = 0, group = 0
): void { ): void {
if (group !== -1 && !(group in this._groups)) { if (group !== -1 && !(group in this._groups)) {
return return
@ -581,7 +577,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
update: ParsedUpdate & T, update: ParsedUpdate & T,
state?: UpdateState<State, SceneName> state?: UpdateState<State, SceneName>
) => MaybeAsync<boolean>) ) => MaybeAsync<boolean>)
| null, | null
): void { ): void {
if (handler) this._errorHandler = handler if (handler) this._errorHandler = handler
else this._errorHandler = undefined else this._errorHandler = undefined
@ -605,7 +601,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
update: ParsedUpdate & T, update: ParsedUpdate & T,
state?: UpdateState<State, SceneName> state?: UpdateState<State, SceneName>
) => MaybeAsync<PropagationAction | void>) ) => MaybeAsync<PropagationAction | void>)
| null, | null
): void { ): void {
if (handler) this._preUpdateHandler = handler if (handler) this._preUpdateHandler = handler
else this._preUpdateHandler = undefined else this._preUpdateHandler = undefined
@ -630,7 +626,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
update: ParsedUpdate & T, update: ParsedUpdate & T,
state?: UpdateState<State, SceneName> state?: UpdateState<State, SceneName>
) => MaybeAsync<void>) ) => MaybeAsync<void>)
| null, | null
): void { ): void {
if (handler) this._postUpdateHandler = handler if (handler) this._postUpdateHandler = handler
else this._postUpdateHandler = undefined else this._postUpdateHandler = undefined
@ -643,9 +639,11 @@ export class Dispatcher<State = never, SceneName extends string = string> {
propagateErrorToParent( propagateErrorToParent(
err: Error, err: Error,
update: ParsedUpdate, update: ParsedUpdate,
state?: UpdateState<State, SceneName>, state?: UpdateState<State, SceneName>
): MaybeAsync<boolean> { ): MaybeAsync<boolean> {
if (!this.parent) { throw new MtArgumentError('This dispatcher is not a child') } if (!this.parent) {
throw new MtArgumentError('This dispatcher is not a child')
}
if (this.parent._errorHandler) { if (this.parent._errorHandler) {
return this.parent._errorHandler(err, update, state) return this.parent._errorHandler(err, update, state)
@ -667,9 +665,9 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if (child._client) { if (child._client) {
throw new MtArgumentError( throw new MtArgumentError(
'Provided dispatcher is ' + 'Provided dispatcher is ' +
(child._parent ? (child._parent
'already a child. Use parent.removeChild() before calling addChild()' : ? 'already a child. Use parent.removeChild() before calling addChild()'
'already bound to a client. Use unbind() before calling addChild()'), : 'already bound to a client. Use unbind() before calling addChild()')
) )
} }
@ -695,7 +693,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* @param child Other dispatcher * @param child Other dispatcher
*/ */
addChild(child: Dispatcher<State, SceneName>): void { addChild(child: Dispatcher<State, SceneName>): void {
if (this._children.indexOf(child) > -1) return if (this._children.includes(child)) return
this._prepareChild(child) this._prepareChild(child)
this._children.push(child) this._children.push(child)
@ -740,13 +738,13 @@ export class Dispatcher<State = never, SceneName extends string = string> {
addScene( addScene(
uid: SceneName, uid: SceneName,
scene: Dispatcher<any, SceneName>, scene: Dispatcher<any, SceneName>,
scoped = true, scoped = true
): void { ): void {
if (!this._scenes) this._scenes = {} if (!this._scenes) this._scenes = {}
if (uid in this._scenes) { if (uid in this._scenes) {
throw new MtArgumentError( throw new MtArgumentError(
`Scene with UID ${uid} is already registered!`, `Scene with UID ${uid} is already registered!`
) )
} }
@ -756,7 +754,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if (scene._scene) { if (scene._scene) {
throw new MtArgumentError( throw new MtArgumentError(
`This dispatcher is already registered as scene ${scene._scene}`, `This dispatcher is already registered as scene ${scene._scene}`
) )
} }
@ -807,7 +805,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
extend(other: Dispatcher<State, SceneName>): void { extend(other: Dispatcher<State, SceneName>): void {
if (other._customStorage || other._customStateKeyDelegate) { if (other._customStorage || other._customStateKeyDelegate) {
throw new MtArgumentError( throw new MtArgumentError(
'Provided dispatcher has custom storage and cannot be extended from.', 'Provided dispatcher has custom storage and cannot be extended from.'
) )
} }
@ -849,7 +847,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
this.addScene( this.addScene(
key as any, key as any,
myScenes[key] as any, myScenes[key] as any,
myScenes[key]._sceneScoped as any, myScenes[key]._sceneScoped as any
) )
}) })
} }
@ -903,7 +901,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
key as any, key as any,
scene as any, scene as any,
this._scenes![key]._sceneScoped as any, this._scenes![key]._sceneScoped as any
) )
}) })
} }
@ -937,22 +935,21 @@ export class Dispatcher<State = never, SceneName extends string = string> {
object: Parameters<StateKeyDelegate>[0] object: Parameters<StateKeyDelegate>[0]
): Promise<UpdateState<S, SceneName>> ): Promise<UpdateState<S, SceneName>>
getState<S = State>( getState<S = State>(
object: string | Parameters<StateKeyDelegate>[0], object: string | Parameters<StateKeyDelegate>[0]
): MaybeAsync<UpdateState<S, SceneName>> { ): MaybeAsync<UpdateState<S, SceneName>> {
if (!this._storage) { if (!this._storage) {
throw new MtArgumentError( throw new MtArgumentError(
'Cannot use getUpdateState() filter without state storage', 'Cannot use getUpdateState() filter without state storage'
) )
} }
if (typeof object === 'string') { if (typeof object === 'string') {
return new UpdateState( return new UpdateState(
this._storage,
this._storage!,
object, object,
this._scene ?? null, this._scene ?? null,
this._sceneScoped, this._sceneScoped,
this._customStorage, this._customStorage
) )
} }
@ -963,12 +960,11 @@ export class Dispatcher<State = never, SceneName extends string = string> {
if (!this._customStateKeyDelegate) { if (!this._customStateKeyDelegate) {
return new UpdateState( return new UpdateState(
this._storage!, this._storage!,
key, key,
this._scene ?? null, this._scene ?? null,
this._sceneScoped, this._sceneScoped,
this._customStorage, this._customStorage
) )
} }
@ -976,20 +972,19 @@ export class Dispatcher<State = never, SceneName extends string = string> {
(customKey) => { (customKey) => {
if (!customKey) { if (!customKey) {
throw new MtArgumentError( throw new MtArgumentError(
'Cannot derive custom key from given object', 'Cannot derive custom key from given object'
) )
} }
return new UpdateState( return new UpdateState(
this._storage!, this._storage!,
key, key,
this._scene ?? null, this._scene ?? null,
this._sceneScoped, this._sceneScoped,
this._customStorage, this._customStorage,
customKey, customKey
) )
}, }
) )
}) })
} }
@ -1001,7 +996,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
* ignoring local custom storage, key delegate and scene scope. * ignoring local custom storage, key delegate and scene scope.
*/ */
getGlobalState<T>( getGlobalState<T>(
object: Parameters<StateKeyDelegate>[0], object: Parameters<StateKeyDelegate>[0]
): Promise<UpdateState<T, SceneName>> { ): Promise<UpdateState<T, SceneName>> {
if (!this._parent) { if (!this._parent) {
throw new MtArgumentError('This dispatcher does not have a parent') throw new MtArgumentError('This dispatcher does not have a parent')
@ -1013,11 +1008,10 @@ export class Dispatcher<State = never, SceneName extends string = string> {
} }
return new UpdateState( return new UpdateState(
this._storage!, this._storage!,
key, key,
this._scene ?? null, this._scene ?? null,
false, false
) )
}) })
} }
@ -1028,7 +1022,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
name: UpdateHandler['name'], name: UpdateHandler['name'],
filter: any, filter: any,
handler?: any, handler?: any,
group?: number, group?: number
): void { ): void {
if (typeof handler === 'number' || typeof handler === 'undefined') { if (typeof handler === 'number' || typeof handler === 'undefined') {
this.addUpdateHandler( this.addUpdateHandler(
@ -1036,7 +1030,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
name, name,
callback: filter, callback: filter,
}, },
handler, handler
) )
} else { } else {
this.addUpdateHandler( this.addUpdateHandler(
@ -1045,7 +1039,7 @@ export class Dispatcher<State = never, SceneName extends string = string> {
callback: handler, callback: handler,
check: filter, check: filter,
}, },
group, group
) )
} }
} }

View file

@ -209,18 +209,18 @@ export namespace filters {
fn2: UpdateFilter<Base, Mod2, State2>, fn2: UpdateFilter<Base, Mod2, State2>,
): UpdateFilter<Base, Mod1 & Mod2, State1 | State2> { ): UpdateFilter<Base, Mod1 & Mod2, State1 | State2> {
return (upd, state) => { return (upd, state) => {
const res1 = fn1(upd, state as any) const res1 = fn1(upd, state as UpdateState<State1>)
if (typeof res1 === 'boolean') { if (typeof res1 === 'boolean') {
if (!res1) return false if (!res1) return false
return fn2(upd, state as any) return fn2(upd, state as UpdateState<State2>)
} }
return res1.then((r1) => { return res1.then((r1) => {
if (!r1) return false if (!r1) return false
return fn2(upd, state as any) return fn2(upd, state as UpdateState<State2>)
}) })
} }
} }
@ -247,18 +247,18 @@ export namespace filters {
fn2: UpdateFilter<Base, Mod2, State2>, fn2: UpdateFilter<Base, Mod2, State2>,
): UpdateFilter<Base, Mod1 | Mod2, State1 | State2> { ): UpdateFilter<Base, Mod1 | Mod2, State1 | State2> {
return (upd, state) => { return (upd, state) => {
const res1 = fn1(upd, state as any) const res1 = fn1(upd, state as UpdateState<State1>)
if (typeof res1 === 'boolean') { if (typeof res1 === 'boolean') {
if (res1) return true if (res1) return true
return fn2(upd, state as any) return fn2(upd, state as UpdateState<State2>)
} }
return res1.then((r1) => { return res1.then((r1) => {
if (r1) return true if (r1) return true
return fn2(upd, state as any) return fn2(upd, state as UpdateState<State2>)
}) })
} }
} }
@ -947,7 +947,7 @@ export namespace filters {
const m = txt.match(regex) const m = txt.match(regex)
if (m) { if (m) {
(obj as any).match = m (obj as typeof obj & { match: RegExpMatchArray }).match = m
return true return true
} }
@ -1002,14 +1002,14 @@ export namespace filters {
return (obj) => { return (obj) => {
const txt = extractText(obj) const txt = extractText(obj)
return txt != null && txt.toLowerCase().indexOf(str) > -1 return txt != null && txt.toLowerCase().includes(str)
} }
} }
return (obj) => { return (obj) => {
const txt = extractText(obj) const txt = extractText(obj)
return txt != null && txt.indexOf(str) > -1 return txt != null && txt.includes(str)
} }
} }
@ -1157,13 +1157,16 @@ export namespace filters {
// we use .replace to iterate over global regex, not to replace the text // we use .replace to iterate over global regex, not to replace the text
withoutPrefix withoutPrefix
.slice(m[0].length) .slice(m[0].length)
.replace(argumentsRe, ($0, $1, $2, $3) => { .replace(
match.push( argumentsRe,
($2 || $3 || '').replace(unescapeRe, '$1'), ($0, $1, $2: string, $3: string) => {
) match.push(
($2 || $3 || '').replace(unescapeRe, '$1'),
)
return '' return ''
}) },
)
;(msg as Message & { command: string[] }).command = match ;(msg as Message & { command: string[] }).command = match
return true return true

View file

@ -18,7 +18,7 @@ export interface IStateStorage {
* *
* @param key Key of the state, as defined by {@link StateKeyDelegate} * @param key Key of the state, as defined by {@link StateKeyDelegate}
*/ */
getState(key: string): MaybeAsync<unknown | null> getState(key: string): MaybeAsync<unknown>
/** /**
* Save state to the storage * Save state to the storage

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests"
] ]
} }

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests",
] ]
} }

View file

@ -28,9 +28,14 @@ export function html(
if (typeof it === 'boolean' || !it) return if (typeof it === 'boolean' || !it) return
if (typeof it === 'string') { if (typeof it === 'string') {
it = HtmlMessageEntityParser.escape(it, Boolean(str.match(/=['"]$/))) it = HtmlMessageEntityParser.escape(
it,
Boolean(str.match(/=['"]$/)),
)
} else { } else {
if (it.mode && it.mode !== 'html') { throw new Error(`Incompatible parse mode: ${it.mode}`) } if (it.mode && it.mode !== 'html') {
throw new Error(`Incompatible parse mode: ${it.mode}`)
}
it = it.value it = it.value
} }
@ -92,7 +97,7 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
function processPendingText(tagEnd = false) { function processPendingText(tagEnd = false) {
if (!pendingText.length) return if (!pendingText.length) return
if (!stacks.pre?.length) { if (!stacks.pre.length) {
pendingText = pendingText.replace(/[^\S\u00A0]+/gs, ' ') pendingText = pendingText.replace(/[^\S\u00A0]+/gs, ' ')
if (tagEnd) pendingText = pendingText.trimEnd() if (tagEnd) pendingText = pendingText.trimEnd()
@ -119,7 +124,7 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
processPendingText() processPendingText()
// ignore tags inside pre (except pre) // ignore tags inside pre (except pre)
if (name !== 'pre' && stacks.pre?.length) return if (name !== 'pre' && stacks.pre.length) return
let entity: tl.TypeMessageEntity let entity: tl.TypeMessageEntity
@ -262,9 +267,9 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
name = name.toLowerCase() name = name.toLowerCase()
// ignore tags inside pre (except pre) // ignore tags inside pre (except pre)
if (name !== 'pre' && stacks.pre?.length) return if (name !== 'pre' && stacks.pre.length) return
const entity = stacks[name]?.pop() const entity = stacks[name].pop()
if (!entity) return // unmatched close tag if (!entity) return // unmatched close tag
@ -338,7 +343,9 @@ export class HtmlMessageEntityParser implements IMessageEntityParser {
relativeOffset = lastOffset relativeOffset = lastOffset
} }
if (length <= 0 || relativeOffset >= end || relativeOffset < 0) { continue } if (length <= 0 || relativeOffset >= end || relativeOffset < 0) {
continue
}
let skip = false let skip = false

View file

@ -620,7 +620,7 @@ describe('HtmlMessageEntityParser', () => {
expect(() => html`${unsafeString}`.value).not.throw(Error) expect(() => html`${unsafeString}`.value).not.throw(Error)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-expect-error
expect(() => html`${unsafeString2}`.value).throw(Error) expect(() => html`${unsafeString2}`.value).throw(Error)
}) })
}) })

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests",
] ]
} }

View file

@ -79,7 +79,9 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport {
} }
connect(dc: tl.RawDcOption): void { connect(dc: tl.RawDcOption): void {
if (this._state !== TransportState.Idle) { throw new Error('Transport is not IDLE') } if (this._state !== TransportState.Idle) {
throw new Error('Transport is not IDLE')
}
if (!this.packetCodecInitialized) { if (!this.packetCodecInitialized) {
this._packetCodec.on('error', (err) => this.emit('error', err)) this._packetCodec.on('error', (err) => this.emit('error', err))
@ -133,9 +135,10 @@ export abstract class BaseHttpProxyTcpTransport extends BaseTcpTransport {
} }
headers['Proxy-Connection'] = 'Keep-Alive' headers['Proxy-Connection'] = 'Keep-Alive'
const packet = `CONNECT ${ip} HTTP/1.1${Object.keys(headers).map( const headersStr = Object.keys(headers)
(k) => `\r\n${k}: ${headers[k]}`, .map((k) => `\r\n${k}: ${headers[k]}`)
)}\r\n\r\n` .join('')
const packet = `CONNECT ${ip} HTTP/1.1${headersStr}\r\n\r\n`
this._socket!.write(packet) this._socket!.write(packet)
this._socket!.once('data', (msg) => { this._socket!.once('data', (msg) => {

View file

@ -1,12 +1,11 @@
import type { BotChatJoinRequestUpdate, BotStoppedUpdate, CallbackQuery, ChatMemberUpdate, ChosenInlineResult, InlineQuery, Message, ParsedUpdate, PollVoteUpdate, User } from '@mtcute/client' import type * as clientNs from '@mtcute/client'
import { I18nStrings, I18nValue } from './types' import { I18nStrings, I18nValue } from './types'
// eslint-disable-next-line @typescript-eslint/no-explicit-any let client: typeof clientNs
let client: any
try { try {
client = require('@mtcute/client') client = require('@mtcute/client') as typeof clientNs
} catch (e) {} } catch (e) {}
/** /**
@ -43,7 +42,7 @@ export function createI18nStringsIndex(
* @param update Update to extract language from * @param update Update to extract language from
*/ */
export function extractLanguageFromUpdate( export function extractLanguageFromUpdate(
update: ParsedUpdate['data'], update: clientNs.ParsedUpdate['data'],
): string | null | undefined { ): string | null | undefined {
if (!client) { if (!client) {
throw new Error( throw new Error(
@ -54,23 +53,26 @@ export function extractLanguageFromUpdate(
switch (update.constructor) { switch (update.constructor) {
case client.Message: case client.Message:
// if sender is Chat it will just be undefined // if sender is Chat it will just be undefined
return ((update as Message).sender as User).language return ((update as clientNs.Message).sender as clientNs.User)
.language
case client.PollVoteUpdate:
// if peer is Chat it will just be undefined
return ((update as clientNs.PollVoteUpdate).peer as clientNs.User)
.language
case client.ChatMemberUpdate: case client.ChatMemberUpdate:
case client.InlineQuery: case client.InlineQuery:
case client.ChosenInlineResult: case client.ChosenInlineResult:
case client.CallbackQuery: case client.CallbackQuery:
case client.PollVoteUpdate:
case client.BotStoppedUpdate: case client.BotStoppedUpdate:
case client.BotChatJoinRequestUpdate: case client.BotChatJoinRequestUpdate:
return ( return (
update as update as
| ChatMemberUpdate | clientNs.ChatMemberUpdate
| InlineQuery | clientNs.InlineQuery
| ChosenInlineResult | clientNs.ChosenInlineResult
| CallbackQuery | clientNs.CallbackQuery
| PollVoteUpdate | clientNs.BotStoppedUpdate
| BotStoppedUpdate | clientNs.BotChatJoinRequestUpdate
| BotChatJoinRequestUpdate
).user.language ).user.language
} }

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests",
] ]
} }

View file

@ -1,11 +1,15 @@
import Long from 'long' import Long from 'long'
import type { FormattedString, IMessageEntityParser, MessageEntity, tl } from '@mtcute/client' import type {
FormattedString,
IMessageEntityParser,
MessageEntity,
tl,
} from '@mtcute/client'
const MENTION_REGEX = const MENTION_REGEX =
/^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/ /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/
const EMOJI_REGEX = const EMOJI_REGEX = /^tg:\/\/emoji\?id=(-?\d+)/
/^tg:\/\/emoji\?id=(-?\d+)/
const TAG_BOLD = '**' const TAG_BOLD = '**'
const TAG_ITALIC = '__' const TAG_ITALIC = '__'
@ -41,7 +45,9 @@ export function md(
if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it) if (typeof it === 'string') it = MarkdownMessageEntityParser.escape(it)
else { else {
if (it.mode && it.mode !== 'markdown') { throw new Error(`Incompatible parse mode: ${it.mode}`) } if (it.mode && it.mode !== 'markdown') {
throw new Error(`Incompatible parse mode: ${it.mode}`)
}
it = it.value it = it.value
} }
@ -124,12 +130,12 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
pos += 3 pos += 3
continue continue
// closed with single or double backtick // closed with single or double backtick
// i.e. not closed actually! this is totally valid md: // i.e. not closed actually! this is totally valid md:
// ```javascript // ```javascript
// const a = ``; // const a = ``;
// ``` // ```
// compensate that `pos` change we made earliers // compensate that `pos` change we made earliers
} else if (c === '\n') { } else if (c === '\n') {
pos -= 1 pos -= 1
} }
@ -165,7 +171,9 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
pos += 1 // ) pos += 1 // )
if (pos > text.length) { throw new Error('Malformed LINK entity, expected )') } if (pos > text.length) {
throw new Error('Malformed LINK entity, expected )')
}
if (url.length) { if (url.length) {
ent.length = result.length - ent.offset ent.length = result.length - ent.offset
@ -200,9 +208,8 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
).userId = userId ).userId = userId
} }
} else if ((m = EMOJI_REGEX.exec(url))) { } else if ((m = EMOJI_REGEX.exec(url))) {
( (ent as tl.Mutable<tl.RawMessageEntityCustomEmoji>)._ =
ent as tl.Mutable<tl.RawMessageEntityCustomEmoji> 'messageEntityCustomEmoji'
)._ = 'messageEntityCustomEmoji'
;( ;(
ent as tl.Mutable<tl.RawMessageEntityCustomEmoji> ent as tl.Mutable<tl.RawMessageEntityCustomEmoji>
).documentId = Long.fromString(m[1]) ).documentId = Long.fromString(m[1])
@ -224,10 +231,11 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
pos += 1 pos += 1
insideLink = true insideLink = true
if (!('link' in stacks)) stacks.link = [] if (!('link' in stacks)) stacks.link = []
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
stacks.link.push({ stacks.link.push({
offset: result.length, offset: result.length,
length: 0, length: 0,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any) // other fields are added after the second part } as any) // other fields are added after the second part
continue continue
} }
@ -308,9 +316,7 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
if (isBegin) { if (isBegin) {
stacks[type].push({ stacks[type].push({
// this is valid, but idk how to make typescript happy _: `messageEntity${type}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_: ('messageEntity' + type) as any,
offset: result.length, offset: result.length,
length: 0, length: 0,
}) })
@ -379,7 +385,8 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
end += escapedPos end += escapedPos
} }
let startTag; let endTag: string let startTag
let endTag: string
switch (type) { switch (type) {
case 'bold': case 'bold':
@ -420,7 +427,7 @@ export class MarkdownMessageEntityParser implements IMessageEntityParser {
break break
case 'emoji': case 'emoji':
startTag = '[' startTag = '['
endTag = `](tg://emoji?id=${entity.emojiId})` endTag = `](tg://emoji?id=${entity.emojiId!.toString()})`
break break
default: default:
continue continue

View file

@ -689,7 +689,7 @@ describe('MarkdownMessageEntityParser', () => {
expect(() => md`${unsafeString}`.value).not.throw(Error) expect(() => md`${unsafeString}`.value).not.throw(Error)
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-expect-error
expect(() => md`${unsafeString2}`.value).throw(Error) expect(() => md`${unsafeString2}`.value).throw(Error)
}) })
}) })

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src" "./src",
"./tests",
] ]
} }

View file

@ -112,8 +112,7 @@ export class MtProxyTcpTransport extends BaseTcpTransport {
this.packetCodecInitialized = false this.packetCodecInitialized = false
this._packetCodec.reset() this._packetCodec.reset()
this._packetCodec.removeAllListeners() this._packetCodec.removeAllListeners()
// eslint-disable-next-line @typescript-eslint/no-explicit-any delete (this as Partial<MtProxyTcpTransport>)._packetCodec
delete (this as any)._packetCodec
} }
if (!this._packetCodec) { if (!this._packetCodec) {
@ -155,12 +154,16 @@ export class MtProxyTcpTransport extends BaseTcpTransport {
this._socket = connect( this._socket = connect(
this._proxy.port, this._proxy.port,
this._proxy.host, this._proxy.host,
// MTQ-55
// eslint-disable-next-line @typescript-eslint/no-misused-promises
this._handleConnectFakeTls.bind(this), this._handleConnectFakeTls.bind(this),
) )
} else { } else {
this._socket = connect( this._socket = connect(
this._proxy.port, this._proxy.port,
this._proxy.host, this._proxy.host,
// MTQ-55
this.handleConnect.bind(this), this.handleConnect.bind(this),
) )
this._socket.on('data', (data) => this._packetCodec.feed(data)) this._socket.on('data', (data) => this._packetCodec.feed(data))
@ -219,18 +222,17 @@ export class MtProxyTcpTransport extends BaseTcpTransport {
} }
} }
const packetHandler = async (buf: Buffer): Promise<void> => { const packetHandler = (buf: Buffer): void => {
try { checkHelloResponse(buf)
await checkHelloResponse(buf) .then(() => {
this._socket!.off('data', packetHandler)
this._socket!.on('data', (data) =>
this._packetCodec.feed(data),
)
this._socket!.on('data', (data) => return this.handleConnect()
this._packetCodec.feed(data), })
) .catch((err) => this._socket!.emit('error', err))
this._socket!.off('data', packetHandler)
this.handleConnect()
} catch (e) {
this._socket!.emit('error', e)
}
} }
this._socket!.write(hello) this._socket!.write(hello)

View file

@ -16,7 +16,7 @@ export { SqliteStorage }
let nativeCrypto: any let nativeCrypto: any
try { try {
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line
nativeCrypto = require('@mtcute/crypto-node').NodeNativeCryptoProvider nativeCrypto = require('@mtcute/crypto-node').NodeNativeCryptoProvider
} catch (e) {} } catch (e) {}
@ -57,6 +57,7 @@ export interface NodeTelegramClientOptions
export class NodeTelegramClient extends TelegramClient { export class NodeTelegramClient extends TelegramClient {
constructor(opts: NodeTelegramClientOptions) { constructor(opts: NodeTelegramClientOptions) {
super({ super({
// eslint-disable-next-line
crypto: nativeCrypto ? () => new nativeCrypto() : undefined, crypto: nativeCrypto ? () => new nativeCrypto() : undefined,
...opts, ...opts,
storage: storage:
@ -68,7 +69,9 @@ export class NodeTelegramClient extends TelegramClient {
this.registerParseMode(new HtmlMessageEntityParser()) this.registerParseMode(new HtmlMessageEntityParser())
this.registerParseMode(new MarkdownMessageEntityParser()) this.registerParseMode(new MarkdownMessageEntityParser())
if (opts.defaultParseMode) { this.setDefaultParseMode(opts.defaultParseMode) } if (opts.defaultParseMode) {
this.setDefaultParseMode(opts.defaultParseMode)
}
} }
private _rl?: RlInterface private _rl?: RlInterface
@ -98,7 +101,9 @@ export class NodeTelegramClient extends TelegramClient {
if (!params.phone) params.phone = () => this.input('Phone > ') if (!params.phone) params.phone = () => this.input('Phone > ')
if (!params.code) params.code = () => this.input('Code > ') if (!params.code) params.code = () => this.input('Code > ')
if (!params.password) { params.password = () => this.input('2FA password > ') } if (!params.password) {
params.password = () => this.input('2FA password > ')
}
} }
return super.start(params).then((user) => { return super.start(params).then((user) => {

View file

@ -1,7 +1,7 @@
// ^^ because of this._socket. we know it's not null, almost everywhere, but TS doesn't // ^^ because of this._socket. we know it's not null, almost everywhere, but TS doesn't
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-expect-error
import { normalize } from 'ip6' import { normalize } from 'ip6'
import { connect } from 'net' import { connect } from 'net'
@ -115,8 +115,12 @@ function buildSocks5Auth(username: string, password: string) {
const usernameBuf = Buffer.from(username) const usernameBuf = Buffer.from(username)
const passwordBuf = Buffer.from(password) const passwordBuf = Buffer.from(password)
if (usernameBuf.length > 255) { throw new Error(`Too long username (${usernameBuf.length} > 255)`) } if (usernameBuf.length > 255) {
if (passwordBuf.length > 255) { throw new Error(`Too long password (${passwordBuf.length} > 255)`) } throw new Error(`Too long username (${usernameBuf.length} > 255)`)
}
if (passwordBuf.length > 255) {
throw new Error(`Too long password (${passwordBuf.length} > 255)`)
}
const buf = Buffer.alloc(3 + usernameBuf.length + passwordBuf.length) const buf = Buffer.alloc(3 + usernameBuf.length + passwordBuf.length)
buf[0] = 0x01 // VER of auth buf[0] = 0x01 // VER of auth
@ -129,7 +133,8 @@ function buildSocks5Auth(username: string, password: string) {
} }
function writeIpv6(ip: string, buf: Buffer, offset: number): void { function writeIpv6(ip: string, buf: Buffer, offset: number): void {
ip = normalize(ip) // eslint-disable-next-line @typescript-eslint/no-unsafe-call
ip = normalize(ip) as string
const parts = ip.split(':') const parts = ip.split(':')
if (parts.length !== 8) { if (parts.length !== 8) {
@ -193,9 +198,14 @@ export abstract class BaseSocksTcpTransport extends BaseTcpTransport {
constructor(proxy: SocksProxySettings) { constructor(proxy: SocksProxySettings) {
super() super()
if (proxy.version != null && proxy.version !== 4 && proxy.version !== 5) { if (
proxy.version != null &&
proxy.version !== 4 &&
proxy.version !== 5
) {
throw new SocksProxyConnectionError( throw new SocksProxyConnectionError(
proxy, proxy,
`Invalid SOCKS version: ${proxy.version}`, `Invalid SOCKS version: ${proxy.version}`,
) )
} }
@ -204,7 +214,9 @@ export abstract class BaseSocksTcpTransport extends BaseTcpTransport {
} }
connect(dc: tl.RawDcOption): void { connect(dc: tl.RawDcOption): void {
if (this._state !== TransportState.Idle) { throw new Error('Transport is not IDLE') } if (this._state !== TransportState.Idle) {
throw new Error('Transport is not IDLE')
}
if (!this.packetCodecInitialized) { if (!this.packetCodecInitialized) {
this._packetCodec.on('error', (err) => this.emit('error', err)) this._packetCodec.on('error', (err) => this.emit('error', err))

View file

@ -359,7 +359,7 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
private _getFromKv<T>(key: string): T | null { private _getFromKv<T>(key: string): T | null {
const row = this._statements.getKv.get(key) as { value: string } | null const row = this._statements.getKv.get(key) as { value: string } | null
return row ? JSON.parse(row.value) : null return row ? (JSON.parse(row.value) as T) : null
} }
private _setToKv(key: string, value: unknown, now = false): void { private _setToKv(key: string, value: unknown, now = false): void {
@ -375,7 +375,7 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
} }
private _runMany!: (stmts: [sqlite3.Statement, unknown[]][]) => void private _runMany!: (stmts: [sqlite3.Statement, unknown[]][]) => void
private _updateManyPeers!: (updates: unknown[]) => void private _updateManyPeers!: (updates: unknown[][]) => void
private _upgradeDatabase(from: number): void { private _upgradeDatabase(from: number): void {
if (from < 2 || from > CURRENT_VERSION) { if (from < 2 || from > CURRENT_VERSION) {
@ -461,14 +461,16 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
} }
// helper methods // helper methods
this._runMany = this._db.transaction((stmts) => { this._runMany = this._db.transaction(
stmts.forEach((stmt: [sqlite3.Statement, unknown[]]) => { (stmts: [sqlite3.Statement, unknown[]][]) => {
stmt[0].run(stmt[1]) stmts.forEach((stmt) => {
}) stmt[0].run(stmt[1])
}) })
},
)
this._updateManyPeers = this._db.transaction((data) => { this._updateManyPeers = this._db.transaction((data: unknown[][]) => {
data.forEach((it: unknown[]) => { data.forEach((it: unknown) => {
this._statements.updateCachedEnt.run(it) this._statements.updateCachedEnt.run(it)
}) })
}) })
@ -727,7 +729,7 @@ export class SqliteStorage implements ITelegramStorage, IStateStorage {
// IStateStorage implementation // IStateStorage implementation
getState(key: string, parse = true): unknown | null { getState(key: string, parse = true): unknown {
let val: FsmItem | undefined = this._fsmCache?.get(key) let val: FsmItem | undefined = this._fsmCache?.get(key)
const cached = val const cached = val

View file

@ -10,6 +10,8 @@ export function gzipInflate(buf: Buffer): Buffer {
return typedArrayToBuffer(inflate(buf)) return typedArrayToBuffer(inflate(buf))
} }
const ERROR_SIZE_LIMIT_REACHED = 'ERR_SIZE_LIMIT_REACHED'
class DeflateLimited extends Deflate { class DeflateLimited extends Deflate {
constructor(readonly limit: number) { constructor(readonly limit: number) {
super() super()
@ -21,7 +23,9 @@ class DeflateLimited extends Deflate {
this._size += (chunk as Uint8Array).length this._size += (chunk as Uint8Array).length
if (this._size > this.limit) { if (this._size > this.limit) {
throw 'ERR_SIZE' // caught locally
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw ERROR_SIZE_LIMIT_REACHED
} }
super.onData(chunk) super.onData(chunk)
@ -36,9 +40,9 @@ export function gzipDeflate(buf: Buffer, maxRatio?: number): Buffer | null {
try { try {
deflator.push(buf, true) deflator.push(buf, true)
} catch (e) { } catch (e) {
if (e === 'ERR_SIZE') return null if (e === ERROR_SIZE_LIMIT_REACHED) return null
throw e throw e
} }
return typedArrayToBuffer(deflator.result as Uint8Array) return typedArrayToBuffer(deflator.result)
} }

View file

@ -1,3 +1,5 @@
// eslint-disable-next-line max-len
/* eslint-disable @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-return */
import { expect } from 'chai' import { expect } from 'chai'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import Long from 'long' import Long from 'long'

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { expect } from 'chai' import { expect } from 'chai'
import { randomBytes } from 'crypto' import { randomBytes } from 'crypto'
import Long from 'long' import Long from 'long'

View file

@ -5,5 +5,6 @@
}, },
"include": [ "include": [
"./src", "./src",
"./tests",
] ]
} }

View file

@ -188,6 +188,23 @@ export function generateReaderCodeForTlEntries(
ret += generateReaderCodeForTlEntry(entry, params) + '\n' ret += generateReaderCodeForTlEntry(entry, params) + '\n'
}) })
const usedInBareVector: Record<string, 1> = {}
ret.replace(
new RegExp(`(?<=r\\.vector\\(${variableName}\\[)(\\d+)(?=])`, 'g'),
(_, id: string) => {
usedInBareVector[id] = 1
return _
},
)
for (const id of Object.keys(usedInBareVector)) {
ret = ret.replace(
new RegExp(`(?<=^${id}:function\\()r(?=\\))`, 'gm'),
'r=this',
)
}
if (params.includeMethodResults) { if (params.includeMethodResults) {
ret += '_results:{\n' ret += '_results:{\n'

View file

@ -60,7 +60,10 @@ export function generateWriterCodeForTlEntry(
if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry) if (entry.id === 0) entry.id = computeConstructorIdFromEntry(entry)
const name = bare ? entry.id : `'${entry.name}'` const name = bare ? entry.id : `'${entry.name}'`
let ret = `${name}:function(w${entry.arguments.length ? ',v' : ''}){` const defaultWriter = bare ? '=this' : ''
let ret = `${name}:function(w${defaultWriter}${
entry.arguments.length ? ',v' : ''
}){`
if (!bare) ret += `w.uint(${entry.id});` if (!bare) ret += `w.uint(${entry.id});`

View file

@ -5,7 +5,8 @@ import { generateWriterCodeForTlEntries } from './codegen/writer'
import { parseTlToEntries } from './parse' import { parseTlToEntries } from './parse'
function evalForResult<T>(js: string): T { function evalForResult<T>(js: string): T {
return new Function(js)() // eslint-disable-next-line @typescript-eslint/no-implied-eval
return new Function(js)() as T
} }
/** /**
@ -57,7 +58,7 @@ export function patchRuntimeTlSchema(
}, },
// ts is not smart enough // ts is not smart enough
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-expect-error
writerMap: { writerMap: {
...writers, ...writers,
...newWriters, ...newWriters,

View file

@ -68,9 +68,9 @@ export function stringifyArgumentType(
if (!modifiers) return type if (!modifiers) return type
let ret = type let ret = type
if (modifiers?.isBareUnion) ret = `%${ret}` if (modifiers.isBareUnion) ret = `%${ret}`
if (modifiers?.isVector) ret = `Vector<${ret}>` if (modifiers.isVector) ret = `Vector<${ret}>`
else if (modifiers?.isBareVector) ret = `vector<${ret}>` else if (modifiers.isBareVector) ret = `vector<${ret}>`
if (modifiers.predicate) ret = `${modifiers.predicate}?${ret}` if (modifiers.predicate) ret = `${modifiers.predicate}?${ret}`
return ret return ret

View file

@ -91,8 +91,8 @@ describe('mergeTlSchemas', () => {
).eq(expected.join('\n')) ).eq(expected.join('\n'))
} }
it('merges different constructors', () => { it('merges different constructors', async () => {
test( await test(
[ [
['testClass = Test;'], ['testClass = Test;'],
['testClass2 = Test;'], ['testClass2 = Test;'],
@ -106,8 +106,8 @@ describe('mergeTlSchemas', () => {
) )
}) })
it('merges true flags in constructors', () => { it('merges true flags in constructors', async () => {
test( await test(
[ [
['test foo:flags.0?true = Test;'], ['test foo:flags.0?true = Test;'],
['test bar:flags.0?true = Test;'], ['test bar:flags.0?true = Test;'],
@ -118,8 +118,8 @@ describe('mergeTlSchemas', () => {
) )
}) })
it('resolves conflict using user-provided option', () => { it('resolves conflict using user-provided option', async () => {
test( await test(
[ [
['test foo:int = Test;'], ['test foo:int = Test;'],
['test bar:int = Test;'], ['test bar:int = Test;'],
@ -128,7 +128,7 @@ describe('mergeTlSchemas', () => {
0, 0,
'test foo:int = Test;', 'test foo:int = Test;',
) )
test( await test(
[ [
['test foo:int = Test;'], ['test foo:int = Test;'],
['test bar:int = Test;'], ['test bar:int = Test;'],
@ -137,11 +137,15 @@ describe('mergeTlSchemas', () => {
1, 1,
'test foo:int = Test;', 'test foo:int = Test;',
) )
test([['test foo:int = Test;'], [], ['test bar:int = Test;']], 1, '') await test(
[['test foo:int = Test;'], [], ['test bar:int = Test;']],
1,
'',
)
}) })
it('merges comments', () => { it('merges comments', async () => {
test( await test(
[ [
['test foo:flags.0?true = Test;'], ['test foo:flags.0?true = Test;'],
['// test ctor', 'test bar:flags.0?true = Test;'], ['// test ctor', 'test bar:flags.0?true = Test;'],
@ -156,8 +160,8 @@ describe('mergeTlSchemas', () => {
) )
}) })
it('merges arguments comments', () => { it('merges arguments comments', async () => {
test( await test(
[ [
['test foo:flags.0?true = Test;'], ['test foo:flags.0?true = Test;'],
['// @bar bar comment', 'test bar:flags.0?true = Test;'], ['// @bar bar comment', 'test bar:flags.0?true = Test;'],

View file

@ -4,6 +4,7 @@
"outDir": "./dist" "outDir": "./dist"
}, },
"include": [ "include": [
"./src/**/*.ts" "./src/**/*.ts",
"./tests/**/*.ts"
] ]
} }

View file

@ -21,7 +21,7 @@ import {
DOC_CACHE_FILE, DOC_CACHE_FILE,
} from './constants' } from './constants'
import { applyDescriptionsYamlFile } from './process-descriptions-yaml' import { applyDescriptionsYamlFile } from './process-descriptions-yaml'
import { packTlSchema, unpackTlSchema } from './schema' import { packTlSchema, TlPackedSchema, unpackTlSchema } from './schema'
import { fetchRetry } from './utils' import { fetchRetry } from './utils'
export interface CachedDocumentationEntry { export interface CachedDocumentationEntry {
@ -38,7 +38,10 @@ export interface CachedDocumentation {
unions: Record<string, string> unions: Record<string, string>
} }
function normalizeLinks(url: string, el: cheerio.Cheerio<cheerio.Element>): void { function normalizeLinks(
url: string,
el: cheerio.Cheerio<cheerio.Element>,
): void {
el.find('a').each((i, _it) => { el.find('a').each((i, _it) => {
const it = cheerio.default(_it) const it = cheerio.default(_it)
let href = it.attr('href') let href = it.attr('href')
@ -314,7 +317,7 @@ export async function getCachedDocumentation(): Promise<CachedDocumentation | nu
try { try {
const file = await readFile(DOC_CACHE_FILE, 'utf8') const file = await readFile(DOC_CACHE_FILE, 'utf8')
return JSON.parse(file) return JSON.parse(file) as CachedDocumentation
} catch (e: unknown) { } catch (e: unknown) {
if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') { if (e && typeof e === 'object' && 'code' in e && e.code === 'ENOENT') {
return null return null
@ -355,7 +358,9 @@ async function main() {
if (act === 1) { if (act === 1) {
const [schema, layer] = unpackTlSchema( const [schema, layer] = unpackTlSchema(
JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8')), JSON.parse(
await readFile(API_SCHEMA_JSON_FILE, 'utf8'),
) as TlPackedSchema,
) )
cached = await fetchDocumentation(schema, layer) cached = await fetchDocumentation(schema, layer)
} }
@ -381,7 +386,9 @@ async function main() {
} }
const [schema, layer] = unpackTlSchema( const [schema, layer] = unpackTlSchema(
JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8')), JSON.parse(
await readFile(API_SCHEMA_JSON_FILE, 'utf8'),
) as TlPackedSchema,
) )
applyDocumentation(schema, cached) applyDocumentation(schema, cached)

View file

@ -36,7 +36,7 @@ import {
fetchDocumentation, fetchDocumentation,
getCachedDocumentation, getCachedDocumentation,
} from './documentation' } from './documentation'
import { packTlSchema, unpackTlSchema } from './schema' import { packTlSchema, TlPackedSchema, unpackTlSchema } from './schema'
import { fetchRetry } from './utils' import { fetchRetry } from './utils'
import { bumpVersion } from '~scripts/version' import { bumpVersion } from '~scripts/version'
@ -137,8 +137,10 @@ async function updatePackageVersion(
rl: readline.Interface, rl: readline.Interface,
currentLayer: number, currentLayer: number,
) { ) {
const packageJson = JSON.parse(await readFile(PACKAGE_JSON_FILE, 'utf8')) const packageJson = JSON.parse(
const version: string = packageJson.version await readFile(PACKAGE_JSON_FILE, 'utf8'),
) as { version: string }
const version = packageJson.version
let [major, minor] = version.split('.').map((i) => parseInt(i)) let [major, minor] = version.split('.').map((i) => parseInt(i))
if (major === currentLayer) { if (major === currentLayer) {
@ -168,7 +170,7 @@ async function overrideInt53(schema: TlFullSchema): Promise<void> {
const config = JSON.parse( const config = JSON.parse(
await readFile(join(__dirname, '../data/int53-overrides.json'), 'utf8'), await readFile(join(__dirname, '../data/int53-overrides.json'), 'utf8'),
) ) as Record<string, Record<string, string[]>>
schema.entries.forEach((entry) => { schema.entries.forEach((entry) => {
const overrides: string[] | undefined = config[entry.kind][entry.name] const overrides: string[] | undefined = config[entry.kind][entry.name]
@ -274,8 +276,8 @@ async function main() {
console.log( console.log(
'Conflict detected at %s %s:', 'Conflict detected at %s %s:',
nonEmptyOptions[0].entry?.kind, nonEmptyOptions[0].entry.kind,
nonEmptyOptions[0].entry?.name, nonEmptyOptions[0].entry.name,
) )
console.log('0. Remove') console.log('0. Remove')
nonEmptyOptions.forEach((opt, idx) => { nonEmptyOptions.forEach((opt, idx) => {
@ -329,7 +331,9 @@ async function main() {
console.log('Writing diff to file...') console.log('Writing diff to file...')
const oldSchema = unpackTlSchema( const oldSchema = unpackTlSchema(
JSON.parse(await readFile(API_SCHEMA_JSON_FILE, 'utf8')), JSON.parse(
await readFile(API_SCHEMA_JSON_FILE, 'utf8'),
) as TlPackedSchema,
) )
await writeFile( await writeFile(
API_SCHEMA_DIFF_JSON_FILE, API_SCHEMA_DIFF_JSON_FILE,

View file

@ -83,6 +83,13 @@ const virtualErrors: TlError[] = [
] ]
virtualErrors.forEach((it) => (it.virtual = true)) virtualErrors.forEach((it) => (it.virtual = true))
interface TelegramErrorsSpec {
errors: Record<string, Record<string, string[]>>
descriptions: Record<string, string>
user_only: string[]
bot_only: string[]
}
async function fetchFromTelegram(errors: TlErrors) { async function fetchFromTelegram(errors: TlErrors) {
const page = await fetch(ERRORS_PAGE_TG).then((it) => it.text()) const page = await fetch(ERRORS_PAGE_TG).then((it) => it.text())
const jsonUrl = page.match( const jsonUrl = page.match(
@ -90,9 +97,9 @@ async function fetchFromTelegram(errors: TlErrors) {
)?.[1] )?.[1]
if (!jsonUrl) throw new Error('Cannot find JSON URL') if (!jsonUrl) throw new Error('Cannot find JSON URL')
const json = await fetch(new URL(jsonUrl, ERRORS_PAGE_TG)).then((it) => const json = (await fetch(new URL(jsonUrl, ERRORS_PAGE_TG)).then((it) =>
it.json(), it.json(),
) )) as TelegramErrorsSpec
// since nobody fucking guarantees that .descriptions // since nobody fucking guarantees that .descriptions
// will have description for each described here (or vice versa), // will have description for each described here (or vice versa),
@ -120,7 +127,7 @@ async function fetchFromTelegram(errors: TlErrors) {
errors.throws[method] = [] errors.throws[method] = []
} }
if (errors.throws[method].indexOf(name) === -1) { if (!errors.throws[method].includes(name)) {
errors.throws[method].push(name) errors.throws[method].push(name)
} }
} }
@ -185,14 +192,17 @@ async function fetchFromTelethon(errors: TlErrors) {
// names for better code insights // names for better code insights
// we also prefer description from telegram, if it's available and doesn't use placeholders // we also prefer description from telegram, if it's available and doesn't use placeholders
if (description) { if (description) {
const desc = description.replace(/{([a-z0-9_]+)}/gi, (_, name) => { const desc = description.replace(
if (!obj._paramNames) { /{([a-z0-9_]+)}/gi,
obj._paramNames = [] (_, name: string) => {
} if (!obj._paramNames) {
obj._paramNames.push(name) obj._paramNames = []
}
obj._paramNames.push(name)
return '%d' return '%d'
}) },
)
if (!obj.description || obj._paramNames?.length) { if (!obj.description || obj._paramNames?.length) {
obj.description = desc obj.description = desc
@ -202,13 +212,24 @@ async function fetchFromTelethon(errors: TlErrors) {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
parser parser
.on('data', ({ name, codes, description }) => .on(
addError(name, codes, description), 'data',
({
name,
codes,
description,
}: {
name: string
codes: string
description: string
}) => addError(name, codes, description),
) )
.on('end', resolve) .on('end', resolve)
.on('error', reject) .on('error', reject)
csv.text().then((it) => parser.write(it)).catch(reject) csv.text()
.then((it) => parser.write(it))
.catch(reject)
}) })
} }
@ -229,7 +250,7 @@ async function main() {
await fetchFromTelethon(errors) await fetchFromTelethon(errors)
virtualErrors.forEach((err) => { virtualErrors.forEach((err) => {
if (errors.errors[err.name]) { if (err.name in errors.errors) {
console.log(`Error ${err.name} already exists and is not virtual`) console.log(`Error ${err.name} already exists and is not virtual`)
return return

View file

@ -29,13 +29,13 @@ async function main() {
// remove manually parsed types // remove manually parsed types
entries = entries.filter( entries = entries.filter(
(it) => (it) =>
[ ![
'mt_msg_container', 'mt_msg_container',
'mt_message', 'mt_message',
'mt_msg_copy', 'mt_msg_copy',
'mt_gzip_packed', 'mt_gzip_packed',
'mt_rpc_result', 'mt_rpc_result',
].indexOf(it.name) === -1, ].includes(it.name),
) )
// mtproto is handled internally, for simplicity we make them all classes // mtproto is handled internally, for simplicity we make them all classes

View file

@ -6,6 +6,7 @@ import {
generateTypescriptDefinitionsForTlSchema, generateTypescriptDefinitionsForTlSchema,
generateWriterCodeForTlEntries, generateWriterCodeForTlEntries,
parseFullTlSchema, parseFullTlSchema,
TlEntry,
TlErrors, TlErrors,
TlFullSchema, TlFullSchema,
} from '@mtcute/tl-utils' } from '@mtcute/tl-utils'
@ -16,7 +17,7 @@ import {
ESM_PRELUDE, ESM_PRELUDE,
MTP_SCHEMA_JSON_FILE, MTP_SCHEMA_JSON_FILE,
} from './constants' } from './constants'
import { unpackTlSchema } from './schema' import { TlPackedSchema, unpackTlSchema } from './schema'
const OUT_TYPINGS_FILE = join(__dirname, '../index.d.ts') const OUT_TYPINGS_FILE = join(__dirname, '../index.d.ts')
const OUT_TYPINGS_JS_FILE = join(__dirname, '../index.js') const OUT_TYPINGS_JS_FILE = join(__dirname, '../index.js')
@ -92,15 +93,17 @@ async function generateWriters(
} }
async function main() { async function main() {
const errors: TlErrors = JSON.parse( const errors = JSON.parse(
await readFile(ERRORS_JSON_FILE, 'utf8'), await readFile(ERRORS_JSON_FILE, 'utf8'),
) ) as TlErrors
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'),
) as TlPackedSchema,
) )
const mtpSchema = parseFullTlSchema( const mtpSchema = parseFullTlSchema(
JSON.parse(await readFile(MTP_SCHEMA_JSON_FILE, 'utf8')), JSON.parse(await readFile(MTP_SCHEMA_JSON_FILE, 'utf8')) as TlEntry[],
) )
await generateTypings(apiSchema, apiLayer, mtpSchema, errors) await generateTypings(apiSchema, apiLayer, mtpSchema, errors)

View file

@ -66,7 +66,7 @@ export function applyDescriptionsYamlFile(
prefix: string, prefix: string,
) { ) {
for (const name in obj) { for (const name in obj) {
objIndex[prefix + name] = obj[name] objIndex[prefix + name] = obj[name]!
} }
} }
@ -75,7 +75,7 @@ export function applyDescriptionsYamlFile(
// process byObjects // process byObjects
for (const name in byObjects) { for (const name in byObjects) {
const rules = byObjects[name] const rules = byObjects[name]!
const obj = objIndex[name] const obj = objIndex[name]
if (!obj) continue if (!obj) continue
@ -88,7 +88,7 @@ export function applyDescriptionsYamlFile(
if (rules.arguments) { if (rules.arguments) {
for (const arg in rules.arguments) { for (const arg in rules.arguments) {
const repl = unwrapMaybe( const repl = unwrapMaybe(
rules.arguments[arg], rules.arguments[arg]!,
obj.arguments !== undefined && arg in obj.arguments, obj.arguments !== undefined && arg in obj.arguments,
) )
@ -102,15 +102,14 @@ export function applyDescriptionsYamlFile(
// process byArguments // process byArguments
for (const i in objIndex) { for (const i in objIndex) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any const obj = objIndex[i]!
const obj = objIndex[i] as any
for (const arg in byArguments) { for (const arg in byArguments) {
if (obj.arguments && !(arg in obj.arguments)) continue if (obj.arguments && !(arg in obj.arguments)) continue
const repl = unwrapMaybe( const repl = unwrapMaybe(
byArguments[arg], byArguments[arg]!,
Boolean(obj.arguments) && arg in obj.arguments, Boolean(obj.arguments && arg in obj.arguments),
) )
if (repl) { if (repl) {
@ -126,7 +125,7 @@ export function applyDescriptionsYamlFile(
if (!rule._cached) { if (!rule._cached) {
let flags = rule.flags || '' let flags = rule.flags || ''
if (flags.indexOf('g') === -1) flags += 'g' if (!flags.includes('g')) flags += 'g'
rule._cached = new RegExp(rule.regex, flags) rule._cached = new RegExp(rule.regex, flags)
} }
@ -135,7 +134,7 @@ export function applyDescriptionsYamlFile(
} }
for (const i in objIndex) { for (const i in objIndex) {
const obj = objIndex[i] const obj = objIndex[i]!
byRegex.forEach((rule) => { byRegex.forEach((rule) => {
obj.comment = applyRegex(obj.comment, rule) obj.comment = applyRegex(obj.comment, rule)

View file

@ -7,6 +7,7 @@
"./binary/reader.d.ts", "./binary/reader.d.ts",
"./binary/writer.d.ts", "./binary/writer.d.ts",
"./binary/rsa-keys.d.ts", "./binary/rsa-keys.d.ts",
"./scripts" "./scripts",
"./tests/types.ts"
] ]
} }

View file

@ -157,6 +157,9 @@ importers:
'@types/ws': '@types/ws':
specifier: 8.5.4 specifier: 8.5.4
version: 8.5.4 version: 8.5.4
exit-hook:
specifier: ^4.0.0
version: 4.0.0
ws: ws:
specifier: 8.13.0 specifier: 8.13.0
version: 8.13.0 version: 8.13.0
@ -2442,6 +2445,11 @@ packages:
strip-final-newline: 3.0.0 strip-final-newline: 3.0.0
dev: true dev: true
/exit-hook@4.0.0:
resolution: {integrity: sha512-Fqs7ChZm72y40wKjOFXBKg7nJZvQJmewP5/7LtePDdnah/+FH9Hp5sgMujSCMPXlxOAW2//1jrW9pnsY7o20vQ==}
engines: {node: '>=18'}
dev: true
/exit-on-epipe@1.0.1: /exit-on-epipe@1.0.1:
resolution: {integrity: sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==} resolution: {integrity: sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==}
engines: {node: '>=0.8'} engines: {node: '>=0.8'}