diff --git a/actions/setup_and_build/action.yml b/actions/setup_and_build/action.yml index c53502656..3d33117d1 100644 --- a/actions/setup_and_build/action.yml +++ b/actions/setup_and_build/action.yml @@ -1,3 +1,4 @@ + name: 'Setup and build' description: 'Setup and build Session Desktop' runs: diff --git a/package.json b/package.json index d6e7e015b..f931b07ea 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "session-desktop", "productName": "Session", "description": "Private messaging from your desktop", - "version": "1.12.3", + "version": "1.12.4", "license": "GPL-3.0", "author": { "name": "Oxen Labs", @@ -96,7 +96,7 @@ "fs-extra": "9.0.0", "glob": "7.1.2", "image-type": "^4.1.0", - "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.4/libsession_util_nodejs-v0.3.4.tar.gz", + "libsession_util_nodejs": "https://github.com/oxen-io/libsession-util-nodejs/releases/download/v0.3.19/libsession_util_nodejs-v0.3.19.tar.gz", "libsodium-wrappers-sumo": "^0.7.9", "linkify-it": "^4.0.1", "lodash": "^4.17.21", diff --git a/ts/receiver/configMessage.ts b/ts/receiver/configMessage.ts index fc5b3b5e2..636ffe162 100644 --- a/ts/receiver/configMessage.ts +++ b/ts/receiver/configMessage.ts @@ -18,7 +18,6 @@ import { OpenGroupUtils } from '../session/apis/open_group_api/utils'; import { getOpenGroupV2ConversationId } from '../session/apis/open_group_api/utils/OpenGroupUtils'; import { getSwarmPollingInstance } from '../session/apis/snode_api'; import { getConversationController } from '../session/conversations'; -import { IncomingMessage } from '../session/messages/incoming/IncomingMessage'; import { Profile, ProfileManager } from '../session/profile_manager/ProfileManager'; import { PubKey } from '../session/types'; import { StringUtils, UserUtils } from '../session/utils'; @@ -37,6 +36,8 @@ import { Registration } from '../util/registration'; import { ReleasedFeatures } from '../util/releaseFeature'; import { Storage, isSignInByLinking, setLastProfileUpdateTimestamp } from '../util/storage'; +import { SnodeNamespaces } from '../session/apis/snode_api/namespaces'; +import { RetrieveMessageItemWithNamespace } from '../session/apis/snode_api/types'; // eslint-disable-next-line import/no-unresolved import { ConfigWrapperObjectTypes } from '../webworker/workers/browser/libsession_worker_functions'; import { @@ -52,18 +53,29 @@ import { HexKeyPair } from './keypairs'; import { queueAllCachedFromSource } from './receiver'; import { EnvelopePlus } from './types'; -function groupByVariant( - incomingConfigs: Array> -) { +function groupByNamespace(incomingConfigs: Array) { const groupedByVariant: Map< ConfigWrapperObjectTypes, - Array> + Array > = new Map(); incomingConfigs.forEach(incomingConfig => { - const { kind } = incomingConfig.message; - - const wrapperId = LibSessionUtil.kindToVariant(kind); + const { namespace } = incomingConfig; + + const wrapperId: ConfigWrapperObjectTypes | null = + namespace === SnodeNamespaces.UserProfile + ? 'UserConfig' + : namespace === SnodeNamespaces.UserContacts + ? 'ContactsConfig' + : namespace === SnodeNamespaces.UserGroups + ? 'UserGroupsConfig' + : namespace === SnodeNamespaces.ConvoInfoVolatile + ? 'ConvoInfoVolatileConfig' + : null; + + if (!wrapperId) { + throw new Error('Unexpected wrapperId'); + } if (!groupedByVariant.has(wrapperId)) { groupedByVariant.set(wrapperId, []); @@ -75,10 +87,10 @@ function groupByVariant( } async function mergeConfigsWithIncomingUpdates( - incomingConfigs: Array> + incomingConfigs: Array ): Promise> { // first, group by variant so we do a single merge call - const groupedByVariant = groupByVariant(incomingConfigs); + const groupedByNamespace = groupByNamespace(incomingConfigs); const groupedResults: Map = new Map(); @@ -86,15 +98,15 @@ async function mergeConfigsWithIncomingUpdates( const publicKey = UserUtils.getOurPubKeyStrFromCache(); try { - for (let index = 0; index < groupedByVariant.size; index++) { - const variant = [...groupedByVariant.keys()][index]; - const sameVariant = groupedByVariant.get(variant); + for (let index = 0; index < groupedByNamespace.size; index++) { + const variant = [...groupedByNamespace.keys()][index]; + const sameVariant = groupedByNamespace.get(variant); if (!sameVariant?.length) { continue; } const toMerge = sameVariant.map(msg => ({ - data: msg.message.data, - hash: msg.messageHash, + data: StringUtils.fromBase64ToArray(msg.data), + hash: msg.hash, })); if (window.sessionFeatureFlags.debug.debugLibsessionDumps) { window.log.info( @@ -105,9 +117,7 @@ async function mergeConfigsWithIncomingUpdates( for (let dumpIndex = 0; dumpIndex < toMerge.length; dumpIndex++) { const element = toMerge[dumpIndex]; window.log.info( - `printDumpsForDebugging: toMerge of ${dumpIndex}:${element.hash}: ${StringUtils.toHex( - element.data - )} `, + `printDumpsForDebugging: toMerge of ${dumpIndex}:${element.hash}: ${element.data} `, StringUtils.toHex(await GenericWrapperActions.dump(variant)) ); } @@ -117,8 +127,8 @@ async function mergeConfigsWithIncomingUpdates( const needsPush = await GenericWrapperActions.needsPush(variant); const needsDump = await GenericWrapperActions.needsDump(variant); const mergedTimestamps = sameVariant - .filter(m => hashesMerged.includes(m.messageHash)) - .map(m => m.envelopeTimestamp); + .filter(m => hashesMerged.includes(m.hash)) + .map(m => m.timestamp); const latestEnvelopeTimestamp = Math.max(...mergedTimestamps); window.log.debug( @@ -891,7 +901,7 @@ async function processMergingResults(results: Map> + configMessages: Array ) { const userConfigLibsession = await ReleasedFeatures.checkIsUserConfigFeatureReleased(); @@ -906,9 +916,8 @@ async function handleConfigMessagesViaLibSession( window?.log?.debug( `Handling our sharedConfig message via libsession_util ${JSON.stringify( configMessages.map(m => ({ - variant: LibSessionUtil.kindToVariant(m.message.kind), - hash: m.messageHash, - seqno: (m.message.seqno as Long).toNumber(), + namespace: m.namespace, + hash: m.hash, })) )}` ); diff --git a/ts/session/apis/snode_api/swarmPolling.ts b/ts/session/apis/snode_api/swarmPolling.ts index 910a6e913..9b4ae3b85 100644 --- a/ts/session/apis/snode_api/swarmPolling.ts +++ b/ts/session/apis/snode_api/swarmPolling.ts @@ -11,8 +11,6 @@ import * as snodePool from './snodePool'; 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 { @@ -22,16 +20,19 @@ import { } 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 { StringUtils, UserUtils } from '../../utils'; import { ed25519Str } from '../../utils/String'; import { NotFoundError } from '../../utils/errors'; import { LibSessionUtil } from '../../utils/libsession/libsession_utils'; import { SnodeNamespace, SnodeNamespaces } from './namespaces'; import { SnodeAPIRetrieve } from './retrieveRequest'; -import { RetrieveMessageItem, RetrieveMessagesResultsBatched } from './types'; +import { + RetrieveMessageItem, + RetrieveMessageItemWithNamespace, + RetrieveMessagesResultsBatched, +} from './types'; -export function extractWebSocketContent( +function extractWebSocketContent( message: string, messageHash: string ): null | { @@ -265,9 +266,16 @@ export class SwarmPolling { // 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 (userConfigLibsession && resultsFromAllNamespaces) { - const userConfigMessages = resultsFromAllNamespaces - .filter(m => SnodeNamespace.isUserConfigNamespace(m.namespace)) - .map(r => r.messages.messages); + const userConfigMessages = resultsFromAllNamespaces.filter(m => + SnodeNamespace.isUserConfigNamespace(m.namespace) + ); + + const userConfigMessagesWithNamespace: Array> = + userConfigMessages.map(r => { + return (r.messages.messages || []).map(m => { + return { ...m, namespace: r.namespace }; + }); + }); allNamespacesWithoutUserConfigIfNeeded = flatten( compact( @@ -283,7 +291,7 @@ export class SwarmPolling { `received userConfigMessages count: ${userConfigMessagesMerged.length} for key ${pubkey.key}` ); try { - await this.handleSharedConfigMessages(userConfigMessagesMerged); + await this.handleSharedConfigMessages(flatten(userConfigMessagesWithNamespace)); } catch (e) { window.log.warn( `handleSharedConfigMessages of ${userConfigMessagesMerged.length} failed with ${e.message}` @@ -366,52 +374,16 @@ export class SwarmPolling { } private async handleSharedConfigMessages( - userConfigMessagesMerged: Array, + userConfigMessagesMerged: Array, returnDisplayNameOnly?: boolean ): Promise { - const extractedUserConfigMessage = compact( - userConfigMessagesMerged.map((m: RetrieveMessageItem) => { - return extractWebSocketContent(m.data, m.hash); - }) - ); - const allDecryptedConfigMessages: Array> = - []; - - for (let index = 0; index < extractedUserConfigMessage.length; index++) { - const userConfigMessage = extractedUserConfigMessage[index]; - - try { - const envelope: EnvelopePlus = SignalService.Envelope.decode(userConfigMessage.body) as any; - const decryptedEnvelope = await decryptEnvelopeWithOurKey(envelope); - if (!decryptedEnvelope?.byteLength) { - continue; - } - const content = SignalService.Content.decode(new Uint8Array(decryptedEnvelope)); - if (content.sharedConfigMessage) { - const asIncomingMsg: IncomingMessage = { - envelopeTimestamp: toNumber(envelope.timestamp), - message: content.sharedConfigMessage, - messageHash: userConfigMessage.messageHash, - authorOrGroupPubkey: envelope.source, - authorInGroup: envelope.senderIdentity, - }; - allDecryptedConfigMessages.push(asIncomingMsg); - } else { - throw new Error( - 'received a message from the namespace reserved for user config but it did not contain a sharedConfigMessage' - ); - } - } catch (e) { - window.log.warn( - `failed to decrypt message with hash "${userConfigMessage.messageHash}": ${e.message}` - ); - } + if (!userConfigMessagesMerged.length) { + return ''; } - if (allDecryptedConfigMessages.length) { try { window.log.info( - `handleConfigMessagesViaLibSession of "${allDecryptedConfigMessages.length}" messages with libsession` + `handleConfigMessagesViaLibSession of "${userConfigMessagesMerged.length}" messages with libsession` ); if (returnDisplayNameOnly) { @@ -424,9 +396,9 @@ export class SwarmPolling { const privateKeyEd25519 = keypair.privKeyBytes; // we take the lastest config message to create the wrapper in memory - const incomingConfigMessages = allDecryptedConfigMessages.map(m => ({ - data: m.message.data, - hash: m.messageHash, + const incomingConfigMessages = userConfigMessagesMerged.map(m => ({ + data: StringUtils.fromBase64ToArray( m.data), + hash: m.hash, })); await GenericWrapperActions.init('UserConfig', privateKeyEd25519, null); @@ -449,15 +421,15 @@ export class SwarmPolling { return ''; } - await ConfigMessageHandler.handleConfigMessagesViaLibSession(allDecryptedConfigMessages); + await ConfigMessageHandler.handleConfigMessagesViaLibSession(userConfigMessagesMerged); } catch (e) { - const allMessageHases = allDecryptedConfigMessages.map(m => m.messageHash).join(','); + const allMessageHases = userConfigMessagesMerged.map(m => m.hash).join(','); window.log.warn( `failed to handle messages hashes "${allMessageHases}" with libsession. Error: "${e.message}"` ); } - } - return ''; + return '' + } // Fetches messages for `pubkey` from `node` potentially updating diff --git a/ts/session/apis/snode_api/types.ts b/ts/session/apis/snode_api/types.ts index 6a27ff44d..9a7925775 100644 --- a/ts/session/apis/snode_api/types.ts +++ b/ts/session/apis/snode_api/types.ts @@ -7,6 +7,8 @@ export type RetrieveMessageItem = { timestamp: number; }; +export type RetrieveMessageItemWithNamespace = RetrieveMessageItem & { namespace: number }; + export type RetrieveMessagesResultsContent = { hf?: Array; messages?: Array; diff --git a/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts b/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts index c888e871f..5718b5a40 100644 --- a/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/SharedConfigMessage.ts @@ -9,36 +9,26 @@ import { MessageParams } from '../Message'; interface SharedConfigParams extends MessageParams { seqno: Long; kind: SignalService.SharedConfigMessage.Kind; - data: Uint8Array; + readyToSendData: Uint8Array; } export class SharedConfigMessage extends ContentMessage { public readonly seqno: Long; public readonly kind: SignalService.SharedConfigMessage.Kind; - public readonly data: Uint8Array; + public readonly readyToSendData: Uint8Array; constructor(params: SharedConfigParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); - this.data = params.data; + this.readyToSendData = params.readyToSendData; this.kind = params.kind; this.seqno = params.seqno; } public contentProto(): SignalService.Content { - return new SignalService.Content({ - sharedConfigMessage: this.sharedConfigProto(), - }); + throw new Error('SharedConfigMessage must not be sent wrapped anymore'); } public ttl(): number { return TTL_DEFAULT.CONFIG_MESSAGE; } - - protected sharedConfigProto(): SignalService.SharedConfigMessage { - return new SignalService.SharedConfigMessage({ - data: this.data, - kind: this.kind, - seqno: this.seqno, - }); - } } diff --git a/ts/session/sending/MessageSender.ts b/ts/session/sending/MessageSender.ts index b7cbac601..f7020c63f 100644 --- a/ts/session/sending/MessageSender.ts +++ b/ts/session/sending/MessageSender.ts @@ -379,32 +379,20 @@ async function sendMessagesToSnode( try { const recipient = PubKey.cast(destination); - const encryptedAndWrapped = await encryptMessagesAndWrap( - params.map(m => ({ - destination: m.pubkey, - plainTextBuffer: m.message.plainTextBuffer(), - namespace: m.namespace, - ttl: m.message.ttl(), + const encryptedAndWrapped: Array> = + []; + + params.forEach(m => { + const wrapped = { identifier: m.message.identifier, isSyncMessage: MessageSender.isContentSyncMessage(m.message), - })) - ); - - // first update all the associated timestamps of our messages in DB, if the outgoing messages are associated with one. - await Promise.all( - encryptedAndWrapped.map(async (m, index) => { - // make sure to update the local sent_at timestamp, because sometimes, we will get the just pushed message in the receiver side - // before we return from the await below. - // and the isDuplicate messages relies on sent_at timestamp to be valid. - const found = await Data.getMessageById(m.identifier); - - // make sure to not update the sent timestamp if this a currently syncing message - if (found && !found.get('sentSync')) { - found.set({ sent_at: encryptedAndWrapped[index].networkTimestamp }); - await found.commit(); - } - }) - ); + namespace: m.namespace, + ttl: m.message.ttl(), + networkTimestamp: GetNetworkTime.getNowWithNetworkOffset(), + data64: ByteBuffer.wrap(m.message.readyToSendData).toString('base64'), + }; + encryptedAndWrapped.push(wrapped); + }); const batchResults = await pRetry( async () => { @@ -432,35 +420,6 @@ async function sendMessagesToSnode( throw new Error('result is empty for sendMessagesToSnode'); } - const isDestinationClosedGroup = getConversationController() - .get(recipient.key) - ?.isClosedGroup(); - - await Promise.all( - encryptedAndWrapped.map(async (message, index) => { - // If message also has a sync message, save that hash. Otherwise save the hash from the regular message send i.e. only closed groups in this case. - if ( - message.identifier && - (message.isSyncMessage || isDestinationClosedGroup) && - batchResults[index] && - !isEmpty(batchResults[index]) && - isString(batchResults[index].body.hash) - ) { - const hashFoundInResponse = batchResults[index].body.hash; - const foundMessage = await Data.getMessageById(message.identifier); - if (foundMessage) { - await foundMessage.updateMessageHash(hashFoundInResponse); - await foundMessage.commit(); - window?.log?.info( - `updated message ${foundMessage.get('id')} with hash: ${foundMessage.get( - 'messageHash' - )}` - ); - } - } - }) - ); - return batchResults; } catch (e) { window.log.warn(`sendMessagesToSnode failed with ${e.message}`); diff --git a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts index 0e94040c0..5fdf8590f 100644 --- a/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts +++ b/ts/session/utils/job_runners/jobs/ConfigurationSyncJob.ts @@ -225,7 +225,7 @@ class ConfigurationSyncJob extends PersistedJob ...m, message: { ...m.message, - data: to_hex(m.message.data), + readyToSendData: to_hex(m.message.readyToSendData), }, }; }) diff --git a/ts/session/utils/libsession/libsession_utils.ts b/ts/session/utils/libsession/libsession_utils.ts index 37855f438..205bc7fc0 100644 --- a/ts/session/utils/libsession/libsession_utils.ts +++ b/ts/session/utils/libsession/libsession_utils.ts @@ -130,14 +130,14 @@ async function pendingChangesForPubkey(pubkey: string): Promise