diff --git a/protos/SignalService.proto b/protos/SignalService.proto index d94cbdfc5..5531136cb 100644 --- a/protos/SignalService.proto +++ b/protos/SignalService.proto @@ -17,10 +17,12 @@ message Envelope { } message TypingMessage { + enum Action { STARTED = 0; STOPPED = 1; } + // @required required uint64 timestamp = 1; // @required @@ -42,7 +44,32 @@ message MessageRequestResponse { optional DataMessage.LokiProfile profile = 3; } +// TODO Syncing disappearing messages +// message SyncedExpiries { + +// message SyncedConversationExpiries { + +// message SyncedExpiry { +// // @required +// required string serverHash = 1; // messageHash for desktop and serverHash for iOS +// // @required +// required uint64 expirationTimestamp = 2; // this is only used for deleteAfterRead +// } +// // @required +// required string syncTarget = 1; // the conversationID those expiries are related to +// repeated SyncedExpiry expiries = 2; +// } + +// repeated SyncedConversationExpiries conversationExpiries = 1; +// } + message Content { + + enum ExpirationType { + DELETE_AFTER_SEND = 1; + DELETE_AFTER_READ = 2; + } + optional DataMessage dataMessage = 1; optional CallMessage callMessage = 3; optional ReceiptMessage receiptMessage = 5; @@ -51,6 +78,10 @@ message Content { optional DataExtractionNotification dataExtractionNotification = 8; optional Unsend unsendMessage = 9; optional MessageRequestResponse messageRequestResponse = 10; + optional ExpirationType expirationType = 11; + optional uint32 expirationTimer = 12; + optional uint64 lastDisappearingMessageChangeTimestamp = 13; + // optional SyncedExpiries syncedExpiries = 14; } message KeyPair { @@ -130,7 +161,7 @@ message DataMessage { message ClosedGroupControlMessage { enum Type { - NEW = 1; // publicKey, name, encryptionKeyPair, members, admins, expireTimer + NEW = 1; // publicKey, name, encryptionKeyPair, members, admins, expirationTimer ENCRYPTION_KEY_PAIR = 3; // publicKey, wrappers NAME_CHANGE = 4; // name MEMBERS_ADDED = 5; // members @@ -156,7 +187,8 @@ message DataMessage { repeated bytes members = 5; repeated bytes admins = 6; repeated KeyPairWrapper wrappers = 7; - optional uint32 expireTimer = 8; + // TODO Make sure rename doesn't break anything + optional uint32 expirationTimer = 8; } @@ -164,6 +196,7 @@ message DataMessage { repeated AttachmentPointer attachments = 2; optional GroupContext group = 3; optional uint32 flags = 4; + // TODO this will be removed 2 weeks after the release optional uint32 expireTimer = 5; optional bytes profileKey = 6; optional uint64 timestamp = 7; diff --git a/ts/interactions/conversationInteractions.ts b/ts/interactions/conversationInteractions.ts index 3f251c474..c9b2f21e5 100644 --- a/ts/interactions/conversationInteractions.ts +++ b/ts/interactions/conversationInteractions.ts @@ -41,6 +41,7 @@ import { getSodiumRenderer } from '../session/crypto'; import { encryptProfile } from '../util/crypto/profileEncrypter'; import { uploadFileToFsWithOnionV4 } from '../session/apis/file_server_api/FileServerApi'; import { DisappearingMessageConversationType } from '../util/expiringMessages'; +import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; export const getCompleteUrlForV2ConvoId = async (convoId: string) => { if (convoId.match(openGroupV2ConversationIdRegex)) { @@ -356,10 +357,19 @@ export async function setDisappearingMessagesByConvoId( return; } + const providedChangeTimestamp = getNowWithNetworkOffset(); + if (!expirationType || expirationType === 'off' || !seconds || seconds <= 0) { - await conversation.updateExpireTimer('off'); + await conversation.updateExpireTimer({ + providedExpirationType: 'off', + providedChangeTimestamp, + }); } else { - await conversation.updateExpireTimer(expirationType, seconds); + await conversation.updateExpireTimer({ + providedExpirationType: expirationType, + providedExpireTimer: seconds, + providedChangeTimestamp, + }); } } diff --git a/ts/models/conversation.ts b/ts/models/conversation.ts index 55651cb36..a64c95f37 100644 --- a/ts/models/conversation.ts +++ b/ts/models/conversation.ts @@ -78,7 +78,6 @@ import { ConversationAttributes, ConversationNotificationSetting, ConversationTypeEnum, - DisappearingMessageConversationType, fillConvoAttributesWithDefaults, } from './conversationAttributes'; import { SogsBlinding } from '../session/apis/open_group_api/sogsv3/sogsBlinding'; @@ -97,6 +96,7 @@ import { import { sogsV3FetchPreviewAndSaveIt } from '../session/apis/open_group_api/sogsv3/sogsV3FetchFile'; import { Reaction } from '../types/Reaction'; import { Reactions } from '../util/reactions'; +import { DisappearingMessageConversationType } from '../util/expiringMessages'; export class ConversationModel extends Backbone.Model { public updateLastMessage: () => any; @@ -1016,31 +1016,34 @@ export class ConversationModel extends Backbone.Model { } } - public async updateExpireTimer( - providedExpirationType: DisappearingMessageConversationType, - providedExpireTimer?: number, - providedSource?: string, - receivedAt?: number, // is set if it comes from outside - options: { - fromSync?: boolean; - } = {}, - shouldCommit = true - ): Promise { + public async updateExpireTimer({ + providedExpirationType, + providedExpireTimer, + providedChangeTimestamp, + providedSource, + receivedAt, // is set if it comes from outside + fromSync, + shouldCommit = true, + }: { + providedExpirationType: DisappearingMessageConversationType; + providedExpireTimer?: number; + providedChangeTimestamp?: number; + providedSource?: string; + receivedAt?: number; // is set if it comes from outside + fromSync?: boolean; + shouldCommit?: boolean; + }): Promise { let expirationType = providedExpirationType; let expireTimer = providedExpireTimer; let source = providedSource; - defaults(options, { fromSync: false }); + defaults({ fromSync }, { fromSync: false }); - if (!expirationType) { + if (!expirationType || !expireTimer) { expirationType = 'off'; expireTimer = 0; } - if (!expireTimer) { - expireTimer = 0; - } - if (this.get('expireTimer') === expireTimer || (!expireTimer && !this.get('expireTimer'))) { return; } @@ -1057,21 +1060,29 @@ export class ConversationModel extends Backbone.Model { source = source || UserUtils.getOurPubKeyStrFromCache(); // When we add a disappearing messages notification to the conversation, we want it - // to be above the message that initiated that change, hence the subtraction. - // TODO Will we use this for lastDisappearingMessageChangeTimestamp + // to be above the message that initiated that change, hence the subtraction. const timestamp = (receivedAt || Date.now()) - 1; - this.set({ expirationType, expireTimer }); + this.set({ + expirationType, + expireTimer, + lastDisappearingMessageChangeTimestamp: providedChangeTimestamp || undefined, + }); + + const lastDisappearingMessageChangeTimestamp = providedChangeTimestamp || 0; - // TODO Update for the new types of Disappearing Messages const commonAttributes = { flags: SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, expirationTimerUpdate: { + expirationType, expireTimer, + lastDisappearingMessageChangeTimestamp, source, - fromSync: options.fromSync, + fromSync, }, - expireTimer: 0, + // TODO do we need this? + // expirationType, + // expireTimer, }; let message: MessageModel | undefined; @@ -1082,6 +1093,7 @@ export class ConversationModel extends Backbone.Model { sent_at: timestamp, }); } else { + // TODO do we still want to handle expiration in incoming messages? message = await this.addSingleIncomingMessage({ ...commonAttributes, // Even though this isn't reflected to the user, we want to place the last seen @@ -1106,23 +1118,28 @@ export class ConversationModel extends Backbone.Model { return; } - // TODO Update for the new types of Disappearing Messages const expireUpdate = { identifier: message.id, timestamp, - expireTimer: expireTimer ? expireTimer : (null as number | null), + expirationType, + expireTimer, + lastDisappearingMessageChangeTimestamp, }; if (this.isMe()) { + // TODO Check that the args are correct const expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdate); return message.sendSyncMessageOnly(expirationTimerMessage); } if (this.isPrivate()) { + // TODO Check that the args are correct const expirationTimerMessage = new ExpirationTimerUpdateMessage(expireUpdate); const pubkey = new PubKey(this.get('id')); await getMessageQueue().sendToPubKey(pubkey, expirationTimerMessage); } else { + // TODO Check that the args are correct + // Cannot be an open group window?.log?.warn('TODO: Expiration update for closed groups are to be updated'); const expireUpdateForGroup = { ...expireUpdate, diff --git a/ts/models/message.ts b/ts/models/message.ts index e14f6d2da..61f87e24d 100644 --- a/ts/models/message.ts +++ b/ts/models/message.ts @@ -1028,20 +1028,25 @@ export class MessageModel extends Backbone.Model { this.set({ sent_to: [UserUtils.getOurPubKeyStrFromCache()], sent: true, + // NOTE if disappearing message is deleteAfterRead then we don't use this expirationStartTimestamp: now, }); await this.commit(); - const data = dataMessage instanceof DataMessage ? dataMessage.dataProto() : dataMessage; - await this.sendSyncMessage(data, now); + await this.sendSyncMessage(dataMessage, now); } - public async sendSyncMessage(dataMessage: SignalService.DataMessage, sentTimestamp: number) { + public async sendSyncMessage( + data: DataMessage | SignalService.DataMessage, + sentTimestamp: number + ) { if (this.get('synced') || this.get('sentSync')) { return; } + const dataMessage = data instanceof DataMessage ? data.dataProto() : data; + // if this message needs to be synced if ( dataMessage.body?.length || @@ -1052,10 +1057,12 @@ export class MessageModel extends Backbone.Model { if (!conversation) { throw new Error('Cannot trigger syncMessage with unknown convo.'); } - const syncMessage = buildSyncMessage(this.id, dataMessage, conversation.id, sentTimestamp); + const syncMessage = buildSyncMessage(this.id, data, conversation.id, sentTimestamp); await getMessageQueue().sendSyncMessage(syncMessage); } - this.set({ sentSync: true }); + this.set({ + sentSync: true, + }); await this.commit(); } diff --git a/ts/models/messageFactory.ts b/ts/models/messageFactory.ts index 5a68fdd29..6b8469b25 100644 --- a/ts/models/messageFactory.ts +++ b/ts/models/messageFactory.ts @@ -35,6 +35,7 @@ export function createSwarmMessageSentFromUs(args: { const messageData: MessageAttributesOptionals = { ...getSharedAttributesForSwarmMessage(args), ...getSharedAttributesForOutgoingMessage(), + // TODO need to update this for delete after read expirationStartTimestamp: Math.min(args.sentAt, Date.now()), }; diff --git a/ts/models/messageType.ts b/ts/models/messageType.ts index 5ad00791a..760f5744f 100644 --- a/ts/models/messageType.ts +++ b/ts/models/messageType.ts @@ -21,11 +21,12 @@ export interface MessageAttributes { reacts?: ReactionList; reactsIndex?: number; body?: string; + // NOTE this is used for the logic expirationType?: DisappearingMessageType; expireTimer: number; expirationStartTimestamp: number; expires_at?: number; - // TODO are having both variables redundant? + // NOTE this is used for conversation setting expirationTimerUpdate?: { expirationType: DisappearingMessageType; expireTimer: number; diff --git a/ts/receiver/closedGroups.ts b/ts/receiver/closedGroups.ts index 9c53dd3aa..ac2b2be08 100644 --- a/ts/receiver/closedGroups.ts +++ b/ts/receiver/closedGroups.ts @@ -32,6 +32,7 @@ import { MessageModel } from '../models/message'; import { updateConfirmModal } from '../state/ducks/modalDialog'; import { perfEnd, perfStart } from '../session/utils/Performance'; import { ConversationTypeEnum } from '../models/conversationAttributes'; +import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; export const distributingClosedGroupEncryptionKeyPairs = new Map(); @@ -240,7 +241,8 @@ export async function handleNewClosedGroup( return; } const groupConvo = getConversationController().get(groupId); - const expireTimer = groupUpdate.expireTimer; + // TODO Rename to expirationTimer + const expireTimer = groupUpdate.expirationTimer; if (groupConvo) { // if we did not left this group, just add the keypair we got if not already there @@ -256,12 +258,13 @@ export async function handleNewClosedGroup( ); // TODO This is only applicable for old closed groups - will be removed in future - await groupConvo.updateExpireTimer( - expireTimer === 0 ? 'off' : 'deleteAfterSend', - expireTimer, - sender, - Date.now() - ); + await groupConvo.updateExpireTimer({ + providedExpirationType: expireTimer === 0 ? 'off' : 'deleteAfterSend', + providedExpireTimer: expireTimer, + providedChangeTimestamp: getNowWithNetworkOffset(), + providedSource: sender, + receivedAt: Date.now(), + }); if (isKeyPairAlreadyHere) { window.log.info('Dropping already saved keypair for group', groupId); @@ -322,12 +325,13 @@ export async function handleNewClosedGroup( // envelope.timestamp and Date.now(). And we need to listen to those (some might even remove us) convo.set('lastJoinedTimestamp', envelopeTimestamp); // TODO This is only applicable for old closed groups - will be removed in future - await convo.updateExpireTimer( - expireTimer === 0 ? 'off' : 'deleteAfterSend', - expireTimer, - sender, - envelopeTimestamp - ); + await convo.updateExpireTimer({ + providedExpirationType: expireTimer === 0 ? 'off' : 'deleteAfterSend', + providedExpireTimer: expireTimer, + providedChangeTimestamp: getNowWithNetworkOffset(), + providedSource: sender, + receivedAt: envelopeTimestamp, + }); convo.updateLastMessage(); await convo.commit(); diff --git a/ts/receiver/contentMessage.ts b/ts/receiver/contentMessage.ts index aa8dc813a..eea71b739 100644 --- a/ts/receiver/contentMessage.ts +++ b/ts/receiver/contentMessage.ts @@ -398,13 +398,25 @@ export async function innerHandleSwarmContentMessage( if (content.dataMessage.profileKey && content.dataMessage.profileKey.length === 0) { content.dataMessage.profileKey = null; } + perfStart(`handleSwarmDataMessage-${envelope.id}`); + + let expireUpdate = null; + + if (content.expirationType && content.expirationTimer) { + expireUpdate = { + expirationType: content.expirationType, + expirationTimer: content.expirationTimer, + }; + } + await handleSwarmDataMessage( envelope, sentAtTimestamp, content.dataMessage as SignalService.DataMessage, messageHash, - senderConversationModel + senderConversationModel, + expireUpdate ); perfEnd(`handleSwarmDataMessage-${envelope.id}`, 'handleSwarmDataMessage'); return; diff --git a/ts/receiver/dataMessage.ts b/ts/receiver/dataMessage.ts index ebd86af01..496df44b6 100644 --- a/ts/receiver/dataMessage.ts +++ b/ts/receiver/dataMessage.ts @@ -152,7 +152,8 @@ export async function handleSwarmDataMessage( sentAtTimestamp: number, rawDataMessage: SignalService.DataMessage, messageHash: string, - senderConversationModel: ConversationModel + senderConversationModel: ConversationModel, + expireUpdate: any ): Promise { window.log.info('handleSwarmDataMessage'); @@ -246,7 +247,8 @@ export async function handleSwarmDataMessage( sentAtTimestamp, cleanDataMessage, convoToAddMessageTo, - () => removeFromCache(envelope) + () => removeFromCache(envelope), + expireUpdate ); } @@ -293,7 +295,8 @@ async function handleSwarmMessage( sentAt: number, rawDataMessage: SignalService.DataMessage, convoToAddMessageTo: ConversationModel, - confirm: () => void + confirm: () => void, + expireUpdate?: any ): Promise { if (!rawDataMessage || !msgModel) { window?.log?.warn('Invalid data passed to handleSwarmMessage.'); @@ -310,6 +313,7 @@ async function handleSwarmMessage( sender: msgModel.get('source'), you: isUsFromCache(msgModel.get('source')), }); + if ( convoToAddMessageTo.isPrivate() && msgModel.get('unread') && @@ -322,6 +326,7 @@ async function handleSwarmMessage( confirm(); return; } + const isDuplicate = await isSwarmMessageDuplicate({ source: msgModel.get('source'), sentAt, @@ -339,7 +344,8 @@ async function handleSwarmMessage( toRegularMessage(rawDataMessage), confirm, msgModel.get('source'), - messageHash + messageHash, + expireUpdate ); }); } diff --git a/ts/receiver/queuedJob.ts b/ts/receiver/queuedJob.ts index 9bf131ed6..2e6b84aba 100644 --- a/ts/receiver/queuedJob.ts +++ b/ts/receiver/queuedJob.ts @@ -1,7 +1,7 @@ import { queueAttachmentDownloads } from './attachments'; import { Quote } from './types'; -import _ from 'lodash'; +import _, { isEmpty } from 'lodash'; import { getConversationController } from '../session/conversations'; import { ConversationModel } from '../models/conversation'; import { MessageModel, sliceQuoteText } from '../models/message'; @@ -14,11 +14,10 @@ import { MessageDirection } from '../models/messageType'; import { LinkPreviews } from '../util/linkPreviews'; import { GoogleChrome } from '../util'; import { appendFetchAvatarAndProfileJob } from './userProfileImageUpdates'; -import { - ConversationTypeEnum, - DisappearingMessageConversationType, -} from '../models/conversationAttributes'; +import { ConversationTypeEnum } from '../models/conversationAttributes'; import { getUsBlindedInThatServer } from '../session/apis/open_group_api/sogsv3/knownBlindedkeys'; +import { DisappearingMessageConversationType } from '../util/expiringMessages'; +import { getNowWithNetworkOffset } from '../session/apis/snode_api/SNodeAPI'; function contentTypeSupported(type: string): boolean { const Chrome = GoogleChrome; @@ -185,7 +184,7 @@ export type RegularMessageType = Pick< | 'reaction' | 'profile' | 'profileKey' - // TODO Add expirationType and other new props + // TODO Will be removed 2 weeks after release | 'expireTimer' > & { isRegularMessage: true }; @@ -317,23 +316,26 @@ async function handleExpirationTimerUpdateNoCommit( expirationType: DisappearingMessageConversationType, expireTimer: number ) { + const providedChangeTimestamp = getNowWithNetworkOffset(); + message.set({ expirationTimerUpdate: { source, + expirationType: expirationType !== 'off' ? expirationType : null, expireTimer, + lastDisappearingMessageChangeTimestamp: providedChangeTimestamp, }, unread: 0, // mark the message as read. }); - conversation.set({ expirationType, expireTimer }); - await conversation.updateExpireTimer( - expirationType, - expireTimer, - source, - message.get('received_at'), - {}, - false - ); + await conversation.updateExpireTimer({ + providedExpirationType: expirationType, + providedExpireTimer: expireTimer, + providedChangeTimestamp, + providedSource: source, + receivedAt: message.get('received_at'), + shouldCommit: false, + }); } export async function handleMessageJob( @@ -342,7 +344,8 @@ export async function handleMessageJob( regularDataMessage: RegularMessageType, confirm: () => void, source: string, - messageHash: string + messageHash: string, + expireUpdate?: any ) { window?.log?.info( `Starting handleMessageJob for message ${messageModel.idForLogging()}, ${messageModel.get( @@ -356,11 +359,13 @@ export async function handleMessageJob( ); try { messageModel.set({ flags: regularDataMessage.flags }); - // TODO update to handle the new disappearing message props - if (messageModel.isExpirationTimerUpdate()) { - // TODO Account for expirationType and lastDisappearingMessageChangeTimestamp - const { expireTimer } = regularDataMessage; + // TODO remove 2 weeks after release + if (messageModel.isExpirationTimerUpdate() || !isEmpty(expireUpdate)) { + const { expireTimer: oldExpireTimer } = regularDataMessage; + const expirationType = expireUpdate.expirationType; + const expireTimer = expireUpdate.expireTimer || oldExpireTimer; + // TODO compare types and change timestamps // const oldTypeValue = conversation.get('expirationType'); const oldTimerValue = conversation.get('expireTimer'); if (expireTimer === oldTimerValue) { @@ -370,11 +375,12 @@ export async function handleMessageJob( ); return; } + await handleExpirationTimerUpdateNoCommit( conversation, messageModel, source, - 'deleteAfterSend', + expirationType, expireTimer ); } else { diff --git a/ts/session/group/closed-group.ts b/ts/session/group/closed-group.ts index e43e6a5c7..97818dacc 100644 --- a/ts/session/group/closed-group.ts +++ b/ts/session/group/closed-group.ts @@ -28,11 +28,8 @@ import { ClosedGroupNewMessage } from '../messages/outgoing/controlMessage/group import { ClosedGroupRemovedMembersMessage } from '../messages/outgoing/controlMessage/group/ClosedGroupRemovedMembersMessage'; import { getSwarmPollingInstance } from '../apis/snode_api'; import { getNowWithNetworkOffset } from '../apis/snode_api/SNodeAPI'; -import { - ConversationAttributes, - ConversationTypeEnum, - DisappearingMessageConversationType, -} from '../../models/conversationAttributes'; +import { ConversationAttributes, ConversationTypeEnum } from '../../models/conversationAttributes'; +import { DisappearingMessageConversationType } from '../../util/expiringMessages'; export type GroupInfo = { id: string; @@ -270,16 +267,16 @@ export async function updateOrCreateClosedGroup(details: GroupInfo) { if (expireTimer === undefined || typeof expireTimer !== 'number') { return; } - // Todo Update here - await conversation.updateExpireTimer( - expirationType || 'deleteAfterSend', - expireTimer, - UserUtils.getOurPubKeyStrFromCache(), - Date.now(), - { - fromSync: true, - } - ); + + await conversation.updateExpireTimer({ + // TODO clean up 2 weeks after release + providedExpirationType: expirationType || 'deleteAfterSend', + providedExpireTimer: expireTimer, + providedChangeTimestamp: getNowWithNetworkOffset(), + providedSource: UserUtils.getOurPubKeyStrFromCache(), + receivedAt: Date.now(), + fromSync: true, + }); } export async function leaveClosedGroup(groupId: string) { diff --git a/ts/session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage.ts b/ts/session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage.ts index 64e0c8d1b..1cbfd0656 100644 --- a/ts/session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/ExpirationTimerUpdateMessage.ts @@ -1,5 +1,6 @@ import { DataMessage } from '..'; import { SignalService } from '../../../../protobuf'; +import { DisappearingMessageType } from '../../../../util/expiringMessages'; import { PubKey } from '../../../types'; import { StringUtils } from '../../../utils'; import { MessageParams } from '../Message'; @@ -7,23 +8,44 @@ import { MessageParams } from '../Message'; interface ExpirationTimerUpdateMessageParams extends MessageParams { groupId?: string | PubKey; syncTarget?: string | PubKey; + expirationType: DisappearingMessageType | null; expireTimer: number | null; + lastDisappearingMessageChangeTimestamp: number | null; } +// Note the old disappearing messages used a data message for the expiration time. +// The new ones use properties on the Content Message +// We will remove support for the old one 2 weeks after the release export class ExpirationTimerUpdateMessage extends DataMessage { public readonly groupId?: PubKey; public readonly syncTarget?: string; + public readonly expirationType: DisappearingMessageType | null; public readonly expireTimer: number | null; + public readonly lastDisappearingMessageChangeTimestamp: number | null; constructor(params: ExpirationTimerUpdateMessageParams) { super({ timestamp: params.timestamp, identifier: params.identifier }); + this.expirationType = params.expirationType; this.expireTimer = params.expireTimer; + this.lastDisappearingMessageChangeTimestamp = params.lastDisappearingMessageChangeTimestamp; const { groupId, syncTarget } = params; this.groupId = groupId ? PubKey.cast(groupId) : undefined; this.syncTarget = syncTarget ? PubKey.cast(syncTarget).key : undefined; } + public contentProto(): SignalService.Content { + return new SignalService.Content({ + dataMessage: this.dataProto(), + expirationType: + this.expirationType === 'deleteAfterSend' + ? SignalService.Content.ExpirationType.DELETE_AFTER_SEND + : SignalService.Content.ExpirationType.DELETE_AFTER_READ, + expirationTimer: this.expireTimer, + lastDisappearingMessageChangeTimestamp: this.lastDisappearingMessageChangeTimestamp, + }); + } + public dataProto(): SignalService.DataMessage { const data = new SignalService.DataMessage(); @@ -46,6 +68,7 @@ export class ExpirationTimerUpdateMessage extends DataMessage { data.syncTarget = this.syncTarget; } + // TODO remove 2 weeks after the release if (this.expireTimer) { data.expireTimer = this.expireTimer; } diff --git a/ts/session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage.ts b/ts/session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage.ts index 9c73ba086..b9b9e2eb2 100644 --- a/ts/session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage.ts +++ b/ts/session/messages/outgoing/controlMessage/group/ClosedGroupNewMessage.ts @@ -63,7 +63,7 @@ export class ClosedGroupNewMessage extends ClosedGroupMessage { dataMessage.closedGroupControlMessage.admins = this.admins.map(fromHexToArray); dataMessage.closedGroupControlMessage.members = this.members.map(fromHexToArray); - dataMessage.closedGroupControlMessage.expireTimer = this.expireTimer; + dataMessage.closedGroupControlMessage.expirationTimer = this.expireTimer; try { dataMessage.closedGroupControlMessage.encryptionKeyPair = new SignalService.KeyPair(); dataMessage.closedGroupControlMessage.encryptionKeyPair.privateKey = new Uint8Array( diff --git a/ts/session/utils/syncUtils.ts b/ts/session/utils/syncUtils.ts index cb41ff728..80e01c90b 100644 --- a/ts/session/utils/syncUtils.ts +++ b/ts/session/utils/syncUtils.ts @@ -26,6 +26,8 @@ import { DURATION } from '../constants'; import { UnsendMessage } from '../messages/outgoing/controlMessage/UnsendMessage'; import { MessageRequestResponse } from '../messages/outgoing/controlMessage/MessageRequestResponse'; import { PubKey } from '../types'; +import { DataMessage } from '../messages/outgoing'; +import { DisappearingMessageType } from '../../util/expiringMessages'; const ITEM_ID_LAST_SYNC_TIMESTAMP = 'lastSyncedTimestamp'; @@ -294,16 +296,18 @@ const buildSyncVisibleMessage = ( const buildSyncExpireTimerMessage = ( identifier: string, - dataMessage: SignalService.DataMessage, + expirationType: DisappearingMessageType, + expireTimer: number, + lastDisappearingMessageChangeTimestamp: number | null, timestamp: number, syncTarget: string ) => { - const expireTimer = dataMessage.expireTimer; - return new ExpirationTimerUpdateMessage({ identifier, timestamp, + expirationType: expirationType || null, expireTimer, + lastDisappearingMessageChangeTimestamp: lastDisappearingMessageChangeTimestamp || null, syncTarget, }); }; @@ -317,24 +321,48 @@ export type SyncMessageType = export const buildSyncMessage = ( identifier: string, - dataMessage: SignalService.DataMessage, + data: DataMessage | SignalService.DataMessage, syncTarget: string, sentTimestamp: number ): VisibleMessage | ExpirationTimerUpdateMessage => { if ( - (dataMessage as any).constructor.name !== 'DataMessage' && - !(dataMessage instanceof SignalService.DataMessage) + (data as any).constructor.name !== 'DataMessage' && + !(data instanceof SignalService.DataMessage) ) { window?.log?.warn('buildSyncMessage with something else than a DataMessage'); } + // TODO Remove DataMessage expireTimer 2 weeks after the release + const dataMessage = data instanceof DataMessage ? data.dataProto() : data; + const contentMessage = data instanceof DataMessage ? data.contentProto() : null; + const expirationType = + contentMessage?.expirationType === SignalService.Content.ExpirationType.DELETE_AFTER_SEND + ? 'deleteAfterSend' + : contentMessage?.expirationType === SignalService.Content.ExpirationType.DELETE_AFTER_READ + ? 'deleteAfterRead' + : null; + const expireTimer = contentMessage?.expirationTimer || dataMessage.expireTimer; + const lastDisappearingMessageChangeTimestamp = contentMessage?.lastDisappearingMessageChangeTimestamp + ? Number(contentMessage?.lastDisappearingMessageChangeTimestamp) + : null; + if (!sentTimestamp || !_.isNumber(sentTimestamp)) { throw new Error('Tried to build a sync message without a sentTimestamp'); } // don't include our profileKey on syncing message. This is to be done by a ConfigurationMessage now const timestamp = _.toNumber(sentTimestamp); - if (dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE) { - return buildSyncExpireTimerMessage(identifier, dataMessage, timestamp, syncTarget); + if ( + contentMessage?.expirationType && + dataMessage.flags === SignalService.DataMessage.Flags.EXPIRATION_TIMER_UPDATE + ) { + return buildSyncExpireTimerMessage( + identifier, + expirationType, + expireTimer, + lastDisappearingMessageChangeTimestamp, + timestamp, + syncTarget + ); } return buildSyncVisibleMessage(identifier, dataMessage, timestamp, syncTarget); };