1
0
Эх сурвалжийг харах

GramJS: Add `LocalStorageSession` with keys and hashes for all DCs

painor 5 жил өмнө
parent
commit
fe7feec869

+ 7 - 5
src/api/gramjs/client.ts

@@ -1,6 +1,10 @@
+<<<<<<< HEAD
 import {
   TelegramClient, session, GramJsApi, MTProto,
 } from '../../lib/gramjs';
+=======
+import { TelegramClient, sessions, Api as GramJs } from '../../lib/gramjs';
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
 import { Logger as GramJsLogger } from '../../lib/gramjs/extensions';
 
 import { DEBUG } from '../../config';
@@ -23,11 +27,9 @@ GramJsLogger.getLogger().level = 'debug';
 let client: TelegramClient;
 
 export async function init(sessionId: string) {
-  const { StringSession } = session;
-
-  const stringSession = new StringSession(sessionId);
+  const session = new sessions.LocalStorageSession(sessionId);
   client = new TelegramClient(
-    stringSession,
+    session,
     process.env.TELEGRAM_T_API_ID,
     process.env.TELEGRAM_T_API_HASH,
     { useWSS: true } as any,
@@ -47,7 +49,7 @@ export async function init(sessionId: string) {
       password: onRequestPassword,
     } as any);
 
-    const newSessionId = stringSession.save();
+    const newSessionId = session.save();
 
     if (DEBUG) {
       // eslint-disable-next-line no-console

+ 6 - 0
src/api/gramjs/connectors/chats.ts

@@ -33,7 +33,13 @@ export async function fetchChats(
     offsetDate,
   }));
 
