From eae9e570fcd4c568535ce8dc90eb6332440a281a Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:31:45 -0700 Subject: [PATCH] Add more timestamp fallbacks for call migration --- ts/sql/migrations/89-call-history.ts | 27 +++++++-- ts/test-node/sql/migration_89_test.ts | 87 +++++++++++++++++++++++++-- 2 files changed, 104 insertions(+), 10 deletions(-) diff --git a/ts/sql/migrations/89-call-history.ts b/ts/sql/migrations/89-call-history.ts index d6be6e459..de7d1f086 100644 --- a/ts/sql/migrations/89-call-history.ts +++ b/ts/sql/migrations/89-call-history.ts @@ -37,7 +37,7 @@ type GroupCallHistoryDetailsType = { callMode: CallMode.Group; creatorUuid: string; eraId: string; - startedTime: number; + startedTime?: number; // Treat this as optional, some calls may be missing it }; export type CallHistoryDetailsType = | DirectCallHistoryDetailsType @@ -113,7 +113,8 @@ function convertLegacyCallDetails( ourUuid: string | undefined, peerId: string, message: MessageType, - partialDetails: CallHistoryDetailsFromDiskType + partialDetails: CallHistoryDetailsFromDiskType, + logger: LoggerType ): CallHistoryDetails { const details = upcastCallHistoryDetailsFromDiskType(partialDetails); const { callMode: mode } = details; @@ -127,6 +128,10 @@ function convertLegacyCallDetails( strictAssert(mode != null, 'mode must exist'); + // If we cannot find any timestamp on the message, we'll use 0 + const fallbackTimestamp = + message.timestamp ?? message.sent_at ?? message.received_at_ms ?? 0; + if (mode === CallMode.Direct) { // We don't have a callId for older calls, generating a uuid instead callId = details.callId ?? generateUuid(); @@ -141,7 +146,7 @@ function convertLegacyCallDetails( ? DirectCallStatus.Declined : DirectCallStatus.Missed; } - timestamp = details.acceptedTime ?? details.endedTime ?? message.timestamp; + timestamp = details.acceptedTime ?? details.endedTime ?? fallbackTimestamp; } else if (mode === CallMode.Group) { callId = Long.fromValue(callIdFromEra(details.eraId)).toString(); type = CallType.Group; @@ -150,7 +155,7 @@ function convertLegacyCallDetails( ? CallDirection.Outgoing : CallDirection.Incoming; status = GroupCallStatus.GenericGroupCall; - timestamp = details.startedTime; + timestamp = details.startedTime ?? fallbackTimestamp; ringerId = details.creatorUuid; } else { throw missingCaseError(mode); @@ -167,7 +172,16 @@ function convertLegacyCallDetails( timestamp, }; - return callHistoryDetailsSchema.parse(callHistory); + const result = callHistoryDetailsSchema.safeParse(callHistory); + if (result.success) { + return result.data; + } + + logger.error( + `convertLegacyCallDetails: Could not convert ${mode} call`, + result.error.toString() + ); + throw new Error(`Failed to convert legacy ${mode} call details`); } export default function updateToSchemaVersion89( @@ -265,7 +279,8 @@ export default function updateToSchemaVersion89( ourUuid, peerId, message, - details + details, + logger ); const [insertQuery, insertParams] = sql` diff --git a/ts/test-node/sql/migration_89_test.ts b/ts/test-node/sql/migration_89_test.ts index b1e518b36..ffb77ec19 100644 --- a/ts/test-node/sql/migration_89_test.ts +++ b/ts/test-node/sql/migration_89_test.ts @@ -41,6 +41,7 @@ describe('SQL/updateToSchemaVersion89', () => { callId: string | null; noCallMode?: boolean; wasDeclined?: boolean; + noTimestamps?: boolean; }): CallHistoryDetailsFromDiskType { return { callId: options.callId ?? undefined, @@ -56,27 +57,42 @@ describe('SQL/updateToSchemaVersion89', () => { function getGroupCallHistoryDetails(options: { eraId: string; noCallMode?: boolean; + noTimestamps?: boolean; }): CallHistoryDetailsFromDiskType { return { eraId: options.eraId, callMode: options.noCallMode ? undefined : CallMode.Group, creatorUuid: generateGuid(), - startedTime: Date.now(), + startedTime: options.noTimestamps ? undefined : Date.now(), }; } + type Timestamps = Pick< + MessageWithCallHistoryDetails, + 'sent_at' | 'received_at_ms' | 'timestamp' + >; + function createCallHistoryMessage(options: { messageId: string; conversationId: string; callHistoryDetails: CallHistoryDetailsFromDiskType; + timestamps?: Partial; }): MessageWithCallHistoryDetails { + // @ts-expect-error Purposefully violating the type to test the migration + const timestamps: Timestamps = options.timestamps + ? options.timestamps + : { + sent_at: Date.now(), + received_at_ms: Date.now(), + timestamp: Date.now(), + }; + const message: MessageWithCallHistoryDetails = { id: options.messageId, type: 'call-history', conversationId: options.conversationId, - sent_at: Date.now() - 10, - received_at: Date.now() - 10, - timestamp: Date.now() - 10, + received_at: Date.now(), + ...timestamps, callHistoryDetails: options.callHistoryDetails, }; @@ -300,6 +316,69 @@ describe('SQL/updateToSchemaVersion89', () => { assert.strictEqual(callHistory[0].peerId, conversation.id); }); + it('migrates call-history messages with no timestamp', () => { + updateToVersion(db, 88); + + const conversation = createConversation('private', Date.now()); + + const timestampCases = { + noTimestamps: { + sent_at: undefined, + received_at_ms: undefined, + timestamp: undefined, + }, + onlyTimestamp: { + sent_at: undefined, + received_at_ms: undefined, + timestamp: 1, + }, + onlySentAt: { + sent_at: 2, + received_at_ms: undefined, + timestamp: undefined, + }, + onlyReceivedAt: { + sent_at: undefined, + received_at_ms: 3, + timestamp: undefined, + }, + } satisfies Record>; + + for (const [id, timestamps] of Object.entries(timestampCases)) { + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation.id, + callHistoryDetails: getDirectCallHistoryDetails({ + callId: id, + noTimestamps: true, + }), + timestamps, + }); + createCallHistoryMessage({ + messageId: generateGuid(), + conversationId: conversation.id, + callHistoryDetails: getGroupCallHistoryDetails({ + eraId: id, + noTimestamps: true, + }), + timestamps, + }); + } + + updateToVersion(db, 89); + + const callHistory = getAllCallHistory(); + assert.strictEqual(callHistory.length, 8); + assert.strictEqual(callHistory[0].timestamp, 0); + assert.strictEqual(callHistory[1].timestamp, 0); + assert.strictEqual(callHistory[2].timestamp, 1); + assert.strictEqual(callHistory[3].timestamp, 1); + assert.strictEqual(callHistory[4].timestamp, 2); + assert.strictEqual(callHistory[5].timestamp, 2); + assert.strictEqual(callHistory[6].timestamp, 3); + assert.strictEqual(callHistory[7].timestamp, 3); + }); + describe('clients with schema version 87', () => { function createCallHistoryTable() { const [query] = sql`