diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index 7cb47924..9a2eb20d 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -377,6 +377,16 @@ export interface TelegramClient extends BaseTelegramClient { * */ start(params: { + /** + * String session exported using {@link TelegramClient.exportSession}. + * + * This simply calls {@link TelegramClient.importSession} before anything else. + * + * Note that passed session will be ignored in case storage already + * contains authorization. + */ + session?: string + /** * Phone number of the account. * If account does not exist, it will be created diff --git a/packages/client/src/methods/auth/_initialize.ts b/packages/client/src/methods/auth/_initialize.ts index bf597b79..f56524bf 100644 --- a/packages/client/src/methods/auth/_initialize.ts +++ b/packages/client/src/methods/auth/_initialize.ts @@ -5,7 +5,7 @@ interface AuthState { // local copy of "self" in storage, // so we can use it w/out relying on storage. // they are both loaded and saved to storage along with the updates - // (see methods/updates/handle-update) + // (see methods/updates) _userId: number | null _isBot: boolean } diff --git a/packages/client/src/methods/auth/start.ts b/packages/client/src/methods/auth/start.ts index fed1f0fd..3912b5fd 100644 --- a/packages/client/src/methods/auth/start.ts +++ b/packages/client/src/methods/auth/start.ts @@ -39,6 +39,16 @@ import { export async function start( this: TelegramClient, params: { + /** + * String session exported using {@link TelegramClient.exportSession}. + * + * This simply calls {@link TelegramClient.importSession} before anything else. + * + * Note that passed session will be ignored in case storage already + * contains authorization. + */ + session?: string + /** * Phone number of the account. * If account does not exist, it will be created @@ -127,6 +137,10 @@ export async function start( catchUp?: boolean } ): Promise { + if (params.session) { + this.importSession(params.session) + } + try { const me = await this.getMe() diff --git a/packages/client/src/methods/updates.ts b/packages/client/src/methods/updates.ts index 256977f8..575f29ac 100644 --- a/packages/client/src/methods/updates.ts +++ b/packages/client/src/methods/updates.ts @@ -30,7 +30,7 @@ interface UpdatesState { _date: number _seq: number - // old values of the updates statej (i.e. as in DB) + // old values of the updates state (i.e. as in DB) // used to avoid redundant storage calls _oldPts: number _oldDate: number @@ -106,9 +106,18 @@ export async function _loadStorage(this: TelegramClient): Promise { /** * @internal */ -export async function _saveStorage(this: TelegramClient): Promise { +export async function _saveStorage(this: TelegramClient, afterImport = false): Promise { // save updates state to the session + if (afterImport) { + // we need to get `self` from db and store it + const self = await this.storage.getSelf() + if (self) { + this._userId = self.userId + this._isBot = self.isBot + } + } + try { // before any authorization pts will be undefined if (this._pts !== undefined) { diff --git a/packages/core/src/base-client.ts b/packages/core/src/base-client.ts index f7714b55..5ff656bf 100644 --- a/packages/core/src/base-client.ts +++ b/packages/core/src/base-client.ts @@ -32,6 +32,9 @@ import { addPublicKey } from './utils/crypto/keys' import { ITelegramStorage, MemoryStorage } from './storage' import { getAllPeersFrom, MAX_CHANNEL_ID } from './utils/peer-utils' import bigInt from 'big-integer' +import { BinaryWriter } from './utils/binary/binary-writer' +import { encodeUrlSafeBase64, parseUrlSafeBase64 } from './utils/buffer-utils' +import { BinaryReader } from './utils/binary/binary-reader' const debug = require('debug')('mtcute:base') @@ -226,6 +229,8 @@ export class BaseTelegramClient { */ primaryConnection: TelegramConnection + private _importFrom?: string + /** * Method which is called every time the client receives a new update. * @@ -236,14 +241,6 @@ export class BaseTelegramClient { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected _handleUpdate(update: tl.TypeUpdates): void {} - /** - * Method which is called for every object - * - * @param obj - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - protected _processApiResponse(obj: tl.TlObject): void {} - constructor(opts: BaseTelegramClient.Options) { const apiId = typeof opts.apiId === 'string' ? parseInt(opts.apiId) : opts.apiId @@ -291,7 +288,7 @@ export class BaseTelegramClient { await this.storage.load?.() } - protected async _saveStorage(): Promise { + protected async _saveStorage(afterImport = false): Promise { await this.storage.save?.() } @@ -370,6 +367,38 @@ export class BaseTelegramClient { this.primaryConnection.authKey = await this.storage.getAuthKeyFor( this._primaryDc.id ) + + if (!this.primaryConnection.authKey && this._importFrom) { + const buf = parseUrlSafeBase64(this._importFrom) + if (buf[0] !== 1) throw new Error(`Invalid session string (version = ${buf[0]})`) + + const reader = new BinaryReader(buf, 1) + + const flags = reader.int32() + const hasSelf = flags & 1 + + const primaryDc = reader.object() + if (primaryDc._ !== 'dcOption') { + throw new Error(`Invalid session string (dc._ = ${primaryDc._})`) + } + + this._primaryDc = this.primaryConnection.params.dc = primaryDc + await this.storage.setDefaultDc(primaryDc) + + if (hasSelf) { + const selfId = reader.int32() + const selfBot = reader.boolean() + + await this.storage.setSelf({ userId: selfId, isBot: selfBot }) + } + + const key = reader.bytes() + this.primaryConnection.authKey = key + await this.storage.setAuthKeyFor(primaryDc.id, key) + + await this._saveStorage(true) + } + this.primaryConnection.connect() } @@ -750,4 +779,64 @@ export class BaseTelegramClient { return hadMin } + + /** + * Export current session to a single *LONG* string, containing + * all the needed information. + * + * > **Warning!** Anyone with this string will be able + * > to authorize as you and do anything. Treat this + * > as your password, and never give it away! + * > + * > In case you have accidentally leaked this string, + * > make sure to revoke this session in account settings: + * > "Privacy & Security" > "Active sessions" > + * > find the one containing `mtcute` > Revoke, + * > or, in case this is a bot, revoke bot token + * > with [@BotFather](//t.me/botfather) + */ + async exportSession(): Promise { + if (!this.primaryConnection.authKey) + throw new Error('Auth key is not generated yet') + + const writer = BinaryWriter.alloc(512) + + const self = await this.storage.getSelf() + + const version = 1 + let flags = 0 + + if (self) { + flags |= 1 + } + + writer.buffer[0] = version + writer.pos += 1 + + writer.int32(flags) + writer.object(this._primaryDc) + + if (self) { + writer.int32(self.userId) + writer.boolean(self.isBot) + } + + writer.bytes(this.primaryConnection.authKey) + + return encodeUrlSafeBase64(writer.result()) + } + + /** + * Request the session to be imported from the given session string. + * + * Note that the string will not be parsed and imported right away, + * instead, it will be imported when `connect()` is called + * + * Also note that the session will only be imported in case + * the storage is missing authorization (i.e. does not contain + * auth key for the primary DC), otherwise it will be ignored. + */ + importSession(session: string): void { + this._importFrom = session + } } diff --git a/packages/core/src/storage/abstract.ts b/packages/core/src/storage/abstract.ts index 542cdcd9..0bc8761a 100644 --- a/packages/core/src/storage/abstract.ts +++ b/packages/core/src/storage/abstract.ts @@ -141,8 +141,4 @@ export interface ITelegramStorage { * update with the peers added, which might not be very efficient. */ getFullPeerById(id: number): MaybeAsync - - // TODO! - // exportToString(): MaybeAsync - // importFromString(): MaybeAsync }