// Copyright 2024 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { v4 as generateGuid } from 'uuid'; import Long from 'long'; import type { ConversationModel } from '../../models/conversations'; import { getRandomBytes } from '../../Crypto'; import * as Bytes from '../../Bytes'; import { SignalService as Proto, Backups } from '../../protobuf'; import { DataWriter } from '../../sql/Client'; import { generateAci } from '../../types/ServiceId'; import { PaymentEventKind } from '../../types/Payment'; import { ContactFormType } from '../../types/EmbeddedContact'; import { DurationInSeconds } from '../../util/durations'; import { ReadStatus } from '../../messages/MessageReadStatus'; import { SeenStatus } from '../../MessageSeenStatus'; import { loadCallsHistory } from '../../services/callHistoryLoader'; import { setupBasics, asymmetricRoundtripHarness, symmetricRoundtripHarness, OUR_ACI, } from './helpers'; const CONTACT_A = generateAci(); const GROUP_ID = Bytes.toBase64(getRandomBytes(32)); describe('backup/non-bubble messages', () => { let contactA: ConversationModel; let group: ConversationModel; beforeEach(async () => { await DataWriter._removeAllMessages(); await DataWriter._removeAllConversations(); window.storage.reset(); await setupBasics(); contactA = await window.ConversationController.getOrCreateAndWait( CONTACT_A, 'private', { systemGivenName: 'CONTACT_A' } ); group = await window.ConversationController.getOrCreateAndWait( GROUP_ID, 'group', { groupVersion: 2, masterKey: Bytes.toBase64(getRandomBytes(32)), name: 'Rock Enthusiasts', } ); await loadCallsHistory(); }); it('roundtrips END_SESSION simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, flags: Proto.DataMessage.Flags.END_SESSION, }, ]); }); it('roundtrips CHAT_SESSION_REFRESH simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'chat-session-refreshed', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: OUR_ACI, }, ]); }); it('roundtrips IDENTITY_CHANGE update in direct convos', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'keychange', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips IDENTITY_CHANGE update in groups', async () => { await symmetricRoundtripHarness([ { conversationId: group.id, id: generateGuid(), type: 'keychange', key_changed: contactA.id, received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips IDENTITY_DEFAULT simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'verified-change', verifiedChanged: contactA.id, verified: false, received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips IDENTITY_VERIFIED simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'verified-change', verifiedChanged: contactA.id, verified: true, received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips CHANGE_NUMBER simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'change-number-notification', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips JOINED_SIGNAL simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'joined-signal-notification', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips BAD_DECRYPT simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'delivery-issue', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, }, ]); }); it('roundtrips PAYMENTS_ACTIVATED simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', sourceServiceId: CONTACT_A, payment: { kind: PaymentEventKind.Activation, }, received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, }, ]); }); it('roundtrips PAYMENT_ACTIVATION_REQUEST simple update', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', sourceServiceId: CONTACT_A, payment: { kind: PaymentEventKind.ActivationRequest, }, received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, }, ]); }); it('roundtrips bare payments notification', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, payment: { kind: PaymentEventKind.Notification, note: 'note with text', }, }, ]); }); it('roundtrips full payments notification', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, payment: { kind: PaymentEventKind.Notification, note: 'note with text', amountMob: '1.01', feeMob: '0.01', transactionDetailsBase64: Bytes.toBase64( Backups.PaymentNotification.TransactionDetails.encode({ transaction: { timestamp: Long.fromNumber(Date.now()), }, }).finish() ), }, }, ]); }); it('roundtrips embedded contact', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, contact: [ { name: { givenName: 'Alice', familyName: 'Smith', }, number: [ { type: ContactFormType.MOBILE, value: '+121255501234', }, ], organization: 'Signal', }, ], reactions: [ { emoji: '👍', fromId: contactA.id, targetTimestamp: 1, timestamp: 1, receivedAtDate: 1, }, ], }, ]); }); it('roundtrips sticker', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, // TODO (DESKTOP-6845): properly handle data FilePointer sticker: { emoji: '👍', packId: Bytes.toHex(getRandomBytes(16)), stickerId: 1, packKey: Bytes.toBase64(getRandomBytes(32)), }, reactions: [ { emoji: '👍', fromId: contactA.id, targetTimestamp: 1, timestamp: 1, receivedAtDate: 1, }, ], }, ]); }); it('roundtrips remote deleted message', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, isErased: true, }, ]); }); it('roundtrips timer notification in direct convos', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'timer-notification', received_at: 1, sent_at: 1, timestamp: 1, flags: Proto.DataMessage.Flags.EXPIRATION_TIMER_UPDATE, sourceServiceId: CONTACT_A, sourceDevice: 1, expirationTimerUpdate: { expireTimer: DurationInSeconds.fromMillis(5000), sourceServiceId: CONTACT_A, }, }, ]); }); it('roundtrips profile change notification', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'profile-change', received_at: 1, sent_at: 1, timestamp: 1, changedId: contactA.id, sourceServiceId: CONTACT_A, profileChange: { type: 'name', oldName: 'Old Name', newName: 'New Name', }, }, ]); }); it('roundtrips title transition notification', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'title-transition-notification', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, titleTransition: { renderInfo: { type: 'private', e164: '+12125551234', }, }, }, ]); }); it('roundtrips thread merge', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'conversation-merge', sourceServiceId: CONTACT_A, received_at: 1, sent_at: 1, timestamp: 1, conversationMerge: { renderInfo: { type: 'private', e164: '+12125551234', }, }, }, ]); }); it('roundtrips session switchover', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'phone-number-discovery', received_at: 1, sent_at: 1, timestamp: 1, sourceServiceId: CONTACT_A, phoneNumberDiscovery: { e164: '+12125551234', }, }, ]); }); it('roundtrips unsupported message', async () => { await symmetricRoundtripHarness([ { conversationId: contactA.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, sent_at: 1, timestamp: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, unidentifiedDeliveryReceived: true, supportedVersionAtReceive: 5, requiredProtocolVersion: 6, }, ]); }); it('creates a tombstone for gv1 update in gv2 group', async () => { await asymmetricRoundtripHarness( [ { conversationId: group.id, id: generateGuid(), type: 'incoming', received_at: 1, received_at_ms: 1, sourceServiceId: CONTACT_A, sourceDevice: 1, sent_at: 1, timestamp: 1, readStatus: ReadStatus.Unread, seenStatus: SeenStatus.Unseen, group_update: {}, }, ], [ { conversationId: group.id, id: 'does not matter', type: 'group-v2-change', groupV2Change: { details: [{ type: 'summary' }], from: CONTACT_A, }, received_at: 1, sent_at: 1, sourceServiceId: CONTACT_A, timestamp: 1, }, ] ); }); });