From a3262d7af91bc690a047ec59fb3c49af9a022e17 Mon Sep 17 00:00:00 2001 From: William Grant Date: Thu, 21 Mar 2024 09:13:07 +1100 Subject: [PATCH] feat: attempting to fetch the display name separate from the usual pipeline --- .../registration/stages/RestoreAccount.tsx | 6 +- ts/receiver/configMessage.ts | 40 ++++-- ts/session/apis/snode_api/retrieveRequest.ts | 75 ++++++++++- ts/session/apis/snode_api/swarmPolling.ts | 125 +++++++++++++++++- ts/session/apis/snode_api/types.ts | 2 +- ts/util/accountManager.ts | 10 +- 6 files changed, 238 insertions(+), 20 deletions(-) diff --git a/ts/components/registration/stages/RestoreAccount.tsx b/ts/components/registration/stages/RestoreAccount.tsx index 9d9261060..8e843686e 100644 --- a/ts/components/registration/stages/RestoreAccount.tsx +++ b/ts/components/registration/stages/RestoreAccount.tsx @@ -1,6 +1,5 @@ import { useState } from 'react'; import { useDispatch } from 'react-redux'; -import { getSwarmPollingInstance } from '../../../session/apis/snode_api'; import { ONBOARDING_TIMES } from '../../../session/constants'; import { InvalidWordsError, NotEnoughWordsError } from '../../../session/crypto/mnemonic'; import { PromiseUtils, ToastUtils } from '../../../session/utils'; @@ -76,10 +75,11 @@ async function signInAndFetchDisplayName( await resetRegistration(); await signInByLinkingDevice(recoveryPassword, 'english', loadingAnimationCallback); - await getSwarmPollingInstance().start(); - await PromiseUtils.waitForTask(done => { window.Whisper.events.on('configurationMessageReceived', async (displayName: string) => { + window.log.debug( + `WIP: [signInAndFetchDisplayName] waitForTask done with displayName: "${displayName}"` + ); window.Whisper.events.off('configurationMessageReceived'); await setSignInByLinking(false); await setSignWithRecoveryPhrase(true); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index f1fbcca32..5a25b909b 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -103,7 +103,7 @@ async function mergeConfigsWithIncomingUpdates( })); if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { window.log.info( - `printDumpsForDebugging: before merge of ${variant}:`, + `WIP: printDumpsForDebugging: before merge of ${variant}:`, StringUtils.toHex(await GenericWrapperActions.dump(variant)) ); @@ -132,7 +132,7 @@ async function mergeConfigsWithIncomingUpdates( if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { window.log.info( - `printDumpsForDebugging: after merge of ${variant}:`, + `WIP: printDumpsForDebugging: after merge of ${variant}:`, StringUtils.toHex(await GenericWrapperActions.dump(variant)) ); } @@ -821,9 +821,11 @@ async function handleConvoInfoVolatileUpdate( return result; } -async function processMergingResults(results: Map) { +async function processMergingResults( + results: Map +): Promise { if (!results || !results.size) { - return; + return undefined; } const keys = [...results.keys()]; @@ -858,6 +860,7 @@ async function processMergingResults(results: Map> -) { + configMessages: Array>, + returnAndKeepInMemory?: boolean +): Promise { const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); if (!userConfigLibsession) { - return; + return undefined; } if (isEmpty(configMessages)) { - return; + return undefined; } window?.log?.debug( @@ -918,7 +925,20 @@ async function handleConfigMessagesViaLibSession( ); const incomingMergeResult = await mergeConfigsWithIncomingUpdates(configMessages); - await processMergingResults(incomingMergeResult); + window.log.debug( + `WIP: [handleConfigMessagesViaLibSession] incomingMergeResult:`, + incomingMergeResult + ); + + if (returnAndKeepInMemory) { + // TODO[epic=899] we should return the display name and keep it in memory + return incomingMergeResult.get('UserConfig'); + // const dump = await GenericWrapperActions.dump('UserConfig'); + // return dump; + } + + const result = await processMergingResults(incomingMergeResult); + return result; } async function updateOurProfileLegacyOrViaLibSession({ diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index 31e5827f3..0ee676221 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -111,7 +111,7 @@ async function retrieveNextMessages( configHashesToBump: Array | null ): Promise { if (namespaces.length !== lastHashes.length) { - throw new Error('namespaces and lasthashes does not match'); + throw new Error('namespaces and last hashes does not match'); } const retrieveRequestsParams = await buildRetrieveRequest( @@ -182,4 +182,75 @@ async function retrieveNextMessages( } } -export const SnodeAPIRetrieve = { retrieveNextMessages }; +async function retrieveDisplayName( + targetNode: Snode, + ourPubkey: string +): Promise { + const retrieveRequestsParams = await buildRetrieveRequest( + [], + ourPubkey, + [SnodeNamespaces.UserProfile], + ourPubkey, + [] + ); + + // let exceptions bubble up + // no retry for this one as this a call we do every few seconds through polling + + const results = await doSnodeBatchRequest(retrieveRequestsParams, targetNode, 4000, ourPubkey); + + if (!results || !results.length) { + window?.log?.warn( + `retrieveDisplayName - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` + ); + throw new Error( + `retrieveDisplayName - sessionRpc could not talk to ${targetNode.ip}:${targetNode.port}` + ); + } + + // the +1 is to take care of the extra `expire` method added once user config is released + if (results.length !== 1 && results.length !== 2) { + throw new Error( + `We asked for updates about a message but got results of length ${results.length}` + ); + } + + // do a basic check to know if we have something kind of looking right (status 200 should always be there for a retrieve) + const firstResult = results[0]; + + if (firstResult.code !== 200) { + window?.log?.warn(`retrieveDisplayName result is not 200 but ${firstResult.code}`); + throw new Error( + `retrieveDisplayName - retrieve result is not 200 with ${targetNode.ip}:${targetNode.port} but ${firstResult.code}` + ); + } + + try { + // we rely on the code of the first one to check for online status + const bodyFirstResult = firstResult.body; + if (!window.inboxStore?.getState().onionPaths.isOnline) { + window.inboxStore?.dispatch(updateIsOnline(true)); + } + + GetNetworkTime.handleTimestampOffsetFromNetwork('retrieve', bodyFirstResult.t); + + // merge results with their corresponding namespaces + const resultsWithNamespaces = results.map(result => ({ + code: result.code, + messages: result.body as RetrieveMessagesResultsContent, + namespace: SnodeNamespaces.UserProfile, + })); + + return resultsWithNamespaces; + } catch (e) { + window?.log?.warn('retrieveDisplayName:', e); + if (!window.inboxStore?.getState().onionPaths.isOnline) { + window.inboxStore?.dispatch(updateIsOnline(true)); + } + throw new Error( + `retrieveDisplayName - exception while parsing json of nextMessage ${targetNode.ip}:${targetNode.port}: ${e?.message}` + ); + } +} + +export const SnodeAPIRetrieve = { retrieveNextMessages, retrieveDisplayName }; diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 4a335dfc6..43da580b3 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -346,7 +346,10 @@ export class SwarmPolling { } } - private async handleSharedConfigMessages(userConfigMessagesMerged: Array) { + private async handleSharedConfigMessages( + userConfigMessagesMerged: Array, + returnAndKeepInMemory?: boolean + ): Promise { const extractedUserConfigMessage = compact( userConfigMessagesMerged.map((m: RetrieveMessageItem) => { return extractWebSocketContent(m.data, m.hash); @@ -391,7 +394,36 @@ export class SwarmPolling { window.log.info( `handleConfigMessagesViaLibSession of "${allDecryptedConfigMessages.length}" messages with libsession` ); - await ConfigMessageHandler.handleConfigMessagesViaLibSession(allDecryptedConfigMessages); + + if (returnAndKeepInMemory) { + try { + // TODO[epic=899] trying to create a dump in memory for the userconfig + const ourKeyPair = await UserUtils.getIdentityKeyPair(); + if (ourKeyPair) { + await GenericWrapperActions.init( + 'UserConfig', + new Uint8Array(ourKeyPair.privKey), + null + ); + // save the newly created dump to the database even if it is empty, just so we do not need to recreate one next run + + // const dump = await GenericWrapperActions.dump('UserConfig'); + } + // await LibSessionUtil.initializeLibSessionUtilWrappers(); + } catch (e) { + window.log.warn( + '[SwarmPolling] LibSessionUtil.initializeLibSessionUtilWrappers failed with', + e.message + ); + } + } + + const result = await ConfigMessageHandler.handleConfigMessagesViaLibSession( + allDecryptedConfigMessages, + returnAndKeepInMemory + ); + window.log.debug(`WIP: [handleSharedConfigMessages] result ${JSON.stringify(result)} `); + return String(result); } catch (e) { const allMessageHases = allDecryptedConfigMessages.map(m => m.messageHash).join(','); window.log.warn( @@ -399,6 +431,7 @@ export class SwarmPolling { ); } } + return ''; } // Fetches messages for `pubkey` from `node` potentially updating @@ -605,4 +638,92 @@ export class SwarmPolling { // return the cached value return this.lastHashes[nodeEdKey][pubkey][namespace]; } + + /** + * Only exposed as public for testing + */ + public async pollOnceForDisplayName(pubkey: PubKey) { + const polledPubkey = pubkey.key; + const swarmSnodes = await snodePool.getSwarmFor(polledPubkey); + + // Select nodes for which we already have lastHashes + const alreadyPolled = swarmSnodes.filter((n: Snode) => this.lastHashes[n.pubkey_ed25519]); + let toPollFrom = alreadyPolled.length ? alreadyPolled[0] : null; + + // If we need more nodes, select randomly from the remaining nodes: + if (!toPollFrom) { + const notPolled = difference(swarmSnodes, alreadyPolled); + toPollFrom = sample(notPolled) as Snode; + } + + let resultsFromUserProfile: RetrieveMessagesResultsBatched | null; + + try { + resultsFromUserProfile = await SnodeAPIRetrieve.retrieveDisplayName( + toPollFrom, + UserUtils.getOurPubKeyStrFromCache() + ); + window.log.debug( + `WIP: [resultsFromUserProfile] resultsFromUserProfile: ${JSON.stringify(resultsFromUserProfile)}` + ); + } catch (e) { + if (e.message === ERROR_CODE_NO_CONNECT) { + if (window.inboxStore?.getState().onionPaths.isOnline) { + window.inboxStore?.dispatch(updateIsOnline(false)); + } + } else if (!window.inboxStore?.getState().onionPaths.isOnline) { + window.inboxStore?.dispatch(updateIsOnline(true)); + } + + window.log.warn( + `pollOnceForDisplayName of ${pubkey} namespace: SnodeNamespaces.UserProfile failed with: ${e.message}` + ); + resultsFromUserProfile = null; + } + + // check if we just fetched the details from the config namespaces. + // If yes, merge them together and exclude them from the rest of the messages. + if (resultsFromUserProfile?.length) { + const userConfigMessages = resultsFromUserProfile + .filter(m => SnodeNamespace.isUserConfigNamespace(m.namespace)) + .map(r => r.messages.messages); + + const userConfigMessagesMerged = flatten(compact(userConfigMessages)); + + if (userConfigMessagesMerged.length) { + window.log.info( + `[pollOnceForDisplayName] received userConfigMessages count: ${userConfigMessagesMerged.length} for key ${pubkey.key}` + ); + try { + const displayName = await this.handleSharedConfigMessages(userConfigMessagesMerged, true); + window.log.debug(`WIP: [pollForOurDisplayName] displayName ${displayName}`); + return displayName; + } catch (e) { + window.log.warn( + `handleSharedConfigMessages of ${userConfigMessagesMerged.length} failed with ${e.message}` + ); + // not rethrowing + } + } + } + return ''; + } + + // TODO[epic=ses-899] add a function that only polls for the display name? + public async pollForOurDisplayName(): Promise { + if (!window.getGlobalOnlineStatus()) { + window?.log?.error('pollForOurDisplayName: offline'); + // Very important to set up a new polling call so we do retry at some point + setTimeout(this.pollForOurDisplayName.bind(this), SWARM_POLLING_TIMEOUT.ACTIVE); + return ''; + } + + try { + const displayName = await this.pollOnceForDisplayName(UserUtils.getOurPubKeyFromCache()); + return displayName; + } catch (e) { + window?.log?.warn('pollForOurDisplayName exception: ', e); + return ''; + } + } } diff --git a/ts/session/apis/snode_api/types.ts b/ts/session/apis/snode_api/types.ts index 3650251af..6a27ff44d 100644 --- a/ts/session/apis/snode_api/types.ts +++ b/ts/session/apis/snode_api/types.ts @@ -3,7 +3,7 @@ import { SnodeNamespaces } from './namespaces'; export type RetrieveMessageItem = { hash: string; expiration: number; - data: string; // base64 encrypted content of the emssage + data: string; // base64 encrypted content of the message timestamp: number; }; diff --git a/ts/util/accountManager.ts b/ts/util/accountManager.ts index 4870cc9e2..ff67f9c82 100644 --- a/ts/util/accountManager.ts +++ b/ts/util/accountManager.ts @@ -7,6 +7,7 @@ import { trigger } from '../shims/events'; import { SettingsKey } from '../data/settings-key'; import { ConversationTypeEnum } from '../models/conversationAttributes'; import { SessionKeyPair } from '../receiver/keypairs'; +import { getSwarmPollingInstance } from '../session/apis/snode_api'; import { mnDecode, mnEncode } from '../session/crypto/mnemonic'; import { LibSessionUtil } from '../session/utils/libsession/libsession_utils'; import { actions as userActions } from '../state/ducks/user'; @@ -98,8 +99,10 @@ export async function signInByLinkingDevice( await saveRecoveryPhrase(mnemonic); const pubKeyString = toHex(identityKeyPair.pubKey); + const displayName = await getSwarmPollingInstance().pollForOurDisplayName(); + // await for the first configuration message to come in. - await registrationDone(pubKeyString, ''); + await registrationDone(pubKeyString, displayName); return pubKeyString; } /** @@ -197,7 +200,10 @@ async function registrationDone(ourPubkey: string, displayName: string) { try { await LibSessionUtil.initializeLibSessionUtilWrappers(); } catch (e) { - window.log.warn('LibSessionUtil.initializeLibSessionUtilWrappers failed with', e.message); + window.log.warn( + '[registrationDone] LibSessionUtil.initializeLibSessionUtilWrappers failed with', + e.message + ); } // Ensure that we always have a conversation for ourself const conversation = await getConversationController().getOrCreateAndWait(