From 7b42c64cf3bcc09dff5afb3aabee1a1ac45d66c6 Mon Sep 17 00:00:00 2001 From: Audric Ackermann Date: Fri, 26 May 2023 10:51:02 +1000 Subject: [PATCH] fix: make sure to include the associatedWith to handle 421 --- ts/components/MainViewController.tsx | 45 ++--- ts/components/leftpane/ActionsPanel.tsx | 6 +- ts/receiver/configMessage.ts | 40 +++-- ts/session/apis/seed_node_api/SeedNodeAPI.ts | 2 +- ts/session/apis/snode_api/batchRequest.ts | 4 +- ts/session/apis/snode_api/getNetworkTime.ts | 2 +- .../apis/snode_api/getServiceNodesList.ts | 2 +- ts/session/apis/snode_api/onions.ts | 2 +- ts/session/apis/snode_api/onsResolve.ts | 2 +- ts/session/apis/snode_api/retrieveRequest.ts | 7 +- ts/session/apis/snode_api/sessionRpc.ts | 6 +- ts/session/apis/snode_api/snodePool.ts | 21 ++- ts/session/apis/snode_api/swarmPolling.ts | 158 ++++++++---------- .../conversations/ConversationController.ts | 2 + ts/session/onions/onionPath.ts | 2 +- ts/session/profile_manager/ProfileManager.ts | 7 + ts/session/sending/MessageSender.ts | 2 +- .../job_runners/jobs/ConfigurationSyncJob.ts | 1 + 18 files changed, 176 insertions(+), 135 deletions(-) diff --git a/ts/components/MainViewController.tsx b/ts/components/MainViewController.tsx index c96a10458..8a81ce785 100644 --- a/ts/components/MainViewController.tsx +++ b/ts/components/MainViewController.tsx @@ -1,25 +1,28 @@ import React from 'react'; +import { CSSProperties } from 'styled-components'; -export class MessageView extends React.Component { - public render() { - return ( -
-
-
-
- full-brand-logo - full-brand-logo -
+export const MessageView = () => { + const noDragStyle = { '-webkit-user-drag': 'none' } as CSSProperties; + + return ( +
+
+
+
+ full-brand-logo + full-brand-logo
- ); - } -} +
+ ); +}; diff --git a/ts/components/leftpane/ActionsPanel.tsx b/ts/components/leftpane/ActionsPanel.tsx index d67994ffe..7d4fb90c8 100644 --- a/ts/components/leftpane/ActionsPanel.tsx +++ b/ts/components/leftpane/ActionsPanel.tsx @@ -40,7 +40,10 @@ import { LeftPaneSectionContainer } from './LeftPaneSectionContainer'; import { SettingsKey } from '../../data/settings-key'; import { getLatestReleaseFromFileServer } from '../../session/apis/file_server_api/FileServerApi'; -import { forceRefreshRandomSnodePool } from '../../session/apis/snode_api/snodePool'; +import { + forceRefreshRandomSnodePool, + getFreshSwarmFor, +} from '../../session/apis/snode_api/snodePool'; import { isDarkTheme } from '../../state/selectors/theme'; import { ThemeStateType } from '../../themes/constants/colors'; import { switchThemeTo } from '../../themes/switchTheme'; @@ -198,6 +201,7 @@ const doAppStartUp = async () => { void triggerSyncIfNeeded(); void getSwarmPollingInstance().start(); void loadDefaultRooms(); + void getFreshSwarmFor(UserUtils.getOurPubKeyStrFromCache()); // refresh our swarm on start to speed up the first message fetching event // TODOLATER make this a job of the JobRunner debounce(triggerAvatarReUploadIfNeeded, 200); diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index 7b2dd6d4b..3b250b0c3 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -104,7 +104,18 @@ async function mergeConfigsWithIncomingUpdates( `printDumpsForDebugging: before merge of ${variant}:`, StringUtils.toHex(await GenericWrapperActions.dump(variant)) ); + + for (let index = 0; index < toMerge.length; index++) { + const element = toMerge[index]; + window.log.info( + `printDumpsForDebugging: toMerge of ${index}:${element.hash}: ${StringUtils.toHex( + element.data + )} `, + StringUtils.toHex(await GenericWrapperActions.dump(variant)) + ); + } } + const mergedCount = await GenericWrapperActions.merge(variant, toMerge); const needsPush = await GenericWrapperActions.needsPush(variant); const needsDump = await GenericWrapperActions.needsDump(variant); @@ -354,17 +365,24 @@ async function handleCommunitiesUpdate() { }); } - // this call can take quite a long time and should not cause issues to not be awaited - void Promise.all( - communitiesToJoinInDB.map(async toJoin => { - window.log.info('joining community with convoId ', toJoin.fullUrlWithPubkey); - return getOpenGroupManager().attemptConnectionV2OneAtATime( - toJoin.baseUrl, - toJoin.roomCasePreserved, - toJoin.pubkeyHex - ); - }) - ); + // this call can take quite a long time but must be awaited (as it is async and create the entry in the DB, used as a diff) + try { + await Promise.all( + communitiesToJoinInDB.map(async toJoin => { + window.log.info('joining community with convoId ', toJoin.fullUrlWithPubkey); + return getOpenGroupManager().attemptConnectionV2OneAtATime( + toJoin.baseUrl, + toJoin.roomCasePreserved, + toJoin.pubkeyHex + ); + }) + ); + } catch (e) { + window.log.warn( + `joining community with failed with one of ${communitiesToJoinInDB}`, + e.message + ); + } // if the convos already exists, make sure to update the fields if needed for (let index = 0; index < allCommunitiesInWrapper.length; index++) { diff --git a/ts/session/apis/seed_node_api/SeedNodeAPI.ts b/ts/session/apis/seed_node_api/SeedNodeAPI.ts index 6803f6763..0dc51a5fd 100644 --- a/ts/session/apis/seed_node_api/SeedNodeAPI.ts +++ b/ts/session/apis/seed_node_api/SeedNodeAPI.ts @@ -303,7 +303,7 @@ async function getSnodesFromSeedUrl(urlObj: URL): Promise> { } return validNodes; } catch (e) { - window?.log?.error('Invalid json response'); + window?.log?.error('Invalid json response. error:', e.message); throw new Error(`getSnodesFromSeedUrl: cannot parse content as JSON from ${urlObj.href}`); } } diff --git a/ts/session/apis/snode_api/batchRequest.ts b/ts/session/apis/snode_api/batchRequest.ts index 01affdf87..94ed2b979 100644 --- a/ts/session/apis/snode_api/batchRequest.ts +++ b/ts/session/apis/snode_api/batchRequest.ts @@ -18,7 +18,7 @@ export async function doSnodeBatchRequest( subRequests: Array, targetNode: Snode, timeout: number, - associatedWith?: string, + associatedWith: string | null, method: 'batch' | 'sequence' = 'batch' ): Promise { // console.warn( @@ -49,7 +49,7 @@ export async function doSnodeBatchRequest( await processOnionRequestErrorAtDestination({ statusCode: resultRow.code, body: JSON.stringify(resultRow.body), - associatedWith, + associatedWith: associatedWith || undefined, destinationSnodeEd25519: targetNode.pubkey_ed25519, }); } diff --git a/ts/session/apis/snode_api/getNetworkTime.ts b/ts/session/apis/snode_api/getNetworkTime.ts index c35de180a..dfbb6bb5d 100644 --- a/ts/session/apis/snode_api/getNetworkTime.ts +++ b/ts/session/apis/snode_api/getNetworkTime.ts @@ -18,7 +18,7 @@ function getNetworkTimeSubRequests(): Array { // tslint:disable-next-line: variable-name const getNetworkTime = async (snode: Snode): Promise => { const subRequests = getNetworkTimeSubRequests(); - const result = await doSnodeBatchRequest(subRequests, snode, 4000); + const result = await doSnodeBatchRequest(subRequests, snode, 4000, null); if (!result || !result.length) { window?.log?.warn(`getNetworkTime on ${snode.ip}:${snode.port} returned falsish value`, result); throw new Error('getNetworkTime: Invalid result'); diff --git a/ts/session/apis/snode_api/getServiceNodesList.ts b/ts/session/apis/snode_api/getServiceNodesList.ts index ffc46c06e..8f35d8288 100644 --- a/ts/session/apis/snode_api/getServiceNodesList.ts +++ b/ts/session/apis/snode_api/getServiceNodesList.ts @@ -32,7 +32,7 @@ function buildSnodeListRequests(): Array { */ async function getSnodePoolFromSnode(targetNode: Snode): Promise> { const requests = buildSnodeListRequests(); - const results = await doSnodeBatchRequest(requests, targetNode, 4000); + const results = await doSnodeBatchRequest(requests, targetNode, 4000, null); const firstResult = results[0]; diff --git a/ts/session/apis/snode_api/onions.ts b/ts/session/apis/snode_api/onions.ts index c4577ff22..5bb089b91 100644 --- a/ts/session/apis/snode_api/onions.ts +++ b/ts/session/apis/snode_api/onions.ts @@ -708,7 +708,7 @@ async function handle421InvalidSwarm({ if (parsedBody?.snodes?.length) { // the snode gave us the new swarm. Save it for the next retry window?.log?.warn( - 'Wrong swarm, now looking at snodes', + `Wrong swarm, now looking for pk ${ed25519Str(associatedWith)} at snodes: `, parsedBody.snodes.map((s: any) => ed25519Str(s.pubkey_ed25519)) ); diff --git a/ts/session/apis/snode_api/onsResolve.ts b/ts/session/apis/snode_api/onsResolve.ts index 4cfc7724e..1643b3f81 100644 --- a/ts/session/apis/snode_api/onsResolve.ts +++ b/ts/session/apis/snode_api/onsResolve.ts @@ -41,7 +41,7 @@ async function getSessionIDForOnsName(onsNameCase: string) { const promises = range(0, validationCount).map(async () => { const targetNode = await getRandomSnode(); - const results = await doSnodeBatchRequest(onsResolveRequests, targetNode, 4000); + const results = await doSnodeBatchRequest(onsResolveRequests, targetNode, 4000, null); const firstResult = results[0]; if (!firstResult || firstResult.code !== 200 || !firstResult.body) { throw new Error('ONSresolve:Failed to resolve ONS'); diff --git a/ts/session/apis/snode_api/retrieveRequest.ts b/ts/session/apis/snode_api/retrieveRequest.ts index e148ad8b3..2502d1add 100644 --- a/ts/session/apis/snode_api/retrieveRequest.ts +++ b/ts/session/apis/snode_api/retrieveRequest.ts @@ -124,7 +124,12 @@ async function retrieveNextMessages( // let exceptions bubble up // no retry for this one as this a call we do every few seconds while polling for messages - const results = await doSnodeBatchRequest(retrieveRequestsParams, targetNode, 4000); + const results = await doSnodeBatchRequest( + retrieveRequestsParams, + targetNode, + 4000, + associatedWith + ); if (!results || !results.length) { window?.log?.warn( diff --git a/ts/session/apis/snode_api/sessionRpc.ts b/ts/session/apis/snode_api/sessionRpc.ts index 776908248..3167476c6 100644 --- a/ts/session/apis/snode_api/sessionRpc.ts +++ b/ts/session/apis/snode_api/sessionRpc.ts @@ -29,7 +29,7 @@ async function doRequest({ url: string; options: LokiFetchOptions; targetNode?: Snode; - associatedWith?: string; + associatedWith: string | null; timeout: number; }): Promise { const method = options.method || 'GET'; @@ -52,7 +52,7 @@ async function doRequest({ targetNode, body: fetchOptions.body, headers: fetchOptions.headers, - associatedWith, + associatedWith: associatedWith || undefined, }); if (!fetchResult) { return undefined; @@ -117,7 +117,7 @@ export async function snodeRpc( method: string; params: Record | Array>; targetNode: Snode; - associatedWith?: string; + associatedWith: string | null; timeout?: number; } //the user pubkey this call is for. if the onion request fails, this is used to handle the error for this user swarm for instance ): Promise { diff --git a/ts/session/apis/snode_api/snodePool.ts b/ts/session/apis/snode_api/snodePool.ts index 9f69ce9bd..7a31d4589 100644 --- a/ts/session/apis/snode_api/snodePool.ts +++ b/ts/session/apis/snode_api/snodePool.ts @@ -313,12 +313,27 @@ export async function getSwarmFor(pubkey: string): Promise> { return goodNodes; } + // Request new node list from the network and save it + return getSwarmFromNetworkAndSave(pubkey); +} + +/** + * Force a request to be made to the network to fetch the swarm of the specificied pubkey, and cache the result. + * Note: should not be called directly unless you know what you are doing. Use the cached `getSwarmFor()` function instead + * @param pubkey the pubkey to request the swarm for + * @returns the fresh swarm, shuffled + */ +export async function getFreshSwarmFor(pubkey: string): Promise> { + return getSwarmFromNetworkAndSave(pubkey); +} + +async function getSwarmFromNetworkAndSave(pubkey: string) { // Request new node list from the network const swarm = await requestSnodesForPubkeyFromNetwork(pubkey); - const mixedSwarm = shuffle(swarm); + const shuffledSwarm = shuffle(swarm); - const edkeys = mixedSwarm.map((n: Snode) => n.pubkey_ed25519); + const edkeys = shuffledSwarm.map((n: Snode) => n.pubkey_ed25519); await internalUpdateSwarmFor(pubkey, edkeys); - return mixedSwarm; + return shuffledSwarm; } diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index e1b1ebea8..88da1ef0e 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -6,27 +6,26 @@ import { PubKey } from '../../types'; import { ERROR_CODE_NO_CONNECT } from './SNodeAPI'; import * as snodePool from './snodePool'; -import pRetry from 'p-retry'; import { ConversationModel } from '../../../models/conversation'; import { ConfigMessageHandler } from '../../../receiver/configMessage'; import { decryptEnvelopeWithOurKey } from '../../../receiver/contentMessage'; import { EnvelopePlus } from '../../../receiver/types'; import { updateIsOnline } from '../../../state/ducks/onion'; +import { ReleasedFeatures } from '../../../util/releaseFeature'; +import { + GenericWrapperActions, + UserGroupsWrapperActions, +} from '../../../webworker/workers/browser/libsession_worker_interface'; import { DURATION, SWARM_POLLING_TIMEOUT } from '../../constants'; import { getConversationController } from '../../conversations'; import { IncomingMessage } from '../../messages/incoming/IncomingMessage'; import { ed25519Str } from '../../onions/onionPath'; import { StringUtils, UserUtils } from '../../utils'; import { perfEnd, perfStart } from '../../utils/Performance'; +import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { SnodeAPIRetrieve } from './retrieveRequest'; import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; -import { ReleasedFeatures } from '../../../util/releaseFeature'; -import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; -import { - GenericWrapperActions, - UserGroupsWrapperActions, -} from '../../../webworker/workers/browser/libsession_worker_interface'; export function extractWebSocketContent( message: string, @@ -404,93 +403,80 @@ export class SwarmPolling { const pkStr = pubkey.key; try { - return await pRetry( - async () => { - const prevHashes = await Promise.all( - namespaces.map(namespace => this.getLastHash(snodeEdkey, pkStr, namespace)) - ); - const configHashesToBump: Array = []; + const prevHashes = await Promise.all( + namespaces.map(namespace => this.getLastHash(snodeEdkey, pkStr, namespace)) + ); + const configHashesToBump: Array = []; - if (await ReleasedFeatures.checkIsUserConfigFeatureReleased()) { - // TODOLATER add the logic to take care of the closed groups too once we have a way to do it with the wrappers - if (isUs) { - for (let index = 0; index < LibSessionUtil.requiredUserVariants.length; index++) { - const variant = LibSessionUtil.requiredUserVariants[index]; - try { - const toBump = await GenericWrapperActions.currentHashes(variant); - - if (toBump?.length) { - configHashesToBump.push(...toBump); - } - } catch (e) { - window.log.warn(`failed to get currentHashes for user variant ${variant}`); - } - } - window.log.debug(`configHashesToBump: ${configHashesToBump}`); - } - } - - let results = await SnodeAPIRetrieve.retrieveNextMessages( - node, - prevHashes, - pkStr, - namespaces, - UserUtils.getOurPubKeyStrFromCache(), - configHashesToBump - ); - - if (!results.length) { - return []; - } - // when we asked to extend the expiry of the config messages, exclude it from the list of results as we do not want to mess up the last hash tracking logic - if (configHashesToBump.length) { + if (await ReleasedFeatures.checkIsUserConfigFeatureReleased()) { + // TODOLATER add the logic to take care of the closed groups too once we have a way to do it with the wrappers + if (isUs) { + for (let index = 0; index < LibSessionUtil.requiredUserVariants.length; index++) { + const variant = LibSessionUtil.requiredUserVariants[index]; try { - const lastResult = results[results.length - 1]; - if (lastResult?.code !== 200) { - // the update expiry of our config messages didn't work. - window.log.warn( - `the update expiry of our tracked config hashes didn't work: ${JSON.stringify( - lastResult - )}` - ); + const toBump = await GenericWrapperActions.currentHashes(variant); + + if (toBump?.length) { + configHashesToBump.push(...toBump); } } catch (e) { - // nothing to do I suppose here. + window.log.warn(`failed to get currentHashes for user variant ${variant}`); } - results = results.slice(0, results.length - 1); } - - const lastMessages = results.map(r => { - return last(r.messages.messages); - }); - - await Promise.all( - lastMessages.map(async (lastMessage, index) => { - if (!lastMessage) { - return; - } - return this.updateLastHash({ - edkey: snodeEdkey, - pubkey, - namespace: namespaces[index], - hash: lastMessage.hash, - expiration: lastMessage.expiration, - }); - }) - ); - - return results; - }, - { - minTimeout: 100, - retries: 1, - onFailedAttempt: e => { - window?.log?.warn( - `retrieveNextMessages attempt #${e.attemptNumber} failed. ${e.retriesLeft} retries left... ${e.name}` - ); - }, + window.log.debug(`configHashesToBump: ${configHashesToBump}`); } + } + + let results = await SnodeAPIRetrieve.retrieveNextMessages( + node, + prevHashes, + pkStr, + namespaces, + UserUtils.getOurPubKeyStrFromCache(), + configHashesToBump ); + + if (!results.length) { + return []; + } + // when we asked to extend the expiry of the config messages, exclude it from the list of results as we do not want to mess up the last hash tracking logic + if (configHashesToBump.length) { + try { + const lastResult = results[results.length - 1]; + if (lastResult?.code !== 200) { + // the update expiry of our config messages didn't work. + window.log.warn( + `the update expiry of our tracked config hashes didn't work: ${JSON.stringify( + lastResult + )}` + ); + } + } catch (e) { + // nothing to do I suppose here. + } + results = results.slice(0, results.length - 1); + } + + const lastMessages = results.map(r => { + return last(r.messages.messages); + }); + + await Promise.all( + lastMessages.map(async (lastMessage, index) => { + if (!lastMessage) { + return; + } + return this.updateLastHash({ + edkey: snodeEdkey, + pubkey, + namespace: namespaces[index], + hash: lastMessage.hash, + expiration: lastMessage.expiration, + }); + }) + ); + + return results; } catch (e) { if (e.message === ERROR_CODE_NO_CONNECT) { if (window.inboxStore?.getState().onionPaths.isOnline) { diff --git a/ts/session/conversations/ConversationController.ts b/ts/session/conversations/ConversationController.ts index a9a9d2fad..42c09414b 100644 --- a/ts/session/conversations/ConversationController.ts +++ b/ts/session/conversations/ConversationController.ts @@ -515,6 +515,8 @@ async function leaveClosedGroup(groupId: string, fromSyncMessage: boolean) { namespace: SnodeNamespaces.ClosedGroupMessage, pubkey: PubKey.cast(groupId), }); + // TODO our leaving message might fail to be sent for some specific reason we want to still delete the group. + // for instance, if we do not have the encryption keypair anymore, we cannot send our left message, but we should still delete it's content if (wasSent) { window?.log?.info( `Leaving message sent ${groupId}. Removing everything related to this group.` diff --git a/ts/session/onions/onionPath.ts b/ts/session/onions/onionPath.ts index 8e1760183..08254af42 100644 --- a/ts/session/onions/onionPath.ts +++ b/ts/session/onions/onionPath.ts @@ -312,7 +312,7 @@ export async function testGuardNode(snode: Snode) { response = await insecureNodeFetch(url, fetchOptions); } catch (e) { if (e.type === 'request-timeout') { - window?.log?.warn('test timeout for node,', ed25519Str(snode.pubkey_ed25519)); + window?.log?.warn('test :,', ed25519Str(snode.pubkey_ed25519)); } if (e.code === 'ENETUNREACH') { window?.log?.warn('no network on node,', snode); diff --git a/ts/session/profile_manager/ProfileManager.ts b/ts/session/profile_manager/ProfileManager.ts index 32ac9bbae..c43fae9fc 100644 --- a/ts/session/profile_manager/ProfileManager.ts +++ b/ts/session/profile_manager/ProfileManager.ts @@ -71,6 +71,13 @@ async function updateProfileOfContact( avatarChanged = true; // allow changes from strings to null/undefined to trigger a AvatarDownloadJob. If that happens, we want to remove the local attachment file. } + // if we have a local path to an downloaded avatar, but no corresponding url/key for it, it means that + // the avatar was most likely removed so let's remove our link to that file. + if ((!profileUrl || !profileKeyHex) && conversation.get('avatarInProfile')) { + conversation.set({ avatarInProfile: undefined }); + changes = true; + } + if (changes) { await conversation.commit(); } diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index 0830d5ca9..f9b231244 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -236,7 +236,7 @@ async function sendMessagesDataToSnode( signedDeleteOldHashesRequest ); - if (snode) { + if (!isEmpty(storeResults)) { window?.log?.info( `sendMessagesToSnode - Successfully stored messages to ${ed25519Str(destination)} via ${ snode.ip diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts index 48d77bfa3..4513c5cf9 100644 --- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts @@ -223,6 +223,7 @@ class ConfigurationSyncJob extends PersistedJob window.log.info( `ConfigurationSyncJob: unexpected result length: expected ${expectedReplyLength} but got ${result?.length}` ); + // this might be a 421 error (already handled) so let's retry this request a little bit later return RunJobResult.RetryJobIfPossible; }