feat(core): support future auth tokens

This commit is contained in:
alina 🌸 2024-06-13 13:50:11 +03:00
parent 237146a4f3
commit 78285d4815
Signed by: teidesu
SSH key fingerprint: SHA256:uNeCpw6aTSU4aIObXLvHfLkDa82HWH9EiOj9AXOIRpI
7 changed files with 150 additions and 68 deletions

View file

@ -13,7 +13,7 @@ import { BaseTelegramClient, BaseTelegramClientOptions } from './base.js'
import { ITelegramClient } from './client.types.js' import { ITelegramClient } from './client.types.js'
import { checkPassword } from './methods/auth/check-password.js' import { checkPassword } from './methods/auth/check-password.js'
import { getPasswordHint } from './methods/auth/get-password-hint.js' import { getPasswordHint } from './methods/auth/get-password-hint.js'
import { logOut } from './methods/auth/log-out.js' import { logOut, LogOutResult } from './methods/auth/log-out.js'
import { recoverPassword } from './methods/auth/recover-password.js' import { recoverPassword } from './methods/auth/recover-password.js'
import { resendCode } from './methods/auth/resend-code.js' import { resendCode } from './methods/auth/resend-code.js'
import { run } from './methods/auth/run.js' import { run } from './methods/auth/run.js'
@ -620,7 +620,7 @@ export interface TelegramClient extends ITelegramClient {
* *
* @returns On success, `true` is returned * @returns On success, `true` is returned
*/ */
logOut(): Promise<true> logOut(): Promise<LogOutResult>
/** /**
* Recover your password with a recovery code and log in. * Recover your password with a recovery code and log in.
* *
@ -672,6 +672,12 @@ export interface TelegramClient extends ITelegramClient {
sendCode(params: { sendCode(params: {
/** Phone number in international format */ /** Phone number in international format */
phone: string phone: string
/** Saved future auth tokens, if any */
futureAuthTokens?: Uint8Array[]
/** Additional code settings to pass to the server */
codeSettings?: Omit<tl.RawCodeSettings, '_' | 'logoutTokens'>
}): Promise<SentCode> }): Promise<SentCode>
/** /**
* Send a code to email needed to recover your password * Send a code to email needed to recover your password
@ -818,6 +824,12 @@ export interface TelegramClient extends ITelegramClient {
* @default `console.log`. * @default `console.log`.
*/ */
codeSentCallback?: (code: SentCode) => MaybePromise<void> codeSentCallback?: (code: SentCode) => MaybePromise<void>
/** Saved future auth tokens, if any */
futureAuthTokens?: Uint8Array[]
/** Additional code settings to pass to the server */
codeSettings?: Omit<tl.RawCodeSettings, '_' | 'logoutTokens'>
}): Promise<User> }): Promise<User>
/** /**
* Check if the given peer/input peer is referring to the current user * Check if the given peer/input peer is referring to the current user
@ -1780,6 +1792,11 @@ export interface TelegramClient extends ITelegramClient {
* Some library logic depends on this, for example, the library will * Some library logic depends on this, for example, the library will
* periodically ping the server to keep the updates flowing. * periodically ping the server to keep the updates flowing.
* *
* > **Warning**: Opening a chat with `openChat` method will make the library make additional requests
* > every so often. Which means that you should **avoid opening more than 5-10 chats at once**,
* > as it will probably trigger server-side limits and you might start getting transport errors
* > or even get banned.
*
* **Available**: both users and bots * **Available**: both users and bots
* *
* @param chat Chat to open * @param chat Chat to open

View file

@ -1,6 +1,7 @@
/* THIS FILE WAS AUTO-GENERATED */ /* THIS FILE WAS AUTO-GENERATED */
export { checkPassword } from './methods/auth/check-password.js' export { checkPassword } from './methods/auth/check-password.js'
export { getPasswordHint } from './methods/auth/get-password-hint.js' export { getPasswordHint } from './methods/auth/get-password-hint.js'
export type { LogOutResult } from './methods/auth/log-out.js'
export { logOut } from './methods/auth/log-out.js' export { logOut } from './methods/auth/log-out.js'
export { recoverPassword } from './methods/auth/recover-password.js' export { recoverPassword } from './methods/auth/recover-password.js'
export { resendCode } from './methods/auth/resend-code.js' export { resendCode } from './methods/auth/resend-code.js'

View file

@ -1,5 +1,14 @@
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
// @exported
export interface LogOutResult {
/**
* Future auth token returned by the server (if any), which can then be passed to
* {@link start} and {@link sendCode} methods to avoid sending the code again.
*/
futureAuthToken?: Uint8Array
}
/** /**
* Log out from Telegram account and optionally reset the session storage. * Log out from Telegram account and optionally reset the session storage.
* *
@ -8,9 +17,11 @@ import { ITelegramClient } from '../../client.types.js'
* *
* @returns On success, `true` is returned * @returns On success, `true` is returned
*/ */
export async function logOut(client: ITelegramClient): Promise<true> { export async function logOut(client: ITelegramClient): Promise<LogOutResult> {
await client.call({ _: 'auth.logOut' }) const res = await client.call({ _: 'auth.logOut' })
await client.notifyLoggedOut() await client.notifyLoggedOut()
return true return {
futureAuthToken: res.futureAuthToken,
}
} }

