feat(core): support future auth tokens
This commit is contained in:
parent
237146a4f3
commit
78285d4815
7 changed files with 150 additions and 68 deletions
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue