Validate and log transitions for call disposition

This commit is contained in:
Jamie Kyle 2023-05-25 14:17:35 -07:00 committed by GitHub
parent e2f39ed5fa
commit 688ddd49d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 143 additions and 14 deletions

View file

@ -160,6 +160,7 @@ import { stripNewlinesForLeftPane } from '../util/stripNewlinesForLeftPane';
import { findAndFormatContact } from '../util/findAndFormatContact';
import { deriveProfileKeyVersion } from '../util/zkgroup';
import { incrementMessageCounter } from '../util/incrementMessageCounter';
import { validateTransition } from '../util/callHistoryDetails';
const EMPTY_ARRAY: Readonly<[]> = [];
const EMPTY_GROUP_COLLISIONS: GroupNameCollisionsWithIdsByTitle = {};
@ -3396,21 +3397,9 @@ export class ConversationModel extends window.Backbone
// awaited it would block on this forever.
drop(
this.queueJob('addCallHistory', async () => {
const message: MessageAttributesType = {
id: generateGuid(),
conversationId: this.id,
type: 'call-history',
sent_at: timestamp,
timestamp,
received_at: receivedAtCounter || incrementMessageCounter(),
received_at_ms: timestamp,
readStatus: unread ? ReadStatus.Unread : ReadStatus.Read,
seenStatus: unread ? SeenStatus.Unseen : SeenStatus.NotApplicable,
callHistoryDetails: detailsToSave,
};
// Force save if we're adding a new call history message for a direct call
let forceSave = true;
let previousMessage: MessageAttributesType | void;
if (callHistoryDetails.callMode === CallMode.Direct) {
const messageId =
await window.Signal.Data.getCallHistoryMessageByCallId(
@ -3421,9 +3410,11 @@ export class ConversationModel extends window.Backbone
log.info(
`addCallHistory: Found existing call history message (Call ID: ${callHistoryDetails.callId}, Message ID: ${messageId})`
);
message.id = messageId;
// We don't want to force save if we're updating an existing message
forceSave = false;
previousMessage = await window.Signal.Data.getMessageById(
messageId
);
} else {
log.info(
`addCallHistory: No existing call history message found (Call ID: ${callHistoryDetails.callId})`
@ -3431,6 +3422,30 @@ export class ConversationModel extends window.Backbone
}
}
if (
!validateTransition(
previousMessage?.callHistoryDetails,
callHistoryDetails,
log
)
) {
log.info("addCallHistory: Transition isn't valid, not saving");
return;
}
const message: MessageAttributesType = {
id: previousMessage?.id ?? generateGuid(),
conversationId: this.id,
type: 'call-history',
sent_at: timestamp,
timestamp,
received_at: receivedAtCounter || incrementMessageCounter(),
received_at_ms: timestamp,
readStatus: unread ? ReadStatus.Unread : ReadStatus.Read,
seenStatus: unread ? SeenStatus.Unseen : SeenStatus.NotApplicable,
callHistoryDetails,
};
const id = await window.Signal.Data.saveMessage(message, {
ourUuid: window.textsecure.storage.user.getCheckedUuid().toString(),
forceSave,

View file

@ -0,0 +1,114 @@
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { CallHistoryDetailsFromDiskType } from '../types/Calling';
import { CallMode } from '../types/Calling';
import type { LoggerType } from '../types/Logging';
import { strictAssert } from './assert';
import { missingCaseError } from './missingCaseError';
enum CallHistoryStatus {
Pending = 'Pending',
Missed = 'Missed',
Accepted = 'Accepted',
NotAccepted = 'NotAccepted',
}
function getCallHistoryStatus(
callHistoryDetails: CallHistoryDetailsFromDiskType
): CallHistoryStatus {
strictAssert(
callHistoryDetails.callMode === CallMode.Direct,
"Can't get call history status for group call (unimplemented)"
);
if (callHistoryDetails.acceptedTime != null) {
return CallHistoryStatus.Accepted;
}
if (callHistoryDetails.wasDeclined) {
return CallHistoryStatus.NotAccepted;
}
if (callHistoryDetails.endedTime != null) {
return CallHistoryStatus.Missed;
}
return CallHistoryStatus.Pending;
}
function isAllowedTransition(
from: CallHistoryStatus,
to: CallHistoryStatus,
log: LoggerType
): boolean {
if (from === CallHistoryStatus.Pending) {
log.info('callHistoryDetails: Can go from pending to anything.');
return true;
}
if (to === CallHistoryStatus.Pending) {
log.info("callHistoryDetails: Can't go to pending once out of it.");
return false;
}
if (from === CallHistoryStatus.Missed) {
log.info(
"callHistoryDetails: A missed call on this device might've been picked up or explicitly declined on a linked device."
);
return true;
}
if (from === CallHistoryStatus.Accepted) {
log.info(
'callHistoryDetails: If we accept anywhere that beats everything.'
);
return false;
}
if (
from === CallHistoryStatus.NotAccepted &&
to === CallHistoryStatus.Accepted
) {
log.info(
'callHistoryDetails: If we declined on this device but picked up on another device, that counts as accepted.'
);
return true;
}
if (from === CallHistoryStatus.NotAccepted) {
log.info(
"callHistoryDetails: Can't transition from NotAccepted to anything else"
);
return false;
}
throw missingCaseError(from);
}
export function validateTransition(
prev: CallHistoryDetailsFromDiskType | void,
next: CallHistoryDetailsFromDiskType,
log: LoggerType
): boolean {
// Only validating Direct calls for now
if (next.callMode !== CallMode.Direct) {
return true;
}
if (prev == null) {
return true;
}
strictAssert(
prev.callMode === CallMode.Direct && next.callMode === CallMode.Direct,
"Call mode must be 'Direct'"
);
strictAssert(prev.callId === next.callId, 'Call ID must not change');
strictAssert(
prev.wasIncoming === next.wasIncoming,
'wasIncoming must not change'
);
strictAssert(
prev.wasVideoCall === next.wasVideoCall,
'wasVideoCall must not change'
);
const before = getCallHistoryStatus(prev);
const after = getCallHistoryStatus(next);
log.info(
`callHistoryDetails: Checking transition (Call ID: ${next.callId}, Before: ${before}, After: ${after})`
);
return isAllowedTransition(before, after, log);
}