diff --git a/src/worker/async-store.js b/src/worker/async-store.js index e9fe0df4b..90c6fa114 100644 --- a/src/worker/async-store.js +++ b/src/worker/async-store.js @@ -6,12 +6,13 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Pinpad, Rpc, Merge, Cache, - SF, Cursor, Support, Integration, OnlyOffice, + SF, AccountTS, Cursor, Support, Integration, OnlyOffice, Mailbox, Profile, Team, Messenger, History, Calendar, Block, NetConfig, AppConfig = {}, Crypto, ChainPad, CpNetflux, Listmap, Netflux, nThen) => { + const Account = AccountTS.Account; const window = globalThis; const Saferphore = Util.Saferphore; var onReadyEvt = Util.mkEvent(true); @@ -2894,160 +2895,56 @@ const factory = (ApiConfig = {}, Sortify, UserObject, ProxyManager, }); }; - var connect = function (clientId, data, cb) { - var hash = data.userHash || data.anonHash || Hash.createRandomHash('drive'); + const connect = (clientId, data, cb) => { + const account = Account.init({ + userHash: data.userHash, + anonHash: data.anonHash, + cache: data.cache, + form_seed: data.form_seed, + store, + broadcast, + postMessage - if (!hash) { - return void cb({error: '[Store.init] Unable to find or create a drive hash. Aborting...'}); - } + }); + const { + channel, onAccountReady, onAccountCacheReady, + onDisconnect, onReconnect + } = account; - var updateProgress = function (data) { - data.type = 'drive'; - postMessage(clientId, 'LOADING_DRIVE', data); - }; - - // No password for drive - var secret = Hash.getSecrets('drive', hash); - store.driveChannel = secret.channel; - var listmapConfig = { - data: {}, - websocketURL: NetConfig.getWebsocketURL(), - network: store.network, - channel: secret.channel, - readOnly: false, - validateKey: secret.keys.validateKey || undefined, - crypto: Crypto.createEncryptor(secret.keys), - Cache: Cache, - userName: 'fs', - logLevel: 1, - ChainPad: ChainPad, - updateProgress: updateProgress, - classic: true, - }; - var rt = window.rt = Listmap.create(listmapConfig); - store.driveSecret = secret; - store.proxy = rt.proxy; - store.onRpcReadyEvt = Util.mkEvent(true); - store.loggedIn = typeof(data.userHash) !== "undefined"; - - var returned = { - loggedIn: Boolean(data.userHash) - }; - rt.proxy.on('create', function (info) { - store.realtime = info.realtime; - store.network = info.network; - if (!data.userHash) { - returned.anonHash = Hash.getEditHashFromKeys(secret); - } - }).on('cacheready', function (info) { - store.offline = true; - store.realtime = info.realtime; - store.networkPromise = info.networkPromise; - store.cacheReturned = returned; - - if (store.networkPromise && store.networkPromise.then) { - // Check if we can connect - var to = setTimeout(function () { - store.networkTimeout = true; - broadcast([], "LOADING_DRIVE", { - type: "offline" - }); - }, 5000); - - store.networkPromise.then(function () { - clearTimeout(to); - }, function (err) { - console.error(err); - clearTimeout(to); - }); - } - - if (!data.cache) { return; } - - // Make sure we have a valid user object before emitting cacheready - if (rt.proxy && !rt.proxy.drive) { return; } - - returned.edPublic = rt.proxy.edPublic; + store.driveChannel = channel; + // XXX don't always call onCacheReady and onReady? + // it depends on what was required by the first tab + onAccountCacheReady(returned => { onCacheReady(clientId, function () { - if (typeof(cb) === "function") { cb(returned); } + if (typeof(cb) === "function") { + cb(returned); + } onCacheReadyEvt.fire(); }); - }).on('ready', function (info) { - delete store.networkTimeout; - if (store.ready) { return; } // the store is already ready, it is a reconnection - store.driveMetadata = info.metadata; - if (!rt.proxy.drive || typeof(rt.proxy.drive) !== 'object') { rt.proxy.drive = {}; } - if (!rt.proxy[Constants.displayNameKey] && store.noDriveName) { - rt.proxy[Constants.displayNameKey] = store.noDriveName; - } - if (!rt.proxy.uid && store.noDriveUid) { - rt.proxy.uid = store.noDriveUid; - } - if (!rt.proxy.form_seed && data.form_seed) { - rt.proxy.form_seed = data.form_seed; - } - - if (rt.proxy.edPublic && Array.isArray(ApiConfig.adminKeys) && - ApiConfig.adminKeys.indexOf(rt.proxy.edPublic) !== -1) { - store.isAdmin = true; - } - - /* - // deprecating localStorage migration as of 4.2.0 - var drive = rt.proxy.drive; - // Creating a new anon drive: import anon pads from localStorage - if ((!drive[Constants.oldStorageKey] || !Array.isArray(drive[Constants.oldStorageKey])) - && !drive['filesData']) { - drive[Constants.oldStorageKey] = []; - } - */ - - returned.edPublic = rt.proxy.edPublic; - // Drive already exist: return the existing drive, don't load data from legacy store + }); + onAccountReady(returned => { if (store.manager) { - // If a cache is loading, make sure it is complete before calling onReady return void onCacheReadyEvt.reg(function () { onReady(clientId, returned, cb); }); } - onReady(clientId, returned, cb); - }) - .on('change', ['drive', 'migrate'], function () { - var path = arguments[2]; - var value = arguments[1]; - if (path[0] === 'drive' && path[1] === "migrate" && value === 1) { - rt.network.disconnect(); - rt.realtime.abort(); - sendDriveEvent('NETWORK_DISCONNECT'); - } - }) - .on('error', function (info) { - if (info.error && info.error === 'EDELETED') { - if (store.ownDeletion) { return; } - store.isDeleted = true; - broadcast([], "DRIVE_DELETED", info.message); - } + }); + // XXX move to drive + onDisconnect(() => { + sendDriveEvent('NETWORK_DISCONNECT'); + }); + onReconnect(() => { + sendDriveEvent('NETWORK_RECONNECT'); }); - // Proxy handlers (reconnect only called when the proxy is ready) - rt.proxy.on('disconnect', function () { - store.offline = true; - sendDriveEvent('NETWORK_DISCONNECT'); - broadcast([], "UPDATE_METADATA"); - }); - rt.proxy.on('reconnect', function () { - store.offline = false; - sendDriveEvent('NETWORK_RECONNECT'); - broadcast([], "UPDATE_METADATA"); - }); // Ping clients regularly to make sure one tab was not closed without sending a removeClient() // command. This allow us to avoid phantom viewers in pads. - var PING_INTERVAL = 120000; - var MAX_PING = 30000; - var MAX_FAILED_PING = 2; + const PING_INTERVAL = 120000; + const MAX_PING = 30000; + const MAX_FAILED_PING = 2; setInterval(function () { var clients = []; @@ -3302,6 +3199,7 @@ if (typeof(module) !== 'undefined' && module.exports) { require('./components/merge-drive'), require('../common/cache-store'), require('./components/sharedfolder'), + require('./components/account'), // XXX require('./modules/cursor'), require('./modules/support'), require('./modules/integration'), diff --git a/src/worker/components/account.ts b/src/worker/components/account.ts new file mode 100644 index 000000000..5914bda7b --- /dev/null +++ b/src/worker/components/account.ts @@ -0,0 +1,163 @@ +import nacl from 'tweetnacl/nacl-fast'; +import { Account } from '../types' + +// node modules +import * as Listmap from 'chainpad-listmap'; +import * as ChainPad from 'chainpad'; +import * as Crypto from 'chainpad-crypto'; +// custom modules +import * as NetConfig from '../../common/network-config.js'; +import * as Constants from '../../common/common-constants.js'; +import * as Hash from '../../common/common-hash.js'; +import * as Util from '../../common/common-util.js'; +import * as Cache from '../../common/cache-store.js'; + +const onCacheReadyEvt: any = Util.mkEvent(true); +const onReadyEvt: any = Util.mkEvent(true); +const onDisconnectEvt: any = Util.mkEvent(); +const onReconnectEvt: any = Util.mkEvent(); + +let ApiConfig:any = {}; +const init = (config) => { + const { broadcast, userHash, anonHash } = config; + const hash:string = userHash || anonHash || Hash.createRandomHash('drive'); + const store = config.store; + + // Update loading screen status + const updateProgress = function (data) { + data.type = 'drive'; + broadcast([], 'LOADING_DRIVE', data); + }; + + // Create account secret from hash + const secret = Hash.getSecrets('drive', hash); + + const listmapConfig = { + data: {}, + websocketURL: NetConfig.getWebsocketURL(), + network: config.store?.network, + channel: secret.channel, + readOnly: false, + validateKey: secret.keys?.validateKey || undefined, + crypto: Crypto.createEncryptor(secret.keys), + Cache: Cache, + userName: 'fs', + logLevel: 1, + ChainPad: ChainPad, + updateProgress: updateProgress, + classic: true, + }; + const rt = globalThis.CP_account_rt = Listmap.create(listmapConfig); + + store.driveSecret = secret; + store.proxy = rt.proxy; + store.onRpcReadyEvt = Util.mkEvent(true); + store.loggedIn = typeof(config.userHash) !== "undefined"; + + const returned:any = { + loggedIn: store.loggedIn + }; + + rt.proxy.on('create', function (info) { + store.realtime = info.realtime; // XXX move to account + store.network = info.network; // init if not exists + if (!store.loggedIn) { + returned.anonHash = Hash.getEditHashFromKeys(secret); + } + }).on('cacheready', function (info) { + store.offline = true; // XXX move to account + store.realtime = info.realtime; // XXX move to account + store.networkPromise = info.networkPromise; + store.cacheReturned = returned; // XXX move to account + + if (store.networkPromise && store.networkPromise.then) { + // Check if we can connect + const to = setTimeout(function () { + store.networkTimeout = true; + broadcast([], "LOADING_DRIVE", { + type: "offline" + }); + }, 5000); + + store.networkPromise.then(function () { + clearTimeout(to); + }, function (err) { + console.error(err); + clearTimeout(to); + }); + } + + if (!config.cache) { return; } + + // Make sure we have a valid user object before emitting cacheready + if (rt.proxy && !rt.proxy.drive) { return; } + + returned.edPublic = rt.proxy.edPublic; + + onCacheReadyEvt.fire(returned); + }).on('ready', function (info) { + delete store.networkTimeout; + if (store.ready) { return; } // the store is already ready, it is a reconnection + + // XXX DRIVE + store.driveMetadata = info.metadata; // XXX move to drive + + // XXX DRIVE + // New drive? create empty object + if (!rt.proxy.drive) { rt.proxy.drive = {}; } + + // New drive: recover data from noDrive session + if (!rt.proxy[Constants.displayNameKey] && store.noDriveName) { + rt.proxy[Constants.displayNameKey] = store.noDriveName; + } + if (!rt.proxy.uid && store.noDriveUid) { + rt.proxy.uid = store.noDriveUid; + } + if (!rt.proxy.form_seed && config.form_seed) { + rt.proxy.form_seed = config.form_seed; + } + + // Are we an admin? + if (rt.proxy.edPublic && Array.isArray(ApiConfig.adminKeys) && + ApiConfig.adminKeys.indexOf(rt.proxy.edPublic) !== -1) { + store.isAdmin = true; + } + + returned.edPublic = rt.proxy.edPublic; + + onReadyEvt.fire(returned); + }).on('error', function (info) { + if (info.error !== 'EDELETED') { return; } + if (store.ownDeletion) { return; } + store.isDeleted = true; + broadcast([], "DRIVE_DELETED", info.message); + }).on('disconnect', function () { + store.offline = true; + onDisconnectEvt.fire(); + broadcast([], "UPDATE_METADATA"); + }).on('reconnect', function () { + store.offline = false; + onReconnectEvt.fire(); + broadcast([], "UPDATE_METADATA"); + }); + + return { + channel: secret.channel, + onAccountCacheReady: onCacheReadyEvt.reg, + onAccountReady: onReadyEvt.reg, + onDisconnect: onDisconnectEvt.reg, + onReconnect: onReconnectEvt.reg + }; +}; + +const Account: Account = { + + setCustomize: (data) => { + ApiConfig = data.ApiConfig; + }, + init: init +}; + +export { Account } + + diff --git a/src/worker/store.ts b/src/worker/store.ts index 8f8332759..916739f0f 100644 --- a/src/worker/store.ts +++ b/src/worker/store.ts @@ -2,6 +2,8 @@ import nacl from 'tweetnacl/nacl-fast'; import { Module } from './types'; import { TestModule, TestModuleObject } from './modules/test'; +import { Account } from './components/account'; + // Manage all imports in order to provide them ApiConfig, AppConfig, etc. // Common @@ -52,7 +54,6 @@ interface StoreConfig { Messages: any } - let start = (cfg: StoreConfig):void => { // Provide custom data to the modules [ @@ -68,7 +69,8 @@ let start = (cfg: StoreConfig):void => { Cursor, Support, Calendar, - Store + Store, + Account ].forEach(dep => { if (typeof(dep.setCustomize) === "function") { dep.setCustomize(cfg); diff --git a/src/worker/types.ts b/src/worker/types.ts index 17af4ec0f..4df9960e1 100644 --- a/src/worker/types.ts +++ b/src/worker/types.ts @@ -1,6 +1,6 @@ // Default CryptPad worker module, extended with // specific methods for each module -type Callback = (...args: any[]) => void +export type Callback = (...args: any[]) => void export type ModuleConfig = { emit: Function @@ -20,3 +20,26 @@ export interface Module { init: (config: ModuleConfig, cb: Callback) => T } + +export type AccountConfig = { + anonHash: string, + userHash: string, + store: any, + cache: boolean, + form_seed: string, + + broadcast: (exclude: object, cmd: string, data?: any, cb?: any) => void, + postMessage: (clientId: string, cmd: string, data?: any, cb?: any) => void +} +export interface AccountObject { + channel: string, + onAccountCacheReady: any, + onAccountReady: any, + onDisconnect: any, + onReconnect: any +} +export interface Account { + init: (config: AccountConfig) => AccountObject, + setCustomize: Callback +} +