View file

@ -1,3 +1,5 @@
import { tl } from '@mtcute/tl'
import { assertTypeIs } from '../../../utils/type-assertions.js' import { assertTypeIs } from '../../../utils/type-assertions.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
import { SentCode } from '../../types/auth/sent-code.js' import { SentCode } from '../../types/auth/sent-code.js'
@ -13,6 +15,12 @@ export async function sendCode(
params: { params: {
/** Phone number in international format */ /** Phone number in international format */
phone: string phone: string
/** Saved future auth tokens, if any */
futureAuthTokens?: Uint8Array[]
/** Additional code settings to pass to the server */
codeSettings?: Omit<tl.RawCodeSettings, '_' | 'logoutTokens'>
}, },
): Promise<SentCode> { ): Promise<SentCode> {
const phone = normalizePhoneNumber(params.phone) const phone = normalizePhoneNumber(params.phone)
@ -24,7 +32,11 @@ export async function sendCode(
phoneNumber: phone, phoneNumber: phone,
apiId: id, apiId: id,
apiHash: hash, apiHash: hash,
settings: { _: 'codeSettings' }, settings: {
_: 'codeSettings',
logoutTokens: params.futureAuthTokens,
...params.codeSettings,
},
}) })
assertTypeIs('sendCode', res, 'auth.sentCode') assertTypeIs('sendCode', res, 'auth.sentCode')

View file

