From fe5b45e4f6e939a93f3f221ac5762d03f6d6a706 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 12 Jun 2021 20:31:44 +0300 Subject: [PATCH] Refactor sessions: Switch to Webogram style, clean up legacy artifacts --- src/App.tsx | 7 +- src/api/gramjs/methods/auth.ts | 8 +- src/api/gramjs/methods/client.ts | 28 +++--- src/api/gramjs/provider.ts | 8 +- src/api/gramjs/worker/provider.ts | 6 +- src/api/gramjs/worker/types.ts | 4 +- src/api/types/index.ts | 52 +--------- src/api/types/misc.ts | 56 +++++++++++ src/api/types/updates.ts | 10 +- src/config.ts | 4 +- src/global/cache.ts | 8 +- src/lib/gramjs/sessions/CallbackSession.js | 108 +++++++++++++++++++++ src/lib/gramjs/sessions/index.js | 2 + src/modules/actions/api/initial.ts | 68 ++++--------- src/modules/actions/api/sessions.ts | 100 +++++++++++++++++++ src/modules/actions/apiUpdaters/initial.ts | 36 ++++--- 16 files changed, 354 insertions(+), 151 deletions(-) create mode 100644 src/api/types/misc.ts create mode 100644 src/lib/gramjs/sessions/CallbackSession.js create mode 100644 src/modules/actions/api/sessions.ts diff --git a/src/App.tsx b/src/App.tsx index 01668300e..ebf514db3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -4,7 +4,7 @@ import React, { withGlobal } from './lib/teact/teactn'; import { GlobalActions, GlobalState } from './global/types'; import { - GRAMJS_SESSION_ID_KEY, INACTIVE_MARKER, LEGACY_SESSION_KEY, PAGE_TITLE, + LEGACY_SESSION_KEY, INACTIVE_MARKER, SESSION_USER_KEY, PAGE_TITLE, } from './config'; import { pick } from './util/iteratees'; import { updateSizes } from './util/windowSize'; @@ -55,10 +55,9 @@ const App: FC = ({ authState, disconnect }) => { } } - const hasSession = localStorage.getItem(GRAMJS_SESSION_ID_KEY); - const hasLegacySession = localStorage.getItem(LEGACY_SESSION_KEY); + const hasSession = localStorage.getItem(SESSION_USER_KEY) || localStorage.getItem(LEGACY_SESSION_KEY); - return (hasSession || hasLegacySession) ? renderMain() : ; + return hasSession ? renderMain() : ; }; function renderMain() { diff --git a/src/api/gramjs/methods/auth.ts b/src/api/gramjs/methods/auth.ts index 68c0a7423..09f32d42d 100644 --- a/src/api/gramjs/methods/auth.ts +++ b/src/api/gramjs/methods/auth.ts @@ -105,12 +105,8 @@ export function onAuthError(err: Error) { }); } -export function onAuthReady(sessionId: string, sessionJson: string) { - onUpdate({ - ...buildAuthStateUpdate('authorizationStateReady'), - sessionId, - sessionJson, - }); +export function onAuthReady() { + onUpdate(buildAuthStateUpdate('authorizationStateReady')); } export function onCurrentUserUpdate(currentUser: ApiUser) { diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts index e055957a0..93432d901 100644 --- a/src/api/gramjs/methods/client.ts +++ b/src/api/gramjs/methods/client.ts @@ -4,7 +4,9 @@ import { import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index'; import { TwoFaParams } from '../../../lib/gramjs/client/2fa'; -import { ApiMediaFormat, ApiOnProgress, OnApiUpdate } from '../../types'; +import { + ApiMediaFormat, ApiOnProgress, ApiSessionData, OnApiUpdate, +} from '../../types'; import { DEBUG, DEBUG_GRAMJS, UPLOAD_WORKERS, IS_TEST, APP_VERSION, @@ -30,7 +32,7 @@ let onUpdate: OnApiUpdate; let client: TelegramClient; let isConnected = false; -export async function init(sessionInfo: string, _onUpdate: OnApiUpdate) { +export async function init(_onUpdate: OnApiUpdate, sessionData?: ApiSessionData) { onUpdate = _onUpdate; if (DEBUG) { @@ -38,12 +40,8 @@ export async function init(sessionInfo: string, _onUpdate: OnApiUpdate) { console.log('>>> START INIT API'); } - const session = IS_TEST - ? new sessions.LocalStorageSession(sessionInfo) - : new sessions.IdbSession(sessionInfo); - client = new TelegramClient( - session, + new sessions.CallbackSession(sessionData, onSessionUpdate), process.env.TELEGRAM_T_API_ID, process.env.TELEGRAM_T_API_HASH, { @@ -72,17 +70,14 @@ export async function init(sessionInfo: string, _onUpdate: OnApiUpdate) { onError: onAuthError, }); - const newSessionId = await session.save(); - const sessionJson = JSON.stringify(session.getSessionData(true)); - if (DEBUG) { // eslint-disable-next-line no-console console.log('>>> FINISH INIT API'); // eslint-disable-next-line no-console - console.log('[GramJs/client] CONNECTED as ', newSessionId); + console.log('[GramJs/client] CONNECTED'); } - onAuthReady(newSessionId, sessionJson); + onAuthReady(); onUpdate({ '@type': 'updateApiReady' }); void fetchCurrentUser(); @@ -109,6 +104,13 @@ export function getClient() { return client; } +function onSessionUpdate(sessionData: ApiSessionData) { + onUpdate({ + '@type': 'updateSession', + sessionData, + }); +} + function handleGramJsUpdate(update: any) { if (update instanceof connection.UpdateConnectionState) { isConnected = update.state === connection.UpdateConnectionState.connected; @@ -125,7 +127,7 @@ export async function invokeRequest( if (!isConnected) { if (DEBUG) { // eslint-disable-next-line no-console - console.warn(`[GramJs/client] INVOKE ${request.className} ERROR: Client is not connected`); + console.warn(`[GramJs/client] INVOKE ERROR ${request.className}: Client is not connected`); } return undefined; diff --git a/src/api/gramjs/provider.ts b/src/api/gramjs/provider.ts index e40ee106d..8d27761e8 100644 --- a/src/api/gramjs/provider.ts +++ b/src/api/gramjs/provider.ts @@ -1,4 +1,6 @@ -import { ApiOnProgress, ApiUpdate, OnApiUpdate } from '../types'; +import { + ApiOnProgress, ApiSessionData, ApiUpdate, OnApiUpdate, +} from '../types'; import { Methods, MethodArgs, MethodResponse } from './methods/types'; import { API_THROTTLE_RESET_UPDATES, API_UPDATE_THROTTLE } from '../../config'; @@ -16,7 +18,7 @@ import * as methods from './methods'; let onUpdate: OnApiUpdate; -export async function initApi(_onUpdate: OnApiUpdate, sessionInfo = '') { +export async function initApi(_onUpdate: OnApiUpdate, sessionData?: ApiSessionData) { onUpdate = _onUpdate; initUpdater(handleUpdate); @@ -28,7 +30,7 @@ export async function initApi(_onUpdate: OnApiUpdate, sessionInfo = '') { initManagement(handleUpdate); initTwoFaSettings(handleUpdate); - await initClient(sessionInfo, handleUpdate); + await initClient(handleUpdate, sessionData); } export function callApi(fnName: T, ...args: MethodArgs): MethodResponse { diff --git a/src/api/gramjs/worker/provider.ts b/src/api/gramjs/worker/provider.ts index 40060e42f..a02057169 100644 --- a/src/api/gramjs/worker/provider.ts +++ b/src/api/gramjs/worker/provider.ts @@ -1,6 +1,6 @@ import Worker from 'worker-loader!./worker'; -import { ApiOnProgress, OnApiUpdate } from '../../types'; +import { ApiOnProgress, ApiSessionData, OnApiUpdate } from '../../types'; import { Methods, MethodArgs, MethodResponse } from '../methods/types'; import { WorkerMessageEvent, ThenArg, OriginRequest } from './types'; @@ -20,7 +20,7 @@ const requestStatesByCallback = new Map(); // TODO Re-use `util/WorkerConnector.ts` -export function initApi(onUpdate: OnApiUpdate, sessionInfo = '') { +export function initApi(onUpdate: OnApiUpdate, sessionData?: ApiSessionData) { if (!worker) { if (DEBUG) { // eslint-disable-next-line no-console @@ -33,7 +33,7 @@ export function initApi(onUpdate: OnApiUpdate, sessionInfo = '') { return makeRequest({ type: 'initApi', - args: [sessionInfo], + args: [sessionData], }); } diff --git a/src/api/gramjs/worker/types.ts b/src/api/gramjs/worker/types.ts index a10075da3..5d440b584 100644 --- a/src/api/gramjs/worker/types.ts +++ b/src/api/gramjs/worker/types.ts @@ -1,4 +1,4 @@ -import { ApiUpdate } from '../../types'; +import { ApiSessionData, ApiUpdate } from '../../types'; import { Methods, MethodArgs, MethodResponse } from '../methods/types'; export type ThenArg = T extends Promise ? U : T; @@ -27,7 +27,7 @@ export interface WorkerMessageEvent { export type OriginRequest = { type: 'initApi'; messageId?: string; - args: [string]; + args: [ApiSessionData | undefined]; } | { type: 'callMethod'; messageId?: string; diff --git a/src/api/types/index.ts b/src/api/types/index.ts index 568e25676..8e2aae4d4 100644 --- a/src/api/types/index.ts +++ b/src/api/types/index.ts @@ -1,5 +1,3 @@ -import { ApiDocument } from './messages'; - export * from './users'; export * from './chats'; export * from './messages'; @@ -7,52 +5,4 @@ export * from './updates'; export * from './media'; export * from './payments'; export * from './settings'; - -export interface ApiOnProgress { - ( - progress: number, // Float between 0 and 1. - ...args: any[] - ): void; - - isCanceled?: boolean; - acceptsBuffer?: boolean; -} - -export interface ApiAttachment { - blobUrl: string; - filename: string; - mimeType: string; - size: number; - quick?: { - width: number; - height: number; - duration?: number; - }; - voice?: { - duration: number; - waveform: number[]; - }; - previewBlobUrl?: string; -} - -export interface ApiWallpaper { - slug: string; - document: ApiDocument; -} - -export interface ApiSession { - hash: string; - isCurrent: boolean; - isOfficialApp: boolean; - isPasswordPending: boolean; - deviceModel: string; - platform: string; - systemVersion: string; - appName: string; - appVersion: string; - dateCreated: number; - dateActive: number; - ip: string; - country: string; - region: string; -} +export * from './misc'; diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts new file mode 100644 index 000000000..d482025e2 --- /dev/null +++ b/src/api/types/misc.ts @@ -0,0 +1,56 @@ +import { ApiDocument } from './messages'; + +export interface ApiOnProgress { + ( + progress: number, // Float between 0 and 1. + ...args: any[] + ): void; + + isCanceled?: boolean; + acceptsBuffer?: boolean; +} + +export interface ApiAttachment { + blobUrl: string; + filename: string; + mimeType: string; + size: number; + quick?: { + width: number; + height: number; + duration?: number; + }; + voice?: { + duration: number; + waveform: number[]; + }; + previewBlobUrl?: string; +} + +export interface ApiWallpaper { + slug: string; + document: ApiDocument; +} + +export interface ApiSession { + hash: string; + isCurrent: boolean; + isOfficialApp: boolean; + isPasswordPending: boolean; + deviceModel: string; + platform: string; + systemVersion: string; + appName: string; + appVersion: string; + dateCreated: number; + dateActive: number; + ip: string; + country: string; + region: string; +} + +export interface ApiSessionData { + mainDcId: number; + keys: Record; + hashes: Record; +} diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 3c150b81d..a6defd790 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -9,6 +9,7 @@ import { ApiMessage, ApiPhoto, ApiPoll, ApiStickerSet, ApiThreadInfo, } from './messages'; import { ApiUser, ApiUserFullInfo, ApiUserStatus } from './users'; +import { ApiSessionData } from './misc'; export type ApiUpdateReady = { '@type': 'updateApiReady'; @@ -35,13 +36,16 @@ export type ApiUpdateConnectionStateType = ( export type ApiUpdateAuthorizationState = { '@type': 'updateAuthorizationState'; authorizationState: ApiUpdateAuthorizationStateType; - sessionId?: string; - sessionJson?: string; isCodeViaApp?: boolean; hint?: string; qrCode?: { token: string; expires: number }; }; +export type ApiUpdateSession = { + '@type': 'updateSession'; + sessionData?: ApiSessionData; +}; + export type ApiUpdateAuthorizationError = { '@type': 'updateAuthorizationError'; message: string; @@ -363,7 +367,7 @@ export type ApiUpdatePrivacy = { }; export type ApiUpdate = ( - ApiUpdateReady | + ApiUpdateReady | ApiUpdateSession | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds | ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds | diff --git a/src/config.ts b/src/config.ts index b7b57c34c..b774463be 100644 --- a/src/config.ts +++ b/src/config.ts @@ -15,8 +15,8 @@ export const IS_PERF = process.env.APP_ENV === 'perf'; export const DEBUG_ALERT_MSG = 'Shoot!\nSomething went wrong, please see the error details in Dev Tools Console.'; export const DEBUG_GRAMJS = false; -export const GRAMJS_SESSION_ID_KEY = 'GramJs:sessionId'; -export const LEGACY_SESSION_KEY = 'user_auth'; +export const SESSION_USER_KEY = 'user_auth'; +export const LEGACY_SESSION_KEY = 'GramJs:sessionId'; export const GLOBAL_STATE_CACHE_DISABLED = false; export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state'; diff --git a/src/global/cache.ts b/src/global/cache.ts index 2f6294f68..6900b35ec 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -11,8 +11,8 @@ import { GLOBAL_STATE_CACHE_DISABLED, GLOBAL_STATE_CACHE_KEY, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT, - GRAMJS_SESSION_ID_KEY, - MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, GLOBAL_STATE_CACHE_USER_LIST_LIMIT, + LEGACY_SESSION_KEY, + MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, GLOBAL_STATE_CACHE_USER_LIST_LIMIT, SESSION_USER_KEY, } from '../config'; import { IS_MOBILE_SCREEN } from '../util/environment'; import { pick } from '../util/iteratees'; @@ -44,8 +44,8 @@ export function initCache() { export function loadCache(initialState: GlobalState) { if (!GLOBAL_STATE_CACHE_DISABLED) { - const hasActiveSession = localStorage.getItem(GRAMJS_SESSION_ID_KEY); - if (hasActiveSession) { + const hasSession = localStorage.getItem(SESSION_USER_KEY) || localStorage.getItem(LEGACY_SESSION_KEY); + if (hasSession) { isAllowed = true; addCallback(updateCacheThrottled); return readCache(initialState); diff --git a/src/lib/gramjs/sessions/CallbackSession.js b/src/lib/gramjs/sessions/CallbackSession.js new file mode 100644 index 000000000..8e5bc5635 --- /dev/null +++ b/src/lib/gramjs/sessions/CallbackSession.js @@ -0,0 +1,108 @@ +const MemorySession = require('./Memory'); +const AuthKey = require('../crypto/AuthKey'); +const utils = require('../Utils'); + +class CallbackSession extends MemorySession { + constructor(sessionData, callback) { + super(); + + this._sessionData = sessionData; + this._callback = callback; + + this._authKeys = {}; + } + + get authKey() { + throw new Error('Not supported'); + } + + set authKey(value) { + throw new Error('Not supported'); + } + + async load() { + if (!this._sessionData) { + return; + } + + const { + mainDcId, + keys, + hashes, + } = this._sessionData; + const { + ipAddress, + port, + } = utils.getDC(mainDcId); + + this.setDC(mainDcId, ipAddress, port, true); + + await Promise.all(Object.keys(keys) + .map(async (dcId) => { + const key = typeof keys[dcId] === 'string' + ? Buffer.from(keys[dcId], 'hex') + : Buffer.from(keys[dcId]); + + if (hashes[dcId]) { + const hash = typeof hashes[dcId] === 'string' + ? Buffer.from(hashes[dcId], 'hex') + : Buffer.from(hashes[dcId]); + + this._authKeys[dcId] = new AuthKey(key, hash); + } else { + this._authKeys[dcId] = new AuthKey(); + await this._authKeys[dcId].setKey(key, true); + } + })); + } + + setDC(dcId, serverAddress, port, skipOnUpdate = false) { + this._dcId = dcId; + this._serverAddress = serverAddress; + this._port = port; + + delete this._authKeys[dcId]; + + if (!skipOnUpdate) { + void this._onUpdate(); + } + } + + getAuthKey(dcId = this._dcId) { + return this._authKeys[dcId]; + } + + setAuthKey(authKey, dcId = this._dcId) { + this._authKeys[dcId] = authKey; + + void this._onUpdate(); + } + + getSessionData() { + const sessionData = { + mainDcId: this._dcId, + keys: {}, + hashes: {}, + }; + + Object + .keys(this._authKeys) + .forEach((dcId) => { + const authKey = this._authKeys[dcId]; + sessionData.keys[dcId] = authKey._key.toString('hex'); + sessionData.hashes[dcId] = authKey._hash.toString('hex'); + }); + + return sessionData; + } + + _onUpdate() { + this._callback(this.getSessionData()); + } + + delete() { + this._callback(undefined); + } +} + +module.exports = CallbackSession; diff --git a/src/lib/gramjs/sessions/index.js b/src/lib/gramjs/sessions/index.js index 8e5bcd82d..5b86708af 100644 --- a/src/lib/gramjs/sessions/index.js +++ b/src/lib/gramjs/sessions/index.js @@ -3,6 +3,7 @@ const StringSession = require('./StringSession'); const CacheApiSession = require('./CacheApiSession'); const LocalStorageSession = require('./LocalStorageSession'); const IdbSession = require('./IdbSession'); +const CallbackSession = require('./CallbackSession'); module.exports = { Memory, @@ -10,4 +11,5 @@ module.exports = { CacheApiSession, LocalStorageSession, IdbSession, + CallbackSession, }; diff --git a/src/modules/actions/api/initial.ts b/src/modules/actions/api/initial.ts index aaa97eeb5..8ea11d6f9 100644 --- a/src/modules/actions/api/initial.ts +++ b/src/modules/actions/api/initial.ts @@ -7,34 +7,30 @@ import { GlobalState } from '../../../global/types'; import { LANG_CACHE_NAME, CUSTOM_BG_CACHE_NAME, - GRAMJS_SESSION_ID_KEY, MEDIA_CACHE_NAME, MEDIA_CACHE_NAME_AVATARS, MEDIA_PROGRESSIVE_CACHE_NAME, - LEGACY_SESSION_KEY, } from '../../../config'; import { initApi, callApi } from '../../../api/gramjs'; import { unsubscribe } from '../../../util/notifications'; import * as cacheApi from '../../../util/cacheApi'; import { HistoryWrapper } from '../../../util/history'; import { updateAppBadge } from '../../../util/appBadge'; +import { + storeSession, + loadStoredSession, + clearStoredSession, + importLegacySession, + clearLegacySessions, +} from './sessions'; addReducer('initApi', (global: GlobalState, actions) => { - let sessionInfo = localStorage.getItem(GRAMJS_SESSION_ID_KEY) || undefined; + (async () => { + await importLegacySession(); + void clearLegacySessions(); - if (!sessionInfo) { - const legacySessionJson = localStorage.getItem(LEGACY_SESSION_KEY); - if (legacySessionJson) { - const { dcID: legacySessionMainDc } = JSON.parse(legacySessionJson); - const legacySessionMainKeyRaw = localStorage.getItem(`dc${legacySessionMainDc}_auth_key`); - if (legacySessionMainKeyRaw) { - const legacySessionMainDcKey = legacySessionMainKeyRaw.replace(/"/g, ''); - sessionInfo = `session:${legacySessionMainDc}:${legacySessionMainDcKey}`; - } - } - } - - void initApi(actions.apiUpdate, sessionInfo); + void initApi(actions.apiUpdate, loadStoredSession()); + })(); }); addReducer('setAuthPhoneNumber', (global, actions, payload) => { @@ -116,10 +112,7 @@ addReducer('gotToAuthQrCode', (global) => { }); addReducer('saveSession', (global, actions, payload) => { - const { sessionId, sessionJson } = payload!; - localStorage.setItem(GRAMJS_SESSION_ID_KEY, sessionId); - - exportLegacySession(sessionJson, global.currentUserId!); + storeSession(payload.sessionData, global.currentUserId); }); addReducer('signOut', () => { @@ -132,20 +125,21 @@ addReducer('signOut', () => { }); addReducer('reset', () => { - localStorage.removeItem(GRAMJS_SESSION_ID_KEY); - clearLegacySession(); + clearStoredSession(); - cacheApi.clear(MEDIA_CACHE_NAME); - cacheApi.clear(MEDIA_CACHE_NAME_AVATARS); - cacheApi.clear(MEDIA_PROGRESSIVE_CACHE_NAME); - cacheApi.clear(CUSTOM_BG_CACHE_NAME); + void cacheApi.clear(MEDIA_CACHE_NAME); + void cacheApi.clear(MEDIA_CACHE_NAME_AVATARS); + void cacheApi.clear(MEDIA_PROGRESSIVE_CACHE_NAME); + void cacheApi.clear(CUSTOM_BG_CACHE_NAME); const langCachePrefix = LANG_CACHE_NAME.replace(/\d+$/, ''); const langCacheVersion = (LANG_CACHE_NAME.match(/\d+$/) || [0])[0]; for (let i = 0; i < langCacheVersion; i++) { - cacheApi.clear(`${langCachePrefix}${i === 0 ? '' : i}`); + void cacheApi.clear(`${langCachePrefix}${i === 0 ? '' : i}`); } + void clearLegacySessions(); + updateAppBadge(0); getDispatch().init(); @@ -188,23 +182,3 @@ addReducer('deleteDeviceToken', (global) => { delete newGlobal.push; setGlobal(newGlobal); }); - -function exportLegacySession(sessionJson: string, currentUserId: number) { - const { mainDcId, keys } = JSON.parse(sessionJson); - const legacySession = { dcID: mainDcId, id: currentUserId }; - localStorage.setItem(LEGACY_SESSION_KEY, JSON.stringify(legacySession)); - localStorage.setItem('dc', mainDcId); - Object.keys(keys).forEach((dcId) => { - localStorage.setItem(`dc${dcId}_auth_key`, `"${keys[dcId]}"`); - }); -} - -function clearLegacySession() { - localStorage.removeItem('dc5_auth_key'); - localStorage.removeItem('dc4_auth_key'); - localStorage.removeItem('dc3_auth_key'); - localStorage.removeItem('dc2_auth_key'); - localStorage.removeItem('dc1_auth_key'); - localStorage.removeItem('dc'); - localStorage.removeItem(LEGACY_SESSION_KEY); -} diff --git a/src/modules/actions/api/sessions.ts b/src/modules/actions/api/sessions.ts new file mode 100644 index 000000000..06a1b3fb9 --- /dev/null +++ b/src/modules/actions/api/sessions.ts @@ -0,0 +1,100 @@ +import * as idb from 'idb-keyval'; + +import { ApiSessionData } from '../../../api/types'; + +import { DEBUG, LEGACY_SESSION_KEY, SESSION_USER_KEY } from '../../../config'; +import * as cacheApi from '../../../util/cacheApi'; + +const DC_IDS = [1, 2, 3, 4, 5]; + +export function storeSession(sessionData: ApiSessionData, currentUserId?: number) { + const { mainDcId, keys, hashes } = sessionData; + + localStorage.setItem(SESSION_USER_KEY, JSON.stringify({ dcID: mainDcId, id: currentUserId })); + localStorage.setItem('dc', String(mainDcId)); + Object.keys(keys).map(Number).forEach((dcId) => { + localStorage.setItem(`dc${dcId}_auth_key`, JSON.stringify(keys[dcId])); + }); + Object.keys(hashes).map(Number).forEach((dcId) => { + localStorage.setItem(`dc${dcId}_hash`, JSON.stringify(hashes[dcId])); + }); +} + +export function clearStoredSession() { + [ + SESSION_USER_KEY, + 'dc', + ...DC_IDS.map((dcId) => `dc${dcId}_auth_key`), + ...DC_IDS.map((dcId) => `dc${dcId}_hash`), + ].forEach((key) => { + localStorage.removeItem(key); + }); +} + +export function loadStoredSession(): ApiSessionData | undefined { + const legacySessionJson = localStorage.getItem(SESSION_USER_KEY); + if (!legacySessionJson) return undefined; + + const { dcID: mainDcId } = JSON.parse(legacySessionJson); + const keys: Record = {}; + const hashes: Record = {}; + + DC_IDS.forEach((dcId) => { + try { + const key = localStorage.getItem(`dc${dcId}_auth_key`); + if (key) { + keys[dcId] = JSON.parse(key); + } + + const hash = localStorage.getItem(`dc${dcId}_hash`); + if (hash) { + hashes[dcId] = JSON.parse(hash); + } + } catch (err) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('Failed to load stored session', err); + } + // Do nothing. + } + }); + + if (!Object.keys(keys).length) return undefined; + + return { + mainDcId, + keys, + hashes, + }; +} + +export async function importLegacySession() { + const sessionId = localStorage.getItem(LEGACY_SESSION_KEY); + if (!sessionId) return; + + const sessionJson = await idb.get(`GramJs:${sessionId}`); + try { + const sessionData = JSON.parse(sessionJson) as ApiSessionData; + storeSession(sessionData); + } catch (err) { + if (DEBUG) { + // eslint-disable-next-line no-console + console.warn('Failed to load legacy session', err); + } + // Do nothing. + } +} + +// Remove previously created IndexedDB and cache API sessions +export async function clearLegacySessions() { + localStorage.removeItem(LEGACY_SESSION_KEY); + + const idbKeys = await idb.keys(); + + await Promise.all>([ + cacheApi.clear('GramJs'), + ...idbKeys + .filter((k) => typeof k === 'string' && k.startsWith('GramJs:GramJs-session-')) + .map((k) => idb.del(k)), + ]); +} diff --git a/src/modules/actions/apiUpdaters/initial.ts b/src/modules/actions/apiUpdaters/initial.ts index 476c24ac5..f705b1431 100644 --- a/src/modules/actions/apiUpdaters/initial.ts +++ b/src/modules/actions/apiUpdaters/initial.ts @@ -9,9 +9,10 @@ import { ApiUpdateAuthorizationState, ApiUpdateAuthorizationError, ApiUpdateConnectionState, + ApiUpdateSession, ApiUpdateCurrentUser, } from '../../../api/types'; -import { DEBUG, LEGACY_SESSION_KEY } from '../../../config'; +import { DEBUG, SESSION_USER_KEY } from '../../../config'; import { subscribe } from '../../../util/notifications'; import { updateUser } from '../../reducers'; import { setLanguage } from '../../../util/langProvider'; @@ -41,6 +42,10 @@ addReducer('apiUpdate', (global, actions, update: ApiUpdate) => { onUpdateConnectionState(update); break; + case 'updateSession': + onUpdateSession(update); + break; + case 'updateCurrentUser': onUpdateCurrentUser(update); break; @@ -102,11 +107,6 @@ function onUpdateAuthorizationState(update: ApiUpdateAuthorizationState) { }); break; case 'authorizationStateReady': { - const { sessionId, sessionJson } = update; - if (sessionId && global.authRememberMe) { - getDispatch().saveSession({ sessionId, sessionJson }); - } - if (wasAuthReady) { break; } @@ -145,6 +145,16 @@ function onUpdateConnectionState(update: ApiUpdateConnectionState) { } } +function onUpdateSession(update: ApiUpdateSession) { + if (!getGlobal().authRememberMe) { + return; + } + + const { sessionData } = update; + + getDispatch().saveSession({ sessionData }); +} + function onUpdateCurrentUser(update: ApiUpdateCurrentUser) { const { currentUser } = update; @@ -153,15 +163,15 @@ function onUpdateCurrentUser(update: ApiUpdateCurrentUser) { currentUserId: currentUser.id, }); - updateLegacySessionUserId(currentUser.id); + updateSessionUserId(currentUser.id); } -function updateLegacySessionUserId(currentUserId: number) { - const legacySessionJson = localStorage.getItem(LEGACY_SESSION_KEY); - if (!legacySessionJson) return; +function updateSessionUserId(currentUserId: number) { + const sessionUserAuth = localStorage.getItem(SESSION_USER_KEY); + if (!sessionUserAuth) return; - const legacySession = JSON.parse(legacySessionJson); - legacySession.id = currentUserId; + const userAuth = JSON.parse(sessionUserAuth); + userAuth.id = currentUserId; - localStorage.setItem(LEGACY_SESSION_KEY, JSON.stringify(legacySession)); + localStorage.setItem(SESSION_USER_KEY, JSON.stringify(userAuth)); }