// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import type { ConversationAttributesType } from '../model-types.d'; import type { SendMetadataType, SendOptionsType, } from '../textsecure/SendMessage'; import * as Bytes from '../Bytes'; import { getRandomBytes, getZeroes } from '../Crypto'; import { getConversationMembers } from './getConversationMembers'; import { isDirectConversation, isMe } from './whatTypeOfConversation'; import { senderCertificateService } from '../services/senderCertificate'; import { shouldSharePhoneNumberWith } from './phoneNumberSharingMode'; import type { SerializedCertificateType } from '../textsecure/OutgoingMessage'; import { SenderCertificateMode } from '../textsecure/OutgoingMessage'; import { isNotNil } from './isNotNil'; const SEALED_SENDER = { UNKNOWN: 0, ENABLED: 1, DISABLED: 2, UNRESTRICTED: 3, }; export async function getSendOptionsForRecipients( recipients: ReadonlyArray, options?: { story?: boolean } ): Promise { const conversations = recipients .map(identifier => window.ConversationController.get(identifier)) .filter(isNotNil); const metadataList = await Promise.all( conversations.map(conversation => getSendOptions(conversation.attributes, options) ) ); return metadataList.reduce( (acc, current): SendOptionsType => { const { sendMetadata: accMetadata } = acc; const { sendMetadata: currentMetadata } = current; if (!currentMetadata) { return acc; } if (!accMetadata) { return current; } Object.assign(accMetadata, currentMetadata); return acc; }, { sendMetadata: {}, } ); } export async function getSendOptions( conversationAttrs: ConversationAttributesType, options: { syncMessage?: boolean; story?: boolean } = {} ): Promise { const { syncMessage, story } = options; if (!isDirectConversation(conversationAttrs)) { const contactCollection = getConversationMembers(conversationAttrs); const sendMetadata: SendMetadataType = {}; await Promise.all( contactCollection.map(async contactAttrs => { const conversation = window.ConversationController.get(contactAttrs.id); if (!conversation) { return; } const { sendMetadata: conversationSendMetadata } = await getSendOptions( conversation.attributes, options ); Object.assign(sendMetadata, conversationSendMetadata || {}); }) ); return { sendMetadata }; } const { accessKey, sealedSender } = conversationAttrs; // We never send sync messages or to our own account as sealed sender if (syncMessage || isMe(conversationAttrs)) { return { sendMetadata: undefined, }; } const { e164, uuid } = conversationAttrs; const senderCertificate = await getSenderCertificateForDirectConversation( conversationAttrs ); // If we've never fetched user's profile, we default to what we have if (sealedSender === SEALED_SENDER.UNKNOWN || story) { const identifierData = { accessKey: accessKey || (story ? Bytes.toBase64(getZeroes(16)) : Bytes.toBase64(getRandomBytes(16))), senderCertificate, }; return { sendMetadata: { ...(e164 ? { [e164]: identifierData } : {}), ...(uuid ? { [uuid]: identifierData } : {}), }, }; } if (sealedSender === SEALED_SENDER.DISABLED) { return { sendMetadata: undefined, }; } const identifierData = { accessKey: accessKey && sealedSender === SEALED_SENDER.ENABLED ? accessKey : Bytes.toBase64(getRandomBytes(16)), senderCertificate, }; return { sendMetadata: { ...(e164 ? { [e164]: identifierData } : {}), ...(uuid ? { [uuid]: identifierData } : {}), }, }; } function getSenderCertificateForDirectConversation( conversationAttrs: ConversationAttributesType ): Promise { if (!isDirectConversation(conversationAttrs)) { throw new Error( 'getSenderCertificateForDirectConversation should only be called for direct conversations' ); } let certificateMode: SenderCertificateMode; if (shouldSharePhoneNumberWith(conversationAttrs)) { certificateMode = SenderCertificateMode.WithE164; } else { certificateMode = SenderCertificateMode.WithoutE164; } return senderCertificateService.get(certificateMode); }