Fix handling CallLogEvent sync for call link targets

Co-authored-by: ayumi-signal <143036029+ayumi-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2024-10-17 18:41:56 -05:00 committed by GitHub
parent 3d1e6cc3c3
commit b94bf9d88f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 136 additions and 71 deletions

View file

@ -3568,7 +3568,7 @@ function clearCallHistory(
return db.transaction(() => { return db.transaction(() => {
const callHistory = getCallHistoryForCallLogEventTarget(db, target); const callHistory = getCallHistoryForCallLogEventTarget(db, target);
if (callHistory == null) { if (callHistory == null) {
logger.error('clearCallHistory: Target call not found'); logger.warn('clearCallHistory: Target call not found');
return []; return [];
} }
const { timestamp } = callHistory; const { timestamp } = callHistory;
@ -3750,7 +3750,10 @@ function getCallHistoryForCallLogEventTarget(
db: ReadableDB, db: ReadableDB,
target: CallLogEventTarget target: CallLogEventTarget
): CallHistoryDetails | null { ): CallHistoryDetails | null {
const { callId, peerId, timestamp } = target; const { callId, timestamp } = target;
if ('peerId' in target) {
const { peerId } = target;
let row: unknown; let row: unknown;
@ -3790,6 +3793,35 @@ function getCallHistoryForCallLogEventTarget(
return parseUnknown(callHistoryDetailsSchema, row as unknown); return parseUnknown(callHistoryDetailsSchema, row as unknown);
} }
// 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;
}
throw new Error(
'Either peerId, or peerIdAsConversationId and peerIdAsRoomId must be present'
);
}
function getConversationIdForCallHistory( function getConversationIdForCallHistory(
db: ReadableDB, db: ReadableDB,
callHistory: CallHistoryDetails callHistory: CallHistoryDetails
@ -3852,10 +3884,6 @@ export function markAllCallHistoryRead(
target: CallLogEventTarget, target: CallLogEventTarget,
inConversation = false inConversation = false
): number { ): number {
if (inConversation) {
strictAssert(target.peerId, 'peerId is required');
}
return db.transaction(() => { return db.transaction(() => {
const callHistory = getCallHistoryForCallLogEventTarget(db, target); const callHistory = getCallHistoryForCallLogEventTarget(db, target);
if (callHistory == null) { if (callHistory == null) {
@ -3870,28 +3898,45 @@ export function markAllCallHistoryRead(
'Call ID must be the same as target if supplied' 'Call ID must be the same as target if supplied'
); );
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); const conversationId = getConversationIdForCallHistory(db, callHistory);
if (conversationId == null) { if (conversationId == null) {
logger.warn('markAllCallHistoryRead: Conversation not found for call'); logger.warn('markAllCallHistoryRead: Conversation not found for call');
return 0; return 0;
} }
logger.info(`markAllCallHistoryRead: Found conversation ${conversationId}`);
const receivedAt = getMessageReceivedAtForCall(db, callId, conversationId); logger.info(
`markAllCallHistoryRead: Found conversation ${conversationId}`
);
receivedAt = getMessageReceivedAtForCall(db, callId, conversationId);
predicate = inConversation
? sqlFragment`messages.conversationId IS ${conversationId}`
: sqlFragment`TRUE`;
}
if (receivedAt == null) { if (receivedAt == null) {
logger.warn('markAllCallHistoryRead: Message not found for call'); logger.warn('markAllCallHistoryRead: Message not found for call');
return 0; return 0;
} }
const predicate = inConversation
? sqlFragment`messages.conversationId IS ${conversationId}`
: sqlFragment`TRUE`;
const jsonPatch = JSON.stringify({ const jsonPatch = JSON.stringify({
seenStatus: SeenStatus.Seen, seenStatus: SeenStatus.Seen,
}); });
logger.warn( logger.info(
`markAllCallHistoryRead: Marking calls before ${receivedAt} read` `markAllCallHistoryRead: Marking calls before ${receivedAt} read`
); );
@ -3915,7 +3960,6 @@ function markAllCallHistoryReadInConversation(
db: WritableDB, db: WritableDB,
target: CallLogEventTarget target: CallLogEventTarget
): number { ): number {
strictAssert(target.peerId, 'peerId is required');
return markAllCallHistoryRead(db, target, true); return markAllCallHistoryRead(db, target, true);
} }

View file

@ -125,16 +125,26 @@ export type CallDetails = Readonly<{
endedTimestamp: number | null; endedTimestamp: number | null;
}>; }>;
export type CallLogEventTarget = Readonly<{ export type CallLogEventTarget = Readonly<
{
timestamp: number; timestamp: number;
callId: string | null; callId: string | null;
} & (
| {
peerId: AciString | string | null; peerId: AciString | string | null;
}>; }
| {
peerIdAsConversationId: AciString | string | null;
peerIdAsRoomId: string | null;
}
)
>;
export type CallLogEventDetails = Readonly<{ export type CallLogEventDetails = Readonly<{
type: CallLogEvent; type: CallLogEvent;
timestamp: number; timestamp: number;
peerId: AciString | string | null; peerIdAsConversationId: AciString | string | null;
peerIdAsRoomId: string | null;
callId: string | null; callId: string | null;
}>; }>;
@ -243,7 +253,9 @@ export const callHistoryGroupSchema = z.object({
), ),
}) satisfies z.ZodType<CallHistoryGroup>; }) satisfies z.ZodType<CallHistoryGroup>;
const peerIdInBytesSchema = z.instanceof(Uint8Array).transform(value => { const conversationPeerIdInBytesSchema = z
.instanceof(Uint8Array)
.transform(value => {
// direct conversationId // direct conversationId
if (value.byteLength === UUID_BYTE_SIZE) { if (value.byteLength === UUID_BYTE_SIZE) {
const uuid = bytesToUuid(value); const uuid = bytesToUuid(value);
@ -287,7 +299,7 @@ export const callEventNormalizeSchema = z
type: z type: z
.nativeEnum(Proto.SyncMessage.CallEvent.Type) .nativeEnum(Proto.SyncMessage.CallEvent.Type)
.refine(val => val !== Proto.SyncMessage.CallEvent.Type.AD_HOC_CALL), .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({ export const callLogEventNormalizeSchema = z.object({
type: z.nativeEnum(Proto.SyncMessage.CallLogEvent.Type), type: z.nativeEnum(Proto.SyncMessage.CallLogEvent.Type),
timestamp: longToNumberSchema, timestamp: longToNumberSchema,
peerId: peerIdInBytesSchema.optional(), peerIdAsConversationId: conversationPeerIdInBytesSchema.optional(),
peerIdAsRoomId: roomIdInBytesSchema.optional(),
callId: longToStringSchema.optional(), callId: longToStringSchema.optional(),
}); });

View file

@ -282,10 +282,14 @@ const callLogEventFromProto: Partial<
export function getCallLogEventForProto( export function getCallLogEventForProto(
callLogEventProto: Proto.SyncMessage.ICallLogEvent callLogEventProto: Proto.SyncMessage.ICallLogEvent
): CallLogEventDetails { ): CallLogEventDetails {
const callLogEvent = parsePartial( // CallLogEvent peerId is ambiguous whether it's a conversationId (direct, or groupId)
callLogEventNormalizeSchema, // or roomId so handle both cases
callLogEventProto const { peerId: peerIdBytes } = callLogEventProto;
); const callLogEvent = parsePartial(callLogEventNormalizeSchema, {
...callLogEventProto,
peerIdAsConversationId: peerIdBytes,
peerIdAsRoomId: peerIdBytes,
});
const type = callLogEventFromProto[callLogEvent.type]; const type = callLogEventFromProto[callLogEvent.type];
if (type == null) { if (type == null) {
@ -295,7 +299,8 @@ export function getCallLogEventForProto(
return { return {
type, type,
timestamp: callLogEvent.timestamp, timestamp: callLogEvent.timestamp,
peerId: callLogEvent.peerId ?? null, peerIdAsConversationId: callLogEvent.peerIdAsConversationId ?? null,
peerIdAsRoomId: callLogEvent.peerIdAsRoomId ?? null,
callId: callLogEvent.callId ?? null, callId: callLogEvent.callId ?? null,
}; };
} }

View file

@ -14,10 +14,12 @@ export async function onCallLogEventSync(
syncEvent: CallLogEventSyncEvent syncEvent: CallLogEventSyncEvent
): Promise<void> { ): Promise<void> {
const { data, confirm } = syncEvent; const { data, confirm } = syncEvent;
const { type, peerId, callId, timestamp } = data.callLogEventDetails; const { type, peerIdAsConversationId, peerIdAsRoomId, callId, timestamp } =
data.callLogEventDetails;
const target: CallLogEventTarget = { const target: CallLogEventTarget = {
peerId, peerIdAsConversationId,
peerIdAsRoomId,
callId, callId,
timestamp, timestamp,
}; };
@ -50,7 +52,8 @@ export async function onCallLogEventSync(
} else if (type === CallLogEvent.MarkedAsReadInConversation) { } else if (type === CallLogEvent.MarkedAsReadInConversation) {
log.info('onCallLogEventSync: Marking call history read in conversation'); log.info('onCallLogEventSync: Marking call history read in conversation');
try { try {
strictAssert(peerId, 'Missing peerId'); strictAssert(peerIdAsConversationId, 'Missing peerIdAsConversationId');
strictAssert(peerIdAsRoomId, 'Missing peerIdAsRoomId');
const count = const count =
await DataWriter.markAllCallHistoryReadInConversation(target); await DataWriter.markAllCallHistoryReadInConversation(target);
log.info( log.info(