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 { checkPassword } from './methods/auth/check-password.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 { resendCode } from './methods/auth/resend-code.js'
import { run } from './methods/auth/run.js'
@ -620,7 +620,7 @@ export interface TelegramClient extends ITelegramClient {
*
* @returns On success, `true` is returned
*/
logOut(): Promise<true>
logOut(): Promise<LogOutResult>
/**
* Recover your password with a recovery code and log in.
*
@ -672,6 +672,12 @@ export interface TelegramClient extends ITelegramClient {
sendCode(params: {
/** Phone number in international format */
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>
/**
* Send a code to email needed to recover your password
@ -818,6 +824,12 @@ export interface TelegramClient extends ITelegramClient {
* @default `console.log`.
*/
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>
/**
* 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
* 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
*
* @param chat Chat to open

View file

@ -1,6 +1,7 @@
/* THIS FILE WAS AUTO-GENERATED */
export { checkPassword } from './methods/auth/check-password.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 { recoverPassword } from './methods/auth/recover-password.js'
export { resendCode } from './methods/auth/resend-code.js'

View file

@ -1,5 +1,14 @@
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.
*
@ -8,9 +17,11 @@ import { ITelegramClient } from '../../client.types.js'
*
* @returns On success, `true` is returned
*/
export async function logOut(client: ITelegramClient): Promise<true> {
await client.call({ _: 'auth.logOut' })
export async function logOut(client: ITelegramClient): Promise<LogOutResult> {
const res = await client.call({ _: 'auth.logOut' })
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 { ITelegramClient } from '../../client.types.js'
import { SentCode } from '../../types/auth/sent-code.js'
@ -13,6 +15,12 @@ export async function sendCode(
params: {
/** Phone number in international format */
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> {
const phone = normalizePhoneNumber(params.phone)
@ -24,7 +32,11 @@ export async function sendCode(
phoneNumber: phone,
apiId: id,
apiHash: hash,
settings: { _: 'codeSettings' },
settings: {
_: 'codeSettings',
logoutTokens: params.futureAuthTokens,
...params.codeSettings,
},
})
assertTypeIs('sendCode', res, 'auth.sentCode')

View file

@ -1,7 +1,7 @@
/* eslint-disable no-console */
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 { ITelegramClient } from '../../client.types.js'
import { SentCode } from '../../types/auth/sent-code.js'
@ -94,12 +94,22 @@ export async function start(
* @default `console.log`.
*/
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> {
if (params.session) {
await client.importSession(params.session, params.sessionForce)
}
let has2fa = false
let sentCode: SentCode | undefined
let phone: string | null = null
try {
const me = await getMe(client)
@ -111,79 +121,101 @@ export async function start(
return me
} catch (e) {
if (!tl.RpcError.is(e, '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`')
if (tl.RpcError.is(e)) {
if (e.text === 'SESSION_PASSWORD_NEEDED') has2fa = true
else if (e.text !== 'AUTH_KEY_UNREGISTERED') throw e
}
} else {
const botToken = params.botToken ? await resolveMaybeDynamic(params.botToken) : null
}
if (!botToken) {
throw new MtArgumentError('Either bot token or phone number must be provided')
// if has2fa == true, then we are half-logged in, but need to enter password
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') {
sentCode = await resendCode(client, { phone, phoneCodeHash: sentCode.phoneCodeHash })
}
if (!params.code) {
throw new MtArgumentError('You must pass `code` to use `phone`')
}
} else {
const botToken = params.botToken ? await resolveMaybeDynamic(params.botToken) : null
if (params.codeSentCallback) {
await params.codeSentCallback(sentCode)
} else {
console.log(`The confirmation code has been sent via ${sentCode.type}.`)
}
if (!botToken) {
throw new MtArgumentError('Either bot token or phone number must be provided')
}
let has2fa = false
for (;;) {
const code = await resolveMaybeDynamic(params.code)
if (!code) throw new tl.RpcError(400, 'PHONE_CODE_EMPTY')
return await signInBot(client, botToken)
}
try {
return await signIn(client, { phone, phoneCodeHash: sentCode.phoneCodeHash, phoneCode: code })
sentCode = await sendCode(client, {
phone,
futureAuthTokens: params.futureAuthTokens,
codeSettings: params.codeSettings,
})
} catch (e) {
if (!tl.RpcError.is(e)) throw e
if (e.is('SESSION_PASSWORD_NEEDED')) {
if (tl.RpcError.is(e, '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')
}
} else {
throw e
}
}
}
if (params.invalidCodeCallback) {
await params.invalidCodeCallback('code')
} else {
console.log('Invalid code. Please try again')
}
continue
} else throw e
if (sentCode) {
if (params.forceSms && (sentCode.type === 'app' || sentCode.type === 'email')) {
sentCode = await resendCode(client, { phone: phone!, phoneCodeHash: sentCode.phoneCodeHash })
}
// if there was no error, code was valid, so it's either 2fa or signup
break
if (params.codeSentCallback) {
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) {

View file

@ -10,9 +10,10 @@ import { resolvePeer } from '../users/resolve-peer.js'
* Some library logic depends on this, for example, the library will
* 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.
* > **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.
*
* @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)
} 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) {
log.error('failed to fetch updates state: %s', e)
}