From 78285d4815333b7a2ce1babf0c110e2f4efe32af Mon Sep 17 00:00:00 2001 From: alina sireneva Date: Thu, 13 Jun 2024 13:50:11 +0300 Subject: [PATCH] feat(core): support future auth tokens --- packages/core/src/highlevel/client.ts | 21 ++- packages/core/src/highlevel/methods.ts | 1 + .../src/highlevel/methods/auth/log-out.ts | 17 +- .../src/highlevel/methods/auth/send-code.ts | 14 +- .../core/src/highlevel/methods/auth/start.ts | 150 +++++++++++------- .../src/highlevel/methods/chats/open-chat.ts | 7 +- .../core/src/highlevel/updates/manager.ts | 8 + 7 files changed, 150 insertions(+), 68 deletions(-) diff --git a/packages/core/src/highlevel/client.ts b/packages/core/src/highlevel/client.ts index a8f82a03..9268c08d 100644 --- a/packages/core/src/highlevel/client.ts +++ b/packages/core/src/highlevel/client.ts @@ -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 + logOut(): Promise /** * 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 }): Promise /** * 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 + + /** Saved future auth tokens, if any */ + futureAuthTokens?: Uint8Array[] + + /** Additional code settings to pass to the server */ + codeSettings?: Omit }): Promise /** * 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 diff --git a/packages/core/src/highlevel/methods.ts b/packages/core/src/highlevel/methods.ts index 8e9c52d3..e7284798 100644 --- a/packages/core/src/highlevel/methods.ts +++ b/packages/core/src/highlevel/methods.ts @@ -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' diff --git a/packages/core/src/highlevel/methods/auth/log-out.ts b/packages/core/src/highlevel/methods/auth/log-out.ts index fe284668..49630d57 100644 --- a/packages/core/src/highlevel/methods/auth/log-out.ts +++ b/packages/core/src/highlevel/methods/auth/log-out.ts @@ -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 { - await client.call({ _: 'auth.logOut' }) +export async function logOut(client: ITelegramClient): Promise { + const res = await client.call({ _: 'auth.logOut' }) await client.notifyLoggedOut() - return true + return { + futureAuthToken: res.futureAuthToken, + } } diff --git a/packages/core/src/highlevel/methods/auth/send-code.ts b/packages/core/src/highlevel/methods/auth/send-code.ts index aa74c715..5b685f85 100644 --- a/packages/core/src/highlevel/methods/auth/send-code.ts +++ b/packages/core/src/highlevel/methods/auth/send-code.ts @@ -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 }, ): Promise { 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') diff --git a/packages/core/src/highlevel/methods/auth/start.ts b/packages/core/src/highlevel/methods/auth/start.ts index 0961cff1..876cdde0 100644 --- a/packages/core/src/highlevel/methods/auth/start.ts +++ b/packages/core/src/highlevel/methods/auth/start.ts @@ -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 + + /** Saved future auth tokens, if any */ + futureAuthTokens?: Uint8Array[] + + /** Additional code settings to pass to the server */ + codeSettings?: Omit }, ): Promise { 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) { diff --git a/packages/core/src/highlevel/methods/chats/open-chat.ts b/packages/core/src/highlevel/methods/chats/open-chat.ts index fdb6a807..e6e8635a 100644 --- a/packages/core/src/highlevel/methods/chats/open-chat.ts +++ b/packages/core/src/highlevel/methods/chats/open-chat.ts @@ -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 */ diff --git a/packages/core/src/highlevel/updates/manager.ts b/packages/core/src/highlevel/updates/manager.ts index 6be62b08..c79b3e5e 100644 --- a/packages/core/src/highlevel/updates/manager.ts +++ b/packages/core/src/highlevel/updates/manager.ts @@ -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) }