From b94bf9d88f457962bfc46114577e8c8693604a62 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:41:56 -0500 Subject: [PATCH] Fix handling CallLogEvent sync for call link targets Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com> --- ts/sql/Server.ts | 134 ++++++++++++++++++++++------------ ts/types/CallDisposition.ts | 49 ++++++++----- ts/util/callDisposition.ts | 15 ++-- ts/util/onCallLogEventSync.ts | 9 ++- 4 files changed, 136 insertions(+), 71 deletions(-) diff --git a/ts/sql/Server.ts b/ts/sql/Server.ts index 2db8b52d028a..c2ea9337c35c 100644 --- a/ts/sql/Server.ts +++ b/ts/sql/Server.ts @@ -3568,7 +3568,7 @@ function clearCallHistory( return db.transaction(() => { const callHistory = getCallHistoryForCallLogEventTarget(db, target); if (callHistory == null) { - logger.error('clearCallHistory: Target call not found'); + logger.warn('clearCallHistory: Target call not found'); return []; } const { timestamp } = callHistory; @@ -3750,44 +3750,76 @@ function getCallHistoryForCallLogEventTarget( db: ReadableDB, target: CallLogEventTarget ): CallHistoryDetails | null { - const { callId, peerId, timestamp } = target; + const { callId, timestamp } = target; - let row: unknown; + if ('peerId' in target) { + const { peerId } = target; - if (callId == null || peerId == null) { - const predicate = - peerId != null - ? sqlFragment`callsHistory.peerId IS ${target.peerId}` - : sqlFragment`TRUE`; + let row: unknown; - // Get the most recent call history timestamp for the target.timestamp - const [selectQuery, selectParams] = sql` - SELECT * - FROM callsHistory - WHERE ${predicate} - AND callsHistory.timestamp <= ${timestamp} - ORDER BY callsHistory.timestamp DESC - LIMIT 1 - `; + if (callId == null || peerId == null) { + const predicate = + peerId != null + ? sqlFragment`callsHistory.peerId IS ${target.peerId}` + : sqlFragment`TRUE`; - row = db.prepare(selectQuery).get(selectParams); - } else { - const [selectQuery, selectParams] = sql` - SELECT * - FROM callsHistory - WHERE callsHistory.peerId IS ${target.peerId} - AND callsHistory.callId IS ${target.callId} - LIMIT 1 - `; + // Get the most recent call history timestamp for the target.timestamp + const [selectQuery, selectParams] = sql` + SELECT * + FROM callsHistory + WHERE ${predicate} + AND callsHistory.timestamp <= ${timestamp} + ORDER BY callsHistory.timestamp DESC + LIMIT 1 + `; - row = db.prepare(selectQuery).get(selectParams); + row = db.prepare(selectQuery).get(selectParams); + } else { + const [selectQuery, selectParams] = sql` + SELECT * + FROM callsHistory + WHERE callsHistory.peerId IS ${target.peerId} + AND callsHistory.callId IS ${target.callId} + LIMIT 1 + `; + + row = db.prepare(selectQuery).get(selectParams); + } + + if (row == null) { + return null; + } + + return parseUnknown(callHistoryDetailsSchema, row as unknown); } - if (row == null) { + // For incoming CallLogEvent sync messages, peerId is ambiguous whether it + // refers to conversation or call link. + if ('peerIdAsConversationId' in target && 'peerIdAsRoomId' in target) { + const resultForConversation = getCallHistoryForCallLogEventTarget(db, { + callId, + timestamp, + peerId: target.peerIdAsConversationId, + }); + if (resultForConversation) { + return resultForConversation; + } + + const resultForCallLink = getCallHistoryForCallLogEventTarget(db, { + callId, + timestamp, + peerId: target.peerIdAsRoomId, + }); + if (resultForCallLink) { + return resultForCallLink; + } + return null; } - return parseUnknown(callHistoryDetailsSchema, row as unknown); + throw new Error( + 'Either peerId, or peerIdAsConversationId and peerIdAsRoomId must be present' + ); } function getConversationIdForCallHistory( @@ -3852,10 +3884,6 @@ export function markAllCallHistoryRead( target: CallLogEventTarget, inConversation = false ): number { - if (inConversation) { - strictAssert(target.peerId, 'peerId is required'); - } - return db.transaction(() => { const callHistory = getCallHistoryForCallLogEventTarget(db, target); if (callHistory == null) { @@ -3870,28 +3898,45 @@ export function markAllCallHistoryRead( 'Call ID must be the same as target if supplied' ); - const conversationId = getConversationIdForCallHistory(db, callHistory); - if (conversationId == null) { - logger.warn('markAllCallHistoryRead: Conversation not found for call'); - return 0; + let predicate: QueryFragment; + let receivedAt: number | null; + if (callHistory.mode === CallMode.Adhoc) { + // If the target is a call link, there's no associated conversation and messages, + // and we can only mark call history read based on timestamp. + strictAssert( + !inConversation, + 'markAllCallHistoryRead: Not possible to mark read in conversation for Adhoc calls' + ); + + receivedAt = callHistory.timestamp; + predicate = sqlFragment`TRUE`; + } else { + const conversationId = getConversationIdForCallHistory(db, callHistory); + if (conversationId == null) { + logger.warn('markAllCallHistoryRead: Conversation not found for call'); + return 0; + } + + logger.info( + `markAllCallHistoryRead: Found conversation ${conversationId}` + ); + receivedAt = getMessageReceivedAtForCall(db, callId, conversationId); + + predicate = inConversation + ? sqlFragment`messages.conversationId IS ${conversationId}` + : sqlFragment`TRUE`; } - logger.info(`markAllCallHistoryRead: Found conversation ${conversationId}`); - const receivedAt = getMessageReceivedAtForCall(db, callId, conversationId); if (receivedAt == null) { logger.warn('markAllCallHistoryRead: Message not found for call'); return 0; } - const predicate = inConversation - ? sqlFragment`messages.conversationId IS ${conversationId}` - : sqlFragment`TRUE`; - const jsonPatch = JSON.stringify({ seenStatus: SeenStatus.Seen, }); - logger.warn( + logger.info( `markAllCallHistoryRead: Marking calls before ${receivedAt} read` ); @@ -3915,7 +3960,6 @@ function markAllCallHistoryReadInConversation( db: WritableDB, target: CallLogEventTarget ): number { - strictAssert(target.peerId, 'peerId is required'); return markAllCallHistoryRead(db, target, true); } diff --git a/ts/types/CallDisposition.ts b/ts/types/CallDisposition.ts index 91332d54c2d4..6f5969d04d22 100644 --- a/ts/types/CallDisposition.ts +++ b/ts/types/CallDisposition.ts @@ -125,16 +125,26 @@ export type CallDetails = Readonly<{ endedTimestamp: number | null; }>; -export type CallLogEventTarget = Readonly<{ - timestamp: number; - callId: string | null; - peerId: AciString | string | null; -}>; +export type CallLogEventTarget = Readonly< + { + timestamp: number; + callId: string | null; + } & ( + | { + peerId: AciString | string | null; + } + | { + peerIdAsConversationId: AciString | string | null; + peerIdAsRoomId: string | null; + } + ) +>; export type CallLogEventDetails = Readonly<{ type: CallLogEvent; timestamp: number; - peerId: AciString | string | null; + peerIdAsConversationId: AciString | string | null; + peerIdAsRoomId: string | null; callId: string | null; }>; @@ -243,18 +253,20 @@ export const callHistoryGroupSchema = z.object({ ), }) satisfies z.ZodType; -const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => { - // direct conversationId - if (value.byteLength === UUID_BYTE_SIZE) { - const uuid = bytesToUuid(value); - if (uuid != null) { - return uuid; +const conversationPeerIdInBytesSchema = z + .instanceof(Uint8Array) + .transform(value => { + // direct conversationId + if (value.byteLength === UUID_BYTE_SIZE) { + const uuid = bytesToUuid(value); + if (uuid != null) { + return uuid; + } } - } - // groupId - return Bytes.toBase64(value); -}); + // groupId + return Bytes.toBase64(value); + }); const roomIdInBytesSchema = z .instanceof(Uint8Array) @@ -287,7 +299,7 @@ export const callEventNormalizeSchema = z type: z .nativeEnum(Proto.SyncMessage.CallEvent.Type) .refine(val => val !== Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL), - peerId: peerIdInBytesSchema, + peerId: conversationPeerIdInBytesSchema, }), ]) ); @@ -295,7 +307,8 @@ export const callEventNormalizeSchema = z export const callLogEventNormalizeSchema = z.object({ type: z.nativeEnum(Proto.SyncMessage.CallLogEvent.Type), timestamp: longToNumberSchema, - peerId: peerIdInBytesSchema.optional(), + peerIdAsConversationId: conversationPeerIdInBytesSchema.optional(), + peerIdAsRoomId: roomIdInBytesSchema.optional(), callId: longToStringSchema.optional(), }); diff --git a/ts/util/callDisposition.ts b/ts/util/callDisposition.ts index a80683ecffc0..9de5e86f170f 100644 --- a/ts/util/callDisposition.ts +++ b/ts/util/callDisposition.ts @@ -282,10 +282,14 @@ const callLogEventFromProto: Partial< export function getCallLogEventForProto( callLogEventProto: Proto.SyncMessage.ICallLogEvent ): CallLogEventDetails { - const callLogEvent = parsePartial( - callLogEventNormalizeSchema, - callLogEventProto - ); + // CallLogEvent peerId is ambiguous whether it's a conversationId (direct, or groupId) + // or roomId so handle both cases + const { peerId: peerIdBytes } = callLogEventProto; + const callLogEvent = parsePartial(callLogEventNormalizeSchema, { + ...callLogEventProto, + peerIdAsConversationId: peerIdBytes, + peerIdAsRoomId: peerIdBytes, + }); const type = callLogEventFromProto[callLogEvent.type]; if (type == null) { @@ -295,7 +299,8 @@ export function getCallLogEventForProto( return { type, timestamp: callLogEvent.timestamp, - peerId: callLogEvent.peerId ?? null, + peerIdAsConversationId: callLogEvent.peerIdAsConversationId ?? null, + peerIdAsRoomId: callLogEvent.peerIdAsRoomId ?? null, callId: callLogEvent.callId ?? null, }; } diff --git a/ts/util/onCallLogEventSync.ts b/ts/util/onCallLogEventSync.ts index 54efbb12aa13..0ed730f8c411 100644 --- a/ts/util/onCallLogEventSync.ts +++ b/ts/util/onCallLogEventSync.ts @@ -14,10 +14,12 @@ export async function onCallLogEventSync( syncEvent: CallLogEventSyncEvent ): Promise { const { data, confirm } = syncEvent; - const { type, peerId, callId, timestamp } = data.callLogEventDetails; + const { type, peerIdAsConversationId, peerIdAsRoomId, callId, timestamp } = + data.callLogEventDetails; const target: CallLogEventTarget = { - peerId, + peerIdAsConversationId, + peerIdAsRoomId, callId, timestamp, }; @@ -50,7 +52,8 @@ export async function onCallLogEventSync( } else if (type === CallLogEvent.MarkedAsReadInConversation) { log.info('onCallLogEventSync: Marking call history read in conversation'); try { - strictAssert(peerId, 'Missing peerId'); + strictAssert(peerIdAsConversationId, 'Missing peerIdAsConversationId'); + strictAssert(peerIdAsRoomId, 'Missing peerIdAsRoomId'); const count = await DataWriter.markAllCallHistoryReadInConversation(target); log.info(