@ -1,7 +1,7 @@
/* eslint-disable no-console */ /* eslint-disable no-console */
import { tl } from '@mtcute/tl' import { tl } from '@mtcute/tl'
import { MtArgumentError } from '../../../types/errors.js' import { MtArgumentError, MtcuteError } from '../../../types/errors.js'
import { MaybePromise } from '../../../types/utils.js' import { MaybePromise } from '../../../types/utils.js'
import { ITelegramClient } from '../../client.types.js' import { ITelegramClient } from '../../client.types.js'
import { SentCode } from '../../types/auth/sent-code.js' import { SentCode } from '../../types/auth/sent-code.js'
@ -94,12 +94,22 @@ export async function start(
* @default `console.log`. * @default `console.log`.
*/ */
codeSentCallback?: (code: SentCode) => MaybePromise<void> codeSentCallback?: (code: SentCode) => MaybePromise<void>
/** Saved future auth tokens, if any */
futureAuthTokens?: Uint8Array[]
/** Additional code settings to pass to the server */
codeSettings?: Omit<tl.RawCodeSettings, '_' | 'logoutTokens'>
}, },
): Promise<User> { ): Promise<User> {
if (params.session) { if (params.session) {
await client.importSession(params.session, params.sessionForce) await client.importSession(params.session, params.sessionForce)
} }
let has2fa = false
let sentCode: SentCode | undefined
let phone: string | null = null
try { try {
const me = await getMe(client) const me = await getMe(client)
@ -111,79 +121,101 @@ export async function start(
return me return me
} catch (e) { } catch (e) {
if (!tl.RpcError.is(e, 'AUTH_KEY_UNREGISTERED')) throw e if (tl.RpcError.is(e)) {
} if (e.text === 'SESSION_PASSWORD_NEEDED') has2fa = true
else if (e.text !== 'AUTH_KEY_UNREGISTERED') throw e
if (!params.phone && !params.botToken) {
throw new MtArgumentError('Neither phone nor bot token were provided')
}
let phone = params.phone ? await resolveMaybeDynamic(params.phone) : null
if (phone) {
phone = normalizePhoneNumber(phone)
if (!params.code) {
throw new MtArgumentError('You must pass `code` to use `phone`')
} }
} else { }
const botToken = params.botToken ? await resolveMaybeDynamic(params.botToken) : null
if (!botToken) { // if has2fa == true, then we are half-logged in, but need to enter password
throw new MtArgumentError('Either bot token or phone number must be provided') if (!has2fa) {
if (!params.phone && !params.botToken) {
throw new MtArgumentError('Neither phone nor bot token were provided')
} }
return await signInBot(client, botToken) phone = params.phone ? await resolveMaybeDynamic(params.phone) : null
}
let sentCode = await sendCode(client, { phone }) if (phone) {
phone = normalizePhoneNumber(phone)
if (params.forceSms && sentCode.type === 'app') { if (!params.code) {
sentCode = await resendCode(client, { phone, phoneCodeHash: sentCode.phoneCodeHash }) throw new MtArgumentError('You must pass `code` to use `phone`')
} }
} else {
const botToken = params.botToken ? await resolveMaybeDynamic(params.botToken) : null
if (params.codeSentCallback) { if (!botToken) {
await params.codeSentCallback(sentCode) throw new MtArgumentError('Either bot token or phone number must be provided')
} else { }
console.log(`The confirmation code has been sent via ${sentCode.type}.`)
}
let has2fa = false return await signInBot(client, botToken)
}
for (;;) {
const code = await resolveMaybeDynamic(params.code)
if (!code) throw new tl.RpcError(400, 'PHONE_CODE_EMPTY')
try { try {
return await signIn(client, { phone, phoneCodeHash: sentCode.phoneCodeHash, phoneCode: code }) sentCode = await sendCode(client, {
phone,
futureAuthTokens: params.futureAuthTokens,
codeSettings: params.codeSettings,
})
} catch (e) { } catch (e) {
if (!tl.RpcError.is(e)) throw e if (tl.RpcError.is(e, 'SESSION_PASSWORD_NEEDED')) {
if (e.is('SESSION_PASSWORD_NEEDED')) {
has2fa = true has2fa = true
break } else {
} else if ( throw e
e.is('PHONE_CODE_EMPTY') || }
e.is('PHONE_CODE_EXPIRED') || }
e.is('PHONE_CODE_INVALID') || }
e.is('PHONE_CODE_HASH_EMPTY')
) {
if (typeof params.code !== 'function') {
throw new MtArgumentError('Provided code was invalid')
}
if (params.invalidCodeCallback) { if (sentCode) {
await params.invalidCodeCallback('code') if (params.forceSms && (sentCode.type === 'app' || sentCode.type === 'email')) {
} else { sentCode = await resendCode(client, { phone: phone!, phoneCodeHash: sentCode.phoneCodeHash })
console.log('Invalid code. Please try again')
}
continue
} else throw e
} }
// if there was no error, code was valid, so it's either 2fa or signup if (params.codeSentCallback) {
break await params.codeSentCallback(sentCode)
} else {
if (sentCode.type === 'email_required') {
throw new MtcuteError('Email login setup is required to sign in')
}
console.log(`The confirmation code has been sent via ${sentCode.type}.`)
}
for (;;) {
const code = await resolveMaybeDynamic(params.code)
if (!code) throw new tl.RpcError(400, 'PHONE_CODE_EMPTY')
try {
return await signIn(client, { phone: phone!, phoneCodeHash: sentCode.phoneCodeHash, phoneCode: code })
} catch (e) {
if (!tl.RpcError.is(e)) throw e
if (e.is('SESSION_PASSWORD_NEEDED')) {
has2fa = true
break
} else if (
e.is('PHONE_CODE_EMPTY') ||
e.is('PHONE_CODE_EXPIRED') ||
e.is('PHONE_CODE_INVALID') ||
e.is('PHONE_CODE_HASH_EMPTY')
) {
if (typeof params.code !== 'function') {
throw new MtArgumentError('Provided code was invalid')
}
if (params.invalidCodeCallback) {
await params.invalidCodeCallback('code')
} else {
console.log('Invalid code. Please try again')
}
continue
} else throw e
}
// if there was no error, code was valid, so it's either 2fa or signup
break
}
} }
if (has2fa) { if (has2fa) {

View file

@ -10,9 +10,10 @@ import { resolvePeer } from '../users/resolve-peer.js'
* Some library logic depends on this, for example, the library will * Some library logic depends on this, for example, the library will
* periodically ping the server to keep the updates flowing. * periodically ping the server to keep the updates flowing.
* *
* > **Warning**: Opening a chat with `openChat` method will make the library make additional requests every so often. * > **Warning**: Opening a chat with `openChat` method will make the library make additional requests
* > Which means that you should **avoid opening more than 5-10 chats at once**, as it will probably trigger * > every so often. Which means that you should **avoid opening more than 5-10 chats at once**,
* > server-side limits and you might start getting transport errors or even get banned. * > as it will probably trigger server-side limits and you might start getting transport errors
* > or even get banned.
* *
* @param chat Chat to open * @param chat Chat to open
*/ */

View file

@ -432,6 +432,14 @@ export class UpdatesManager {
log.debug('loaded initial state: pts=%d, qts=%d, date=%d, seq=%d', this.pts, this.qts, this.date, this.seq) log.debug('loaded initial state: pts=%d, qts=%d, date=%d, seq=%d', this.pts, this.qts, this.date, this.seq)
} catch (e) { } catch (e) {
if (tl.RpcError.is(e, 'AUTH_KEY_UNREGISTERED')) {
// we are logged out, stop updates loop
lock.release()
this.stopLoop()
return
}
if (this.client.isConnected) { if (this.client.isConnected) {
log.error('failed to fetch updates state: %s', e) log.error('failed to fetch updates state: %s', e)
} }