UNPKG

dograma

Version:

NodeJS/Browser MTProto API Telegram client library,

505 lines (454 loc) 16.3 kB
import { Api } from "../tl"; import * as utils from "../Utils"; import { sleep } from "../Helpers"; import { computeCheck as computePasswordSrpCheck } from "../Password"; import type { TelegramClient } from "./TelegramClient"; /** * For when you want to login as a {@link Api.User}<br/> * this should handle all needed steps for authorization as a user.<br/> * to stop the operation at any point just raise and error with the message `AUTH_USER_CANCEL`. */ export interface UserAuthParams { /** Either a string or a callback that returns a string for the phone to use to login. */ phoneNumber: string | (() => Promise<string>); /** callback that should return the login code that telegram sent.<br/> * has optional bool `isCodeViaApp` param for whether the code was sent through the app (true) or an SMS (false). */ phoneCode: (isCodeViaApp?: boolean) => Promise<string>; /** optional string or callback that should return the 2FA password if present.<br/> * the password hint will be sent in the hint param */ password?: (hint?: string) => Promise<string>; /** in case of a new account creation this callback should return a first name and last name `[first,last]`. */ firstAndLastNames?: () => Promise<[string, string?]>; /** a qrCode token for login through qrCode.<br/> * this would need a QR code that you should scan with another app to login with. */ qrCode?: (qrCode: { token: Buffer; expires: number }) => Promise<void>; /** when an error happens during auth this function will be called with the error.<br/> * if this returns true the auth operation will stop. */ onError: (err: Error) => Promise<boolean> | void; /** whether to send the code through SMS or not. */ forceSMS?: boolean; } export interface UserPasswordAuthParams { /** optional string or callback that should return the 2FA password if present.<br/> * the password hint will be sent in the hint param */ password?: (hint?: string) => Promise<string>; /** when an error happens during auth this function will be called with the error.<br/> * if this returns true the auth operation will stop. */ onError: (err: Error) => Promise<boolean> | void; } export interface QrCodeAuthParams extends UserPasswordAuthParams { /** a qrCode token for login through qrCode.<br/> * this would need a QR code that you should scan with another app to login with. */ qrCode?: (qrCode: { token: Buffer; expires: number }) => Promise<void>; /** when an error happens during auth this function will be called with the error.<br/> * if this returns true the auth operation will stop. */ onError: (err: Error) => Promise<boolean> | void; } interface ReturnString { (): string; } /** * For when you want as a normal bot created by https://t.me/Botfather.<br/> * Logging in as bot is simple and requires no callbacks */ export interface BotAuthParams { /** * the bot token to use. */ botAuthToken: string | ReturnString; } /** * Credential needed for the authentication. you can get theses from https://my.telegram.org/auth<br/> * Note: This is required for both logging in as a bot and a user.<br/> */ export interface ApiCredentials { /** The app api id. */ apiId: number; /** the app api hash */ apiHash: string; } const QR_CODE_TIMEOUT = 30000; // region public methods /** @hidden */ export async function start( client: TelegramClient, authParams: UserAuthParams | BotAuthParams ) { if (!client.connected) { await client.connect(); } if (await client.checkAuthorization()) { return; } const apiCredentials = { apiId: client.apiId, apiHash: client.apiHash, }; await _authFlow(client, apiCredentials, authParams); } /** @hidden */ export async function checkAuthorization(client: TelegramClient) { try { await client.invoke(new Api.updates.GetState()); return true; } catch (e) { return false; } } /** @hidden */ export async function signInUser( client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams ): Promise<Api.TypeUser> { let phoneNumber; let phoneCodeHash; let isCodeViaApp = false; while (1) { try { if (typeof authParams.phoneNumber === "function") { try { phoneNumber = await authParams.phoneNumber(); } catch (err: any) { if (err.errorMessage === "RESTART_AUTH_WITH_QR") { return client.signInUserWithQrCode( apiCredentials, authParams ); } throw err; } } else { phoneNumber = authParams.phoneNumber; } const sendCodeResult = await client.sendCode( apiCredentials, phoneNumber, authParams.forceSMS ); phoneCodeHash = sendCodeResult.phoneCodeHash; isCodeViaApp = sendCodeResult.isCodeViaApp; if (typeof phoneCodeHash !== "string") { throw new Error("Failed to retrieve phone code hash"); } break; } catch (err: any) { if (typeof authParams.phoneNumber !== "function") { throw err; } const shouldWeStop = await authParams.onError(err); if (shouldWeStop) { throw new Error("AUTH_USER_CANCEL"); } } } let phoneCode; let isRegistrationRequired = false; let termsOfService; while (1) { try { try { phoneCode = await authParams.phoneCode(isCodeViaApp); } catch (err: any) { // This is the support for changing phone number from the phone code screen. if (err.errorMessage === "RESTART_AUTH") { return client.signInUser(apiCredentials, authParams); } } if (!phoneCode) { throw new Error("Code is empty"); } // May raise PhoneCodeEmptyError, PhoneCodeExpiredError, // PhoneCodeHashEmptyError or PhoneCodeInvalidError. const result = await client.invoke( new Api.auth.SignIn({ phoneNumber, phoneCodeHash, phoneCode, }) ); if (result instanceof Api.auth.AuthorizationSignUpRequired) { isRegistrationRequired = true; termsOfService = result.termsOfService; break; } return result.user; } catch (err: any) { if (err.errorMessage === "SESSION_PASSWORD_NEEDED") { return client.signInWithPassword(apiCredentials, authParams); } else { const shouldWeStop = await authParams.onError(err); if (shouldWeStop) { throw new Error("AUTH_USER_CANCEL"); } } } } if (isRegistrationRequired) { while (1) { try { let lastName; let firstName = "first name"; if (authParams.firstAndLastNames) { const result = await authParams.firstAndLastNames(); firstName = result[0]; lastName = result[1]; } if (!firstName) { throw new Error("First name is required"); } const { user } = (await client.invoke( new Api.auth.SignUp({ phoneNumber, phoneCodeHash, firstName, lastName, }) )) as Api.auth.Authorization; if (termsOfService) { // This is a violation of Telegram rules: the user should be presented with and accept TOS. await client.invoke( new Api.help.AcceptTermsOfService({ id: termsOfService.id, }) ); } return user; } catch (err: any) { const shouldWeStop = await authParams.onError(err); if (shouldWeStop) { throw new Error("AUTH_USER_CANCEL"); } } } } await authParams.onError(new Error("Auth failed")); return client.signInUser(apiCredentials, authParams); } /** @hidden */ export async function signInUserWithQrCode( client: TelegramClient, apiCredentials: ApiCredentials, authParams: QrCodeAuthParams ): Promise<Api.TypeUser> { let isScanningComplete = false; if (authParams.qrCode == undefined) { throw new Error("qrCode callback not defined"); } const inputPromise = (async () => { while (1) { if (isScanningComplete) { break; } const result = await client.invoke( new Api.auth.ExportLoginToken({ apiId: Number(apiCredentials.apiId), apiHash: apiCredentials.apiHash, exceptIds: [], }) ); if (!(result instanceof Api.auth.LoginToken)) { throw new Error("Unexpected"); } const { token, expires } = result; await Promise.race([ authParams.qrCode!({ token, expires }), sleep(QR_CODE_TIMEOUT), ]); await sleep(QR_CODE_TIMEOUT); } })(); const updatePromise = new Promise((resolve) => { client.addEventHandler((update: Api.TypeUpdate) => { if (update instanceof Api.UpdateLoginToken) { resolve(undefined); } }); }); try { await Promise.race([updatePromise, inputPromise]); } catch (err) { throw err; } finally { isScanningComplete = true; } try { const result2 = await client.invoke( new Api.auth.ExportLoginToken({ apiId: Number(apiCredentials.apiId), apiHash: apiCredentials.apiHash, exceptIds: [], }) ); if ( result2 instanceof Api.auth.LoginTokenSuccess && result2.authorization instanceof Api.auth.Authorization ) { return result2.authorization.user; } else if (result2 instanceof Api.auth.LoginTokenMigrateTo) { await client._switchDC(result2.dcId); const migratedResult = await client.invoke( new Api.auth.ImportLoginToken({ token: result2.token, }) ); if ( migratedResult instanceof Api.auth.LoginTokenSuccess && migratedResult.authorization instanceof Api.auth.Authorization ) { return migratedResult.authorization.user; } else { client._log.error( `Received unknown result while scanning QR ${result2.className}` ); throw new Error( `Received unknown result while scanning QR ${result2.className}` ); } } else { client._log.error( `Received unknown result while scanning QR ${result2.className}` ); throw new Error( `Received unknown result while scanning QR ${result2.className}` ); } } catch (err: any) { if (err.errorMessage === "SESSION_PASSWORD_NEEDED") { return client.signInWithPassword(apiCredentials, authParams); } throw err; } await authParams.onError(new Error("QR auth failed")); throw new Error("QR auth failed"); } /** @hidden */ export async function sendCode( client: TelegramClient, apiCredentials: ApiCredentials, phoneNumber: string, forceSMS = false ): Promise<{ phoneCodeHash: string; isCodeViaApp: boolean; }> { try { const { apiId, apiHash } = apiCredentials; const sendResult = await client.invoke( new Api.auth.SendCode({ phoneNumber, apiId, apiHash, settings: new Api.CodeSettings({}), }) ); // If we already sent a SMS, do not resend the phoneCode (hash may be empty) if (!forceSMS || sendResult.type instanceof Api.auth.SentCodeTypeSms) { return { phoneCodeHash: sendResult.phoneCodeHash, isCodeViaApp: sendResult.type instanceof Api.auth.SentCodeTypeApp, }; } const resendResult = await client.invoke( new Api.auth.ResendCode({ phoneNumber, phoneCodeHash: sendResult.phoneCodeHash, }) ); return { phoneCodeHash: resendResult.phoneCodeHash, isCodeViaApp: resendResult.type instanceof Api.auth.SentCodeTypeApp, }; } catch (err: any) { if (err.errorMessage === "AUTH_RESTART") { return client.sendCode(apiCredentials, phoneNumber, forceSMS); } else { throw err; } } } /** @hidden */ export async function signInWithPassword( client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserPasswordAuthParams ): Promise<Api.TypeUser> { let emptyPassword = false; while (1) { try { const passwordSrpResult = await client.invoke( new Api.account.GetPassword() ); if (!authParams.password) { emptyPassword = true; break; } const password = await authParams.password(passwordSrpResult.hint); if (!password) { throw new Error("Password is empty"); } const passwordSrpCheck = await computePasswordSrpCheck( passwordSrpResult, password ); const { user } = (await client.invoke( new Api.auth.CheckPassword({ password: passwordSrpCheck, }) )) as Api.auth.Authorization; return user; } catch (err: any) { const shouldWeStop = await authParams.onError(err); if (shouldWeStop) { throw new Error("AUTH_USER_CANCEL"); } } } if (emptyPassword) { throw new Error("Account has 2FA enabled."); } return undefined!; // Never reached (TypeScript fix) } /** @hidden */ export async function signInBot( client: TelegramClient, apiCredentials: ApiCredentials, authParams: BotAuthParams ) { const { apiId, apiHash } = apiCredentials; let { botAuthToken } = authParams; if (!botAuthToken) { throw new Error("a valid BotToken is required"); } if (typeof botAuthToken === "function") { let token; while (true) { token = await botAuthToken(); if (token) { botAuthToken = token; break; } } } const { user } = (await client.invoke( new Api.auth.ImportBotAuthorization({ apiId, apiHash, botAuthToken, }) )) as Api.auth.Authorization; return user; } /** @hidden */ export async function _authFlow( client: TelegramClient, apiCredentials: ApiCredentials, authParams: UserAuthParams | BotAuthParams ) { const me = "phoneNumber" in authParams ? await client.signInUser(apiCredentials, authParams) : await client.signInBot(apiCredentials, authParams); client._log.info("Signed in successfully as " + utils.getDisplayName(me)); }