diff --git a/ts/protobuf/utils.ts b/ts/protobuf/utils.ts index 12c0f2a5d..bb5a403e3 100644 --- a/ts/protobuf/utils.ts +++ b/ts/protobuf/utils.ts @@ -1,3 +1,5 @@ +import { isNil } from 'lodash'; + /** * This function is used to check that an optional property on a Protobuf object is not undefined or using a type-specific default value. * https://protobuf.dev/programming-guides/proto/#optional @@ -10,7 +12,7 @@ function hasDefinedProperty( object: A, property: B ) { - return Object.prototype.hasOwnProperty.call(object, property) !== false; + return !isNil(object) && Object.prototype.hasOwnProperty.call(object, property) !== false; } export const ProtobufUtils = { diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index af221bb9e..50d83658f 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -19,7 +19,7 @@ import { getConversationController } from '../session/conversations'; import { concatUInt8Array, getSodiumRenderer } from '../session/crypto'; import { removeMessagePadding } from '../session/crypto/BufferPadding'; import { DisappearingMessages } from '../session/disappearing_messages'; -import { DisappearingMessageUpdate } from '../session/disappearing_messages/types'; +import { ReadyToDisappearMsgUpdate } from '../session/disappearing_messages/types'; import { ProfileManager } from '../session/profile_manager/ProfileManager'; import { GroupUtils, UserUtils } from '../session/utils'; import { perfEnd, perfStart } from '../session/utils/Performance'; @@ -547,10 +547,19 @@ export async function innerHandleSwarmContentMessage({ if (content.dataExtractionNotification) { perfStart(`handleDataExtractionNotification-${envelope.id}`); + // DataExtractionNotification uses the expiration setting of our side of the 1o1 conversation. whatever we get in the contentMessage + const expirationTimer = senderConversationModel.getExpireTimer(); + + const expirationType = DisappearingMessages.changeToDisappearingMessageType( + senderConversationModel, + expirationTimer, + senderConversationModel.getExpirationMode() + ); + await handleDataExtractionNotification( envelope, content.dataExtractionNotification as SignalService.DataExtractionNotification, - expireUpdate || null + { expirationTimer, expirationType, messageExpirationFromRetrieve } ); perfEnd( `handleDataExtractionNotification-${envelope.id}`, @@ -838,7 +847,7 @@ async function handleMessageRequestResponse( export async function handleDataExtractionNotification( envelope: EnvelopePlus, dataNotificationMessage: SignalService.DataExtractionNotification, - expireUpdate: DisappearingMessageUpdate | null + expireUpdate: ReadyToDisappearMsgUpdate ): Promise { // we currently don't care about the timestamp included in the field itself, just the timestamp of the envelope const { type, timestamp: referencedAttachment } = dataNotificationMessage; @@ -871,6 +880,7 @@ export async function handleDataExtractionNotification( source, }, }); + created = DisappearingMessages.getMessageReadyToDisappear( convo, created, diff --git a/ts/session/disappearing_messages/index.ts b/ts/session/disappearing_messages/index.ts index 5144eea2c..3ae068867 100644 --- a/ts/session/disappearing_messages/index.ts +++ b/ts/session/disappearing_messages/index.ts @@ -21,6 +21,7 @@ import { DisappearingMessageMode, DisappearingMessageType, DisappearingMessageUpdate, + ReadyToDisappearMsgUpdate, } from './types'; async function destroyMessagesAndUpdateRedux( @@ -303,17 +304,15 @@ async function checkForExpireUpdateInContentMessage( convoToUpdate: ConversationModel, messageExpirationFromRetrieve: number | null ): Promise { - const dataMessage = content.dataMessage as SignalService.DataMessage; + const dataMessage = content.dataMessage as SignalService.DataMessage | undefined; // We will only support legacy disappearing messages for a short period before disappearing messages v2 is unlocked const isDisappearingMessagesV2Released = await ReleasedFeatures.checkIsDisappearMessageV2FeatureReleased(); const couldBeLegacyContentMessage = couldBeLegacyDisappearingMessageContent(content); - const isLegacyDataMessage = checkIsLegacyDisappearingDataMessage( - couldBeLegacyContentMessage, - dataMessage as SignalService.DataMessage - ); + const isLegacyDataMessage = + dataMessage && checkIsLegacyDisappearingDataMessage(couldBeLegacyContentMessage, dataMessage); const hasExpirationUpdateFlags = - dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; + dataMessage?.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE; const isLegacyConversationSettingMessage = isDisappearingMessagesV2Released ? (isLegacyDataMessage || couldBeLegacyContentMessage) && hasExpirationUpdateFlags @@ -428,7 +427,7 @@ function getMessageReadyToDisappear( conversationModel: ConversationModel, messageModel: MessageModel, messageFlags: number, - expireUpdate?: DisappearingMessageUpdate + expireUpdate?: ReadyToDisappearMsgUpdate ) { if (conversationModel.isPublic()) { throw Error( diff --git a/ts/session/disappearing_messages/types.ts b/ts/session/disappearing_messages/types.ts index d9aaa22a2..550df0663 100644 --- a/ts/session/disappearing_messages/types.ts +++ b/ts/session/disappearing_messages/types.ts @@ -34,3 +34,8 @@ export type DisappearingMessageUpdate = { isDisappearingMessagesV2Released?: boolean; messageExpirationFromRetrieve: number | null; }; + +export type ReadyToDisappearMsgUpdate = Pick< + DisappearingMessageUpdate, + 'expirationType' | 'expirationTimer' | 'messageExpirationFromRetrieve' +>; diff --git a/ts/session/messages/outgoing/controlMessage/CallMessage.ts b/ts/session/messages/outgoing/controlMessage/CallMessage.ts index cfc54426f..04b122ea1 100644 --- a/ts/session/messages/outgoing/controlMessage/CallMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/CallMessage.ts @@ -1,10 +1,9 @@ -import { ContentMessage } from '..'; import { SignalService } from '../../../../protobuf'; import { signalservice } from '../../../../protobuf/compiled'; import { TTL_DEFAULT } from '../../../constants'; -import { MessageParams } from '../Message'; +import { ExpirableMessage, ExpirableMessageParams } from '../ExpirableMessage'; -interface CallMessageParams extends MessageParams { +interface CallMessageParams extends ExpirableMessageParams { type: SignalService.CallMessage.Type; sdpMLineIndexes?: Array; sdpMids?: Array; @@ -12,7 +11,7 @@ interface CallMessageParams extends MessageParams { uuid: string; } -export class CallMessage extends ContentMessage { +export class CallMessage extends ExpirableMessage { public readonly type: signalservice.CallMessage.Type; public readonly sdpMLineIndexes?: Array; public readonly sdpMids?: Array; diff --git a/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts index 945e85a52..a0a90f253 100644 --- a/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/DataExtractionNotificationMessage.ts @@ -2,7 +2,6 @@ import { getMessageQueue } from '../../..'; import { SignalService } from '../../../../protobuf'; import { SnodeNamespaces } from '../../../apis/snode_api/namespaces'; import { getConversationController } from '../../../conversations'; -import { DisappearingMessages } from '../../../disappearing_messages'; import { PubKey } from '../../../types'; import { UserUtils } from '../../../utils'; import { ExpirableMessage, ExpirableMessageParams } from '../ExpirableMessage'; @@ -55,18 +54,12 @@ export const sendDataExtractionNotification = async ( return; } - const expireTimer = convo.getExpireTimer(); - const expirationType = DisappearingMessages.changeToDisappearingMessageType( - convo, - expireTimer, - convo.getExpirationMode() - ); - + // DataExtractionNotification are expiring with the recipient, so don't include ours const dataExtractionNotificationMessage = new DataExtractionNotificationMessage({ referencedAttachmentTimestamp, timestamp: Date.now(), - expirationType, - expireTimer, + expirationType: null, + expireTimer: null, }); const pubkey = PubKey.cast(conversationId); diff --git a/ts/session/utils/calling/CallManager.ts b/ts/session/utils/calling/CallManager.ts index c92fc3de3..77ff05d5b 100644 --- a/ts/session/utils/calling/CallManager.ts +++ b/ts/session/utils/calling/CallManager.ts @@ -417,6 +417,8 @@ async function createOfferAndSendIt(recipient: string) { type: SignalService.CallMessage.Type.OFFER, sdps: [overridenSdps], uuid: currentCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); window.log.info(`sending '${offer.type}'' with callUUID: ${currentCallUUID}`); @@ -503,6 +505,8 @@ export async function USER_callRecipient(recipient: string) { timestamp: now, type: SignalService.CallMessage.Type.PRE_OFFER, uuid: currentCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); window.log.info('Sending preOffer message to ', ed25519Str(recipient)); @@ -527,7 +531,7 @@ export async function USER_callRecipient(recipient: string) { if ( expirationMode === 'legacy' || expirationMode === 'deleteAfterSend' || - expirationMode === 'deleteAfterRead' // we are the one iniating the call, so that message is already read + expirationMode === 'deleteAfterRead' // we are the one initiaing the call, so that message is already read ) { expirationStartTimestamp = DisappearingMessages.setExpirationStartTimestamp( expirationMode, @@ -537,6 +541,7 @@ export async function USER_callRecipient(recipient: string) { } } + // Locally, that message must expire await calledConvo?.addSingleOutgoingMessage({ callNotificationType: 'started-call', sent_at: now, @@ -610,6 +615,8 @@ const iceSenderDebouncer = _.debounce(async (recipient: string) => { sdpMids: validCandidates.map(c => c.sdpMid), sdps: validCandidates.map(c => c.candidate), uuid: currentCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); window.log.info( @@ -957,6 +964,8 @@ export async function rejectCallAlreadyAnotherCall(fromSender: string, forcedUUI type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), uuid: forcedUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); await sendCallMessageAndSync(rejectCallMessage, fromSender); @@ -980,6 +989,8 @@ export async function USER_rejectIncomingCallRequest(fromSender: string) { type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), uuid: aboutCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); // sync the reject event so our other devices remove the popup too await sendCallMessageAndSync(endCallMessage, fromSender); @@ -1022,6 +1033,8 @@ export async function USER_hangup(fromSender: string) { type: SignalService.CallMessage.Type.END_CALL, timestamp: Date.now(), uuid: currentCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); void getMessageQueue().sendToPubKeyNonDurably({ pubkey: PubKey.cast(fromSender), @@ -1098,6 +1111,8 @@ async function buildAnswerAndSendIt(sender: string) { type: SignalService.CallMessage.Type.ANSWER, sdps: [answerSdp], uuid: currentCallUUID, + expirationType: null, // Note: CallMessages are expiring based on the recipient's side expiration setting + expireTimer: null, }); window.log.info('sending ANSWER MESSAGE and sync'); @@ -1272,6 +1287,8 @@ async function addMissedCallMessage(callerPubkey: string, sentAt: number) { await incomingCallConversation.unhideIfNeeded(false); } + // Note: Missed call messages are expiring with our side of the conversation settings. + const expirationMode = incomingCallConversation.getExpirationMode(); const expireTimer = incomingCallConversation.getExpireTimer() || 0; let expirationType;