+<<<<<<< HEAD
   if (!result || !(result instanceof ctors.messages.DialogsSlice) || !result.dialogs.length) {
+=======
+  // I had to change it to Dialogs to work for me.
+  if (!result || !(result instanceof GramJs.messages.DialogsSlice
+      || result instanceof GramJs.messages.Dialogs) || !result.dialogs.length) {
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
     throw new Error(UNSUPPORTED_RESPONSE);
   }
 

+ 110 - 17
src/lib/gramjs/Utils.js

@@ -66,7 +66,10 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
     }
     if (entity instanceof constructors.Channel) {
         if ((entity.accessHash !== undefined && !entity.min) || !checkHash) {
-            return new constructors.InputPeerChannel({ channelId: entity.id, accessHash: entity.accessHash })
+            return new constructors.InputPeerChannel({
+                channelId: entity.id,
+                accessHash: entity.accessHash
+            })
         } else {
             throw new TypeError('Channel without access_hash or min info cannot be input')
         }
@@ -74,14 +77,23 @@ function getInputPeer(entity, allowSelf = true, checkHash = true) {
     if (entity instanceof constructors.ChannelForbidden) {
         // "channelForbidden are never min", and since their hash is
         // also not optional, we assume that this truly is the case.
-        return new constructors.InputPeerChannel({ channelId: entity.id, accessHash: entity.accessHash })
+        return new constructors.InputPeerChannel({
+            channelId: entity.id,
+            accessHash: entity.accessHash
+        })
     }
 
     if (entity instanceof constructors.InputUser) {
-        return new constructors.InputPeerUser({ userId: entity.userId, accessHash: entity.accessHash })
+        return new constructors.InputPeerUser({
+            userId: entity.userId,
+            accessHash: entity.accessHash
+        })
     }
     if (entity instanceof constructors.InputChannel) {
-        return new constructors.InputPeerChannel({ channelId: entity.channelId, accessHash: entity.accessHash })
+        return new constructors.InputPeerChannel({
+            channelId: entity.channelId,
+            accessHash: entity.accessHash
+        })
     }
     if (entity instanceof constructors.UserEmpty) {
         return new constructors.InputPeerEmpty()
@@ -123,11 +135,17 @@ function getInputChannel(entity) {
         return entity
     }
     if (entity instanceof constructors.Channel || entity instanceof constructors.ChannelForbidden) {
-        return new constructors.InputChannel({ channelId: entity.id, accessHash: entity.accessHash || 0 })
+        return new constructors.InputChannel({
+            channelId: entity.id,
+            accessHash: entity.accessHash || 0
+        })
     }
 
     if (entity instanceof constructors.InputPeerChannel) {
-        return new constructors.InputChannel({ channelId: entity.channelId, accessHash: entity.accessHash })
+        return new constructors.InputChannel({
+            channelId: entity.channelId,
+            accessHash: entity.accessHash
+        })
     }
     _raiseCastFail(entity, 'InputChannel')
 }
@@ -173,7 +191,10 @@ function getInputUser(entity) {
     }
 
     if (entity instanceof constructors.InputPeerUser) {
-        return new constructors.InputUser({ userId: entity.userId, accessHash: entity.accessHash })
+        return new constructors.InputUser({
+            userId: entity.userId,
+            accessHash: entity.accessHash
+        })
     }
 
     _raiseCastFail(entity, 'InputUser')
@@ -248,7 +269,10 @@ function getInputLocation(location) {
             throw new Error()
         }
         if (location.SUBCLASS_OF_ID === 0x1523d462) {
-            return { dcId: null, inputLocation: location }
+            return {
+                dcId: null,
+                inputLocation: location
+            }
         }
     } catch (e) {
         _raiseCastFail(location, 'InputFileLocation')
@@ -265,7 +289,8 @@ function getInputLocation(location) {
 
     if (location instanceof constructors.Document) {
         return {
-            dcId: location.dcId, inputLocation: new constructors.InputDocumentFileLocation({
+            dcId: location.dcId,
+            inputLocation: new constructors.InputDocumentFileLocation({
                 id: location.id,
                 accessHash: location.accessHash,
                 fileReference: location.fileReference,
@@ -274,7 +299,8 @@ function getInputLocation(location) {
         }
     } else if (location instanceof constructors.Photo) {
         return {
-            dcId: location.dcId, inputLocation: new constructors.InputPhotoFileLocation({
+            dcId: location.dcId,
+            inputLocation: new constructors.InputPhotoFileLocation({
                 id: location.id,
                 accessHash: location.accessHash,
                 fileReference: location.fileReference,
@@ -429,7 +455,8 @@ function resolveId(markedId) {
     // marked version is -10000xyz, which in turn looks like a channel but
     // it becomes 00xyz (= xyz). Hence, we must assert that there are only
     // two zeroes.
-    const m = markedId.toString().match(/-100([^0]\d*)/)
+    const m = markedId.toString()
+        .match(/-100([^0]\d*)/)
     if (m) {
         return [parseInt(m[1]), constructors.PeerChannel]
     }
@@ -455,7 +482,10 @@ function _getEntityPair(entityId, entities, cache, getInputPeer = getInputPeer)
             inputEntity = null
         }
     }
-    return { entity, inputEntity }
+    return {
+        entity,
+        inputEntity
+    }
 }
 
 function getMessageId(message) {
@@ -479,7 +509,8 @@ function parsePhone(phone) {
     if (typeof phone === 'number') {
         return phone.toString()
     } else {
-        phone = phone.toString().replace(/[+()\s-]/gm, '')
+        phone = phone.toString()
+            .replace(/[+()\s-]/gm, '')
         if (!isNaN(phone)) {
             return phone
         }
@@ -502,15 +533,24 @@ function parseUsername(username) {
     if (m) {
         username = username.replace(m[0], '')
         if (m[1]) {
-            return { username: username, isInvite: true }
+            return {
+                username: username,
+                isInvite: true
+            }
         } else {
             username = rtrim(username, '/')
         }
     }
     if (username.match(VALID_USERNAME_RE)) {
-        return { username: username.toLowerCase(), isInvite: false }
+        return {
+            username: username.toLowerCase(),
+            isInvite: false
+        }
     } else {
-        return { username: null, isInvite: false }
+        return {
+            username: null,
+            isInvite: false
+        }
     }
 }
 
@@ -562,6 +602,59 @@ function isListLike(item) {
     )
 }
 
+function getDC(dcId, cdn = false) {
+    switch (dcId) {
+        case 1:
+            return {
+                id: 1,
+                ipAddress: 'pluto.web.telegram.org',
+                port: 443
+            }
+        case 2:
+            return {
+                id: 2,
+                ipAddress: 'venus.web.telegram.org',
+                port: 443
+            }
+        case 3:
+            return {
+                id: 3,
+                ipAddress: 'aurora.web.telegram.org',
+                port: 443
+            }
+        case 4:
+            return {
+                id: 4,
+                ipAddress: 'vesta.web.telegram.org',
+                port: 443
+            }
+        case 5:
+            return {
+                id: 5,
+                ipAddress: 'flora.web.telegram.org',
+                port: 443
+            }
+        default:
+            throw new Error(`Cannot find the DC with the ID of ${dcId}`)
+    }
+    // TODO chose based on current connection method
+    /*
+    if (!this._config) {
+        this._config = await this.invoke(new requests.help.GetConfig())
+    }
+    if (cdn && !this._cdnConfig) {
+        this._cdnConfig = await this.invoke(new requests.help.GetCdnConfig())
+        for (const pk of this._cdnConfig.publicKeys) {
+            addKey(pk.publicKey)
+        }
+    }
+    for (const DC of this._config.dcOptions) {
+        if (DC.id === dcId && Boolean(DC.ipv6) === this._useIPV6 && Boolean(DC.cdn) === cdn) {
+            return DC
+        }
+    }*/
+}
+
 module.exports = {
     getMessageId,
     _getEntityPair,
@@ -580,5 +673,5 @@ module.exports = {
     getAppropriatedPartSize,
     getInputLocation,
     strippedPhotoToJpg,
-
+    getDC
 }

+ 68 - 36
src/lib/gramjs/client/TelegramClient.js

@@ -12,7 +12,7 @@ const { LAYER } = require('../tl/AllTLObjects')
 const { constructors, requests } = require('../tl')
 const { computeCheck } = require('../Password')
 const MTProtoSender = require('../network/MTProtoSender')
-const { FloodWaitError } = require("../errors/RPCErrorList")
+const { FloodWaitError } = require('../errors/RPCErrorList')
 const { ConnectionTCPObfuscated } = require('../network/connection/TCPObfuscated')
 
 const DEFAULT_DC_ID = 2
@@ -45,7 +45,13 @@ class TelegramClient {
         useWSS: false,
     }
 
-
+    /**
+     *
+     * @param session {StringSession|LocalStorageSession}
+     * @param apiId
+     * @param apiHash
+     * @param opts
+     */
     constructor(session, apiId, apiHash, opts = TelegramClient.DEFAULT_OPTIONS) {
         if (apiId === undefined || apiHash === undefined) {
             throw Error('Your API ID or Hash are invalid. Please read "Requirements" on README.md')
@@ -63,7 +69,7 @@ class TelegramClient {
         // Determine what session we will use
         if (typeof session === 'string' || !session) {
             try {
-                throw new Error("not implemented")
+                throw new Error('not implemented')
             } catch (e) {
                 console.log(e)
                 session = new MemorySession()
@@ -103,8 +109,10 @@ class TelegramClient {
                 layer: LAYER,
                 query: new requests.InitConnectionRequest({
                     apiId: this.apiId,
-                    deviceModel: args.deviceModel || os.type().toString() || 'Unknown',
-                    systemVersion: args.systemVersion || os.release().toString() || '1.0',
+                    deviceModel: args.deviceModel || os.type()
+                        .toString() || 'Unknown',
+                    systemVersion: args.systemVersion || os.release()
+                        .toString() || '1.0',
                     appVersion: args.appVersion || '1.0',
                     langCode: args.langCode,
                     langPack: '', // this should be left empty.
@@ -130,10 +138,17 @@ class TelegramClient {
      * @returns {Promise<void>}
      */
     async connect() {
+<<<<<<< HEAD
         await initRSA()
         await this.session.load()
         this._sender = new MTProtoSender(this.session.authKey, {
+=======
+        //await this.session.load()
+
+        this._sender = new MTProtoSender(this.session.getAuthKey(), {
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
             logger: this._log,
+            dcId:this.session.dcId,
             retries: this._connectionRetries,
             delay: this._retryDelay,
             autoReconnect: this._autoReconnect,
@@ -147,8 +162,7 @@ class TelegramClient {
         if (!await this._sender.connect(connection)) {
             return
         }
-        this.session.authKey = this._sender.authKey
-        await this.session.save()
+        this.session.setAuthKey(this._sender.authKey)
         await this._sender.send(this._initWith(
             new requests.help.GetConfigRequest({}),
         ))
@@ -169,8 +183,6 @@ class TelegramClient {
                 console.log('err is ', e)
             }
 
-            // this.session.save()
-
             // We need to send some content-related request at least hourly
             // for Telegram to keep delivering updates, otherwise they will
             // just stop even if we're connected. Do so every 30 minutes.
@@ -199,20 +211,18 @@ class TelegramClient {
 
     async _switchDC(newDc) {
         this._log.info(`Reconnecting to new data center ${newDc}`)
-        const DC = await this._getDC(newDc)
+        const DC = utils.getDC(newDc)
         this.session.setDC(newDc, DC.ipAddress, DC.port)
         // authKey's are associated with a server, which has now changed
         // so it's not valid anymore. Set to None to force recreating it.
         await this._sender.authKey.setKey(null)
-        this.session.authKey = null
-        await this.session.save()
+        this.session.setAuthKey(null)
         await this.disconnect()
         return await this.connect()
     }
 
-    async _authKeyCallback(authKey) {
-        this.session.authKey = authKey
-        await this.session.save()
+    async _authKeyCallback(authKey,dcId) {
+        this.session.setAuthKey(authKey,dcId)
     }
 
     // endregion
@@ -240,9 +250,16 @@ class TelegramClient {
     }
 
     async _createExportedSender(dcId, retries) {
-        const dc = await this._getDC(dcId)
-        const sender = new MTProtoSender(this.session.dcId === dcId ? this._sender.authKey : null,
-            { logger: this._log })
+        const dc = utils.getDC(dcId)
+        const sender = new MTProtoSender(this.session.getAuthKey(dcId),
+            { logger: this._log,
+                dcId:dcId,
+                retries: this._connectionRetries,
+                delay: this._retryDelay,
+                autoReconnect: this._autoReconnect,
+                connectTimeout: this._timeout,
+                authKeyCallback: this._authKeyCallback.bind(this),
+            })
         for (let i = 0; i < retries; i++) {
             try {
                 await sender.connect(new this._connection(
@@ -253,9 +270,16 @@ class TelegramClient {
                 ))
                 if (this.session.dcId !== dcId) {
                     this._log.info(`Exporting authorization for data center ${dc.ipAddress}`)
+<<<<<<< HEAD
                     const auth = await this.invoke(new requests.auth.ExportAuthorizationRequest({ dcId: dcId }))
                     const req = this._initWith(new requests.auth.ImportAuthorizationRequest({
                             id: auth.id, bytes: auth.bytes,
+=======
+                    const auth = await this.invoke(new requests.auth.ExportAuthorization({ dcId: dcId }))
+                    const req = this._initWith(new requests.auth.ImportAuthorization({
+                            id: auth.id,
+                            bytes: auth.bytes,
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
                         },
                     ))
                     await sender.send(req)
@@ -312,7 +336,7 @@ class TelegramClient {
             try {
                 sender = await this._borrowExportedSender(dcId)
             } catch (e) {
-                if (e.message==='DC_ID_INVALID') {
+                if (e.message === 'DC_ID_INVALID') {
                     // Can't export a sender for the ID we are currently in
                     sender = this._sender
                     exported = false
@@ -476,7 +500,7 @@ class TelegramClient {
             })
             return result
         } catch (e) {
-            if (e.message==='LOCATION_INVALID') {
+            if (e.message === 'LOCATION_INVALID') {
                 const ie = await this.getInputEntity(entity)
                 if (ie instanceof constructors.InputPeerChannel) {
                     const full = await this.invoke(new requests.channels.GetFullChannelRequest({
@@ -500,10 +524,10 @@ class TelegramClient {
 
     _pickFileSize(sizes, sizeType) {
         if (!sizeType || !sizes || !sizes.length) {
-            return null;
+            return null
         }
 
-        return sizes.find((s) => s.type === sizeType);
+        return sizes.find((s) => s.type === sizeType)
     }
 
 
@@ -577,7 +601,7 @@ class TelegramClient {
             return
         }
 
-        const size = doc.thumbs ? this._pickFileSize(doc.thumbs, args.sizeType) : null;
+        const size = doc.thumbs ? this._pickFileSize(doc.thumbs, args.sizeType) : null
         if (size && (size instanceof constructors.PhotoCachedSize || size instanceof constructors.PhotoStrippedSize)) {
             return this._downloadCachedPhotoSize(size)
         }
@@ -614,6 +638,7 @@ class TelegramClient {
         throw new Error('not implemented')
     }
 
+<<<<<<< HEAD
     // region Working with different connections/Data Centers
 
     async _getDC(dcId, cdn = false) {
@@ -651,6 +676,8 @@ class TelegramClient {
 
     // endregion
 
+=======
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
     // region Invoking Telegram request
     /**
      * Invokes a MTProtoRequest (sends and receives it) and returns its result
@@ -689,8 +716,8 @@ class TelegramClient {
                 this._entityCache.add(result)
                 return result
             } catch (e) {
-                if (e instanceof errors.ServerError || e.message==='RPC_CALL_FAIL' ||
-                    e.message==='RPC_MCGET_FAIL') {
+                if (e instanceof errors.ServerError || e.message === 'RPC_CALL_FAIL' ||
+                    e.message === 'RPC_MCGET_FAIL') {
                     this._log.warn(`Telegram is having internal issues ${e.constructor.name}`)
                     await sleep(2000)
                 } else if (e instanceof errors.FloodWaitError || e instanceof errors.FloodTestPhoneWaitError) {
@@ -777,7 +804,7 @@ class TelegramClient {
             try {
                 const value = await args.code()
                 if (!value) {
-                    throw new Error("the phone code is empty")
+                    throw new Error('the phone code is empty')
                 }
 
                 if (signUp) {
@@ -795,17 +822,17 @@ class TelegramClient {
                 }
                 break
             } catch (e) {
-                if (e.message==='SESSION_PASSWORD_NEEDED') {
+                if (e.message === 'SESSION_PASSWORD_NEEDED') {
                     twoStepDetected = true
                     break
-                } else if (e.message==='PHONE_NUMBER_OCCUPIED') {
+                } else if (e.message === 'PHONE_NUMBER_OCCUPIED') {
                     signUp = true
-                } else if (e.message==='PHONE_NUMBER_UNOCCUPIED') {
+                } else if (e.message === 'PHONE_NUMBER_UNOCCUPIED') {
                     signUp = true
-                } else if (e.message==='PHONE_CODE_EMPTY' ||
-                    e.message==='PHONE_CODE_EXPIRED' ||
-                    e.message==='PHONE_CODE_HASH_EMPTY' ||
-                    e.message==='PHONE_CODE_INVALID') {
+                } else if (e.message === 'PHONE_CODE_EMPTY' ||
+                    e.message === 'PHONE_CODE_EXPIRED' ||
+                    e.message === 'PHONE_CODE_HASH_EMPTY' ||
+                    e.message === 'PHONE_CODE_INVALID') {
                     console.log('Invalid code. Please try again.')
                 } else {
                     throw e
@@ -954,11 +981,15 @@ class TelegramClient {
                     settings: new constructors.CodeSettings(),
                 }))
             } catch (e) {
+<<<<<<< HEAD
 <<<<<<< HEAD
                 if (e instanceof errors.AuthRestartError) {
                     return await this.sendCodeRequest(phone, forceSMS)
 =======
                 if (e.message==='AUTH_RESTART') {
+=======
+                if (e.message === 'AUTH_RESTART') {
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
                     return this.sendCodeRequest(phone, forceSMS)
 >>>>>>> f70d85dd... Gram JS: Replace generated `tl/*` contents with runtime logic; TypeScript typings
                 }
@@ -1054,7 +1085,7 @@ class TelegramClient {
                     }
                 }
             } catch (e) {
-                if (e.message==='BOT_METHOD_INVALID') {
+                if (e.message === 'BOT_METHOD_INVALID') {
                     throw new Error('Cannot get entity by phone number as a ' +
                         'bot (try using integer IDs, not strings)')
                 }
@@ -1099,7 +1130,7 @@ class TelegramClient {
                         }
                     }
                 } catch (e) {
-                    if (e.message ==='USERNAME_NOT_OCCUPIED') {
+                    if (e.message === 'USERNAME_NOT_OCCUPIED') {
                         throw new Error(`No user has "${username}" as username`)
                     }
                     throw e
@@ -1217,7 +1248,8 @@ class TelegramClient {
         if (peer instanceof constructors.PeerUser) {
             const users = await this.invoke(new requests.users.GetUsersRequest({
                 id: [new constructors.InputUser({
-                    userId: peer.userId, accessHash: 0,
+                    userId: peer.userId,
+                    accessHash: 0,
                 })],
             }))
             if (users && !(users[0] instanceof constructors.UserEmpty)) {

+ 16 - 2
src/lib/gramjs/crypto/AuthKey.js

@@ -4,19 +4,33 @@ const { sleep } = require('../Helpers')
 
 class AuthKey {
 
+    constructor(value, hash) {
+        if (!hash || !value) {
+            return
+        }
+        this._key = value
+        this._hash = hash
+        const reader = new BinaryReader(hash)
+        this.auxHash = reader.readLong(false)
+        reader.read(4)
+        this.keyId = reader.readLong(false)
+    }
+
     async setKey(value) {
         if (!value) {
-            this._key = this.auxHash = this.keyId = null
+            this._key = this.auxHash = this.keyId = this._hash = null
             return
         }
         if (value instanceof AuthKey) {
             this._key = value._key
             this.auxHash = value.auxHash
             this.keyId = value.keyId
+            this._hash = value._hash
             return
         }
         this._key = value
-        const reader = new BinaryReader(Buffer.from(await sha1(this._key)))
+        this._hash = await sha1(this._key)
+        const reader = new BinaryReader(this._hash)
         this.auxHash = reader.readLong(false)
         reader.read(4)
         this.keyId = reader.readLong(false)

+ 10 - 0
src/lib/gramjs/index.d.ts

@@ -2,6 +2,7 @@ export * from './tl/gramJsApi';
 export { default as gramJsApi } from './tl/gramJsApi';
 
 export { default as TelegramClient } from './client/TelegramClient';
+<<<<<<< HEAD
 
 import connection from './network';
 import tl from './tl';
@@ -20,3 +21,12 @@ export {
     utils,
     errors,
 };
+=======
+export { default as connection } from './network';
+export { default as tl } from './tl';
+export { default as version } from './Version';
+export { default as events } from './events';
+export { default as utils } from './Utils';
+export { default as errors } from './errors';
+export { default as sessions } from './sessions';
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs

+ 5 - 1
src/lib/gramjs/index.js

@@ -13,10 +13,14 @@ const version = require('./Version')
 const events = require('./events')
 const utils = require('./Utils')
 const errors = require('./errors')
-const session = require('./sessions')
+const sessions = require('./sessions')
 const extensions = require('./extensions')
 
 module.exports = {
+<<<<<<< HEAD
     gramJsApi, TelegramClient, session, connection, extensions,
+=======
+    Api, TelegramClient, sessions, connection, extensions,
+>>>>>>> 42589b8b... GramJS: Add `LocalStorageSession` with keys and hashes for all DCs
     tl, version, events, utils, errors,
 }

+ 4 - 2
src/lib/gramjs/network/MTProtoSender.js

@@ -62,6 +62,7 @@ class MTProtoSender {
         const args = { ...MTProtoSender.DEFAULT_OPTIONS, ...opts }
         this._connection = null
         this._log = args.logger
+        this._dcId = args.dcId
         this._retries = args.retries
         this._delay = args.delay
         this._autoReconnect = args.autoReconnect
@@ -218,7 +219,8 @@ class MTProtoSender {
         this._log.info('Connecting to {0}...'.replace('{0}', this._connection))
         await this._connection.connect()
         this._log.debug('Connection success!')
-        if (!this.authKey._key) {
+        //process.exit(0)
+        if (!this.authKey.getKey()) {
             const plain = new MtProtoPlainSender(this._connection, this._log)
             this._log.debug('New auth_key attempt ...')
             const res = await doAuthentication(plain, this._log)
@@ -234,7 +236,7 @@ class MTProtoSender {
              * switch to different data centers.
              */
             if (this._authKeyCallback) {
-                await this._authKeyCallback(this.authKey)
+                await this._authKeyCallback(this.authKey,this._dcId)
             }
         } else {
             this._log.debug('Already have an auth key ...')

+ 103 - 0
src/lib/gramjs/sessions/LocalStorageSession.js

@@ -0,0 +1,103 @@
+const MemorySession = require('./Memory')
+const AuthKey = require('../crypto/AuthKey')
+const utils = require('../Utils')
+
+const STORAGE_KEY_BASE = 'GramJs:session:'
+
+class LocalStorageSession extends MemorySession {
+    constructor(sessionId) {
+        super()
+        this._storageKey = null
+        this._authKeys = {}
+
+        if (sessionId) {
+            try {
+                const json = localStorage.getItem(sessionId)
+                const { mainDcId, keys, hashes } = JSON.parse(json)
+                const { ipAddress, port } = utils.getDC(mainDcId)
+
+                this.setDC(mainDcId, ipAddress, port)
+
+                Object.keys(keys).forEach((dcId) => {
+                    this._authKeys[dcId] = new AuthKey(
+                        Buffer.from(keys[dcId].data),
+                        Buffer.from(hashes[dcId].data)
+                    )
+                })
+
+                this._storageKey = sessionId
+            } catch (err) {
+                throw new Error(`Failed to retrieve or parse JSON from Local Storage for key ${sessionId}`)
+            }
+        }
+    }
+
+    setDC(dcId, serverAddress, port) {
+        this._dcId = dcId
+        this._serverAddress = serverAddress
+        this._port = port
+
+        if (this._storageKey) {
+            this._save()
+        }
+
+        delete this._authKeys[dcId]
+
+        this._updateStorage()
+    }
+
+    save() {
+        if (!this._storageKey) {
+            this._storageKey = generateStorageKey()
+        }
+
+        this._updateStorage()
+
+        return this._storageKey
+    }
+
+    _updateStorage() {
+        if (!this._storageKey) {
+            return;
+        }
+
+        const sessionData = {
+            mainDcId: this._dcId,
+            keys: {},
+            hashes: {}
+        }
+
+        Object.keys(this._authKeys).map((dcId) => {
+            const authKey = this._authKeys[dcId]
+            sessionData.keys[dcId] = authKey._key
+            sessionData.hashes[dcId] = authKey._hash
+        })
+
+        localStorage.setItem(this._storageKey, JSON.stringify(sessionData))
+    }
+
+    get authKey() {
+        throw new Error('Not supported')
+    }
+
+    set authKey(value) {
+        throw new Error('Not supported')
+    }
+
+    getAuthKey(dcId = this._dcId) {
+        return this._authKeys[dcId]
+    }
+
+    setAuthKey(authKey, dcId = this._dcId) {
+        this._authKeys[dcId] = authKey
+
+        this._updateStorage()
+    }
+}
+
+function generateStorageKey() {
+    // Creating two sessions at the same moment is not expected nor supported.
+    return `${STORAGE_KEY_BASE}${Date.now()}`
+}
+
+module.exports = LocalStorageSession

+ 0 - 1
src/lib/gramjs/sessions/Memory.js

@@ -9,7 +9,6 @@ class MemorySession extends Session {
         this._serverAddress = null
         this._dcId = 0
         this._port = null
-        this._authKey = null
         this._takeoutId = null
 
         this._entities = new Set()

+ 3 - 0
src/lib/gramjs/sessions/index.js

@@ -1,6 +1,9 @@
 const Memory = require('./Memory')
 const StringSession = require('./StringSession')
+const LocalStorageSession = require('./LocalStorageSession')
+
 module.exports = {
     Memory,
     StringSession,
+    LocalStorageSession
 }