123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482 |
- import { Api } from "../tl";
- import * as utils from "../Utils";
- import { sleep } from "../Helpers";
- import { computeCheck as computePasswordSrpCheck } from "../Password";
- import type { TelegramClient } from "./TelegramClient";
- import { RPCError } from "../errors";
- /**
- * 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> {
- const inputPromise = (async () => {
- while (1) {
- 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;
- if (authParams.qrCode) {
- 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;
- }
- 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;
- }
- }
- } catch (err: any) {
- if (err.errorMessage === "SESSION_PASSWORD_NEEDED") {
- return client.signInWithPassword(apiCredentials, authParams);
- }
- }
- 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));
- }
|